diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4cfe57d --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +# Something perhaps diff --git a/backend/__pycache__/database.cpython-310.pyc b/backend/__pycache__/database.cpython-310.pyc new file mode 100644 index 0000000..9b3daa7 Binary files /dev/null and b/backend/__pycache__/database.cpython-310.pyc differ diff --git a/backend/app.py b/backend/app.py new file mode 100644 index 0000000..c2359d9 --- /dev/null +++ b/backend/app.py @@ -0,0 +1,162 @@ +import redfish +import sqlite3 +import bcrypt +from flask import Flask, jsonify, request +from flask_cors import CORS +from database import initialize_db, add_server, get_servers, add_action, get_actions + + +app = Flask(__name__, static_folder='../frontend', static_url_path='/') +CORS(app) # Enable CORS + +initialize_db() + +username = "admin" +password = "Pbg68dqUJ" + +@app.route('/api/servers', methods=['GET']) +def list_servers(): + servers = get_servers() + return jsonify(servers) + +@app.route('/api/actions', methods=['GET']) +def list_actions(): + actions = get_actions() + return jsonify(actions) + +@app.route('/api/add-server', methods=['POST']) +def add_server_route(): + data = request.json + ip = data.get('ip') + name = data.get('name') + + if not ip or not name: + return jsonify({"error": "Missing required parameters"}), 400 + + try: + add_server(ip, name) + return jsonify({"message": "Server added successfully!"}), 201 + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/add-endpoint', methods=['POST']) +def add_endpoint_route(): + data = request.json + name = data.get('name') + endpoint = data.get('endpoint') + + if not name or not endpoint: + return jsonify({"error": "Missing required parameters"}), 400 + + try: + add_action(name, endpoint) + return jsonify({"message": "Endpoint added successfully!"}), 201 + except Exception as e: + return jsonify({"error": str(e)}), 500 + +@app.route('/api/server-info', methods=['GET']) +def server_info(): + ip = request.args.get('ip') + #username = "admin" + #password = get_admin_password() + + if not ip or not username or not password: + return jsonify({"error": "Missing required parameters"}), 400 + + try: + # Connect to the iLO + rest_obj = redfish.LegacyRestClient(base_url=ip, username=username, password=password) + rest_obj.login() + response = rest_obj.get('/rest/v1/managers/1') + rest_obj.logout() + + # If the response contains data, return it + if response.status == 200: + return jsonify(response.dict) + + else: + # Return more detailed error information + return jsonify({ + "error": "Error retrieving data from iLO", + "status": response.status, + "message": response.dict # Add this line to capture more details + }), response.status + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# Get SNMP service information +@app.route('/api/snmp-info', methods=['GET']) +def snmp_info(): + ip = request.args.get('ip') + #username = "admin" + #password = get_admin_password() + + if not ip or not username or not password: + return jsonify({"error": "Missing required parameters"}), 400 + + try: + # Connect to the iLO + rest_obj = redfish.LegacyRestClient(base_url=ip, username=username, password=password) + rest_obj.login() + # response = rest_obj.get('/redfish/v1/Managers/1/SnmpService/') + response = rest_obj.get('/redfish/v1/Managers/1/NetworkService/') + rest_obj.logout() + + # If the response contains data, return it + if response.status == 200: + return jsonify(response.dict) + + else: + # Return more detailed error information + return jsonify({ + "error": "Error retrieving data from iLO", + "status": response.status, + "message": response.dict # Add this line to capture more details + }), response.status + + except Exception as e: + return jsonify({"error": str(e)}), 500 + +# Send SNMP test message +@app.route('/api/send-test-mail', methods=['GET']) +def snmp_test(): + ip = request.args.get('ip') + #username = "admin" + #password = get_admin_password() + + if not ip or not username or not password: + return jsonify({"error": "Missing required parameters"}), 400 + + try: + # Connect to the iLO + rest_obj = redfish.LegacyRestClient(base_url=ip, username=username, password=password) + rest_obj.login() + #response = rest_obj.post('/redfish/v1/Managers/1/SnmpService', body={"Action": "SendSNMPTestAlert"}) + #response = rest_obj.post('/redfish/v1/Managers/1/SnmpService/Actions/SnmpService.SendSNMPTestAlert/', body={}) + #response = rest_obj.post('/redfish/v1/Managers/1/NetworkService/Actions/Oem/Hp/HpiLOManagerNetworkService', body={"Action": "SendTestAlertMail"}) + response = rest_obj.get('/rest/v1/Managers/1/UpdateService') + rest_obj.logout() + + # If the response contains data, return it + if response.status == 200: + return jsonify(response.dict) + + else: + # Return more detailed error information + return jsonify({ + "error": "Error retrieving data from iLO", + "status": response.status, + "message": response.dict # Add this line to capture more details + }), response.status + + except Exception as e: + return jsonify({"error": str(e)}), 500 + + +@app.route('/') +def serve_frontend(): + return send_from_directory(app.static_folder, 'index.html') + +if __name__ == '__main__': + app.run(debug=True) diff --git a/backend/database.py b/backend/database.py new file mode 100644 index 0000000..d1b57cb --- /dev/null +++ b/backend/database.py @@ -0,0 +1,86 @@ +import sqlite3 +import bcrypt + +def initialize_db(): + conn = sqlite3.connect('servers.db') + cursor = conn.cursor() + + # Create table for servers + cursor.execute(''' + CREATE TABLE IF NOT EXISTS servers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + ip_address TEXT NOT NULL, + name TEXT NOT NULL + ) + ''') + + # Create table for users + cursor.execute(''' + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT NOT NULL UNIQUE, + password TEXT NOT NULL + ) + ''') + + # Create table for actions + cursor.execute(''' + CREATE TABLE IF NOT EXISTS actions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + endpoint TEXT NOT NULL + ) + ''') + + conn.commit() + conn.close() + +def add_user(username, password): + hashed_password = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + conn = sqlite3.connect('servers.db') + cursor = conn.cursor() + cursor.execute("INSERT INTO users (username, password) VALUES (?, ?)", (username, hashed_password)) + conn.commit() + conn.close() + +def check_user(username, password): + conn = sqlite3.connect('servers.db') + cursor = conn.cursor() + cursor.execute("SELECT password FROM users WHERE username = ?", (username,)) + user = cursor.fetchone() + + if user and bcrypt.checkpw(password.encode('utf-8'), user[0].encode('utf-8')): + return True + return False + +def add_server(ip_address, name): + conn = sqlite3.connect('servers.db') + cursor = conn.cursor() + cursor.execute("INSERT INTO servers (ip_address, name) VALUES (?, ?)", (ip_address, name)) + conn.commit() + conn.close() + +def get_servers(): + conn = sqlite3.connect('servers.db') + cursor = conn.cursor() + cursor.execute("SELECT * FROM servers") + data = cursor.fetchall() + conn.close() + return data + +def add_action(name, endpoint): + conn = sqlite3.connect('servers.db') + cursor = conn.cursor() + cursor.execute("INSERT INTO actions (name, endpoint) VALUES (?, ?)", (name, endpoint)) + conn.commit() + conn.close() + + +def get_actions(): + conn = sqlite3.connect('servers.db') + cursor = conn.cursor() + cursor.execute("SELECT * FROM actions") + data = cursor.fetchall() + conn.close() + return data + diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..ce59f17 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,4 @@ +Flask +ilorest +flask-cors +bcrypt diff --git a/backend/servers.db b/backend/servers.db new file mode 100644 index 0000000..aa65b2f Binary files /dev/null and b/backend/servers.db differ diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..6697a5a --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,55 @@ + + + + + + + HPE Server Manager + + +
+

HPE Server Manager

+ + +

Add Server

+
+ + + +
+ + +

Add API Call

+
+ + + + +
+ + + +

Select Server

+ + + + +

+    
+ + + diff --git a/frontend/script.js b/frontend/script.js new file mode 100644 index 0000000..fa6af4a --- /dev/null +++ b/frontend/script.js @@ -0,0 +1,128 @@ +async function loadServers() { + const serverSelect = document.getElementById('server-select'); + const actionSelect = document.getElementById('action-select'); + const response = await fetch('http://localhost:5000/api/servers'); + const responseAc = await fetch('http://localhost:5000/api/actions'); + const servers = await response.json(); + const actions = await responseAc.json(); + + // Clear existing options + serverSelect.innerHTML = ''; + + servers.forEach(server => { + const option = document.createElement('option'); + option.value = server[1]; // IP address + option.textContent = server[2]; // Server name + serverSelect.appendChild(option); + }); + // Clear existing options + actionSelect.innerHTML = ''; + + actions.forEach(action => { + const optionAc = document.createElement('option'); + optionAc.textContent = action[2]; // Do not know + optionAc.textContent = action[1]; + actionSelect.appendChild(optionAc); + }); +} + +//async function loadActions() { +// const actionSelect = document.getElementById('action-select'); +// const responseAc = await fetch('http://localhost:5000/api/actions'); +// const actions = await responseAc.json(); +// +// // Clear existing options +// actionSelect.innerHTML = ''; +// +// actions.forEach(action => { +// const optionAc = document.createElement('option'); +// optionAc.textContent = action[2]; // Do not know +// optionAc.textContent = action[1]; +// actionSelect.appendChild(optionAc); +// }); +//} + + +// Load servers and actions when the page loads +window.onload = loadServers; + +// Handle API call choice. +document.getElementById('confirmButton').addEventListener('click', async () => { + const ip = document.getElementById('server-select').value; + const action = document.getElementById('action-select').value; + const action_low = action.toLowerCase(); + const action_done = action_low.replace(/ /g, "-"); + + const response = await fetch(`http://localhost:5000/api/${action_done}?ip=${ip}`); + const data = await response.json(); + document.getElementById('server-info').textContent = JSON.stringify(data, null, 2); +}); + +//// Handle SNMP info fetch +//document.getElementById('confirmButton').addEventListener('click', async () => { +// const ip = document.getElementById('server-select').value; +// const action = document.getElementById('action-select').value; +// +// const response = await fetch(`http://localhost:5000/api/${action}?ip=${ip}`); +// const data = await response.json(); +// document.getElementById('server-info').textContent = JSON.stringify(data, null, 2); +//}); +// +//// SNMP send test message +//document.getElementById('confirmButton').addEventListener('click', async () => { +// const ip = document.getElementById('server-select').value; +// const action = document.getElementById('action-select').value; +// +// const response = await fetch(`http://localhost:5000/api/${action}?ip=${ip}`); +// const data = await response.json(); +// document.getElementById('server-info').textContent = JSON.stringify(data, null, 2); +//}); + +// Handle form submission to add new server +document.getElementById('add-server-form').addEventListener('submit', async (event) => { + event.preventDefault(); // Prevent form from reloading the page + + const ip = document.getElementById('server-ip').value; + const name = document.getElementById('server-name').value; + + const response = await fetch('http://localhost:5000/api/add-server', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ ip: ip, name: name }) + }); + + if (response.ok) { + alert('Server added successfully!'); + // Reload server list + loadServers(); + } else { + alert('Error adding server'); + } +}); + + +// Handle form submission to add new API call +document.getElementById('add-action-form').addEventListener('submit', async (event) => { + event.preventDefault(); // Prevent form from reloading the page + + const name = document.getElementById('action-name').value; + const endpoint = document.getElementById('action-call').value; + + const response = await fetch('http://localhost:5000/api/add-endpoint', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ name: name, endpoint: endpoint }) + }); + + if (response.ok) { + alert('API call added successfully!'); + // Reload server list + loadServers(); + } else { + alert('Error adding API call'); + } +}); diff --git a/frontend/styles.css b/frontend/styles.css new file mode 100644 index 0000000..2322b9a --- /dev/null +++ b/frontend/styles.css @@ -0,0 +1,107 @@ +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + margin: 0; + padding: 20px; +} + +h1 { + text-align: center; +} + +.container { + max-width: 800px; + margin: auto; + background: #fff; + word-wrap: break-word; + padding: 20px; + border-radius: 5px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); +} + +button { + padding: 10px 15px; + background-color: #007BFF; + border: none; + color: white; + font-size: 16px; + cursor: pointer; + width: 150px; + height: 40px; +} + +#confirmButton { + float: right; + margin-right: 30px; + background-color: #4CAF50; +} + +#confirmButton:hover { + background-color: green; +} + +#server-select { + padding: 10px 15px; + background-color: #007BFF; + border: none; + color: white; + font-size: 16px; + cursor: pointer; + width: 150px; + height: 40px; +} + +#action-select { + padding: 10px 15px; + background-color: #007BFF; + border: none; + color: white; + font-size: 16px; + cursor: pointer; + width: 150px; + height: 40px; +} + +#action-select:hover { + background-color: #0056b3; +} + +#server-select:hover { + background-color: #0056b3; +} + + + +button:hover { + background-color: #0056b3; +} + + +/* General input styling */ +input[type="text"], +input[type="email"], +textarea, +select { + width: 100%; /* Full width */ + padding: 10px; /* Inner spacing */ + margin: 8px 0; /* Vertical spacing */ + border: 1px solid #007BFF; /* Border */ + border-radius: 5px; /* Rounded corners */ + font-size: 16px; /* Font size */ + box-sizing: border-box; /* Include padding in width */ + transition: border-color 0.3s; /* Smooth transition */ +} + +/* Focus effect */ +input[type="text"]:focus, +textarea:focus, +select:focus { + border-color: #4CAF50; /* Green border on focus */ + outline: none; /* Remove default outline */ +} + +/* Specific styles for the textarea */ +textarea { + resize: vertical; /* Allow vertical resizing only */ +} + diff --git a/get_info.py b/get_info.py new file mode 100644 index 0000000..f43aa47 --- /dev/null +++ b/get_info.py @@ -0,0 +1,6 @@ +import redfish + +def get_server_info(ip, username, password): + redfish_obj = redfish.LegacyRestClient(ip, username, password) + response = rest_obj.post('/redfish/v1/Managers/1/SnmpService/Actions/SnmpService.SendSNMPTestAlert/', body={}) + return response diff --git a/venv/bin/Activate.ps1 b/venv/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/venv/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/venv/bin/activate b/venv/bin/activate new file mode 100644 index 0000000..e70d0ad --- /dev/null +++ b/venv/bin/activate @@ -0,0 +1,69 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV=/home/js219/projects/ilo-manager/venv +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/"bin":$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1='(venv) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(venv) ' + export VIRTUAL_ENV_PROMPT +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/venv/bin/activate.csh b/venv/bin/activate.csh new file mode 100644 index 0000000..5fe375d --- /dev/null +++ b/venv/bin/activate.csh @@ -0,0 +1,26 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV /home/js219/projects/ilo-manager/venv + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(venv) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(venv) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/venv/bin/activate.fish b/venv/bin/activate.fish new file mode 100644 index 0000000..90cddb3 --- /dev/null +++ b/venv/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/); you cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV /home/js219/projects/ilo-manager/venv + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/"bin $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) '(venv) ' (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT '(venv) ' +end diff --git a/venv/bin/flask b/venv/bin/flask new file mode 100755 index 0000000..afa76c8 --- /dev/null +++ b/venv/bin/flask @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from flask.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/venv/bin/futurize b/venv/bin/futurize new file mode 100755 index 0000000..6f4112e --- /dev/null +++ b/venv/bin/futurize @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from libfuturize.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/venv/bin/ilorest b/venv/bin/ilorest new file mode 100755 index 0000000..9952844 --- /dev/null +++ b/venv/bin/ilorest @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from ilorest.rdmc import ilorestcommand +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(ilorestcommand()) diff --git a/venv/bin/jdiff b/venv/bin/jdiff new file mode 100755 index 0000000..fb674b8 --- /dev/null +++ b/venv/bin/jdiff @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from jsondiff.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/venv/bin/jsondiff b/venv/bin/jsondiff new file mode 100755 index 0000000..e9a4e1e --- /dev/null +++ b/venv/bin/jsondiff @@ -0,0 +1,41 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- + +from __future__ import print_function + +import sys +import json +import jsonpatch +import argparse + + +parser = argparse.ArgumentParser(description='Diff two JSON files') +parser.add_argument('FILE1', type=argparse.FileType('r')) +parser.add_argument('FILE2', type=argparse.FileType('r')) +parser.add_argument('--indent', type=int, default=None, + help='Indent output by n spaces') +parser.add_argument('-u', '--preserve-unicode', action='store_true', + help='Output Unicode character as-is without using Code Point') +parser.add_argument('-v', '--version', action='version', + version='%(prog)s ' + jsonpatch.__version__) + + +def main(): + try: + diff_files() + except KeyboardInterrupt: + sys.exit(1) + + +def diff_files(): + """ Diffs two JSON files and prints a patch """ + args = parser.parse_args() + doc1 = json.load(args.FILE1) + doc2 = json.load(args.FILE2) + patch = jsonpatch.make_patch(doc1, doc2) + if patch.patch: + print(json.dumps(patch.patch, indent=args.indent, ensure_ascii=not(args.preserve_unicode))) + sys.exit(1) + +if __name__ == "__main__": + main() diff --git a/venv/bin/jsonpatch b/venv/bin/jsonpatch new file mode 100755 index 0000000..f22382a --- /dev/null +++ b/venv/bin/jsonpatch @@ -0,0 +1,107 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- + +import sys +import os.path +import json +import jsonpatch +import tempfile +import argparse + + +parser = argparse.ArgumentParser( + description='Apply a JSON patch on a JSON file') +parser.add_argument('ORIGINAL', type=argparse.FileType('r'), + help='Original file') +parser.add_argument('PATCH', type=argparse.FileType('r'), + nargs='?', default=sys.stdin, + help='Patch file (read from stdin if omitted)') +parser.add_argument('--indent', type=int, default=None, + help='Indent output by n spaces') +parser.add_argument('-b', '--backup', action='store_true', + help='Back up ORIGINAL if modifying in-place') +parser.add_argument('-i', '--in-place', action='store_true', + help='Modify ORIGINAL in-place instead of to stdout') +parser.add_argument('-v', '--version', action='version', + version='%(prog)s ' + jsonpatch.__version__) +parser.add_argument('-u', '--preserve-unicode', action='store_true', + help='Output Unicode character as-is without using Code Point') + +def main(): + try: + patch_files() + except KeyboardInterrupt: + sys.exit(1) + + +def patch_files(): + """ Diffs two JSON files and prints a patch """ + args = parser.parse_args() + doc = json.load(args.ORIGINAL) + patch = json.load(args.PATCH) + result = jsonpatch.apply_patch(doc, patch) + + if args.in_place: + dirname = os.path.abspath(os.path.dirname(args.ORIGINAL.name)) + + try: + # Attempt to replace the file atomically. We do this by + # creating a temporary file in the same directory as the + # original file so we can atomically move the new file over + # the original later. (This is done in the same directory + # because atomic renames do not work across mount points.) + + fd, pathname = tempfile.mkstemp(dir=dirname) + fp = os.fdopen(fd, 'w') + atomic = True + + except OSError: + # We failed to create the temporary file for an atomic + # replace, so fall back to non-atomic mode by backing up + # the original (if desired) and writing a new file. + + if args.backup: + os.rename(args.ORIGINAL.name, args.ORIGINAL.name + '.orig') + fp = open(args.ORIGINAL.name, 'w') + atomic = False + + else: + # Since we're not replacing the original file in-place, write + # the modified JSON to stdout instead. + + fp = sys.stdout + + # By this point we have some sort of file object we can write the + # modified JSON to. + + json.dump(result, fp, indent=args.indent, ensure_ascii=not(args.preserve_unicode)) + fp.write('\n') + + if args.in_place: + # Close the new file. If we aren't replacing atomically, this + # is our last step, since everything else is already in place. + + fp.close() + + if atomic: + try: + # Complete the atomic replace by linking the original + # to a backup (if desired), fixing up the permissions + # on the temporary file, and moving it into place. + + if args.backup: + os.link(args.ORIGINAL.name, args.ORIGINAL.name + '.orig') + os.chmod(pathname, os.stat(args.ORIGINAL.name).st_mode) + os.rename(pathname, args.ORIGINAL.name) + + except OSError: + # In the event we could not actually do the atomic + # replace, unlink the original to move it out of the + # way and finally move the temporary file into place. + + os.unlink(args.ORIGINAL.name) + os.rename(pathname, args.ORIGINAL.name) + + +if __name__ == "__main__": + main() diff --git a/venv/bin/jsonpath.py b/venv/bin/jsonpath.py new file mode 100755 index 0000000..2ba6b9e --- /dev/null +++ b/venv/bin/jsonpath.py @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from jsonpath_rw.bin.jsonpath import entry_point +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(entry_point()) diff --git a/venv/bin/jsonpointer b/venv/bin/jsonpointer new file mode 100755 index 0000000..d06cecc --- /dev/null +++ b/venv/bin/jsonpointer @@ -0,0 +1,67 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- + + +import argparse +import json +import sys + +import jsonpointer + +parser = argparse.ArgumentParser( + description='Resolve a JSON pointer on JSON files') + +# Accept pointer as argument or as file +ptr_group = parser.add_mutually_exclusive_group(required=True) + +ptr_group.add_argument('-f', '--pointer-file', type=argparse.FileType('r'), + nargs='?', + help='File containing a JSON pointer expression') + +ptr_group.add_argument('POINTER', type=str, nargs='?', + help='A JSON pointer expression') + +parser.add_argument('FILE', type=argparse.FileType('r'), nargs='+', + help='Files for which the pointer should be resolved') +parser.add_argument('--indent', type=int, default=None, + help='Indent output by n spaces') +parser.add_argument('-v', '--version', action='version', + version='%(prog)s ' + jsonpointer.__version__) + + +def main(): + try: + resolve_files() + except KeyboardInterrupt: + sys.exit(1) + + +def parse_pointer(args): + if args.POINTER: + ptr = args.POINTER + elif args.pointer_file: + ptr = args.pointer_file.read().strip() + else: + parser.print_usage() + sys.exit(1) + + return ptr + + +def resolve_files(): + """ Resolve a JSON pointer on JSON files """ + args = parser.parse_args() + + ptr = parse_pointer(args) + + for f in args.FILE: + doc = json.load(f) + try: + result = jsonpointer.resolve_pointer(doc, ptr) + print(json.dumps(result, indent=args.indent)) + except jsonpointer.JsonPointerException as e: + print('Could not resolve pointer: %s' % str(e), file=sys.stderr) + + +if __name__ == "__main__": + main() diff --git a/venv/bin/normalizer b/venv/bin/normalizer new file mode 100755 index 0000000..5f1d83a --- /dev/null +++ b/venv/bin/normalizer @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from charset_normalizer.cli import cli_detect +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli_detect()) diff --git a/venv/bin/pasteurize b/venv/bin/pasteurize new file mode 100755 index 0000000..a0c4560 --- /dev/null +++ b/venv/bin/pasteurize @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from libpasteurize.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/venv/bin/pip b/venv/bin/pip new file mode 100755 index 0000000..854f801 --- /dev/null +++ b/venv/bin/pip @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/venv/bin/pip3 b/venv/bin/pip3 new file mode 100755 index 0000000..854f801 --- /dev/null +++ b/venv/bin/pip3 @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/venv/bin/pip3.10 b/venv/bin/pip3.10 new file mode 100755 index 0000000..854f801 --- /dev/null +++ b/venv/bin/pip3.10 @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/venv/bin/python b/venv/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/venv/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/venv/bin/python3 b/venv/bin/python3 new file mode 120000 index 0000000..ae65fda --- /dev/null +++ b/venv/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/venv/bin/python3.10 b/venv/bin/python3.10 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/venv/bin/python3.10 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/venv/bin/tabulate b/venv/bin/tabulate new file mode 100755 index 0000000..e399f73 --- /dev/null +++ b/venv/bin/tabulate @@ -0,0 +1,8 @@ +#!/home/js219/projects/ilo-manager/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from tabulate import _main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(_main()) diff --git a/venv/lib/python3.10/site-packages/__pycache__/decorator.cpython-310.pyc b/venv/lib/python3.10/site-packages/__pycache__/decorator.cpython-310.pyc new file mode 100644 index 0000000..39cc085 Binary files /dev/null and b/venv/lib/python3.10/site-packages/__pycache__/decorator.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/__pycache__/jsonpatch.cpython-310.pyc b/venv/lib/python3.10/site-packages/__pycache__/jsonpatch.cpython-310.pyc new file mode 100644 index 0000000..e5f5bbb Binary files /dev/null and b/venv/lib/python3.10/site-packages/__pycache__/jsonpatch.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/__pycache__/jsonpointer.cpython-310.pyc b/venv/lib/python3.10/site-packages/__pycache__/jsonpointer.cpython-310.pyc new file mode 100644 index 0000000..94b8a72 Binary files /dev/null and b/venv/lib/python3.10/site-packages/__pycache__/jsonpointer.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/__pycache__/six.cpython-310.pyc b/venv/lib/python3.10/site-packages/__pycache__/six.cpython-310.pyc new file mode 100644 index 0000000..3d39e0e Binary files /dev/null and b/venv/lib/python3.10/site-packages/__pycache__/six.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/_distutils_hack/__init__.py b/venv/lib/python3.10/site-packages/_distutils_hack/__init__.py new file mode 100644 index 0000000..f707416 --- /dev/null +++ b/venv/lib/python3.10/site-packages/_distutils_hack/__init__.py @@ -0,0 +1,132 @@ +import sys +import os +import re +import importlib +import warnings + + +is_pypy = '__pypy__' in sys.builtin_module_names + + +warnings.filterwarnings('ignore', + r'.+ distutils\b.+ deprecated', + DeprecationWarning) + + +def warn_distutils_present(): + if 'distutils' not in sys.modules: + return + if is_pypy and sys.version_info < (3, 7): + # PyPy for 3.6 unconditionally imports distutils, so bypass the warning + # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250 + return + warnings.warn( + "Distutils was imported before Setuptools, but importing Setuptools " + "also replaces the `distutils` module in `sys.modules`. This may lead " + "to undesirable behaviors or errors. To avoid these issues, avoid " + "using distutils directly, ensure that setuptools is installed in the " + "traditional way (e.g. not an editable install), and/or make sure " + "that setuptools is always imported before distutils.") + + +def clear_distutils(): + if 'distutils' not in sys.modules: + return + warnings.warn("Setuptools is replacing distutils.") + mods = [name for name in sys.modules if re.match(r'distutils\b', name)] + for name in mods: + del sys.modules[name] + + +def enabled(): + """ + Allow selection of distutils by environment variable. + """ + which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'stdlib') + return which == 'local' + + +def ensure_local_distutils(): + clear_distutils() + + # With the DistutilsMetaFinder in place, + # perform an import to cause distutils to be + # loaded from setuptools._distutils. Ref #2906. + add_shim() + importlib.import_module('distutils') + remove_shim() + + # check that submodules load as expected + core = importlib.import_module('distutils.core') + assert '_distutils' in core.__file__, core.__file__ + + +def do_override(): + """ + Ensure that the local copy of distutils is preferred over stdlib. + + See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401 + for more motivation. + """ + if enabled(): + warn_distutils_present() + ensure_local_distutils() + + +class DistutilsMetaFinder: + def find_spec(self, fullname, path, target=None): + if path is not None: + return + + method_name = 'spec_for_{fullname}'.format(**locals()) + method = getattr(self, method_name, lambda: None) + return method() + + def spec_for_distutils(self): + import importlib.abc + import importlib.util + + class DistutilsLoader(importlib.abc.Loader): + + def create_module(self, spec): + return importlib.import_module('setuptools._distutils') + + def exec_module(self, module): + pass + + return importlib.util.spec_from_loader('distutils', DistutilsLoader()) + + def spec_for_pip(self): + """ + Ensure stdlib distutils when running under pip. + See pypa/pip#8761 for rationale. + """ + if self.pip_imported_during_build(): + return + clear_distutils() + self.spec_for_distutils = lambda: None + + @staticmethod + def pip_imported_during_build(): + """ + Detect if pip is being imported in a build script. Ref #2355. + """ + import traceback + return any( + frame.f_globals['__file__'].endswith('setup.py') + for frame, line in traceback.walk_stack(None) + ) + + +DISTUTILS_FINDER = DistutilsMetaFinder() + + +def add_shim(): + sys.meta_path.insert(0, DISTUTILS_FINDER) + + +def remove_shim(): + try: + sys.meta_path.remove(DISTUTILS_FINDER) + except ValueError: + pass diff --git a/venv/lib/python3.10/site-packages/_distutils_hack/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/_distutils_hack/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..d2715ca Binary files /dev/null and b/venv/lib/python3.10/site-packages/_distutils_hack/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/_distutils_hack/__pycache__/override.cpython-310.pyc b/venv/lib/python3.10/site-packages/_distutils_hack/__pycache__/override.cpython-310.pyc new file mode 100644 index 0000000..251dd67 Binary files /dev/null and b/venv/lib/python3.10/site-packages/_distutils_hack/__pycache__/override.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/_distutils_hack/override.py b/venv/lib/python3.10/site-packages/_distutils_hack/override.py new file mode 100644 index 0000000..2cc433a --- /dev/null +++ b/venv/lib/python3.10/site-packages/_distutils_hack/override.py @@ -0,0 +1 @@ +__import__('_distutils_hack').do_override() diff --git a/venv/lib/python3.10/site-packages/_yaml/__init__.py b/venv/lib/python3.10/site-packages/_yaml/__init__.py new file mode 100644 index 0000000..7baa8c4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/_yaml/__init__.py @@ -0,0 +1,33 @@ +# This is a stub package designed to roughly emulate the _yaml +# extension module, which previously existed as a standalone module +# and has been moved into the `yaml` package namespace. +# It does not perfectly mimic its old counterpart, but should get +# close enough for anyone who's relying on it even when they shouldn't. +import yaml + +# in some circumstances, the yaml module we imoprted may be from a different version, so we need +# to tread carefully when poking at it here (it may not have the attributes we expect) +if not getattr(yaml, '__with_libyaml__', False): + from sys import version_info + + exc = ModuleNotFoundError if version_info >= (3, 6) else ImportError + raise exc("No module named '_yaml'") +else: + from yaml._yaml import * + import warnings + warnings.warn( + 'The _yaml extension module is now located at yaml._yaml' + ' and its location is subject to change. To use the' + ' LibYAML-based parser and emitter, import from `yaml`:' + ' `from yaml import CLoader as Loader, CDumper as Dumper`.', + DeprecationWarning + ) + del warnings + # Don't `del yaml` here because yaml is actually an existing + # namespace member of _yaml. + +__name__ = '_yaml' +# If the module is top-level (i.e. not a part of any specific package) +# then the attribute should be set to ''. +# https://docs.python.org/3.8/library/types.html +__package__ = '' diff --git a/venv/lib/python3.10/site-packages/_yaml/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/_yaml/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..55a04f5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/_yaml/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/METADATA b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/METADATA new file mode 100644 index 0000000..629a992 --- /dev/null +++ b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/METADATA @@ -0,0 +1,343 @@ +Metadata-Version: 2.4 +Name: bcrypt +Version: 5.0.0 +Summary: Modern password hashing for your software and your servers +Author-email: The Python Cryptographic Authority developers +License: Apache-2.0 +Project-URL: homepage, https://github.com/pyca/bcrypt/ +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Free Threading :: 3 - Stable +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +License-File: LICENSE +Provides-Extra: tests +Requires-Dist: pytest!=3.3.0,>=3.2.1; extra == "tests" +Provides-Extra: typecheck +Requires-Dist: mypy; extra == "typecheck" +Dynamic: license-file + +bcrypt +====== + +.. image:: https://img.shields.io/pypi/v/bcrypt.svg + :target: https://pypi.org/project/bcrypt/ + :alt: Latest Version + +.. image:: https://github.com/pyca/bcrypt/workflows/CI/badge.svg?branch=main + :target: https://github.com/pyca/bcrypt/actions?query=workflow%3ACI+branch%3Amain + +Acceptable password hashing for your software and your servers (but you should +really use argon2id or scrypt) + + +Installation +============ + +To install bcrypt, simply: + +.. code:: console + + $ pip install bcrypt + +Note that bcrypt should build very easily on Linux provided you have a C +compiler and a Rust compiler (the minimum supported Rust version is 1.56.0). + +For Debian and Ubuntu, the following command will ensure that the required dependencies are installed: + +.. code:: console + + $ sudo apt-get install build-essential cargo + +For Fedora and RHEL-derivatives, the following command will ensure that the required dependencies are installed: + +.. code:: console + + $ sudo yum install gcc cargo + +For Alpine, the following command will ensure that the required dependencies are installed: + +.. code:: console + + $ apk add --update musl-dev gcc cargo + + +Alternatives +============ + +While bcrypt remains an acceptable choice for password storage, depending on your specific use case you may also want to consider using scrypt (either via `standard library`_ or `cryptography`_) or argon2id via `argon2_cffi`_. + +Changelog +========= + +5.0.0 +----- + +* Bumped MSRV to 1.74. +* Added support for Python 3.14 and free-threaded Python 3.14. +* Added support for Windows on ARM. +* Passing ``hashpw`` a password longer than 72 bytes now raises a + ``ValueError``. Previously the password was silently truncated, following the + behavior of the original OpenBSD ``bcrypt`` implementation. + +4.3.0 +----- + +* Dropped support for Python 3.7. +* We now support free-threaded Python 3.13. +* We now support PyPy 3.11. +* We now publish wheels for free-threaded Python 3.13, for PyPy 3.11 on + ``manylinux``, and for ARMv7l on ``manylinux``. + +4.2.1 +----- + +* Bump Rust dependency versions - this should resolve crashes on Python 3.13 + free-threaded builds. +* We no longer build ``manylinux`` wheels for PyPy 3.9. + +4.2.0 +----- + +* Bump Rust dependency versions +* Removed the ``BCRYPT_ALLOW_RUST_163`` environment variable. + +4.1.3 +----- + +* Bump Rust dependency versions + +4.1.2 +----- + +* Publish both ``py37`` and ``py39`` wheels. This should resolve some errors + relating to initializing a module multiple times per process. + +4.1.1 +----- + +* Fixed the type signature on the ``kdf`` method. +* Fixed packaging bug on Windows. +* Fixed incompatibility with passlib package detection assumptions. + +4.1.0 +----- + +* Dropped support for Python 3.6. +* Bumped MSRV to 1.64. (Note: Rust 1.63 can be used by setting the ``BCRYPT_ALLOW_RUST_163`` environment variable) + +4.0.1 +----- + +* We now build PyPy ``manylinux`` wheels. +* Fixed a bug where passing an invalid ``salt`` to ``checkpw`` could result in + a ``pyo3_runtime.PanicException``. It now correctly raises a ``ValueError``. + +4.0.0 +----- + +* ``bcrypt`` is now implemented in Rust. Users building from source will need + to have a Rust compiler available. Nothing will change for users downloading + wheels. +* We no longer ship ``manylinux2010`` wheels. Users should upgrade to the latest + ``pip`` to ensure this doesn’t cause issues downloading wheels on their + platform. We now ship ``manylinux_2_28`` wheels for users on new enough platforms. +* ``NUL`` bytes are now allowed in inputs. + + +3.2.2 +----- + +* Fixed packaging of ``py.typed`` files in wheels so that ``mypy`` works. + +3.2.1 +----- + +* Added support for compilation on z/OS +* The next release of ``bcrypt`` with be 4.0 and it will require Rust at + compile time, for users building from source. There will be no additional + requirement for users who are installing from wheels. Users on most + platforms will be able to obtain a wheel by making sure they have an up to + date ``pip``. The minimum supported Rust version will be 1.56.0. +* This will be the final release for which we ship ``manylinux2010`` wheels. + Going forward the minimum supported manylinux ABI for our wheels will be + ``manylinux2014``. The vast majority of users will continue to receive + ``manylinux`` wheels provided they have an up to date ``pip``. + + +3.2.0 +----- + +* Added typehints for library functions. +* Dropped support for Python versions less than 3.6 (2.7, 3.4, 3.5). +* Shipped ``abi3`` Windows wheels (requires pip >= 20). + +3.1.7 +----- + +* Set a ``setuptools`` lower bound for PEP517 wheel building. +* We no longer distribute 32-bit ``manylinux1`` wheels. Continuing to produce + them was a maintenance burden. + +3.1.6 +----- + +* Added support for compilation on Haiku. + +3.1.5 +----- + +* Added support for compilation on AIX. +* Dropped Python 2.6 and 3.3 support. +* Switched to using ``abi3`` wheels for Python 3. If you are not getting a + wheel on a compatible platform please upgrade your ``pip`` version. + +3.1.4 +----- + +* Fixed compilation with mingw and on illumos. + +3.1.3 +----- +* Fixed a compilation issue on Solaris. +* Added a warning when using too few rounds with ``kdf``. + +3.1.2 +----- +* Fixed a compile issue affecting big endian platforms. +* Fixed invalid escape sequence warnings on Python 3.6. +* Fixed building in non-UTF8 environments on Python 2. + +3.1.1 +----- +* Resolved a ``UserWarning`` when used with ``cffi`` 1.8.3. + +3.1.0 +----- +* Added support for ``checkpw``, a convenience method for verifying a password. +* Ensure that you get a ``$2y$`` hash when you input a ``$2y$`` salt. +* Fixed a regression where ``$2a`` hashes were vulnerable to a wraparound bug. +* Fixed compilation under Alpine Linux. + +3.0.0 +----- +* Switched the C backend to code obtained from the OpenBSD project rather than + openwall. +* Added support for ``bcrypt_pbkdf`` via the ``kdf`` function. + +2.0.0 +----- +* Added support for an adjustible prefix when calling ``gensalt``. +* Switched to CFFI 1.0+ + +Usage +----- + +Password Hashing +~~~~~~~~~~~~~~~~ + +Hashing and then later checking that a password matches the previous hashed +password is very simple: + +.. code:: pycon + + >>> import bcrypt + >>> password = b"super secret password" + >>> # Hash a password for the first time, with a randomly-generated salt + >>> hashed = bcrypt.hashpw(password, bcrypt.gensalt()) + >>> # Check that an unhashed password matches one that has previously been + >>> # hashed + >>> if bcrypt.checkpw(password, hashed): + ... print("It Matches!") + ... else: + ... print("It Does not Match :(") + +KDF +~~~ + +As of 3.0.0 ``bcrypt`` now offers a ``kdf`` function which does ``bcrypt_pbkdf``. +This KDF is used in OpenSSH's newer encrypted private key format. + +.. code:: pycon + + >>> import bcrypt + >>> key = bcrypt.kdf( + ... password=b'password', + ... salt=b'salt', + ... desired_key_bytes=32, + ... rounds=100) + + +Adjustable Work Factor +~~~~~~~~~~~~~~~~~~~~~~ +One of bcrypt's features is an adjustable logarithmic work factor. To adjust +the work factor merely pass the desired number of rounds to +``bcrypt.gensalt(rounds=12)`` which defaults to 12): + +.. code:: pycon + + >>> import bcrypt + >>> password = b"super secret password" + >>> # Hash a password for the first time, with a certain number of rounds + >>> hashed = bcrypt.hashpw(password, bcrypt.gensalt(14)) + >>> # Check that a unhashed password matches one that has previously been + >>> # hashed + >>> if bcrypt.checkpw(password, hashed): + ... print("It Matches!") + ... else: + ... print("It Does not Match :(") + + +Adjustable Prefix +~~~~~~~~~~~~~~~~~ + +Another one of bcrypt's features is an adjustable prefix to let you define what +libraries you'll remain compatible with. To adjust this, pass either ``2a`` or +``2b`` (the default) to ``bcrypt.gensalt(prefix=b"2b")`` as a bytes object. + +As of 3.0.0 the ``$2y$`` prefix is still supported in ``hashpw`` but deprecated. + +Maximum Password Length +~~~~~~~~~~~~~~~~~~~~~~~ + +The bcrypt algorithm only handles passwords up to 72 characters, any characters +beyond that are ignored. To work around this, a common approach is to hash a +password with a cryptographic hash (such as ``sha256``) and then base64 +encode it to prevent NULL byte problems before hashing the result with +``bcrypt``: + +.. code:: pycon + + >>> password = b"an incredibly long password" * 10 + >>> hashed = bcrypt.hashpw( + ... base64.b64encode(hashlib.sha256(password).digest()), + ... bcrypt.gensalt() + ... ) + +Compatibility +------------- + +This library should be compatible with py-bcrypt and it will run on Python +3.8+ (including free-threaded builds), and PyPy 3. + +Security +-------- + +``bcrypt`` follows the `same security policy as cryptography`_, if you +identify a vulnerability, we ask you to contact us privately. + +.. _`same security policy as cryptography`: https://cryptography.io/en/latest/security.html +.. _`standard library`: https://docs.python.org/3/library/hashlib.html#hashlib.scrypt +.. _`argon2_cffi`: https://argon2-cffi.readthedocs.io +.. _`cryptography`: https://cryptography.io/en/latest/hazmat/primitives/key-derivation-functions/#cryptography.hazmat.primitives.kdf.scrypt.Scrypt diff --git a/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/RECORD b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/RECORD new file mode 100644 index 0000000..031593d --- /dev/null +++ b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/RECORD @@ -0,0 +1,12 @@ +bcrypt-5.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +bcrypt-5.0.0.dist-info/METADATA,sha256=yV1BfLlI6udlVy23eNbzDa62DSEbUrlWvlLBCI6UAdI,10524 +bcrypt-5.0.0.dist-info/RECORD,, +bcrypt-5.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +bcrypt-5.0.0.dist-info/WHEEL,sha256=WieEZvWpc0Erab6-NfTu9412g-GcE58js6gvBn3Q7B4,111 +bcrypt-5.0.0.dist-info/licenses/LICENSE,sha256=gXPVwptPlW1TJ4HSuG5OMPg-a3h43OGMkZRR1rpwfJA,10850 +bcrypt-5.0.0.dist-info/top_level.txt,sha256=BkR_qBzDbSuycMzHWE1vzXrfYecAzUVmQs6G2CukqNI,7 +bcrypt/__init__.py,sha256=cv-NupIX6P7o6A4PK_F0ur6IZoDr3GnvyzFO9k16wKQ,1000 +bcrypt/__init__.pyi,sha256=ITUCB9mPVU8sKUbJQMDUH5YfQXZb1O55F9qvKZR_o8I,333 +bcrypt/__pycache__/__init__.cpython-310.pyc,, +bcrypt/_bcrypt.abi3.so,sha256=oFwJu4Gq44FqJDttx_oWpypfuUQ30BkCWzD2FhojdYw,631768 +bcrypt/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/REQUESTED b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/WHEEL b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/WHEEL new file mode 100644 index 0000000..eb203c1 --- /dev/null +++ b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: false +Tag: cp39-abi3-manylinux_2_34_x86_64 + diff --git a/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/licenses/LICENSE b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..11069ed --- /dev/null +++ b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/licenses/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/top_level.txt b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/top_level.txt new file mode 100644 index 0000000..7f0b6e7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/bcrypt-5.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +bcrypt diff --git a/venv/lib/python3.10/site-packages/bcrypt/__init__.py b/venv/lib/python3.10/site-packages/bcrypt/__init__.py new file mode 100644 index 0000000..81a92fd --- /dev/null +++ b/venv/lib/python3.10/site-packages/bcrypt/__init__.py @@ -0,0 +1,43 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from ._bcrypt import ( + __author__, + __copyright__, + __email__, + __license__, + __summary__, + __title__, + __uri__, + checkpw, + gensalt, + hashpw, + kdf, +) +from ._bcrypt import ( + __version_ex__ as __version__, +) + +__all__ = [ + "__author__", + "__copyright__", + "__email__", + "__license__", + "__summary__", + "__title__", + "__uri__", + "__version__", + "checkpw", + "gensalt", + "hashpw", + "kdf", +] diff --git a/venv/lib/python3.10/site-packages/bcrypt/__init__.pyi b/venv/lib/python3.10/site-packages/bcrypt/__init__.pyi new file mode 100644 index 0000000..12e4a2e --- /dev/null +++ b/venv/lib/python3.10/site-packages/bcrypt/__init__.pyi @@ -0,0 +1,10 @@ +def gensalt(rounds: int = 12, prefix: bytes = b"2b") -> bytes: ... +def hashpw(password: bytes, salt: bytes) -> bytes: ... +def checkpw(password: bytes, hashed_password: bytes) -> bool: ... +def kdf( + password: bytes, + salt: bytes, + desired_key_bytes: int, + rounds: int, + ignore_few_rounds: bool = False, +) -> bytes: ... diff --git a/venv/lib/python3.10/site-packages/bcrypt/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/bcrypt/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..9c83bf2 Binary files /dev/null and b/venv/lib/python3.10/site-packages/bcrypt/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/bcrypt/_bcrypt.abi3.so b/venv/lib/python3.10/site-packages/bcrypt/_bcrypt.abi3.so new file mode 100755 index 0000000..4806fec Binary files /dev/null and b/venv/lib/python3.10/site-packages/bcrypt/_bcrypt.abi3.so differ diff --git a/venv/lib/python3.10/site-packages/bcrypt/py.typed b/venv/lib/python3.10/site-packages/bcrypt/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/LICENSE.txt b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/LICENSE.txt new file mode 100644 index 0000000..79c9825 --- /dev/null +++ b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright 2010 Jason Kirtland + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/METADATA b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/METADATA new file mode 100644 index 0000000..6d343f5 --- /dev/null +++ b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/METADATA @@ -0,0 +1,60 @@ +Metadata-Version: 2.3 +Name: blinker +Version: 1.9.0 +Summary: Fast, simple object-to-object and broadcast signaling +Author: Jason Kirtland +Maintainer-email: Pallets Ecosystem +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://blinker.readthedocs.io +Project-URL: Source, https://github.com/pallets-eco/blinker/ + +# Blinker + +Blinker provides a fast dispatching system that allows any number of +interested parties to subscribe to events, or "signals". + + +## Pallets Community Ecosystem + +> [!IMPORTANT]\ +> This project is part of the Pallets Community Ecosystem. Pallets is the open +> source organization that maintains Flask; Pallets-Eco enables community +> maintenance of related projects. If you are interested in helping maintain +> this project, please reach out on [the Pallets Discord server][discord]. +> +> [discord]: https://discord.gg/pallets + + +## Example + +Signal receivers can subscribe to specific senders or receive signals +sent by any sender. + +```pycon +>>> from blinker import signal +>>> started = signal('round-started') +>>> def each(round): +... print(f"Round {round}") +... +>>> started.connect(each) + +>>> def round_two(round): +... print("This is round two.") +... +>>> started.connect(round_two, sender=2) + +>>> for round in range(1, 4): +... started.send(round) +... +Round 1! +Round 2! +This is round two. +Round 3! +``` + diff --git a/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/RECORD b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/RECORD new file mode 100644 index 0000000..3f9f399 --- /dev/null +++ b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/RECORD @@ -0,0 +1,12 @@ +blinker-1.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +blinker-1.9.0.dist-info/LICENSE.txt,sha256=nrc6HzhZekqhcCXSrhvjg5Ykx5XphdTw6Xac4p-spGc,1054 +blinker-1.9.0.dist-info/METADATA,sha256=uIRiM8wjjbHkCtbCyTvctU37IAZk0kEe5kxAld1dvzA,1633 +blinker-1.9.0.dist-info/RECORD,, +blinker-1.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82 +blinker/__init__.py,sha256=I2EdZqpy4LyjX17Hn1yzJGWCjeLaVaPzsMgHkLfj_cQ,317 +blinker/__pycache__/__init__.cpython-310.pyc,, +blinker/__pycache__/_utilities.cpython-310.pyc,, +blinker/__pycache__/base.cpython-310.pyc,, +blinker/_utilities.py,sha256=0J7eeXXTUx0Ivf8asfpx0ycVkp0Eqfqnj117x2mYX9E,1675 +blinker/base.py,sha256=QpDuvXXcwJF49lUBcH5BiST46Rz9wSG7VW_p7N_027M,19132 +blinker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/WHEEL b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/WHEEL new file mode 100644 index 0000000..e3c6fee --- /dev/null +++ b/venv/lib/python3.10/site-packages/blinker-1.9.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.10.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.10/site-packages/blinker/__init__.py b/venv/lib/python3.10/site-packages/blinker/__init__.py new file mode 100644 index 0000000..1772fa4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/blinker/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from .base import ANY +from .base import default_namespace +from .base import NamedSignal +from .base import Namespace +from .base import Signal +from .base import signal + +__all__ = [ + "ANY", + "default_namespace", + "NamedSignal", + "Namespace", + "Signal", + "signal", +] diff --git a/venv/lib/python3.10/site-packages/blinker/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/blinker/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..3a4cc8b Binary files /dev/null and b/venv/lib/python3.10/site-packages/blinker/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/blinker/__pycache__/_utilities.cpython-310.pyc b/venv/lib/python3.10/site-packages/blinker/__pycache__/_utilities.cpython-310.pyc new file mode 100644 index 0000000..0fafaf2 Binary files /dev/null and b/venv/lib/python3.10/site-packages/blinker/__pycache__/_utilities.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/blinker/__pycache__/base.cpython-310.pyc b/venv/lib/python3.10/site-packages/blinker/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000..60ede30 Binary files /dev/null and b/venv/lib/python3.10/site-packages/blinker/__pycache__/base.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/blinker/_utilities.py b/venv/lib/python3.10/site-packages/blinker/_utilities.py new file mode 100644 index 0000000..000c902 --- /dev/null +++ b/venv/lib/python3.10/site-packages/blinker/_utilities.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import collections.abc as c +import inspect +import typing as t +from weakref import ref +from weakref import WeakMethod + +T = t.TypeVar("T") + + +class Symbol: + """A constant symbol, nicer than ``object()``. Repeated calls return the + same instance. + + >>> Symbol('foo') is Symbol('foo') + True + >>> Symbol('foo') + foo + """ + + symbols: t.ClassVar[dict[str, Symbol]] = {} + + def __new__(cls, name: str) -> Symbol: + if name in cls.symbols: + return cls.symbols[name] + + obj = super().__new__(cls) + cls.symbols[name] = obj + return obj + + def __init__(self, name: str) -> None: + self.name = name + + def __repr__(self) -> str: + return self.name + + def __getnewargs__(self) -> tuple[t.Any, ...]: + return (self.name,) + + +def make_id(obj: object) -> c.Hashable: + """Get a stable identifier for a receiver or sender, to be used as a dict + key or in a set. + """ + if inspect.ismethod(obj): + # The id of a bound method is not stable, but the id of the unbound + # function and instance are. + return id(obj.__func__), id(obj.__self__) + + if isinstance(obj, (str, int)): + # Instances with the same value always compare equal and have the same + # hash, even if the id may change. + return obj + + # Assume other types are not hashable but will always be the same instance. + return id(obj) + + +def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]: + if inspect.ismethod(obj): + return WeakMethod(obj, callback) # type: ignore[arg-type, return-value] + + return ref(obj, callback) diff --git a/venv/lib/python3.10/site-packages/blinker/base.py b/venv/lib/python3.10/site-packages/blinker/base.py new file mode 100644 index 0000000..d051b94 --- /dev/null +++ b/venv/lib/python3.10/site-packages/blinker/base.py @@ -0,0 +1,512 @@ +from __future__ import annotations + +import collections.abc as c +import sys +import typing as t +import weakref +from collections import defaultdict +from contextlib import contextmanager +from functools import cached_property +from inspect import iscoroutinefunction + +from ._utilities import make_id +from ._utilities import make_ref +from ._utilities import Symbol + +F = t.TypeVar("F", bound=c.Callable[..., t.Any]) + +ANY = Symbol("ANY") +"""Symbol for "any sender".""" + +ANY_ID = 0 + + +class Signal: + """A notification emitter. + + :param doc: The docstring for the signal. + """ + + ANY = ANY + """An alias for the :data:`~blinker.ANY` sender symbol.""" + + set_class: type[set[t.Any]] = set + """The set class to use for tracking connected receivers and senders. + Python's ``set`` is unordered. If receivers must be dispatched in the order + they were connected, an ordered set implementation can be used. + + .. versionadded:: 1.7 + """ + + @cached_property + def receiver_connected(self) -> Signal: + """Emitted at the end of each :meth:`connect` call. + + The signal sender is the signal instance, and the :meth:`connect` + arguments are passed through: ``receiver``, ``sender``, and ``weak``. + + .. versionadded:: 1.2 + """ + return Signal(doc="Emitted after a receiver connects.") + + @cached_property + def receiver_disconnected(self) -> Signal: + """Emitted at the end of each :meth:`disconnect` call. + + The sender is the signal instance, and the :meth:`disconnect` arguments + are passed through: ``receiver`` and ``sender``. + + This signal is emitted **only** when :meth:`disconnect` is called + explicitly. This signal cannot be emitted by an automatic disconnect + when a weakly referenced receiver or sender goes out of scope, as the + instance is no longer be available to be used as the sender for this + signal. + + An alternative approach is available by subscribing to + :attr:`receiver_connected` and setting up a custom weakref cleanup + callback on weak receivers and senders. + + .. versionadded:: 1.2 + """ + return Signal(doc="Emitted after a receiver disconnects.") + + def __init__(self, doc: str | None = None) -> None: + if doc: + self.__doc__ = doc + + self.receivers: dict[ + t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any] + ] = {} + """The map of connected receivers. Useful to quickly check if any + receivers are connected to the signal: ``if s.receivers:``. The + structure and data is not part of the public API, but checking its + boolean value is. + """ + + self.is_muted: bool = False + self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {} + + def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F: + """Connect ``receiver`` to be called when the signal is sent by + ``sender``. + + :param receiver: The callable to call when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument + along with any extra keyword arguments. + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. A receiver may be connected + to multiple senders by calling :meth:`connect` multiple times. + :param weak: Track the receiver with a :mod:`weakref`. The receiver will + be automatically disconnected when it is garbage collected. When + connecting a receiver defined within a function, set to ``False``, + otherwise it will be disconnected when the function scope ends. + """ + receiver_id = make_id(receiver) + sender_id = ANY_ID if sender is ANY else make_id(sender) + + if weak: + self.receivers[receiver_id] = make_ref( + receiver, self._make_cleanup_receiver(receiver_id) + ) + else: + self.receivers[receiver_id] = receiver + + self._by_sender[sender_id].add(receiver_id) + self._by_receiver[receiver_id].add(sender_id) + + if sender is not ANY and sender_id not in self._weak_senders: + # store a cleanup for weakref-able senders + try: + self._weak_senders[sender_id] = make_ref( + sender, self._make_cleanup_sender(sender_id) + ) + except TypeError: + pass + + if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers: + try: + self.receiver_connected.send( + self, receiver=receiver, sender=sender, weak=weak + ) + except TypeError: + # TODO no explanation or test for this + self.disconnect(receiver, sender) + raise + + return receiver + + def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]: + """Connect the decorated function to be called when the signal is sent + by ``sender``. + + The decorated function will be called when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument along + with any extra keyword arguments. + + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. A receiver may be connected + to multiple senders by calling :meth:`connect` multiple times. + :param weak: Track the receiver with a :mod:`weakref`. The receiver will + be automatically disconnected when it is garbage collected. When + connecting a receiver defined within a function, set to ``False``, + otherwise it will be disconnected when the function scope ends.= + + .. versionadded:: 1.1 + """ + + def decorator(fn: F) -> F: + self.connect(fn, sender, weak) + return fn + + return decorator + + @contextmanager + def connected_to( + self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY + ) -> c.Generator[None, None, None]: + """A context manager that temporarily connects ``receiver`` to the + signal while a ``with`` block executes. When the block exits, the + receiver is disconnected. Useful for tests. + + :param receiver: The callable to call when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument + along with any extra keyword arguments. + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. + + .. versionadded:: 1.1 + """ + self.connect(receiver, sender=sender, weak=False) + + try: + yield None + finally: + self.disconnect(receiver) + + @contextmanager + def muted(self) -> c.Generator[None, None, None]: + """A context manager that temporarily disables the signal. No receivers + will be called if the signal is sent, until the ``with`` block exits. + Useful for tests. + """ + self.is_muted = True + + try: + yield None + finally: + self.is_muted = False + + def send( + self, + sender: t.Any | None = None, + /, + *, + _async_wrapper: c.Callable[ + [c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any] + ] + | None = None, + **kwargs: t.Any, + ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: + """Call all receivers that are connected to the given ``sender`` + or :data:`ANY`. Each receiver is called with ``sender`` as a positional + argument along with any extra keyword arguments. Return a list of + ``(receiver, return value)`` tuples. + + The order receivers are called is undefined, but can be influenced by + setting :attr:`set_class`. + + If a receiver raises an exception, that exception will propagate up. + This makes debugging straightforward, with an assumption that correctly + implemented receivers will not raise. + + :param sender: Call receivers connected to this sender, in addition to + those connected to :data:`ANY`. + :param _async_wrapper: Will be called on any receivers that are async + coroutines to turn them into sync callables. For example, could run + the receiver with an event loop. + :param kwargs: Extra keyword arguments to pass to each receiver. + + .. versionchanged:: 1.7 + Added the ``_async_wrapper`` argument. + """ + if self.is_muted: + return [] + + results = [] + + for receiver in self.receivers_for(sender): + if iscoroutinefunction(receiver): + if _async_wrapper is None: + raise RuntimeError("Cannot send to a coroutine function.") + + result = _async_wrapper(receiver)(sender, **kwargs) + else: + result = receiver(sender, **kwargs) + + results.append((receiver, result)) + + return results + + async def send_async( + self, + sender: t.Any | None = None, + /, + *, + _sync_wrapper: c.Callable[ + [c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]] + ] + | None = None, + **kwargs: t.Any, + ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: + """Await all receivers that are connected to the given ``sender`` + or :data:`ANY`. Each receiver is called with ``sender`` as a positional + argument along with any extra keyword arguments. Return a list of + ``(receiver, return value)`` tuples. + + The order receivers are called is undefined, but can be influenced by + setting :attr:`set_class`. + + If a receiver raises an exception, that exception will propagate up. + This makes debugging straightforward, with an assumption that correctly + implemented receivers will not raise. + + :param sender: Call receivers connected to this sender, in addition to + those connected to :data:`ANY`. + :param _sync_wrapper: Will be called on any receivers that are sync + callables to turn them into async coroutines. For example, + could call the receiver in a thread. + :param kwargs: Extra keyword arguments to pass to each receiver. + + .. versionadded:: 1.7 + """ + if self.is_muted: + return [] + + results = [] + + for receiver in self.receivers_for(sender): + if not iscoroutinefunction(receiver): + if _sync_wrapper is None: + raise RuntimeError("Cannot send to a non-coroutine function.") + + result = await _sync_wrapper(receiver)(sender, **kwargs) + else: + result = await receiver(sender, **kwargs) + + results.append((receiver, result)) + + return results + + def has_receivers_for(self, sender: t.Any) -> bool: + """Check if there is at least one receiver that will be called with the + given ``sender``. A receiver connected to :data:`ANY` will always be + called, regardless of sender. Does not check if weakly referenced + receivers are still live. See :meth:`receivers_for` for a stronger + search. + + :param sender: Check for receivers connected to this sender, in addition + to those connected to :data:`ANY`. + """ + if not self.receivers: + return False + + if self._by_sender[ANY_ID]: + return True + + if sender is ANY: + return False + + return make_id(sender) in self._by_sender + + def receivers_for( + self, sender: t.Any + ) -> c.Generator[c.Callable[..., t.Any], None, None]: + """Yield each receiver to be called for ``sender``, in addition to those + to be called for :data:`ANY`. Weakly referenced receivers that are not + live will be disconnected and skipped. + + :param sender: Yield receivers connected to this sender, in addition + to those connected to :data:`ANY`. + """ + # TODO: test receivers_for(ANY) + if not self.receivers: + return + + sender_id = make_id(sender) + + if sender_id in self._by_sender: + ids = self._by_sender[ANY_ID] | self._by_sender[sender_id] + else: + ids = self._by_sender[ANY_ID].copy() + + for receiver_id in ids: + receiver = self.receivers.get(receiver_id) + + if receiver is None: + continue + + if isinstance(receiver, weakref.ref): + strong = receiver() + + if strong is None: + self._disconnect(receiver_id, ANY_ID) + continue + + yield strong + else: + yield receiver + + def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None: + """Disconnect ``receiver`` from being called when the signal is sent by + ``sender``. + + :param receiver: A connected receiver callable. + :param sender: Disconnect from only this sender. By default, disconnect + from all senders. + """ + sender_id: c.Hashable + + if sender is ANY: + sender_id = ANY_ID + else: + sender_id = make_id(sender) + + receiver_id = make_id(receiver) + self._disconnect(receiver_id, sender_id) + + if ( + "receiver_disconnected" in self.__dict__ + and self.receiver_disconnected.receivers + ): + self.receiver_disconnected.send(self, receiver=receiver, sender=sender) + + def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None: + if sender_id == ANY_ID: + if self._by_receiver.pop(receiver_id, None) is not None: + for bucket in self._by_sender.values(): + bucket.discard(receiver_id) + + self.receivers.pop(receiver_id, None) + else: + self._by_sender[sender_id].discard(receiver_id) + self._by_receiver[receiver_id].discard(sender_id) + + def _make_cleanup_receiver( + self, receiver_id: c.Hashable + ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]: + """Create a callback function to disconnect a weakly referenced + receiver when it is garbage collected. + """ + + def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None: + # If the interpreter is shutting down, disconnecting can result in a + # weird ignored exception. Don't call it in that case. + if not sys.is_finalizing(): + self._disconnect(receiver_id, ANY_ID) + + return cleanup + + def _make_cleanup_sender( + self, sender_id: c.Hashable + ) -> c.Callable[[weakref.ref[t.Any]], None]: + """Create a callback function to disconnect all receivers for a weakly + referenced sender when it is garbage collected. + """ + assert sender_id != ANY_ID + + def cleanup(ref: weakref.ref[t.Any]) -> None: + self._weak_senders.pop(sender_id, None) + + for receiver_id in self._by_sender.pop(sender_id, ()): + self._by_receiver[receiver_id].discard(sender_id) + + return cleanup + + def _cleanup_bookkeeping(self) -> None: + """Prune unused sender/receiver bookkeeping. Not threadsafe. + + Connecting & disconnecting leaves behind a small amount of bookkeeping + data. Typical workloads using Blinker, for example in most web apps, + Flask, CLI scripts, etc., are not adversely affected by this + bookkeeping. + + With a long-running process performing dynamic signal routing with high + volume, e.g. connecting to function closures, senders are all unique + object instances. Doing all of this over and over may cause memory usage + to grow due to extraneous bookkeeping. (An empty ``set`` for each stale + sender/receiver pair.) + + This method will prune that bookkeeping away, with the caveat that such + pruning is not threadsafe. The risk is that cleanup of a fully + disconnected receiver/sender pair occurs while another thread is + connecting that same pair. If you are in the highly dynamic, unique + receiver/sender situation that has lead you to this method, that failure + mode is perhaps not a big deal for you. + """ + for mapping in (self._by_sender, self._by_receiver): + for ident, bucket in list(mapping.items()): + if not bucket: + mapping.pop(ident, None) + + def _clear_state(self) -> None: + """Disconnect all receivers and senders. Useful for tests.""" + self._weak_senders.clear() + self.receivers.clear() + self._by_sender.clear() + self._by_receiver.clear() + + +class NamedSignal(Signal): + """A named generic notification emitter. The name is not used by the signal + itself, but matches the key in the :class:`Namespace` that it belongs to. + + :param name: The name of the signal within the namespace. + :param doc: The docstring for the signal. + """ + + def __init__(self, name: str, doc: str | None = None) -> None: + super().__init__(doc) + + #: The name of this signal. + self.name: str = name + + def __repr__(self) -> str: + base = super().__repr__() + return f"{base[:-1]}; {self.name!r}>" # noqa: E702 + + +class Namespace(dict[str, NamedSignal]): + """A dict mapping names to signals.""" + + def signal(self, name: str, doc: str | None = None) -> NamedSignal: + """Return the :class:`NamedSignal` for the given ``name``, creating it + if required. Repeated calls with the same name return the same signal. + + :param name: The name of the signal. + :param doc: The docstring of the signal. + """ + if name not in self: + self[name] = NamedSignal(name, doc) + + return self[name] + + +class _PNamespaceSignal(t.Protocol): + def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ... + + +default_namespace: Namespace = Namespace() +"""A default :class:`Namespace` for creating named signals. :func:`signal` +creates a :class:`NamedSignal` in this namespace. +""" + +signal: _PNamespaceSignal = default_namespace.signal +"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given +``name``, creating it if required. Repeated calls with the same name return the +same signal. +""" diff --git a/venv/lib/python3.10/site-packages/blinker/py.typed b/venv/lib/python3.10/site-packages/blinker/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/METADATA b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/METADATA new file mode 100644 index 0000000..d1bc526 --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/METADATA @@ -0,0 +1,78 @@ +Metadata-Version: 2.4 +Name: certifi +Version: 2026.1.4 +Summary: Python package for providing Mozilla's CA Bundle. +Home-page: https://github.com/certifi/python-certifi +Author: Kenneth Reitz +Author-email: me@kennethreitz.com +License: MPL-2.0 +Project-URL: Source, https://github.com/certifi/python-certifi +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Natural Language :: English +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Requires-Python: >=3.7 +License-File: LICENSE +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: project-url +Dynamic: requires-python +Dynamic: summary + +Certifi: Python SSL Certificates +================================ + +Certifi provides Mozilla's carefully curated collection of Root Certificates for +validating the trustworthiness of SSL certificates while verifying the identity +of TLS hosts. It has been extracted from the `Requests`_ project. + +Installation +------------ + +``certifi`` is available on PyPI. Simply install it with ``pip``:: + + $ pip install certifi + +Usage +----- + +To reference the installed certificate authority (CA) bundle, you can use the +built-in function:: + + >>> import certifi + + >>> certifi.where() + '/usr/local/lib/python3.7/site-packages/certifi/cacert.pem' + +Or from the command line:: + + $ python -m certifi + /usr/local/lib/python3.7/site-packages/certifi/cacert.pem + +Enjoy! + +.. _`Requests`: https://requests.readthedocs.io/en/master/ + +Addition/Removal of Certificates +-------------------------------- + +Certifi does not support any addition/removal or other modification of the +CA trust store content. This project is intended to provide a reliable and +highly portable root of trust to python deployments. Look to upstream projects +for methods to use alternate trust. diff --git a/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/RECORD b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/RECORD new file mode 100644 index 0000000..5be66f1 --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/RECORD @@ -0,0 +1,14 @@ +certifi-2026.1.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +certifi-2026.1.4.dist-info/METADATA,sha256=FSfJEfKuMo6bJlofUrtRpn4PFTYtbYyXpHN_A3ZFpIY,2473 +certifi-2026.1.4.dist-info/RECORD,, +certifi-2026.1.4.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91 +certifi-2026.1.4.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 +certifi-2026.1.4.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 +certifi/__init__.py,sha256=969deMMS7Uchipr0oO4dbRBUvRi0uNYCn07VmG1aTrg,94 +certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 +certifi/__pycache__/__init__.cpython-310.pyc,, +certifi/__pycache__/__main__.cpython-310.pyc,, +certifi/__pycache__/core.cpython-310.pyc,, +certifi/cacert.pem,sha256=Tzl1_zCrvzVEO0hgZK6Ly0Hf9wf_31dsdtKS-0WKoKk,270954 +certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 +certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/WHEEL b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/WHEEL new file mode 100644 index 0000000..e7fa31b --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/licenses/LICENSE b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/licenses/LICENSE new file mode 100644 index 0000000..62b076c --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +This package contains a modified version of ca-bundle.crt: + +ca-bundle.crt -- Bundle of CA Root Certificates + +This is a bundle of X.509 certificates of public Certificate Authorities +(CA). These were automatically extracted from Mozilla's root certificates +file (certdata.txt). This file can be found in the mozilla source tree: +https://hg.mozilla.org/mozilla-central/file/tip/security/nss/lib/ckfw/builtins/certdata.txt +It contains the certificates in PEM format and therefore +can be directly used with curl / libcurl / php_curl, or with +an Apache+mod_ssl webserver for SSL client authentication. +Just configure this file as the SSLCACertificateFile.# + +***** BEGIN LICENSE BLOCK ***** +This Source Code Form is subject to the terms of the Mozilla Public License, +v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain +one at http://mozilla.org/MPL/2.0/. + +***** END LICENSE BLOCK ***** +@(#) $RCSfile: certdata.txt,v $ $Revision: 1.80 $ $Date: 2011/11/03 15:11:58 $ diff --git a/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/top_level.txt b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/top_level.txt new file mode 100644 index 0000000..963eac5 --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi-2026.1.4.dist-info/top_level.txt @@ -0,0 +1 @@ +certifi diff --git a/venv/lib/python3.10/site-packages/certifi/__init__.py b/venv/lib/python3.10/site-packages/certifi/__init__.py new file mode 100644 index 0000000..090fd58 --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi/__init__.py @@ -0,0 +1,4 @@ +from .core import contents, where + +__all__ = ["contents", "where"] +__version__ = "2026.01.04" diff --git a/venv/lib/python3.10/site-packages/certifi/__main__.py b/venv/lib/python3.10/site-packages/certifi/__main__.py new file mode 100644 index 0000000..8945b5d --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi/__main__.py @@ -0,0 +1,12 @@ +import argparse + +from certifi import contents, where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true") +args = parser.parse_args() + +if args.contents: + print(contents()) +else: + print(where()) diff --git a/venv/lib/python3.10/site-packages/certifi/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/certifi/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..ebc90ca Binary files /dev/null and b/venv/lib/python3.10/site-packages/certifi/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/certifi/__pycache__/__main__.cpython-310.pyc b/venv/lib/python3.10/site-packages/certifi/__pycache__/__main__.cpython-310.pyc new file mode 100644 index 0000000..ae816ca Binary files /dev/null and b/venv/lib/python3.10/site-packages/certifi/__pycache__/__main__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/certifi/__pycache__/core.cpython-310.pyc b/venv/lib/python3.10/site-packages/certifi/__pycache__/core.cpython-310.pyc new file mode 100644 index 0000000..cb9bf43 Binary files /dev/null and b/venv/lib/python3.10/site-packages/certifi/__pycache__/core.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/certifi/cacert.pem b/venv/lib/python3.10/site-packages/certifi/cacert.pem new file mode 100644 index 0000000..132db0d --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi/cacert.pem @@ -0,0 +1,4468 @@ + +# Issuer: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2" +# Serial: 1289 +# MD5 Fingerprint: 5e:39:7b:dd:f8:ba:ec:82:e9:ac:62:ba:0c:54:00:2b +# SHA1 Fingerprint: ca:3a:fb:cf:12:40:36:4b:44:b2:16:20:88:80:48:39:19:93:7c:f7 +# SHA256 Fingerprint: 85:a0:dd:7d:d7:20:ad:b7:ff:05:f8:3d:54:2b:20:9d:c7:ff:45:28:f7:d6:77:b1:83:89:fe:a5:e5:c4:9e:86 +-----BEGIN CERTIFICATE----- +MIIFtzCCA5+gAwIBAgICBQkwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMjAeFw0wNjExMjQxODI3MDBaFw0zMTExMjQxODIzMzNaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCa +GMpLlA0ALa8DKYrwD4HIrkwZhR0In6spRIXzL4GtMh6QRr+jhiYaHv5+HBg6XJxg +Fyo6dIMzMH1hVBHL7avg5tKifvVrbxi3Cgst/ek+7wrGsxDp3MJGF/hd/aTa/55J +WpzmM+Yklvc/ulsrHHo1wtZn/qtmUIttKGAr79dgw8eTvI02kfN/+NsRE8Scd3bB +rrcCaoF6qUWD4gXmuVbBlDePSHFjIuwXZQeVikvfj8ZaCuWw419eaxGrDPmF60Tp ++ARz8un+XJiM9XOva7R+zdRcAitMOeGylZUtQofX1bOQQ7dsE/He3fbE+Ik/0XX1 +ksOR1YqI0JDs3G3eicJlcZaLDQP9nL9bFqyS2+r+eXyt66/3FsvbzSUr5R/7mp/i +Ucw6UwxI5g69ybR2BlLmEROFcmMDBOAENisgGQLodKcftslWZvB1JdxnwQ5hYIiz +PtGo/KPaHbDRsSNU30R2be1B2MGyIrZTHN81Hdyhdyox5C315eXbyOD/5YDXC2Og +/zOhD7osFRXql7PSorW+8oyWHhqPHWykYTe5hnMz15eWniN9gqRMgeKh0bpnX5UH +oycR7hYQe7xFSkyyBNKr79X9DFHOUGoIMfmR2gyPZFwDwzqLID9ujWc9Otb+fVuI +yV77zGHcizN300QyNQliBJIWENieJ0f7OyHj+OsdWwIDAQABo4GwMIGtMA8GA1Ud +EwEB/wQFMAMBAf8wCwYDVR0PBAQDAgEGMB0GA1UdDgQWBBQahGK8SEwzJQTU7tD2 +A8QZRtGUazBuBgNVHSMEZzBlgBQahGK8SEwzJQTU7tD2A8QZRtGUa6FJpEcwRTEL +MAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMT +ElF1b1ZhZGlzIFJvb3QgQ0EgMoICBQkwDQYJKoZIhvcNAQEFBQADggIBAD4KFk2f +BluornFdLwUvZ+YTRYPENvbzwCYMDbVHZF34tHLJRqUDGCdViXh9duqWNIAXINzn +g/iN/Ae42l9NLmeyhP3ZRPx3UIHmfLTJDQtyU/h2BwdBR5YM++CCJpNVjP4iH2Bl +fF/nJrP3MpCYUNQ3cVX2kiF495V5+vgtJodmVjB3pjd4M1IQWK4/YY7yarHvGH5K +WWPKjaJW1acvvFYfzznB4vsKqBUsfU16Y8Zsl0Q80m/DShcK+JDSV6IZUaUtl0Ha +B0+pUNqQjZRG4T7wlP0QADj1O+hA4bRuVhogzG9Yje0uRY/W6ZM/57Es3zrWIozc +hLsib9D45MY56QSIPMO661V6bYCZJPVsAfv4l7CUW+v90m/xd2gNNWQjrLhVoQPR +TUIZ3Ph1WVaj+ahJefivDrkRoHy3au000LYmYjgahwz46P0u05B/B5EqHdZ+XIWD +mbA4CD/pXvk1B+TJYm5Xf6dQlfe6yJvmjqIBxdZmv3lh8zwc4bmCXF2gw+nYSL0Z +ohEUGW6yhhtoPkg3Goi3XZZenMfvJ2II4pEZXNLxId26F0KCl3GBUzGpn/Z9Yr9y +4aOTHcyKJloJONDO1w2AFrR4pTqHTI2KpdVGl/IsELm8VCLAAVBpQ570su9t+Oza +8eOx79+Rj1QqCyXBJhnEUhAFZdWCEOrCMc0u +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3" +# Serial: 1478 +# MD5 Fingerprint: 31:85:3c:62:94:97:63:b9:aa:fd:89:4e:af:6f:e0:cf +# SHA1 Fingerprint: 1f:49:14:f7:d8:74:95:1d:dd:ae:02:c0:be:fd:3a:2d:82:75:51:85 +# SHA256 Fingerprint: 18:f1:fc:7f:20:5d:f8:ad:dd:eb:7f:e0:07:dd:57:e3:af:37:5a:9c:4d:8d:73:54:6b:f4:f1:fe:d1:e1:8d:35 +-----BEGIN CERTIFICATE----- +MIIGnTCCBIWgAwIBAgICBcYwDQYJKoZIhvcNAQEFBQAwRTELMAkGA1UEBhMCQk0x +GTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxGzAZBgNVBAMTElF1b1ZhZGlzIFJv +b3QgQ0EgMzAeFw0wNjExMjQxOTExMjNaFw0zMTExMjQxOTA2NDRaMEUxCzAJBgNV +BAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBMaW1pdGVkMRswGQYDVQQDExJRdW9W +YWRpcyBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDM +V0IWVJzmmNPTTe7+7cefQzlKZbPoFog02w1ZkXTPkrgEQK0CSzGrvI2RaNggDhoB +4hp7Thdd4oq3P5kazethq8Jlph+3t723j/z9cI8LoGe+AaJZz3HmDyl2/7FWeUUr +H556VOijKTVopAFPD6QuN+8bv+OPEKhyq1hX51SGyMnzW9os2l2ObjyjPtr7guXd +8lyyBTNvijbO0BNO/79KDDRMpsMhvVAEVeuxu537RR5kFd5VAYwCdrXLoT9Cabwv +vWhDFlaJKjdhkf2mrk7AyxRllDdLkgbvBNDInIjbC3uBr7E9KsRlOni27tyAsdLT +mZw67mtaa7ONt9XOnMK+pUsvFrGeaDsGb659n/je7Mwpp5ijJUMv7/FfJuGITfhe +btfZFG4ZM2mnO4SJk8RTVROhUXhA+LjJou57ulJCg54U7QVSWllWp5f8nT8KKdjc +T5EOE7zelaTfi5m+rJsziO+1ga8bxiJTyPbH7pcUsMV8eFLI8M5ud2CEpukqdiDt +WAEXMJPpGovgc2PZapKUSU60rUqFxKMiMPwJ7Wgic6aIDFUhWMXhOp8q3crhkODZ +c6tsgLjoC2SToJyMGf+z0gzskSaHirOi4XCPLArlzW1oUevaPwV/izLmE1xr/l9A +4iLItLRkT9a6fUg+qGkM17uGcclzuD87nSVL2v9A6wIDAQABo4IBlTCCAZEwDwYD +VR0TAQH/BAUwAwEB/zCB4QYDVR0gBIHZMIHWMIHTBgkrBgEEAb5YAAMwgcUwgZMG +CCsGAQUFBwICMIGGGoGDQW55IHVzZSBvZiB0aGlzIENlcnRpZmljYXRlIGNvbnN0 +aXR1dGVzIGFjY2VwdGFuY2Ugb2YgdGhlIFF1b1ZhZGlzIFJvb3QgQ0EgMyBDZXJ0 +aWZpY2F0ZSBQb2xpY3kgLyBDZXJ0aWZpY2F0aW9uIFByYWN0aWNlIFN0YXRlbWVu +dC4wLQYIKwYBBQUHAgEWIWh0dHA6Ly93d3cucXVvdmFkaXNnbG9iYWwuY29tL2Nw +czALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFPLAE+CCQz777i9nMpY1XNu4ywLQMG4G +A1UdIwRnMGWAFPLAE+CCQz777i9nMpY1XNu4ywLQoUmkRzBFMQswCQYDVQQGEwJC +TTEZMBcGA1UEChMQUXVvVmFkaXMgTGltaXRlZDEbMBkGA1UEAxMSUXVvVmFkaXMg +Um9vdCBDQSAzggIFxjANBgkqhkiG9w0BAQUFAAOCAgEAT62gLEz6wPJv92ZVqyM0 +7ucp2sNbtrCD2dDQ4iH782CnO11gUyeim/YIIirnv6By5ZwkajGxkHon24QRiSem +d1o417+shvzuXYO8BsbRd2sPbSQvS3pspweWyuOEn62Iix2rFo1bZhfZFvSLgNLd ++LJ2w/w4E6oM3kJpK27zPOuAJ9v1pkQNn1pVWQvVDVJIxa6f8i+AxeoyUDUSly7B +4f/xI4hROJ/yZlZ25w9Rl6VSDE1JUZU2Pb+iSwwQHYaZTKrzchGT5Or2m9qoXadN +t54CrnMAyNojA+j56hl0YgCUyyIgvpSnWbWCar6ZeXqp8kokUvd0/bpO5qgdAm6x +DYBEwa7TIzdfu4V8K5Iu6H6li92Z4b8nby1dqnuH/grdS/yO9SbkbnBCbjPsMZ57 +k8HkyWkaPcBrTiJt7qtYTcbQQcEr6k8Sh17rRdhs9ZgC06DYVYoGmRmioHfRMJ6s +zHXug/WwYjnPbFfiTNKRCw51KBuav/0aQ/HKd/s7j2G4aSgWQgRecCocIdiP4b0j +Wy10QJLZYxkNc91pvGJHvOB0K7Lrfb5BG7XARsWhIstfTsEokt4YutUqKLsRixeT +mJlglFwjz1onl14LBQaTNx47aTbrqZ5hHY8y2o4M1nQ+ewkk2gF3R8Q7zTSMmfXK +4SVhM7JZG+Ju1zdXtg2pEto= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root CA" +# Serial: 17154717934120587862167794914071425081 +# MD5 Fingerprint: 87:ce:0b:7b:2a:0e:49:00:e1:58:71:9b:37:a8:93:72 +# SHA1 Fingerprint: 05:63:b8:63:0d:62:d7:5a:bb:c8:ab:1e:4b:df:b5:a8:99:b2:4d:43 +# SHA256 Fingerprint: 3e:90:99:b5:01:5e:8f:48:6c:00:bc:ea:9d:11:1e:e7:21:fa:ba:35:5a:89:bc:f1:df:69:56:1e:3d:c6:32:5c +-----BEGIN CERTIFICATE----- +MIIDtzCCAp+gAwIBAgIQDOfg5RfYRv6P5WD8G/AwOTANBgkqhkiG9w0BAQUFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgQ0EwHhcNMDYxMTEwMDAwMDAwWhcNMzExMTEwMDAwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCtDhXO5EOAXLGH87dg+XESpa7c +JpSIqvTO9SA5KFhgDPiA2qkVlTJhPLWxKISKityfCgyDF3qPkKyK53lTXDGEKvYP +mDI2dsze3Tyoou9q+yHyUmHfnyDXH+Kx2f4YZNISW1/5WBg1vEfNoTb5a3/UsDg+ +wRvDjDPZ2C8Y/igPs6eD1sNuRMBhNZYW/lmci3Zt1/GiSw0r/wty2p5g0I6QNcZ4 +VYcgoc/lbQrISXwxmDNsIumH0DJaoroTghHtORedmTpyoeb6pNnVFzF1roV9Iq4/ +AUaG9ih5yLHa5FcXxH4cDrC0kqZWs72yl+2qp/C3xag/lRbQ/6GW6whfGHdPAgMB +AAGjYzBhMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBRF66Kv9JLLgjEtUYunpyGd823IDzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYun +pyGd823IDzANBgkqhkiG9w0BAQUFAAOCAQEAog683+Lt8ONyc3pklL/3cmbYMuRC +dWKuh+vy1dneVrOfzM4UKLkNl2BcEkxY5NM9g0lFWJc1aRqoR+pWxnmrEthngYTf +fwk8lOa4JiwgvT2zKIn3X/8i4peEH+ll74fg38FnSbNd67IJKusm7Xi+fT8r87cm +NW1fiQG2SVufAQWbqz0lwcy2f8Lxb4bG+mRo64EtlOtCt/qMHt1i8b5QZ7dsvfPx +H2sMNgcWfzd8qVttevESRmCD1ycEvkvOl77DZypoEd+A5wwzZr8TDRRu838fYxAe ++o0bJW1sj6W3YQGx0qMmoRBxna3iw/nDmVG3KwcIzi7mULKn+gpFL6Lw8g== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root CA" +# Serial: 10944719598952040374951832963794454346 +# MD5 Fingerprint: 79:e4:a9:84:0d:7d:3a:96:d7:c0:4f:e2:43:4c:89:2e +# SHA1 Fingerprint: a8:98:5d:3a:65:e5:e5:c4:b2:d7:d6:6d:40:c6:dd:2f:b1:9c:54:36 +# SHA256 Fingerprint: 43:48:a0:e9:44:4c:78:cb:26:5e:05:8d:5e:89:44:b4:d8:4f:96:62:bd:26:db:25:7f:89:34:a4:43:c7:01:61 +-----BEGIN CERTIFICATE----- +MIIDrzCCApegAwIBAgIQCDvgVpBCRrGhdWrJWZHHSjANBgkqhkiG9w0BAQUFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBD +QTAeFw0wNjExMTAwMDAwMDBaFw0zMTExMTAwMDAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IENBMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4jvhEXLeqKTTo1eqUKKPC3eQyaKl7hLOllsB +CSDMAZOnTjC3U/dDxGkAV53ijSLdhwZAAIEJzs4bg7/fzTtxRuLWZscFs3YnFo97 +nh6Vfe63SKMI2tavegw5BmV/Sl0fvBf4q77uKNd0f3p4mVmFaG5cIzJLv07A6Fpt +43C/dxC//AH2hdmoRBBYMql1GNXRor5H4idq9Joz+EkIYIvUX7Q6hL+hqkpMfT7P +T19sdl6gSzeRntwi5m3OFBqOasv+zbMUZBfHWymeMr/y7vrTC0LUq7dBMtoM1O/4 +gdW7jVg/tRvoSSiicNoxBN33shbyTApOB6jtSj1etX+jkMOvJwIDAQABo2MwYTAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUA95QNVbR +TLtm8KPiGxvDl7I90VUwHwYDVR0jBBgwFoAUA95QNVbRTLtm8KPiGxvDl7I90VUw +DQYJKoZIhvcNAQEFBQADggEBAMucN6pIExIK+t1EnE9SsPTfrgT1eXkIoyQY/Esr +hMAtudXH/vTBH1jLuG2cenTnmCmrEbXjcKChzUyImZOMkXDiqw8cvpOp/2PV5Adg +06O/nVsJ8dWO41P0jmP6P6fbtGbfYmbW0W5BjfIttep3Sp+dWOIrWcBAI+0tKIJF +PnlUkiaY4IBIqDfv8NZ5YBberOgOzW6sRBc4L0na4UU+Krk2U886UAb3LujEV0ls +YSEY1QSteDwsOoBrp+uvFRTp2InBuThs4pFsiv9kuXclVzDAGySj4dzp30d8tbQk +CAUw7C29C79Fv1C5qfPrmAESrciIxpg0X40KPMbp1ZWVbd4= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert High Assurance EV Root CA O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert High Assurance EV Root CA" +# Serial: 3553400076410547919724730734378100087 +# MD5 Fingerprint: d4:74:de:57:5c:39:b2:d3:9c:85:83:c5:c0:65:49:8a +# SHA1 Fingerprint: 5f:b7:ee:06:33:e2:59:db:ad:0c:4c:9a:e6:d3:8f:1a:61:c7:dc:25 +# SHA256 Fingerprint: 74:31:e5:f4:c3:c1:ce:46:90:77:4f:0b:61:e0:54:40:88:3b:a9:a0:1e:d0:0b:a6:ab:d7:80:6e:d3:b1:18:cf +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIQAqxcJmoLQJuPC3nyrkYldzANBgkqhkiG9w0BAQUFADBs +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSswKQYDVQQDEyJEaWdpQ2VydCBIaWdoIEFzc3VyYW5j +ZSBFViBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTMxMTExMDAwMDAwMFowbDEL +MAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3 +LmRpZ2ljZXJ0LmNvbTErMCkGA1UEAxMiRGlnaUNlcnQgSGlnaCBBc3N1cmFuY2Ug +RVYgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMbM5XPm ++9S75S0tMqbf5YE/yc0lSbZxKsPVlDRnogocsF9ppkCxxLeyj9CYpKlBWTrT3JTW +PNt0OKRKzE0lgvdKpVMSOO7zSW1xkX5jtqumX8OkhPhPYlG++MXs2ziS4wblCJEM +xChBVfvLWokVfnHoNb9Ncgk9vjo4UFt3MRuNs8ckRZqnrG0AFFoEt7oT61EKmEFB +Ik5lYYeBQVCmeVyJ3hlKV9Uu5l0cUyx+mM0aBhakaHPQNAQTXKFx01p8VdteZOE3 +hzBWBOURtCmAEvF5OYiiAhF8J2a3iLd48soKqDirCmTCv2ZdlYTBoSUeh10aUAsg +EsxBu24LUTi4S8sCAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFLE+w2kD+L9HAdSYJhoIAu9jZCvDMB8GA1UdIwQYMBaA +FLE+w2kD+L9HAdSYJhoIAu9jZCvDMA0GCSqGSIb3DQEBBQUAA4IBAQAcGgaX3Nec +nzyIZgYIVyHbIUf4KmeqvxgydkAQV8GK83rZEWWONfqe/EW1ntlMMUu4kehDLI6z +eM7b41N5cdblIZQB2lWHmiRk9opmzN6cN82oNLFpmyPInngiK3BD41VHMWEZ71jF +hS9OMPagMRYjyOfiZRYzy78aG6A9+MpeizGLYAiJLQwGXFK3xPkKmNEVX58Svnw2 +Yzi9RKR/5CYrCsSXaQ3pjOLAEFe4yHYSkVXySGnYvCoCWw9E1CAx2/S6cCZdkGCe +vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep ++OkuE6N36B9K +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Subject: CN=SwissSign Gold CA - G2 O=SwissSign AG +# Label: "SwissSign Gold CA - G2" +# Serial: 13492815561806991280 +# MD5 Fingerprint: 24:77:d9:a8:91:d1:3b:fa:88:2d:c2:ff:f8:cd:33:93 +# SHA1 Fingerprint: d8:c5:38:8a:b7:30:1b:1b:6e:d4:7a:e6:45:25:3a:6f:9f:1a:27:61 +# SHA256 Fingerprint: 62:dd:0b:e9:b9:f5:0a:16:3e:a0:f8:e7:5c:05:3b:1e:ca:57:ea:55:c8:68:8f:64:7c:68:81:f2:c8:35:7b:95 +-----BEGIN CERTIFICATE----- +MIIFujCCA6KgAwIBAgIJALtAHEP1Xk+wMA0GCSqGSIb3DQEBBQUAMEUxCzAJBgNV +BAYTAkNIMRUwEwYDVQQKEwxTd2lzc1NpZ24gQUcxHzAdBgNVBAMTFlN3aXNzU2ln +biBHb2xkIENBIC0gRzIwHhcNMDYxMDI1MDgzMDM1WhcNMzYxMDI1MDgzMDM1WjBF +MQswCQYDVQQGEwJDSDEVMBMGA1UEChMMU3dpc3NTaWduIEFHMR8wHQYDVQQDExZT +d2lzc1NpZ24gR29sZCBDQSAtIEcyMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAr+TufoskDhJuqVAtFkQ7kpJcyrhdhJJCEyq8ZVeCQD5XJM1QiyUqt2/8 +76LQwB8CJEoTlo8jE+YoWACjR8cGp4QjK7u9lit/VcyLwVcfDmJlD909Vopz2q5+ +bbqBHH5CjCA12UNNhPqE21Is8w4ndwtrvxEvcnifLtg+5hg3Wipy+dpikJKVyh+c +6bM8K8vzARO/Ws/BtQpgvd21mWRTuKCWs2/iJneRjOBiEAKfNA+k1ZIzUd6+jbqE +emA8atufK+ze3gE/bk3lUIbLtK/tREDFylqM2tIrfKjuvqblCqoOpd8FUrdVxyJd +MmqXl2MT28nbeTZ7hTpKxVKJ+STnnXepgv9VHKVxaSvRAiTysybUa9oEVeXBCsdt +MDeQKuSeFDNeFhdVxVu1yzSJkvGdJo+hB9TGsnhQ2wwMC3wLjEHXuendjIj3o02y +MszYF9rNt85mndT9Xv+9lz4pded+p2JYryU0pUHHPbwNUMoDAw8IWh+Vc3hiv69y +FGkOpeUDDniOJihC8AcLYiAQZzlG+qkDzAQ4embvIIO1jEpWjpEA/I5cgt6IoMPi +aG59je883WX0XaxR7ySArqpWl2/5rX3aYT+YdzylkbYcjCbaZaIJbcHiVOO5ykxM +gI93e2CaHt+28kgeDrpOVG2Y4OGiGqJ3UM/EY5LsRxmd6+ZrzsECAwEAAaOBrDCB +qTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUWyV7 +lqRlUX64OfPAeGZe6Drn8O4wHwYDVR0jBBgwFoAUWyV7lqRlUX64OfPAeGZe6Drn +8O4wRgYDVR0gBD8wPTA7BglghXQBWQECAQEwLjAsBggrBgEFBQcCARYgaHR0cDov +L3JlcG9zaXRvcnkuc3dpc3NzaWduLmNvbS8wDQYJKoZIhvcNAQEFBQADggIBACe6 +45R88a7A3hfm5djV9VSwg/S7zV4Fe0+fdWavPOhWfvxyeDgD2StiGwC5+OlgzczO +UYrHUDFu4Up+GC9pWbY9ZIEr44OE5iKHjn3g7gKZYbge9LgriBIWhMIxkziWMaa5 +O1M/wySTVltpkuzFwbs4AOPsF6m43Md8AYOfMke6UiI0HTJ6CVanfCU2qT1L2sCC +bwq7EsiHSycR+R4tx5M/nttfJmtS2S6K8RTGRI0Vqbe/vd6mGu6uLftIdxf+u+yv +GPUqUfA5hJeVbG4bwyvEdGB5JbAKJ9/fXtI5z0V9QkvfsywexcZdylU6oJxpmo/a +77KwPJ+HbBIrZXAVUjEaJM9vMSNQH4xPjyPDdEFjHFWoFN0+4FFQz/EbMFYOkrCC +hdiDyyJkvC24JdVUorgG6q2SpCSgwYa1ShNqR88uC1aVVMvOmttqtKay20EIhid3 +92qgQmwLOM7XdVAyksLfKzAiSNDVQTglXaTpXZ/GlHXQRf0wl0OPkKsKx4ZzYEpp +Ld6leNcG2mqeSz53OiATIgHQv2ieY2BrNU0LbbqhPcCT4H8js1WtciVORvnSFu+w +ZMEBnunKoGqYDs/YYPIvSbjkQuE4NRb0yG5P94FW6LqjviOvrv1vA+ACOzB2+htt +Qc8Bsem4yWb02ybzOqR08kkkW8mw0FfB+j564ZfJ +-----END CERTIFICATE----- + +# Issuer: CN=SecureTrust CA O=SecureTrust Corporation +# Subject: CN=SecureTrust CA O=SecureTrust Corporation +# Label: "SecureTrust CA" +# Serial: 17199774589125277788362757014266862032 +# MD5 Fingerprint: dc:32:c3:a7:6d:25:57:c7:68:09:9d:ea:2d:a9:a2:d1 +# SHA1 Fingerprint: 87:82:c6:c3:04:35:3b:cf:d2:96:92:d2:59:3e:7d:44:d9:34:ff:11 +# SHA256 Fingerprint: f1:c1:b5:0a:e5:a2:0d:d8:03:0e:c9:f6:bc:24:82:3d:d3:67:b5:25:57:59:b4:e7:1b:61:fc:e9:f7:37:5d:73 +-----BEGIN CERTIFICATE----- +MIIDuDCCAqCgAwIBAgIQDPCOXAgWpa1Cf/DrJxhZ0DANBgkqhkiG9w0BAQUFADBI +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +FzAVBgNVBAMTDlNlY3VyZVRydXN0IENBMB4XDTA2MTEwNzE5MzExOFoXDTI5MTIz +MTE5NDA1NVowSDELMAkGA1UEBhMCVVMxIDAeBgNVBAoTF1NlY3VyZVRydXN0IENv +cnBvcmF0aW9uMRcwFQYDVQQDEw5TZWN1cmVUcnVzdCBDQTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAKukgeWVzfX2FI7CT8rU4niVWJxB4Q2ZQCQXOZEz +Zum+4YOvYlyJ0fwkW2Gz4BERQRwdbvC4u/jep4G6pkjGnx29vo6pQT64lO0pGtSO +0gMdA+9tDWccV9cGrcrI9f4Or2YlSASWC12juhbDCE/RRvgUXPLIXgGZbf2IzIao +wW8xQmxSPmjL8xk037uHGFaAJsTQ3MBv396gwpEWoGQRS0S8Hvbn+mPeZqx2pHGj +7DaUaHp3pLHnDi+BeuK1cobvomuL8A/b01k/unK8RCSc43Oz969XL0Imnal0ugBS +8kvNU3xHCzaFDmapCJcWNFfBZveA4+1wVMeT4C4oFVmHursCAwEAAaOBnTCBmjAT +BgkrBgEEAYI3FAIEBh4EAEMAQTALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQUQjK2FvoE/f5dS3rD/fdMQB1aQ68wNAYDVR0fBC0wKzApoCeg +JYYjaHR0cDovL2NybC5zZWN1cmV0cnVzdC5jb20vU1RDQS5jcmwwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBADDtT0rhWDpSclu1pqNlGKa7UTt3 +6Z3q059c4EVlew3KW+JwULKUBRSuSceNQQcSc5R+DCMh/bwQf2AQWnL1mA6s7Ll/ +3XpvXdMc9P+IBWlCqQVxyLesJugutIxq/3HcuLHfmbx8IVQr5Fiiu1cprp6poxkm +D5kuCLDv/WnPmRoJjeOnnyvJNjR7JLN4TJUXpAYmHrZkUjZfYGfZnMUFdAvnZyPS +CPyI6a6Lf+Ew9Dd+/cYy2i2eRDAwbO4H3tI0/NL/QPZL9GZGBlSm8jIKYyYwa5vR +3ItHuuG51WLQoqD0ZwV4KWMabwTW+MZMo5qxN7SN5ShLHZ4swrhovO0C7jE= +-----END CERTIFICATE----- + +# Issuer: CN=Secure Global CA O=SecureTrust Corporation +# Subject: CN=Secure Global CA O=SecureTrust Corporation +# Label: "Secure Global CA" +# Serial: 9751836167731051554232119481456978597 +# MD5 Fingerprint: cf:f4:27:0d:d4:ed:dc:65:16:49:6d:3d:da:bf:6e:de +# SHA1 Fingerprint: 3a:44:73:5a:e5:81:90:1f:24:86:61:46:1e:3b:9c:c4:5f:f5:3a:1b +# SHA256 Fingerprint: 42:00:f5:04:3a:c8:59:0e:bb:52:7d:20:9e:d1:50:30:29:fb:cb:d4:1c:a1:b5:06:ec:27:f1:5a:de:7d:ac:69 +-----BEGIN CERTIFICATE----- +MIIDvDCCAqSgAwIBAgIQB1YipOjUiolN9BPI8PjqpTANBgkqhkiG9w0BAQUFADBK +MQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3QgQ29ycG9yYXRpb24x +GTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwHhcNMDYxMTA3MTk0MjI4WhcNMjkx +MjMxMTk1MjA2WjBKMQswCQYDVQQGEwJVUzEgMB4GA1UEChMXU2VjdXJlVHJ1c3Qg +Q29ycG9yYXRpb24xGTAXBgNVBAMTEFNlY3VyZSBHbG9iYWwgQ0EwggEiMA0GCSqG +SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvNS7YrGxVaQZx5RNoJLNP2MwhR/jxYDiJ +iQPpvepeRlMJ3Fz1Wuj3RSoC6zFh1ykzTM7HfAo3fg+6MpjhHZevj8fcyTiW89sa +/FHtaMbQbqR8JNGuQsiWUGMu4P51/pinX0kuleM5M2SOHqRfkNJnPLLZ/kG5VacJ +jnIFHovdRIWCQtBJwB1g8NEXLJXr9qXBkqPFwqcIYA1gBBCWeZ4WNOaptvolRTnI +HmX5k/Wq8VLcmZg9pYYaDDUz+kulBAYVHDGA76oYa8J719rO+TMg1fW9ajMtgQT7 +sFzUnKPiXB3jqUJ1XnvUd+85VLrJChgbEplJL4hL/VBi0XPnj3pDAgMBAAGjgZ0w +gZowEwYJKwYBBAGCNxQCBAYeBABDAEEwCwYDVR0PBAQDAgGGMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFK9EBMJBfkiD2045AuzshHrmzsmkMDQGA1UdHwQtMCsw +KaAnoCWGI2h0dHA6Ly9jcmwuc2VjdXJldHJ1c3QuY29tL1NHQ0EuY3JsMBAGCSsG +AQQBgjcVAQQDAgEAMA0GCSqGSIb3DQEBBQUAA4IBAQBjGghAfaReUw132HquHw0L +URYD7xh8yOOvaliTFGCRsoTciE6+OYo68+aCiV0BN7OrJKQVDpI1WkpEXk5X+nXO +H0jOZvQ8QCaSmGwb7iRGDBezUqXbpZGRzzfTb+cnCDpOGR86p1hcF895P4vkp9Mm +I50mD1hp/Ed+stCNi5O/KU9DaXR2Z0vPB4zmAve14bRDtUstFJ/53CYNv6ZHdAbY +iNE6KTCEztI5gGIbqMdXSbxqVVFnFUq+NQfk1XWYN3kwFNspnWzFacxHVaIw98xc +f8LDmBxrThaA63p4ZUWiABqvDA1VZDRIuJK58bRQKfJPIx/abKwfROHdI3hRW8cW +-----END CERTIFICATE----- + +# Issuer: CN=COMODO Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO Certification Authority O=COMODO CA Limited +# Label: "COMODO Certification Authority" +# Serial: 104350513648249232941998508985834464573 +# MD5 Fingerprint: 5c:48:dc:f7:42:72:ec:56:94:6d:1c:cc:71:35:80:75 +# SHA1 Fingerprint: 66:31:bf:9e:f7:4f:9e:b6:c9:d5:a6:0c:ba:6a:be:d1:f7:bd:ef:7b +# SHA256 Fingerprint: 0c:2c:d6:3d:f7:80:6f:a3:99:ed:e8:09:11:6b:57:5b:f8:79:89:f0:65:18:f9:80:8c:86:05:03:17:8b:af:66 +-----BEGIN CERTIFICATE----- +MIIEHTCCAwWgAwIBAgIQToEtioJl4AsC7j41AkblPTANBgkqhkiG9w0BAQUFADCB +gTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxJzAlBgNV +BAMTHkNPTU9ETyBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEyMDEwMDAw +MDBaFw0yOTEyMzEyMzU5NTlaMIGBMQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3Jl +YXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01P +RE8gQ0EgTGltaXRlZDEnMCUGA1UEAxMeQ09NT0RPIENlcnRpZmljYXRpb24gQXV0 +aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0ECLi3LjkRv3 +UcEbVASY06m/weaKXTuH+7uIzg3jLz8GlvCiKVCZrts7oVewdFFxze1CkU1B/qnI +2GqGd0S7WWaXUF601CxwRM/aN5VCaTwwxHGzUvAhTaHYujl8HJ6jJJ3ygxaYqhZ8 +Q5sVW7euNJH+1GImGEaaP+vB+fGQV+useg2L23IwambV4EajcNxo2f8ESIl33rXp ++2dtQem8Ob0y2WIC8bGoPW43nOIv4tOiJovGuFVDiOEjPqXSJDlqR6sA1KGzqSX+ +DT+nHbrTUcELpNqsOO9VUCQFZUaTNE8tja3G1CEZ0o7KBWFxB3NH5YoZEr0ETc5O +nKVIrLsm9wIDAQABo4GOMIGLMB0GA1UdDgQWBBQLWOWLxkwVN6RAqTCpIb5HNlpW +/zAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zBJBgNVHR8EQjBAMD6g +PKA6hjhodHRwOi8vY3JsLmNvbW9kb2NhLmNvbS9DT01PRE9DZXJ0aWZpY2F0aW9u +QXV0aG9yaXR5LmNybDANBgkqhkiG9w0BAQUFAAOCAQEAPpiem/Yb6dc5t3iuHXIY +SdOH5EOC6z/JqvWote9VfCFSZfnVDeFs9D6Mk3ORLgLETgdxb8CPOGEIqB6BCsAv +IC9Bi5HcSEW88cbeunZrM8gALTFGTO3nnc+IlP8zwFboJIYmuNg4ON8qa90SzMc/ +RxdMosIGlgnW2/4/PEZB31jiVg88O8EckzXZOFKs7sjsLjBOlDW0JB9LeGna8gI4 +zJVSk/BwJVmcIGfE7vmLV2H0knZ9P4SNVbfo5azV8fUZVqZa+5Acr5Pr5RzUZ5dd +BA6+C4OmF4O5MBKgxTMVBbkN+8cFduPYSo38NBejxiEovjBFMR7HeL5YYTisO+IB +ZQ== +-----END CERTIFICATE----- + +# Issuer: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO ECC Certification Authority O=COMODO CA Limited +# Label: "COMODO ECC Certification Authority" +# Serial: 41578283867086692638256921589707938090 +# MD5 Fingerprint: 7c:62:ff:74:9d:31:53:5e:68:4a:d5:78:aa:1e:bf:23 +# SHA1 Fingerprint: 9f:74:4e:9f:2b:4d:ba:ec:0f:31:2c:50:b6:56:3b:8e:2d:93:c3:11 +# SHA256 Fingerprint: 17:93:92:7a:06:14:54:97:89:ad:ce:2f:8f:34:f7:f0:b6:6d:0f:3a:e3:a3:b8:4d:21:ec:15:db:ba:4f:ad:c7 +-----BEGIN CERTIFICATE----- +MIICiTCCAg+gAwIBAgIQH0evqmIAcFBUTAGem2OZKjAKBggqhkjOPQQDAzCBhTEL +MAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UE +BxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMT +IkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwMzA2MDAw +MDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdy +ZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09N +T0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBFQ0MgQ2VydGlmaWNhdGlv +biBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQDR3svdcmCFYX7deSR +FtSrYpn1PlILBs5BAH+X4QokPB0BBO490o0JlwzgdeT6+3eKKvUDYEs2ixYjFq0J +cfRK9ChQtP6IHG4/bC8vCVlbpVsLM5niwz2J+Wos77LTBumjQjBAMB0GA1UdDgQW +BBR1cacZSBm8nZ3qQUfflMRId5nTeTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjEA7wNbeqy3eApyt4jf/7VGFAkK+qDm +fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv +GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= +-----END CERTIFICATE----- + +# Issuer: CN=Certigna O=Dhimyotis +# Subject: CN=Certigna O=Dhimyotis +# Label: "Certigna" +# Serial: 18364802974209362175 +# MD5 Fingerprint: ab:57:a6:5b:7d:42:82:19:b5:d8:58:26:28:5e:fd:ff +# SHA1 Fingerprint: b1:2e:13:63:45:86:a4:6f:1a:b2:60:68:37:58:2d:c4:ac:fd:94:97 +# SHA256 Fingerprint: e3:b6:a2:db:2e:d7:ce:48:84:2f:7a:c5:32:41:c7:b7:1d:54:14:4b:fb:40:c1:1f:3f:1d:0b:42:f5:ee:a1:2d +-----BEGIN CERTIFICATE----- +MIIDqDCCApCgAwIBAgIJAP7c4wEPyUj/MA0GCSqGSIb3DQEBBQUAMDQxCzAJBgNV +BAYTAkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hMB4X +DTA3MDYyOTE1MTMwNVoXDTI3MDYyOTE1MTMwNVowNDELMAkGA1UEBhMCRlIxEjAQ +BgNVBAoMCURoaW15b3RpczERMA8GA1UEAwwIQ2VydGlnbmEwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDIaPHJ1tazNHUmgh7stL7qXOEm7RFHYeGifBZ4 +QCHkYJ5ayGPhxLGWkv8YbWkj4Sti993iNi+RB7lIzw7sebYs5zRLcAglozyHGxny +gQcPOJAZ0xH+hrTy0V4eHpbNgGzOOzGTtvKg0KmVEn2lmsxryIRWijOp5yIVUxbw +zBfsV1/pogqYCd7jX5xv3EjjhQsVWqa6n6xI4wmy9/Qy3l40vhx4XUJbzg4ij02Q +130yGLMLLGq/jj8UEYkgDncUtT2UCIf3JR7VsmAA7G8qKCVuKj4YYxclPz5EIBb2 +JsglrgVKtOdjLPOMFlN+XPsRGgjBRmKfIrjxwo1p3Po6WAbfAgMBAAGjgbwwgbkw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUGu3+QTmQtCRZvgHyUtVF9lo53BEw +ZAYDVR0jBF0wW4AUGu3+QTmQtCRZvgHyUtVF9lo53BGhOKQ2MDQxCzAJBgNVBAYT +AkZSMRIwEAYDVQQKDAlEaGlteW90aXMxETAPBgNVBAMMCENlcnRpZ25hggkA/tzj +AQ/JSP8wDgYDVR0PAQH/BAQDAgEGMBEGCWCGSAGG+EIBAQQEAwIABzANBgkqhkiG +9w0BAQUFAAOCAQEAhQMeknH2Qq/ho2Ge6/PAD/Kl1NqV5ta+aDY9fm4fTIrv0Q8h +bV6lUmPOEvjvKtpv6zf+EwLHyzs+ImvaYS5/1HI93TDhHkxAGYwP15zRgzB7mFnc +fca5DClMoTOi62c6ZYTTluLtdkVwj7Ur3vkj1kluPBS1xp81HlDQwY9qcEQCYsuu +HWhBp6pX6FOqB9IG9tUUBguRA3UsbHK1YZWaDYu5Def131TN3ubY1gkIl2PlwS6w +t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw +WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== +-----END CERTIFICATE----- + +# Issuer: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Subject: O=Chunghwa Telecom Co., Ltd. OU=ePKI Root Certification Authority +# Label: "ePKI Root Certification Authority" +# Serial: 28956088682735189655030529057352760477 +# MD5 Fingerprint: 1b:2e:00:ca:26:06:90:3d:ad:fe:6f:15:68:d3:6b:b3 +# SHA1 Fingerprint: 67:65:0d:f1:7e:8e:7e:5b:82:40:a4:f4:56:4b:cf:e2:3d:69:c6:f0 +# SHA256 Fingerprint: c0:a6:f4:dc:63:a2:4b:fd:cf:54:ef:2a:6a:08:2a:0a:72:de:35:80:3e:2f:f5:ff:52:7a:e5:d8:72:06:df:d5 +-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQFci9ZUdcr7iXAF7kBtK8nTANBgkqhkiG9w0BAQUFADBe +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xKjAoBgNVBAsMIWVQS0kgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAe +Fw0wNDEyMjAwMjMxMjdaFw0zNDEyMjAwMjMxMjdaMF4xCzAJBgNVBAYTAlRXMSMw +IQYDVQQKDBpDaHVuZ2h3YSBUZWxlY29tIENvLiwgTHRkLjEqMCgGA1UECwwhZVBL +SSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA4SUP7o3biDN1Z82tH306Tm2d0y8U82N0ywEhajfqhFAH +SyZbCUNsIZ5qyNUD9WBpj8zwIuQf5/dqIjG3LBXy4P4AakP/h2XGtRrBp0xtInAh +ijHyl3SJCRImHJ7K2RKilTza6We/CKBk49ZCt0Xvl/T29de1ShUCWH2YWEtgvM3X +DZoTM1PRYfl61dd4s5oz9wCGzh1NlDivqOx4UXCKXBCDUSH3ET00hl7lSM2XgYI1 +TBnsZfZrxQWh7kcT1rMhJ5QQCtkkO7q+RBNGMD+XPNjX12ruOzjjK9SXDrkb5wdJ +fzcq+Xd4z1TtW0ado4AOkUPB1ltfFLqfpo0kR0BZv3I4sjZsN/+Z0V0OWQqraffA +sgRFelQArr5T9rXn4fg8ozHSqf4hUmTFpmfwdQcGlBSBVcYn5AGPF8Fqcde+S/uU +WH1+ETOxQvdibBjWzwloPn9s9h6PYq2lY9sJpx8iQkEeb5mKPtf5P0B6ebClAZLS +nT0IFaUQAS2zMnaolQ2zepr7BxB4EW/hj8e6DyUadCrlHJhBmd8hh+iVBmoKs2pH +dmX2Os+PYhcZewoozRrSgx4hxyy/vv9haLdnG7t4TY3OZ+XkwY63I2binZB1NJip +NiuKmpS5nezMirH4JYlcWrYvjB9teSSnUmjDhDXiZo1jDiVN1Rmy5nk3pyKdVDEC +AwEAAaNqMGgwHQYDVR0OBBYEFB4M97Zn8uGSJglFwFU5Lnc/QkqiMAwGA1UdEwQF +MAMBAf8wOQYEZyoHAAQxMC8wLQIBADAJBgUrDgMCGgUAMAcGBWcqAwAABBRFsMLH +ClZ87lt4DJX5GFPBphzYEDANBgkqhkiG9w0BAQUFAAOCAgEACbODU1kBPpVJufGB +uvl2ICO1J2B01GqZNF5sAFPZn/KmsSQHRGoqxqWOeBLoR9lYGxMqXnmbnwoqZ6Yl +PwZpVnPDimZI+ymBV3QGypzqKOg4ZyYr8dW1P2WT+DZdjo2NQCCHGervJ8A9tDkP +JXtoUHRVnAxZfVo9QZQlUgjgRywVMRnVvwdVxrsStZf0X4OFunHB2WyBEXYKCrC/ +gpf36j36+uwtqSiUO1bd0lEursC9CBWMd1I0ltabrNMdjmEPNXubrjlpC2JgQCA2 +j6/7Nu4tCEoduL+bXPjqpRugc6bY+G7gMwRfaKonh+3ZwZCc7b3jajWvY9+rGNm6 +5ulK6lCKD2GTHuItGeIwlDWSXQ62B68ZgI9HkFFLLk3dheLSClIKF5r8GrBQAuUB +o2M3IUxExJtRmREOc5wGj1QupyheRDmHVi03vYVElOEMSyycw5KFNGHLD7ibSkNS +/jQ6fbjpKdx2qcgw+BRxgMYeNkh0IkFch4LoGHGLQYlE535YW6i4jRPpp2zDR+2z +Gp1iro2C6pSe3VkQw63d4k3jMdXH7OjysP6SHhYKGvzZ8/gntsm+HbRsZJB/9OTE +W9c3rkIO3aQab3yIVMUWbuF6aC74Or8NpDyJO3inTmODBCEIZ43ygknQW/2xzQ+D +hNQ+IIX3Sj0rnP0qCglN6oH4EZw= +-----END CERTIFICATE----- + +# Issuer: O=certSIGN OU=certSIGN ROOT CA +# Subject: O=certSIGN OU=certSIGN ROOT CA +# Label: "certSIGN ROOT CA" +# Serial: 35210227249154 +# MD5 Fingerprint: 18:98:c0:d6:e9:3a:fc:f9:b0:f5:0c:f7:4b:01:44:17 +# SHA1 Fingerprint: fa:b7:ee:36:97:26:62:fb:2d:b0:2a:f6:bf:03:fd:e8:7c:4b:2f:9b +# SHA256 Fingerprint: ea:a9:62:c4:fa:4a:6b:af:eb:e4:15:19:6d:35:1c:cd:88:8d:4f:53:f3:fa:8a:e6:d7:c4:66:a9:4e:60:42:bb +-----BEGIN CERTIFICATE----- +MIIDODCCAiCgAwIBAgIGIAYFFnACMA0GCSqGSIb3DQEBBQUAMDsxCzAJBgNVBAYT +AlJPMREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBD +QTAeFw0wNjA3MDQxNzIwMDRaFw0zMTA3MDQxNzIwMDRaMDsxCzAJBgNVBAYTAlJP +MREwDwYDVQQKEwhjZXJ0U0lHTjEZMBcGA1UECxMQY2VydFNJR04gUk9PVCBDQTCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALczuX7IJUqOtdu0KBuqV5Do +0SLTZLrTk+jUrIZhQGpgV2hUhE28alQCBf/fm5oqrl0Hj0rDKH/v+yv6efHHrfAQ +UySQi2bJqIirr1qjAOm+ukbuW3N7LBeCgV5iLKECZbO9xSsAfsT8AzNXDe3i+s5d +RdY4zTW2ssHQnIFKquSyAVwdj1+ZxLGt24gh65AIgoDzMKND5pCCrlUoSe1b16kQ +OA7+j0xbm0bqQfWwCHTD0IgztnzXdN/chNFDDnU5oSVAKOp4yw4sLjmdjItuFhwv +JoIQ4uNllAoEwF73XVv4EOLQunpL+943AAAaWyjj0pxzPjKHmKHJUS/X3qwzs08C +AwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAcYwHQYDVR0O +BBYEFOCMm9slSbPxfIbWskKHC9BroNnkMA0GCSqGSIb3DQEBBQUAA4IBAQA+0hyJ +LjX8+HXd5n9liPRyTMks1zJO890ZeUe9jjtbkw9QSSQTaxQGcu8J06Gh40CEyecY +MnQ8SG4Pn0vU9x7Tk4ZkVJdjclDVVc/6IJMCopvDI5NOFlV2oHB5bc0hH88vLbwZ +44gx+FkagQnIl6Z0x2DEW8xXjrJ1/RsCCdtZb3KTafcxQdaIOL+Hsr0Wefmq5L6I +Jd1hJyMctTEHBDa0GpC9oHRxUIltvBTjD4au8as+x6AJzKNI0eDbZOeStc+vckNw +i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN +9u6wWk5JRFRYX0KD +-----END CERTIFICATE----- + +# Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) +# Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" +# Serial: 80544274841616 +# MD5 Fingerprint: c5:a1:b7:ff:73:dd:d6:d7:34:32:18:df:fc:3c:ad:88 +# SHA1 Fingerprint: 06:08:3f:59:3f:15:a1:04:a0:69:a4:6b:a9:03:d0:06:b7:97:09:91 +# SHA256 Fingerprint: 6c:61:da:c3:a2:de:f0:31:50:6b:e0:36:d2:a6:fe:40:19:94:fb:d1:3d:f9:c8:d4:66:59:92:74:c4:46:ec:98 +-----BEGIN CERTIFICATE----- +MIIEFTCCAv2gAwIBAgIGSUEs5AAQMA0GCSqGSIb3DQEBCwUAMIGnMQswCQYDVQQG +EwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFTATBgNVBAoMDE5ldExvY2sgS2Z0LjE3 +MDUGA1UECwwuVGFuw7pzw610dsOhbnlraWFkw7NrIChDZXJ0aWZpY2F0aW9uIFNl +cnZpY2VzKTE1MDMGA1UEAwwsTmV0TG9jayBBcmFueSAoQ2xhc3MgR29sZCkgRsWR +dGFuw7pzw610dsOhbnkwHhcNMDgxMjExMTUwODIxWhcNMjgxMjA2MTUwODIxWjCB +pzELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRUwEwYDVQQKDAxOZXRM +b2NrIEtmdC4xNzA1BgNVBAsMLlRhbsO6c8OtdHbDoW55a2lhZMOzayAoQ2VydGlm +aWNhdGlvbiBTZXJ2aWNlcykxNTAzBgNVBAMMLE5ldExvY2sgQXJhbnkgKENsYXNz +IEdvbGQpIEbFkXRhbsO6c8OtdHbDoW55MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A +MIIBCgKCAQEAxCRec75LbRTDofTjl5Bu0jBFHjzuZ9lk4BqKf8owyoPjIMHj9DrT +lF8afFttvzBPhCf2nx9JvMaZCpDyD/V/Q4Q3Y1GLeqVw/HpYzY6b7cNGbIRwXdrz +AZAj/E4wqX7hJ2Pn7WQ8oLjJM2P+FpD/sLj916jAwJRDC7bVWaaeVtAkH3B5r9s5 +VA1lddkVQZQBr17s9o3x/61k/iCa11zr/qYfCGSji3ZVrR47KGAuhyXoqq8fxmRG +ILdwfzzeSNuWU7c5d+Qa4scWhHaXWy+7GRWF+GmF9ZmnqfI0p6m2pgP8b4Y9VHx2 +BJtr+UBdADTHLpl1neWIA6pN+APSQnbAGwIDAKiLo0UwQzASBgNVHRMBAf8ECDAG +AQH/AgEEMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUzPpnk/C2uNClwB7zU/2M +U9+D15YwDQYJKoZIhvcNAQELBQADggEBAKt/7hwWqZw8UQCgwBEIBaeZ5m8BiFRh +bvG5GK1Krf6BQCOUL/t1fC8oS2IkgYIL9WHxHG64YTjrgfpioTtaYtOUZcTh5m2C ++C8lcLIhJsFyUR+MLMOEkMNaj7rP9KdlpeuY0fsFskZ1FSNqb4VjMIDw1Z4fKRzC +bLBQWV2QWzuoDTDPv31/zvGdg73JRm4gpvlhUbohL3u+pRVjodSVh/GeufOJ8z2F +uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 +XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= +-----END CERTIFICATE----- + +# Issuer: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Subject: CN=Microsec e-Szigno Root CA 2009 O=Microsec Ltd. +# Label: "Microsec e-Szigno Root CA 2009" +# Serial: 14014712776195784473 +# MD5 Fingerprint: f8:49:f4:03:bc:44:2d:83:be:48:69:7d:29:64:fc:b1 +# SHA1 Fingerprint: 89:df:74:fe:5c:f4:0f:4a:80:f9:e3:37:7d:54:da:91:e1:01:31:8e +# SHA256 Fingerprint: 3c:5f:81:fe:a5:fa:b8:2c:64:bf:a2:ea:ec:af:cd:e8:e0:77:fc:86:20:a7:ca:e5:37:16:3d:f3:6e:db:f3:78 +-----BEGIN CERTIFICATE----- +MIIECjCCAvKgAwIBAgIJAMJ+QwRORz8ZMA0GCSqGSIb3DQEBCwUAMIGCMQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3ppZ25vIFJvb3QgQ0EgMjAwOTEfMB0G +CSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5odTAeFw0wOTA2MTYxMTMwMThaFw0y +OTEyMzAxMTMwMThaMIGCMQswCQYDVQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3Qx +FjAUBgNVBAoMDU1pY3Jvc2VjIEx0ZC4xJzAlBgNVBAMMHk1pY3Jvc2VjIGUtU3pp +Z25vIFJvb3QgQ0EgMjAwOTEfMB0GCSqGSIb3DQEJARYQaW5mb0BlLXN6aWduby5o +dTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOn4j/NjrdqG2KfgQvvP +kd6mJviZpWNwrZuuyjNAfW2WbqEORO7hE52UQlKavXWFdCyoDh2Tthi3jCyoz/tc +cbna7P7ofo/kLx2yqHWH2Leh5TvPmUpG0IMZfcChEhyVbUr02MelTTMuhTlAdX4U +fIASmFDHQWe4oIBhVKZsTh/gnQ4H6cm6M+f+wFUoLAKApxn1ntxVUwOXewdI/5n7 +N4okxFnMUBBjjqqpGrCEGob5X7uxUG6k0QrM1XF+H6cbfPVTbiJfyyvm1HxdrtbC +xkzlBQHZ7Vf8wSN5/PrIJIOV87VqUQHQd9bpEqH5GoP7ghu5sJf0dgYzQ0mg/wu1 ++rUCAwEAAaOBgDB+MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBTLD8bfQkPMPcu1SCOhGnqmKrs0aDAfBgNVHSMEGDAWgBTLD8bfQkPM +Pcu1SCOhGnqmKrs0aDAbBgNVHREEFDASgRBpbmZvQGUtc3ppZ25vLmh1MA0GCSqG +SIb3DQEBCwUAA4IBAQDJ0Q5eLtXMs3w+y/w9/w0olZMEyL/azXm4Q5DwpL7v8u8h +mLzU1F0G9u5C7DBsoKqpyvGvivo/C3NqPuouQH4frlRheesuCDfXI/OMn74dseGk +ddug4lQUsbocKaQY9hK6ohQU4zE1yED/t+AFdlfBHFny+L/k7SViXITwfn4fs775 +tyERzAMBVnCnEJIeGzSBHq2cGsMEPO0CYdYeBvNfOofyK/FFh+U9rNHHV4S9a67c +2Pm2G2JwCz02yULyMtd6YebS2z3PyKnJm9zbWETXbzivf3jTo60adbocwTZ8jx5t +HMN1Rq41Bab2XD0h7lbwyYIiLXpUq3DDfSJlgnCW +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R3 +# Label: "GlobalSign Root CA - R3" +# Serial: 4835703278459759426209954 +# MD5 Fingerprint: c5:df:b8:49:ca:05:13:55:ee:2d:ba:1a:c3:3e:b0:28 +# SHA1 Fingerprint: d6:9b:56:11:48:f0:1c:77:c5:45:78:c1:09:26:df:5b:85:69:76:ad +# SHA256 Fingerprint: cb:b5:22:d7:b7:f1:27:ad:6a:01:13:86:5b:df:1c:d4:10:2e:7d:07:59:af:63:5a:7c:f4:72:0d:c9:63:c5:3b +-----BEGIN CERTIFICATE----- +MIIDXzCCAkegAwIBAgILBAAAAAABIVhTCKIwDQYJKoZIhvcNAQELBQAwTDEgMB4G +A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjMxEzARBgNVBAoTCkdsb2JhbFNp +Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDkwMzE4MTAwMDAwWhcNMjkwMzE4 +MTAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMzETMBEG +A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAMwldpB5BngiFvXAg7aEyiie/QV2EcWtiHL8 +RgJDx7KKnQRfJMsuS+FggkbhUqsMgUdwbN1k0ev1LKMPgj0MK66X17YUhhB5uzsT +gHeMCOFJ0mpiLx9e+pZo34knlTifBtc+ycsmWQ1z3rDI6SYOgxXG71uL0gRgykmm +KPZpO/bLyCiR5Z2KYVc3rHQU3HTgOu5yLy6c+9C7v/U9AOEGM+iCK65TpjoWc4zd +QQ4gOsC0p6Hpsk+QLjJg6VfLuQSSaGjlOCZgdbKfd/+RFO+uIEn8rUAVSNECMWEZ +XriX7613t2Saer9fwRPvm2L7DWzgVGkWqQPabumDk3F2xmmFghcCAwEAAaNCMEAw +DgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI/wS3+o +LkUkrk1Q+mOai97i3Ru8MA0GCSqGSIb3DQEBCwUAA4IBAQBLQNvAUKr+yAzv95ZU +RUm7lgAJQayzE4aGKAczymvmdLm6AC2upArT9fHxD4q/c2dKg8dEe3jgr25sbwMp +jjM5RcOO5LlXbKr8EpbsU8Yt5CRsuZRj+9xTaGdWPoO4zzUhw8lo/s7awlOqzJCK +6fBdRoyV3XpYKBovHd7NADdBj+1EbddTKJd+82cEHhXXipa0095MJ6RMG3NzdvQX +mcIfeg7jLQitChws/zyrVQ4PkX4268NXSb7hLi18YIvDQVETI53O9zJrlAGomecs +Mx86OyXShkDOOyyGeMlhLxS67ttVb9+E7gUJTb0o2HLO02JQZR7rkpeDMdmztcpH +WD9f +-----END CERTIFICATE----- + +# Issuer: CN=Izenpe.com O=IZENPE S.A. +# Subject: CN=Izenpe.com O=IZENPE S.A. +# Label: "Izenpe.com" +# Serial: 917563065490389241595536686991402621 +# MD5 Fingerprint: a6:b0:cd:85:80:da:5c:50:34:a3:39:90:2f:55:67:73 +# SHA1 Fingerprint: 2f:78:3d:25:52:18:a7:4a:65:39:71:b5:2c:a2:9c:45:15:6f:e9:19 +# SHA256 Fingerprint: 25:30:cc:8e:98:32:15:02:ba:d9:6f:9b:1f:ba:1b:09:9e:2d:29:9e:0f:45:48:bb:91:4f:36:3b:c0:d4:53:1f +-----BEGIN CERTIFICATE----- +MIIF8TCCA9mgAwIBAgIQALC3WhZIX7/hy/WL1xnmfTANBgkqhkiG9w0BAQsFADA4 +MQswCQYDVQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6 +ZW5wZS5jb20wHhcNMDcxMjEzMTMwODI4WhcNMzcxMjEzMDgyNzI1WjA4MQswCQYD +VQQGEwJFUzEUMBIGA1UECgwLSVpFTlBFIFMuQS4xEzARBgNVBAMMCkl6ZW5wZS5j +b20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDJ03rKDx6sp4boFmVq +scIbRTJxldn+EFvMr+eleQGPicPK8lVx93e+d5TzcqQsRNiekpsUOqHnJJAKClaO +xdgmlOHZSOEtPtoKct2jmRXagaKH9HtuJneJWK3W6wyyQXpzbm3benhB6QiIEn6H +LmYRY2xU+zydcsC8Lv/Ct90NduM61/e0aL6i9eOBbsFGb12N4E3GVFWJGjMxCrFX +uaOKmMPsOzTFlUFpfnXCPCDFYbpRR6AgkJOhkEvzTnyFRVSa0QUmQbC1TR0zvsQD +yCV8wXDbO/QJLVQnSKwv4cSsPsjLkkxTOTcj7NMB+eAJRE1NZMDhDVqHIrytG6P+ +JrUV86f8hBnp7KGItERphIPzidF0BqnMC9bC3ieFUCbKF7jJeodWLBoBHmy+E60Q +rLUk9TiRodZL2vG70t5HtfG8gfZZa88ZU+mNFctKy6lvROUbQc/hhqfK0GqfvEyN +BjNaooXlkDWgYlwWTvDjovoDGrQscbNYLN57C9saD+veIR8GdwYDsMnvmfzAuU8L +hij+0rnq49qlw0dpEuDb8PYZi+17cNcC1u2HGCgsBCRMd+RIihrGO5rUD8r6ddIB +QFqNeb+Lz0vPqhbBleStTIo+F5HUsWLlguWABKQDfo2/2n+iD5dPDNMN+9fR5XJ+ +HMh3/1uaD7euBUbl8agW7EekFwIDAQABo4H2MIHzMIGwBgNVHREEgagwgaWBD2lu +Zm9AaXplbnBlLmNvbaSBkTCBjjFHMEUGA1UECgw+SVpFTlBFIFMuQS4gLSBDSUYg +QTAxMzM3MjYwLVJNZXJjLlZpdG9yaWEtR2FzdGVpeiBUMTA1NSBGNjIgUzgxQzBB +BgNVBAkMOkF2ZGEgZGVsIE1lZGl0ZXJyYW5lbyBFdG9yYmlkZWEgMTQgLSAwMTAx +MCBWaXRvcmlhLUdhc3RlaXowDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AQYwHQYDVR0OBBYEFB0cZQ6o8iV7tJHP5LGx5r1VdGwFMA0GCSqGSIb3DQEBCwUA +A4ICAQB4pgwWSp9MiDrAyw6lFn2fuUhfGI8NYjb2zRlrrKvV9pF9rnHzP7MOeIWb +laQnIUdCSnxIOvVFfLMMjlF4rJUT3sb9fbgakEyrkgPH7UIBzg/YsfqikuFgba56 +awmqxinuaElnMIAkejEWOVt+8Rwu3WwJrfIxwYJOubv5vr8qhT/AQKM6WfxZSzwo +JNu0FXWuDYi6LnPAvViH5ULy617uHjAimcs30cQhbIHsvm0m5hzkQiCeR7Csg1lw +LDXWrzY0tM07+DKo7+N4ifuNRSzanLh+QBxh5z6ikixL8s36mLYp//Pye6kfLqCT +VyvehQP5aTfLnnhqBbTFMXiJ7HqnheG5ezzevh55hM6fcA5ZwjUukCox2eRFekGk +LhObNA5me0mrZJfQRsN5nXJQY6aYWwa9SG3YOYNw6DXwBdGqvOPbyALqfP2C2sJb +UjWumDqtujWTI6cfSN01RpiyEGjkpTHCClguGYEQyVB1/OpaFs4R1+7vUIgtYf8/ +QnMFlEPVjjxOAToZpR9GTnfQXeWBIiGH/pR9hNiTrdZoQ0iy2+tzJOeRf1SktoA+ +naM8THLCV8Sg1Mw4J87VBp6iSNnpn86CcDaTmjvfliHjWbcM2pE38P1ZWrOZyGls +QyYBNWNgVYkDOnXYukrZVP/u3oDYLdE41V4tC5h9Pmzb/CaIxw== +-----END CERTIFICATE----- + +# Issuer: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Subject: CN=Go Daddy Root Certificate Authority - G2 O=GoDaddy.com, Inc. +# Label: "Go Daddy Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 80:3a:bc:22:c1:e6:fb:8d:9b:3b:27:4a:32:1b:9a:01 +# SHA1 Fingerprint: 47:be:ab:c9:22:ea:e8:0e:78:78:34:62:a7:9f:45:c2:54:fd:e6:8b +# SHA256 Fingerprint: 45:14:0b:32:47:eb:9c:c8:c5:b4:f0:d7:b5:30:91:f7:32:92:08:9e:6e:5a:63:e2:74:9d:d3:ac:a9:19:8e:da +-----BEGIN CERTIFICATE----- +MIIDxTCCAq2gAwIBAgIBADANBgkqhkiG9w0BAQsFADCBgzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxGjAYBgNVBAoT +EUdvRGFkZHkuY29tLCBJbmMuMTEwLwYDVQQDEyhHbyBEYWRkeSBSb290IENlcnRp +ZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAwMFoXDTM3MTIzMTIz +NTk1OVowgYMxCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6b25hMRMwEQYDVQQH +EwpTY290dHNkYWxlMRowGAYDVQQKExFHb0RhZGR5LmNvbSwgSW5jLjExMC8GA1UE +AxMoR28gRGFkZHkgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL9xYgjx+lk09xvJGKP3gElY6SKD +E6bFIEMBO4Tx5oVJnyfq9oQbTqC023CYxzIBsQU+B07u9PpPL1kwIuerGVZr4oAH +/PMWdYA5UXvl+TW2dE6pjYIT5LY/qQOD+qK+ihVqf94Lw7YZFAXK6sOoBJQ7Rnwy +DfMAZiLIjWltNowRGLfTshxgtDj6AozO091GB94KPutdfMh8+7ArU6SSYmlRJQVh +GkSBjCypQ5Yj36w6gZoOKcUcqeldHraenjAKOc7xiID7S13MMuyFYkMlNAJWJwGR +tDtwKj9useiciAF9n9T521NtYJ2/LOdYq7hfRvzOxBsDPAnrSTFcaUaz4EcCAwEA +AaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FDqahQcQZyi27/a9BUFuIMGU2g/eMA0GCSqGSIb3DQEBCwUAA4IBAQCZ21151fmX +WWcDYfF+OwYxdS2hII5PZYe096acvNjpL9DbWu7PdIxztDhC2gV7+AJ1uP2lsdeu +9tfeE8tTEH6KRtGX+rcuKxGrkLAngPnon1rpN5+r5N9ss4UXnT3ZJE95kTXWXwTr +gIOrmgIttRD02JDHBHNA7XIloKmf7J6raBKZV8aPEjoJpL1E/QYVN8Gb5DKj7Tjo +2GTzLH4U/ALqn83/B2gX2yKQOC16jdFU8WnjXzPKej17CuPKf1855eJ1usV2GDPO +LPAvTK33sefOT6jEm0pUBsV/fdUID+Ic/n4XuKxe9tQWskMJDE32p2u0mYRlynqI +4uJEvlz36hz1 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: d6:39:81:c6:52:7e:96:69:fc:fc:ca:66:ed:05:f2:96 +# SHA1 Fingerprint: b5:1c:06:7c:ee:2b:0c:3d:f8:55:ab:2d:92:f4:fe:39:d4:e7:0f:0e +# SHA256 Fingerprint: 2c:e1:cb:0b:f9:d2:f9:e1:02:99:3f:be:21:51:52:c3:b2:dd:0c:ab:de:1c:68:e5:31:9b:83:91:54:db:b7:f5 +-----BEGIN CERTIFICATE----- +MIID3TCCAsWgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBjzELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xMjAwBgNVBAMTKVN0YXJmaWVs +ZCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5MDkwMTAwMDAw +MFoXDTM3MTIzMTIzNTk1OVowgY8xCzAJBgNVBAYTAlVTMRAwDgYDVQQIEwdBcml6 +b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFyZmllbGQgVGVj +aG5vbG9naWVzLCBJbmMuMTIwMAYDVQQDEylTdGFyZmllbGQgUm9vdCBDZXJ0aWZp +Y2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoC +ggEBAL3twQP89o/8ArFvW59I2Z154qK3A2FWGMNHttfKPTUuiUP3oWmb3ooa/RMg +nLRJdzIpVv257IzdIvpy3Cdhl+72WoTsbhm5iSzchFvVdPtrX8WJpRBSiUZV9Lh1 +HOZ/5FSuS/hVclcCGfgXcVnrHigHdMWdSL5stPSksPNkN3mSwOxGXn/hbVNMYq/N +Hwtjuzqd+/x5AJhhdM8mgkBj87JyahkNmcrUDnXMN/uLicFZ8WJ/X7NfZTD4p7dN +dloedl40wOiWVpmKs/B/pM293DIxfJHP4F8R+GuqSVzRmZTRouNjWwl2tVZi4Ut0 +HZbUJtQIBFnQmA4O5t78w+wfkPECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAO +BgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFHwMMh+n2TB/xH1oo2Kooc6rB1snMA0G +CSqGSIb3DQEBCwUAA4IBAQARWfolTwNvlJk7mh+ChTnUdgWUXuEok21iXQnCoKjU +sHU48TRqneSfioYmUeYs0cYtbpUgSpIB7LiKZ3sx4mcujJUDJi5DnUox9g61DLu3 +4jd/IroAow57UvtruzvE03lRTs2Q9GcHGcg8RnoNAX3FWOdt5oUwF5okxBDgBPfg +8n/Uqgr/Qh037ZTlZFkSIHc40zI+OIF1lnP6aI+xy84fxez6nH7PfrHxBy22/L/K +pL/QlwVKvOoYKAKQvVR4CSFx09F9HdkWsKlhPdAKACL8x3vLCWRFCztAgfd9fDL1 +mMpYjn0q7pBZc2T5NnReJaH1ZgUufzkVqSr7UIuOhWn0 +-----END CERTIFICATE----- + +# Issuer: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Subject: CN=Starfield Services Root Certificate Authority - G2 O=Starfield Technologies, Inc. +# Label: "Starfield Services Root Certificate Authority - G2" +# Serial: 0 +# MD5 Fingerprint: 17:35:74:af:7b:61:1c:eb:f4:f9:3c:e2:ee:40:f9:a2 +# SHA1 Fingerprint: 92:5a:8f:8d:2c:6d:04:e0:66:5f:59:6a:ff:22:d8:63:e8:25:6f:3f +# SHA256 Fingerprint: 56:8d:69:05:a2:c8:87:08:a4:b3:02:51:90:ed:cf:ed:b1:97:4a:60:6a:13:c6:e5:29:0f:cb:2a:e6:3e:da:b5 +-----BEGIN CERTIFICATE----- +MIID7zCCAtegAwIBAgIBADANBgkqhkiG9w0BAQsFADCBmDELMAkGA1UEBhMCVVMx +EDAOBgNVBAgTB0FyaXpvbmExEzARBgNVBAcTClNjb3R0c2RhbGUxJTAjBgNVBAoT +HFN0YXJmaWVsZCBUZWNobm9sb2dpZXMsIEluYy4xOzA5BgNVBAMTMlN0YXJmaWVs +ZCBTZXJ2aWNlcyBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAtIEcyMB4XDTA5 +MDkwMTAwMDAwMFoXDTM3MTIzMTIzNTk1OVowgZgxCzAJBgNVBAYTAlVTMRAwDgYD +VQQIEwdBcml6b25hMRMwEQYDVQQHEwpTY290dHNkYWxlMSUwIwYDVQQKExxTdGFy +ZmllbGQgVGVjaG5vbG9naWVzLCBJbmMuMTswOQYDVQQDEzJTdGFyZmllbGQgU2Vy +dmljZXMgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgLSBHMjCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBANUMOsQq+U7i9b4Zl1+OiFOxHz/Lz58gE20p +OsgPfTz3a3Y4Y9k2YKibXlwAgLIvWX/2h/klQ4bnaRtSmpDhcePYLQ1Ob/bISdm2 +8xpWriu2dBTrz/sm4xq6HZYuajtYlIlHVv8loJNwU4PahHQUw2eeBGg6345AWh1K +Ts9DkTvnVtYAcMtS7nt9rjrnvDH5RfbCYM8TWQIrgMw0R9+53pBlbQLPLJGmpufe +hRhJfGZOozptqbXuNC66DQO4M99H67FrjSXZm86B0UVGMpZwh94CDklDhbZsc7tk +6mFBrMnUVN+HL8cisibMn1lUaJ/8viovxFUcdUBgF4UCVTmLfwUCAwEAAaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJxfAN+q +AdcwKziIorhtSpzyEZGDMA0GCSqGSIb3DQEBCwUAA4IBAQBLNqaEd2ndOxmfZyMI +bw5hyf2E3F/YNoHN2BtBLZ9g3ccaaNnRbobhiCPPE95Dz+I0swSdHynVv/heyNXB +ve6SbzJ08pGCL72CQnqtKrcgfU28elUSwhXqvfdqlS5sdJ/PHLTyxQGjhdByPq1z +qwubdQxtRbeOlKyWN7Wg0I8VRw7j6IPdj/3vQQF3zCepYoUz8jcI73HPdwbeyBkd +iEDPfUYd/x7H4c7/I9vG+o1VTqkC50cRRj70/b17KSa7qWFiNyi2LSr2EIZkyXCn +0q23KXB56jzaYyWf/Wi3MOxw+3WKt21gZ7IeyLnp2KhvAotnDU0mV3HaIPzBSlCN +sSi6 +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA" +# Serial: 279744 +# MD5 Fingerprint: d5:e9:81:40:c5:18:69:fc:46:2c:89:75:62:0f:aa:78 +# SHA1 Fingerprint: 07:e0:32:e0:20:b7:2c:3f:19:2f:06:28:a2:59:3a:19:a7:0f:06:9e +# SHA256 Fingerprint: 5c:58:46:8d:55:f5:8e:49:7e:74:39:82:d2:b5:00:10:b6:d1:65:37:4a:cf:83:a7:d4:a3:2d:b7:68:c4:40:8e +-----BEGIN CERTIFICATE----- +MIIDuzCCAqOgAwIBAgIDBETAMA0GCSqGSIb3DQEBBQUAMH4xCzAJBgNVBAYTAlBM +MSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5D +ZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBU +cnVzdGVkIE5ldHdvcmsgQ0EwHhcNMDgxMDIyMTIwNzM3WhcNMjkxMjMxMTIwNzM3 +WjB+MQswCQYDVQQGEwJQTDEiMCAGA1UEChMZVW5pemV0byBUZWNobm9sb2dpZXMg +Uy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSIw +IAYDVQQDExlDZXJ0dW0gVHJ1c3RlZCBOZXR3b3JrIENBMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEA4/t9o3K6wvDJFIf1awFO4W5AB7ptJ11/91sts1rH +UV+rpDKmYYe2bg+G0jACl/jXaVehGDldamR5xgFZrDwxSjh80gTSSyjoIF87B6LM +TXPb865Px1bVWqeWifrzq2jUI4ZZJ88JJ7ysbnKDHDBy3+Ci6dLhdHUZvSqeexVU +BBvXQzmtVSjF4hq79MDkrjhJM8x2hZ85RdKknvISjFH4fOQtf/WsX+sWn7Et0brM +kUJ3TCXJkDhv2/DM+44el1k+1WBO5gUo7Ul5E0u6SNsv+XLTOcr+H9g0cvW0QM8x +AcPs3hEtF10fuFDRXhmnad4HMyjKUJX5p1TLVIZQRan5SQIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBQIds3LB/8k9sXN7buQvOKEN0Z19zAOBgNV +HQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQEFBQADggEBAKaorSLOAT2mo/9i0Eidi15y +sHhE49wcrwn9I0j6vSrEuVUEtRCjjSfeC4Jj0O7eDDd5QVsisrCaQVymcODU0HfL +I9MA4GxWL+FpDQ3Zqr8hgVDZBqWo/5U30Kr+4rP1mS1FhIrlQgnXdAIv94nYmem8 +J9RHjboNRhx3zxSkHLmkMcScKHQDNP8zGSal6Q10tz6XxnboJ5ajZt3hrvJBW8qY +VoNzcOSGGtIxQbovvi0TWnZvTuhOgQ4/WwMioBK+ZlgRSssDxLQqKi2WF+A5VLxI +03YnnZotBqbJ7DnSq9ufmgsnAjUpsUCV5/nonFWIGUbWtzT1fs45mtk48VH3Tyw= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Root Certification Authority O=TAIWAN-CA OU=Root CA +# Label: "TWCA Root Certification Authority" +# Serial: 1 +# MD5 Fingerprint: aa:08:8f:f6:f9:7b:b7:f2:b1:a7:1e:9b:ea:ea:bd:79 +# SHA1 Fingerprint: cf:9e:87:6d:d3:eb:fc:42:26:97:a3:b5:a3:7a:a0:76:a9:06:23:48 +# SHA256 Fingerprint: bf:d8:8f:e1:10:1c:41:ae:3e:80:1b:f8:be:56:35:0e:e9:ba:d1:a6:b9:bd:51:5e:dc:5c:6d:5b:87:11:ac:44 +-----BEGIN CERTIFICATE----- +MIIDezCCAmOgAwIBAgIBATANBgkqhkiG9w0BAQUFADBfMQswCQYDVQQGEwJUVzES +MBAGA1UECgwJVEFJV0FOLUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFU +V0NBIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMDgwODI4MDcyNDMz +WhcNMzAxMjMxMTU1OTU5WjBfMQswCQYDVQQGEwJUVzESMBAGA1UECgwJVEFJV0FO +LUNBMRAwDgYDVQQLDAdSb290IENBMSowKAYDVQQDDCFUV0NBIFJvb3QgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB +AQCwfnK4pAOU5qfeCTiRShFAh6d8WWQUe7UREN3+v9XAu1bihSX0NXIP+FPQQeFE +AcK0HMMxQhZHhTMidrIKbw/lJVBPhYa+v5guEGcevhEFhgWQxFnQfHgQsIBct+HH +K3XLfJ+utdGdIzdjp9xCoi2SBBtQwXu4PhvJVgSLL1KbralW6cH/ralYhzC2gfeX +RfwZVzsrb+RH9JlF/h3x+JejiB03HFyP4HYlmlD4oFT/RJB2I9IyxsOrBr/8+7/z +rX2SYgJbKdM1o5OaQ2RgXbL6Mv87BK9NQGr5x+PvI/1ry+UPizgN7gr8/g+YnzAx +3WxSZfmLgb4i4RxYA7qRG4kHAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqOFsmjd6LWvJPelSDGRjjCDWmujANBgkq +hkiG9w0BAQUFAAOCAQEAPNV3PdrfibqHDAhUaiBQkr6wQT25JmSDCi/oQMCXKCeC +MErJk/9q56YAf4lCmtYR5VPOL8zy2gXE/uJQxDqGfczafhAJO5I1KlOy/usrBdls +XebQ79NqZp4VKIV66IIArB6nCWlWQtNoURi+VJq/REG6Sb4gumlc7rh3zc5sH62D +lhh9DrUUOYTxKOkto557HnpyWoOzeW/vtPzQCqVYT0bf+215WfKEIlKuD8z7fDvn +aspHYcN6+NOSBB+4IIThNlQWx0DeO4pz3N/GCUzf7Nr/1FNCocnyYh0igzyXxfkZ +YiesZSLX0zzG5Y6yU8xJzrww/nsOM5D77dIUkR8Hrw== +-----END CERTIFICATE----- + +# Issuer: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Subject: O=SECOM Trust Systems CO.,LTD. OU=Security Communication RootCA2 +# Label: "Security Communication RootCA2" +# Serial: 0 +# MD5 Fingerprint: 6c:39:7d:a4:0e:55:59:b2:3f:d6:41:b1:12:50:de:43 +# SHA1 Fingerprint: 5f:3b:8c:f2:f8:10:b3:7d:78:b4:ce:ec:19:19:c3:73:34:b9:c7:74 +# SHA256 Fingerprint: 51:3b:2c:ec:b8:10:d4:cd:e5:dd:85:39:1a:df:c6:c2:dd:60:d8:7b:b7:36:d2:b5:21:48:4a:a4:7a:0e:be:f6 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIBADANBgkqhkiG9w0BAQsFADBdMQswCQYDVQQGEwJKUDEl +MCMGA1UEChMcU0VDT00gVHJ1c3QgU3lzdGVtcyBDTy4sTFRELjEnMCUGA1UECxMe +U2VjdXJpdHkgQ29tbXVuaWNhdGlvbiBSb290Q0EyMB4XDTA5MDUyOTA1MDAzOVoX +DTI5MDUyOTA1MDAzOVowXTELMAkGA1UEBhMCSlAxJTAjBgNVBAoTHFNFQ09NIFRy +dXN0IFN5c3RlbXMgQ08uLExURC4xJzAlBgNVBAsTHlNlY3VyaXR5IENvbW11bmlj +YXRpb24gUm9vdENBMjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANAV +OVKxUrO6xVmCxF1SrjpDZYBLx/KWvNs2l9amZIyoXvDjChz335c9S672XewhtUGr +zbl+dp+++T42NKA7wfYxEUV0kz1XgMX5iZnK5atq1LXaQZAQwdbWQonCv/Q4EpVM +VAX3NuRFg3sUZdbcDE3R3n4MqzvEFb46VqZab3ZpUql6ucjrappdUtAtCms1FgkQ +hNBqyjoGADdH5H5XTz+L62e4iKrFvlNVspHEfbmwhRkGeC7bYRr6hfVKkaHnFtWO +ojnflLhwHyg/i/xAXmODPIMqGplrz95Zajv8bxbXH/1KEOtOghY6rCcMU/Gt1SSw +awNQwS08Ft1ENCcadfsCAwEAAaNCMEAwHQYDVR0OBBYEFAqFqXdlBZh8QIH4D5cs +OPEK7DzPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3 +DQEBCwUAA4IBAQBMOqNErLlFsceTfsgLCkLfZOoc7llsCLqJX2rKSpWeeo8HxdpF +coJxDjrSzG+ntKEju/Ykn8sX/oymzsLS28yN/HH8AynBbF0zX2S2ZTuJbxh2ePXc +okgfGT+Ok+vx+hfuzU7jBBJV1uXk3fs+BXziHV7Gp7yXT2g69ekuCkO2r1dcYmh8 +t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy +1UkC9gLl9B/rfNmWVan/7Ir5mUf/NVoCqgTLiluHcSmRvaS0eg29mvVXIwAHIRc/ +SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 +-----END CERTIFICATE----- + +# Issuer: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Subject: CN=Actalis Authentication Root CA O=Actalis S.p.A./03358520967 +# Label: "Actalis Authentication Root CA" +# Serial: 6271844772424770508 +# MD5 Fingerprint: 69:c1:0d:4f:07:a3:1b:c3:fe:56:3d:04:bc:11:f6:a6 +# SHA1 Fingerprint: f3:73:b3:87:06:5a:28:84:8a:f2:f3:4a:ce:19:2b:dd:c7:8e:9c:ac +# SHA256 Fingerprint: 55:92:60:84:ec:96:3a:64:b9:6e:2a:be:01:ce:0b:a8:6a:64:fb:fe:bc:c7:aa:b5:af:c1:55:b3:7f:d7:60:66 +-----BEGIN CERTIFICATE----- +MIIFuzCCA6OgAwIBAgIIVwoRl0LE48wwDQYJKoZIhvcNAQELBQAwazELMAkGA1UE +BhMCSVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8w +MzM1ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290 +IENBMB4XDTExMDkyMjExMjIwMloXDTMwMDkyMjExMjIwMlowazELMAkGA1UEBhMC +SVQxDjAMBgNVBAcMBU1pbGFuMSMwIQYDVQQKDBpBY3RhbGlzIFMucC5BLi8wMzM1 +ODUyMDk2NzEnMCUGA1UEAwweQWN0YWxpcyBBdXRoZW50aWNhdGlvbiBSb290IENB +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAp8bEpSmkLO/lGMWwUKNv +UTufClrJwkg4CsIcoBh/kbWHuUA/3R1oHwiD1S0eiKD4j1aPbZkCkpAW1V8IbInX +4ay8IMKx4INRimlNAJZaby/ARH6jDuSRzVju3PvHHkVH3Se5CAGfpiEd9UEtL0z9 +KK3giq0itFZljoZUj5NDKd45RnijMCO6zfB9E1fAXdKDa0hMxKufgFpbOr3JpyI/ +gCczWw63igxdBzcIy2zSekciRDXFzMwujt0q7bd9Zg1fYVEiVRvjRuPjPdA1Yprb +rxTIW6HMiRvhMCb8oJsfgadHHwTrozmSBp+Z07/T6k9QnBn+locePGX2oxgkg4YQ +51Q+qDp2JE+BIcXjDwL4k5RHILv+1A7TaLndxHqEguNTVHnd25zS8gebLra8Pu2F +be8lEfKXGkJh90qX6IuxEAf6ZYGyojnP9zz/GPvG8VqLWeICrHuS0E4UT1lF9gxe +KF+w6D9Fz8+vm2/7hNN3WpVvrJSEnu68wEqPSpP4RCHiMUVhUE4Q2OM1fEwZtN4F +v6MGn8i1zeQf1xcGDXqVdFUNaBr8EBtiZJ1t4JWgw5QHVw0U5r0F+7if5t+L4sbn +fpb2U8WANFAoWPASUHEXMLrmeGO89LKtmyuy/uE5jF66CyCU3nuDuP/jVo23Eek7 +jPKxwV2dpAtMK9myGPW1n0sCAwEAAaNjMGEwHQYDVR0OBBYEFFLYiDrIn3hm7Ynz +ezhwlMkCAjbQMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUUtiIOsifeGbt +ifN7OHCUyQICNtAwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3DQEBCwUAA4ICAQAL +e3KHwGCmSUyIWOYdiPcUZEim2FgKDk8TNd81HdTtBjHIgT5q1d07GjLukD0R0i70 +jsNjLiNmsGe+b7bAEzlgqqI0JZN1Ut6nna0Oh4lScWoWPBkdg/iaKWW+9D+a2fDz +WochcYBNy+A4mz+7+uAwTc+G02UQGRjRlwKxK3JCaKygvU5a2hi/a5iB0P2avl4V +SM0RFbnAKVy06Ij3Pjaut2L9HmLecHgQHEhb2rykOLpn7VU+Xlff1ANATIGk0k9j +pwlCCRT8AKnCgHNPLsBA2RF7SOp6AsDT6ygBJlh0wcBzIm2Tlf05fbsq4/aC4yyX +X04fkZT6/iyj2HYauE2yOE+b+h1IYHkm4vP9qdCa6HCPSXrW5b0KDtst842/6+Ok +fcvHlXHo2qN8xcL4dJIEG4aspCJTQLas/kx2z/uUMsA1n3Y/buWQbqCmJqK4LL7R +K4X9p2jIugErsWx0Hbhzlefut8cl8ABMALJ+tguLHPPAUJ4lueAI3jZm/zel0btU +ZCzJJ7VLkn5l/9Mt4blOvH+kQSGQQXemOR/qnuOf0GZvBeyqdn6/axag67XH/JJU +LysRJyU3eExRarDzzFhdFPFqSBX/wge2sY0PjlxQRrM9vwGYT7JZVEc+NHt4bVaT +LnPqZih4zR0Uv6CPLy64Lo7yFIrM6bV8+2ydDKXhlg== +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 2 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 2 Root CA" +# Serial: 2 +# MD5 Fingerprint: 46:a7:d2:fe:45:fb:64:5a:a8:59:90:9b:78:44:9b:29 +# SHA1 Fingerprint: 49:0a:75:74:de:87:0a:47:fe:58:ee:f6:c7:6b:eb:c6:0b:12:40:99 +# SHA256 Fingerprint: 9a:11:40:25:19:7c:5b:b9:5d:94:e6:3d:55:cd:43:79:08:47:b6:46:b2:3c:df:11:ad:a4:a0:0e:ff:15:fb:48 +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMiBSb290IENBMB4XDTEwMTAyNjA4MzgwM1oXDTQwMTAyNjA4MzgwM1ow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDIgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBANfHXvfBB9R3+0Mh9PT1aeTuMgHbo4Yf5FkNuud1g1Lr +6hxhFUi7HQfKjK6w3Jad6sNgkoaCKHOcVgb/S2TwDCo3SbXlzwx87vFKu3MwZfPV +L4O2fuPn9Z6rYPnT8Z2SdIrkHJasW4DptfQxh6NR/Md+oW+OU3fUl8FVM5I+GC91 +1K2GScuVr1QGbNgGE41b/+EmGVnAJLqBcXmQRFBoJJRfuLMR8SlBYaNByyM21cHx +MlAQTn/0hpPshNOOvEu/XAFOBz3cFIqUCqTqc/sLUegTBxj6DvEr0VQVfTzh97QZ +QmdiXnfgolXsttlpF9U6r0TtSsWe5HonfOV116rLJeffawrbD02TTqigzXsu8lkB +arcNuAeBfos4GzjmCleZPe4h6KP1DBbdi+w0jpwqHAAVF41og9JwnxgIzRFo1clr +Us3ERo/ctfPYV3Me6ZQ5BL/T3jjetFPsaRyifsSP5BtwrfKi+fv3FmRmaZ9JUaLi +FRhnBkp/1Wy1TbMz4GHrXb7pmA8y1x1LPC5aAVKRCfLf6o3YBkBjqhHk/sM3nhRS +P/TizPJhk9H9Z2vXUq6/aKtAQ6BXNVN48FP4YUIHZMbXb5tMOA1jrGKvNouicwoN +9SG9dKpN6nIDSdvHXx1iY8f93ZHsM+71bbRuMGjeyNYmsHVee7QHIJihdjK4TWxP +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFMmAd+BikoL1Rpzz +uvdMw964o605MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAU18h +9bqwOlI5LJKwbADJ784g7wbylp7ppHR/ehb8t/W2+xUbP6umwHJdELFx7rxP462s +A20ucS6vxOOto70MEae0/0qyexAQH6dXQbLArvQsWdZHEIjzIVEpMMpghq9Gqx3t +OluwlN5E40EIosHsHdb9T7bWR9AUC8rmyrV7d35BH16Dx7aMOZawP5aBQW9gkOLo ++fsicdl9sz1Gv7SEr5AcD48Saq/v7h56rgJKihcrdv6sVIkkLE8/trKnToyokZf7 +KcZ7XC25y2a2t6hbElGFtQl+Ynhw/qlqYLYdDnkM/crqJIByw5c/8nerQyIKx+u2 +DISCLIBrQYoIwOula9+ZEsuK1V6ADJHgJgg2SMX6OBE1/yWDLfJ6v9r9jv6ly0Us +H8SIU653DtmadsWOLB2jutXsMq7Aqqz30XpN69QH4kj3Io6wpJ9qzo6ysmD0oyLQ +I+uUWnpp3Q+/QFesa1lQ2aOZ4W7+jQF5JyMV3pKdewlNWudLSDBaGOYKbeaP4NK7 +5t98biGCwWg5TbSYWGZizEqQXsP6JwSxeRV0mcy+rSDeJmAc61ZRpqPq5KM/p/9h +3PFaTWwyI0PurKju7koSCTxdccK+efrCh2gdC/1cacwG0Jp9VJkqyTkaGa9LKkPz +Y11aWOIv4x3kqdbQCtCev9eBCfHJxyYNrJgWVqA= +-----END CERTIFICATE----- + +# Issuer: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Subject: CN=Buypass Class 3 Root CA O=Buypass AS-983163327 +# Label: "Buypass Class 3 Root CA" +# Serial: 2 +# MD5 Fingerprint: 3d:3b:18:9e:2c:64:5a:e8:d5:88:ce:0e:f9:37:c2:ec +# SHA1 Fingerprint: da:fa:f7:fa:66:84:ec:06:8f:14:50:bd:c7:c2:81:a5:bc:a9:64:57 +# SHA256 Fingerprint: ed:f7:eb:bc:a2:7a:2a:38:4d:38:7b:7d:40:10:c6:66:e2:ed:b4:84:3e:4c:29:b4:ae:1d:5b:93:32:e6:b2:4d +-----BEGIN CERTIFICATE----- +MIIFWTCCA0GgAwIBAgIBAjANBgkqhkiG9w0BAQsFADBOMQswCQYDVQQGEwJOTzEd +MBsGA1UECgwUQnV5cGFzcyBBUy05ODMxNjMzMjcxIDAeBgNVBAMMF0J1eXBhc3Mg +Q2xhc3MgMyBSb290IENBMB4XDTEwMTAyNjA4Mjg1OFoXDTQwMTAyNjA4Mjg1OFow +TjELMAkGA1UEBhMCTk8xHTAbBgNVBAoMFEJ1eXBhc3MgQVMtOTgzMTYzMzI3MSAw +HgYDVQQDDBdCdXlwYXNzIENsYXNzIDMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEB +BQADggIPADCCAgoCggIBAKXaCpUWUOOV8l6ddjEGMnqb8RB2uACatVI2zSRHsJ8Y +ZLya9vrVediQYkwiL944PdbgqOkcLNt4EemOaFEVcsfzM4fkoF0LXOBXByow9c3E +N3coTRiR5r/VUv1xLXA+58bEiuPwKAv0dpihi4dVsjoT/Lc+JzeOIuOoTyrvYLs9 +tznDDgFHmV0ST9tD+leh7fmdvhFHJlsTmKtdFoqwNxxXnUX/iJY2v7vKB3tvh2PX +0DJq1l1sDPGzbjniazEuOQAnFN44wOwZZoYS6J1yFhNkUsepNxz9gjDthBgd9K5c +/3ATAOux9TN6S9ZV+AWNS2mw9bMoNlwUxFFzTWsL8TQH2xc519woe2v1n/MuwU8X +KhDzzMro6/1rqy6any2CbgTUUgGTLT2G/H783+9CHaZr77kgxve9oKeV/afmiSTY +zIw0bOIjL9kSGiG5VZFvC5F5GQytQIgLcOJ60g7YaEi7ghM5EFjp2CoHxhLbWNvS +O1UQRwUVZ2J+GGOmRj8JDlQyXr8NYnon74Do29lLBlo3WiXQCBJ31G8JUJc9yB3D +34xFMFbG02SrZvPAXpacw8Tvw3xrizp5f7NJzz3iiZ+gMEuFuZyUJHmPfWupRWgP +K9Dx2hzLabjKSWJtyNBjYt1gD1iqj6G8BaVmos8bdrKEZLFMOVLAMLrwjEsCsLa3 +AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEe4zf/lb+74suwv +Tg75JbCOPGvDMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAACAj +QTUEkMJAYmDv4jVM1z+s4jSQuKFvdvoWFqRINyzpkMLyPPgKn9iB5btb2iUspKdV +cSQy9sgL8rxq+JOssgfCX5/bzMiKqr5qb+FJEMwx14C7u8jYog5kV+qi9cKpMRXS +IGrs/CIBKM+GuIAeqcwRpTzyFrNHnfzSgCHEy9BHcEGhyoMZCCxt8l13nIoUE9Q2 +HJLw5QY33KbmkJs4j1xrG0aGQ0JfPgEHU1RdZX33inOhmlRaHylDFCfChQ+1iHsa +O5S3HWCntZznKWlXWpuTekMwGwPXYshApqr8ZORK15FTAaggiG6cX0S5y2CBNOxv +033aSF/rtJC8LakcC6wc1aJoIIAE1vyxjy+7SjENSoYc6+I2KSb12tjE8nVhz36u +dmNKekBlk4f4HoCMhuWG1o8O/FMsYOgWYRqiPkN7zTlgVGr18okmAWiDSKIz6MkE +kbIRNBE+6tBDGR8Dk5AM/1E9V/RBbuHLoL7ryWPNbczk+DaqaJ3tvV2XcEQNtg41 +3OEMXbugUZTLfhbrES+jkkXITHHZvMmZUldGL1DPvTVp9D0VzgalLA8+9oG6lLvD +u79leNKGef9JOxqDDPDeeOzI8k1MGt6CKfjBWtrt7uYnXuhF0J0cUahoq0Tj0Itq +4/g7u9xN12TyUb7mqqta6THuBrxzvxNiCp/HuZc= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 3 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 3" +# Serial: 1 +# MD5 Fingerprint: ca:fb:40:a8:4e:39:92:8a:1d:fe:8e:2f:c4:27:ea:ef +# SHA1 Fingerprint: 55:a6:72:3e:cb:f2:ec:cd:c3:23:74:70:19:9d:2a:be:11:e3:81:d1 +# SHA256 Fingerprint: fd:73:da:d3:1c:64:4f:f1:b4:3b:ef:0c:cd:da:96:71:0b:9c:d9:87:5e:ca:7e:31:70:7a:f3:e9:6d:52:2b:bd +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDMwHhcNMDgxMDAxMTAyOTU2WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDMwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC9dZPwYiJvJK7genasfb3ZJNW4t/zN +8ELg63iIVl6bmlQdTQyK9tPPcPRStdiTBONGhnFBSivwKixVA9ZIw+A5OO3yXDw/ +RLyTPWGrTs0NvvAgJ1gORH8EGoel15YUNpDQSXuhdfsaa3Ox+M6pCSzyU9XDFES4 +hqX2iys52qMzVNn6chr3IhUciJFrf2blw2qAsCTz34ZFiP0Zf3WHHx+xGwpzJFu5 +ZeAsVMhg02YXP+HMVDNzkQI6pn97djmiH5a2OK61yJN0HZ65tOVgnS9W0eDrXltM +EnAMbEQgqxHY9Bn20pxSN+f6tsIxO0rUFJmtxxr1XV/6B7h8DR/Wgx6zAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS1 +A/d2O2GCahKqGFPrAyGUv/7OyjANBgkqhkiG9w0BAQsFAAOCAQEAVj3vlNW92nOy +WL6ukK2YJ5f+AbGwUgC4TeQbIXQbfsDuXmkqJa9c1h3a0nnJ85cp4IaH3gRZD/FZ +1GSFS5mvJQQeyUapl96Cshtwn5z2r3Ex3XsFpSzTucpH9sry9uetuUg/vBa3wW30 +6gmv7PO15wWeph6KU1HWk4HMdJP2udqmJQV0eVp+QD6CSyYRMG7hP0HHRwA11fXT +91Q+gT3aSWqas+8QPebrb9HIIkfLzM8BMZLZGOMivgkeGj5asuRrDFR6fUNOuIml +e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p +TpPDpFQUWw== +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 2009" +# Serial: 623603 +# MD5 Fingerprint: cd:e0:25:69:8d:47:ac:9c:89:35:90:f7:fd:51:3d:2f +# SHA1 Fingerprint: 58:e8:ab:b0:36:15:33:fb:80:f7:9b:1b:6d:29:d3:ff:8d:5f:00:f0 +# SHA256 Fingerprint: 49:e7:a4:42:ac:f0:ea:62:87:05:00:54:b5:25:64:b6:50:e4:f4:9e:42:e3:48:d6:aa:38:e0:39:e9:57:b1:c1 +-----BEGIN CERTIFICATE----- +MIIEMzCCAxugAwIBAgIDCYPzMA0GCSqGSIb3DQEBCwUAME0xCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMMHkQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgMjAwOTAeFw0wOTExMDUwODM1NThaFw0yOTExMDUwODM1NTha +ME0xCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxJzAlBgNVBAMM +HkQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgMjAwOTCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBANOySs96R+91myP6Oi/WUEWJNTrGa9v+2wBoqOADER03 +UAifTUpolDWzU9GUY6cgVq/eUXjsKj3zSEhQPgrfRlWLJ23DEE0NkVJD2IfgXU42 +tSHKXzlABF9bfsyjxiupQB7ZNoTWSPOSHjRGICTBpFGOShrvUD9pXRl/RcPHAY9R +ySPocq60vFYJfxLLHLGvKZAKyVXMD9O0Gu1HNVpK7ZxzBCHQqr0ME7UAyiZsxGsM +lFqVlNpQmvH/pStmMaTJOKDfHR+4CS7zp+hnUquVH+BGPtikw8paxTGA6Eian5Rp +/hnd2HN8gcqW3o7tszIFZYQ05ub9VxC1X3a/L7AQDcUCAwEAAaOCARowggEWMA8G +A1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFP3aFMSfMN4hvR5COfyrYyNJ4PGEMA4G +A1UdDwEB/wQEAwIBBjCB0wYDVR0fBIHLMIHIMIGAoH6gfIZ6bGRhcDovL2RpcmVj +dG9yeS5kLXRydXN0Lm5ldC9DTj1ELVRSVVNUJTIwUm9vdCUyMENsYXNzJTIwMyUy +MENBJTIwMiUyMDIwMDksTz1ELVRydXN0JTIwR21iSCxDPURFP2NlcnRpZmljYXRl +cmV2b2NhdGlvbmxpc3QwQ6BBoD+GPWh0dHA6Ly93d3cuZC10cnVzdC5uZXQvY3Js +L2QtdHJ1c3Rfcm9vdF9jbGFzc18zX2NhXzJfMjAwOS5jcmwwDQYJKoZIhvcNAQEL +BQADggEBAH+X2zDI36ScfSF6gHDOFBJpiBSVYEQBrLLpME+bUMJm2H6NMLVwMeni +acfzcNsgFYbQDfC+rAF1hM5+n02/t2A7nPPKHeJeaNijnZflQGDSNiH+0LS4F9p0 +o3/U37CYAqxva2ssJSRyoWXuJVrl5jLn8t+rSfrzkGkj2wTZ51xY/GXUl77M/C4K +zCUqNQT4YJEVdT1B/yMfGchs64JTBKbkTCJNjYy6zltz7GRUUG3RnFX7acM2w4y8 +PIWmawomDeCTmGCufsYkl4phX5GOZpIJhzbNi5stPvZR1FDUWSi9g/LMKHtThm3Y +Johw1+qRzT65ysCQblrGXnRl11z+o+I= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Subject: CN=D-TRUST Root Class 3 CA 2 EV 2009 O=D-Trust GmbH +# Label: "D-TRUST Root Class 3 CA 2 EV 2009" +# Serial: 623604 +# MD5 Fingerprint: aa:c6:43:2c:5e:2d:cd:c4:34:c0:50:4f:11:02:4f:b6 +# SHA1 Fingerprint: 96:c9:1b:0b:95:b4:10:98:42:fa:d0:d8:22:79:fe:60:fa:b9:16:83 +# SHA256 Fingerprint: ee:c5:49:6b:98:8c:e9:86:25:b9:34:09:2e:ec:29:08:be:d0:b0:f3:16:c2:d4:73:0c:84:ea:f1:f3:d3:48:81 +-----BEGIN CERTIFICATE----- +MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF +MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD +bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw +NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV +BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI +hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn +ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0 +3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z +qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR +p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8 +HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw +ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea +HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw +Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh +c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E +RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt +dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku +Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp +3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05 +nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF +CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na +xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX +KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1 +-----END CERTIFICATE----- + +# Issuer: CN=CA Disig Root R2 O=Disig a.s. +# Subject: CN=CA Disig Root R2 O=Disig a.s. +# Label: "CA Disig Root R2" +# Serial: 10572350602393338211 +# MD5 Fingerprint: 26:01:fb:d8:27:a7:17:9a:45:54:38:1a:43:01:3b:03 +# SHA1 Fingerprint: b5:61:eb:ea:a4:de:e4:25:4b:69:1a:98:a5:57:47:c2:34:c7:d9:71 +# SHA256 Fingerprint: e2:3d:4a:03:6d:7b:70:e9:f5:95:b1:42:20:79:d2:b9:1e:df:bb:1f:b6:51:a0:63:3e:aa:8a:9d:c5:f8:07:03 +-----BEGIN CERTIFICATE----- +MIIFaTCCA1GgAwIBAgIJAJK4iNuwisFjMA0GCSqGSIb3DQEBCwUAMFIxCzAJBgNV +BAYTAlNLMRMwEQYDVQQHEwpCcmF0aXNsYXZhMRMwEQYDVQQKEwpEaXNpZyBhLnMu +MRkwFwYDVQQDExBDQSBEaXNpZyBSb290IFIyMB4XDTEyMDcxOTA5MTUzMFoXDTQy +MDcxOTA5MTUzMFowUjELMAkGA1UEBhMCU0sxEzARBgNVBAcTCkJyYXRpc2xhdmEx +EzARBgNVBAoTCkRpc2lnIGEucy4xGTAXBgNVBAMTEENBIERpc2lnIFJvb3QgUjIw +ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCio8QACdaFXS1tFPbCw3Oe +NcJxVX6B+6tGUODBfEl45qt5WDza/3wcn9iXAng+a0EE6UG9vgMsRfYvZNSrXaNH +PWSb6WiaxswbP7q+sos0Ai6YVRn8jG+qX9pMzk0DIaPY0jSTVpbLTAwAFjxfGs3I +x2ymrdMxp7zo5eFm1tL7A7RBZckQrg4FY8aAamkw/dLukO8NJ9+flXP04SXabBbe +QTg06ov80egEFGEtQX6sx3dOy1FU+16SGBsEWmjGycT6txOgmLcRK7fWV8x8nhfR +yyX+hk4kLlYMeE2eARKmK6cBZW58Yh2EhN/qwGu1pSqVg8NTEQxzHQuyRpDRQjrO +QG6Vrf/GlK1ul4SOfW+eioANSW1z4nuSHsPzwfPrLgVv2RvPN3YEyLRa5Beny912 +H9AZdugsBbPWnDTYltxhh5EF5EQIM8HauQhl1K6yNg3ruji6DOWbnuuNZt2Zz9aJ +QfYEkoopKW1rOhzndX0CcQ7zwOe9yxndnWCywmZgtrEE7snmhrmaZkCo5xHtgUUD +i/ZnWejBBhG93c+AAk9lQHhcR1DIm+YfgXvkRKhbhZri3lrVx/k6RGZL5DJUfORs +nLMOPReisjQS1n6yqEm70XooQL6iFh/f5DcfEXP7kAplQ6INfPgGAVUzfbANuPT1 +rqVCV3w2EYx7XsQDnYx5nQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1Ud +DwEB/wQEAwIBBjAdBgNVHQ4EFgQUtZn4r7CU9eMg1gqtzk5WpC5uQu0wDQYJKoZI +hvcNAQELBQADggIBACYGXnDnZTPIgm7ZnBc6G3pmsgH2eDtpXi/q/075KMOYKmFM +tCQSin1tERT3nLXK5ryeJ45MGcipvXrA1zYObYVybqjGom32+nNjf7xueQgcnYqf +GopTpti72TVVsRHFqQOzVju5hJMiXn7B9hJSi+osZ7z+Nkz1uM/Rs0mSO9MpDpkb +lvdhuDvEK7Z4bLQjb/D907JedR+Zlais9trhxTF7+9FGs9K8Z7RiVLoJ92Owk6Ka ++elSLotgEqv89WBW7xBci8QaQtyDW2QOy7W81k/BfDxujRNt+3vrMNDcTa/F1bal +TFtxyegxvug4BkihGuLq0t4SOVga/4AOgnXmt8kHbA7v/zjxmHHEt38OFdAlab0i +nSvtBfZGR6ztwPDUO+Ls7pZbkBNOHlY667DvlruWIxG68kOGdGSVyCh13x01utI3 +gzhTODY7z2zp+WsO0PsE6E9312UBeIYMej4hYvF/Y3EMyZ9E26gnonW+boE+18Dr +G5gPcFw0sorMwIUY6256s/daoQe/qUKS82Ail+QUoQebTnbAjn39pCXHR+3/H3Os +zMOl6W8KjptlwlCFtaOgUxLMVYdh84GuEEZhvUQhuMI9dM9+JDX6HAcOmz0iyu8x +L4ysEr3vQCj8KWefshNPZiTEUxnpHikV7+ZtsH8tZ/3zbBt1RqPlShfppNcL +-----END CERTIFICATE----- + +# Issuer: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Subject: CN=ACCVRAIZ1 O=ACCV OU=PKIACCV +# Label: "ACCVRAIZ1" +# Serial: 6828503384748696800 +# MD5 Fingerprint: d0:a0:5a:ee:05:b6:09:94:21:a1:7d:f1:b2:29:82:02 +# SHA1 Fingerprint: 93:05:7a:88:15:c6:4f:ce:88:2f:fa:91:16:52:28:78:bc:53:64:17 +# SHA256 Fingerprint: 9a:6e:c0:12:e1:a7:da:9d:be:34:19:4d:47:8a:d7:c0:db:18:22:fb:07:1d:f1:29:81:49:6e:d1:04:38:41:13 +-----BEGIN CERTIFICATE----- +MIIH0zCCBbugAwIBAgIIXsO3pkN/pOAwDQYJKoZIhvcNAQEFBQAwQjESMBAGA1UE +AwwJQUNDVlJBSVoxMRAwDgYDVQQLDAdQS0lBQ0NWMQ0wCwYDVQQKDARBQ0NWMQsw +CQYDVQQGEwJFUzAeFw0xMTA1MDUwOTM3MzdaFw0zMDEyMzEwOTM3MzdaMEIxEjAQ +BgNVBAMMCUFDQ1ZSQUlaMTEQMA4GA1UECwwHUEtJQUNDVjENMAsGA1UECgwEQUND +VjELMAkGA1UEBhMCRVMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCb +qau/YUqXry+XZpp0X9DZlv3P4uRm7x8fRzPCRKPfmt4ftVTdFXxpNRFvu8gMjmoY +HtiP2Ra8EEg2XPBjs5BaXCQ316PWywlxufEBcoSwfdtNgM3802/J+Nq2DoLSRYWo +G2ioPej0RGy9ocLLA76MPhMAhN9KSMDjIgro6TenGEyxCQ0jVn8ETdkXhBilyNpA +lHPrzg5XPAOBOp0KoVdDaaxXbXmQeOW1tDvYvEyNKKGno6e6Ak4l0Squ7a4DIrhr +IA8wKFSVf+DuzgpmndFALW4ir50awQUZ0m/A8p/4e7MCQvtQqR0tkw8jq8bBD5L/ +0KIV9VMJcRz/RROE5iZe+OCIHAr8Fraocwa48GOEAqDGWuzndN9wrqODJerWx5eH +k6fGioozl2A3ED6XPm4pFdahD9GILBKfb6qkxkLrQaLjlUPTAYVtjrs78yM2x/47 +4KElB0iryYl0/wiPgL/AlmXz7uxLaL2diMMxs0Dx6M/2OLuc5NF/1OVYm3z61PMO +m3WR5LpSLhl+0fXNWhn8ugb2+1KoS5kE3fj5tItQo05iifCHJPqDQsGH+tUtKSpa +cXpkatcnYGMN285J9Y0fkIkyF/hzQ7jSWpOGYdbhdQrqeWZ2iE9x6wQl1gpaepPl +uUsXQA+xtrn13k/c4LOsOxFwYIRKQ26ZIMApcQrAZQIDAQABo4ICyzCCAscwfQYI +KwYBBQUHAQEEcTBvMEwGCCsGAQUFBzAChkBodHRwOi8vd3d3LmFjY3YuZXMvZmls +ZWFkbWluL0FyY2hpdm9zL2NlcnRpZmljYWRvcy9yYWl6YWNjdjEuY3J0MB8GCCsG +AQUFBzABhhNodHRwOi8vb2NzcC5hY2N2LmVzMB0GA1UdDgQWBBTSh7Tj3zcnk1X2 +VuqB5TbMjB4/vTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNKHtOPfNyeT +VfZW6oHlNsyMHj+9MIIBcwYDVR0gBIIBajCCAWYwggFiBgRVHSAAMIIBWDCCASIG +CCsGAQUFBwICMIIBFB6CARAAQQB1AHQAbwByAGkAZABhAGQAIABkAGUAIABDAGUA +cgB0AGkAZgBpAGMAYQBjAGkA8wBuACAAUgBhAO0AegAgAGQAZQAgAGwAYQAgAEEA +QwBDAFYAIAAoAEEAZwBlAG4AYwBpAGEAIABkAGUAIABUAGUAYwBuAG8AbABvAGcA +7QBhACAAeQAgAEMAZQByAHQAaQBmAGkAYwBhAGMAaQDzAG4AIABFAGwAZQBjAHQA +cgDzAG4AaQBjAGEALAAgAEMASQBGACAAUQA0ADYAMAAxADEANQA2AEUAKQAuACAA +QwBQAFMAIABlAG4AIABoAHQAdABwADoALwAvAHcAdwB3AC4AYQBjAGMAdgAuAGUA +czAwBggrBgEFBQcCARYkaHR0cDovL3d3dy5hY2N2LmVzL2xlZ2lzbGFjaW9uX2Mu +aHRtMFUGA1UdHwROMEwwSqBIoEaGRGh0dHA6Ly93d3cuYWNjdi5lcy9maWxlYWRt +aW4vQXJjaGl2b3MvY2VydGlmaWNhZG9zL3JhaXphY2N2MV9kZXIuY3JsMA4GA1Ud +DwEB/wQEAwIBBjAXBgNVHREEEDAOgQxhY2N2QGFjY3YuZXMwDQYJKoZIhvcNAQEF +BQADggIBAJcxAp/n/UNnSEQU5CmH7UwoZtCPNdpNYbdKl02125DgBS4OxnnQ8pdp +D70ER9m+27Up2pvZrqmZ1dM8MJP1jaGo/AaNRPTKFpV8M9xii6g3+CfYCS0b78gU +JyCpZET/LtZ1qmxNYEAZSUNUY9rizLpm5U9EelvZaoErQNV/+QEnWCzI7UiRfD+m +AM/EKXMRNt6GGT6d7hmKG9Ww7Y49nCrADdg9ZuM8Db3VlFzi4qc1GwQA9j9ajepD +vV+JHanBsMyZ4k0ACtrJJ1vnE5Bc5PUzolVt3OAJTS+xJlsndQAJxGJ3KQhfnlms +tn6tn1QwIgPBHnFk/vk4CpYY3QIUrCPLBhwepH2NDd4nQeit2hW3sCPdK6jT2iWH +7ehVRE2I9DZ+hJp4rPcOVkkO1jMl1oRQQmwgEh0q1b688nCBpHBgvgW1m54ERL5h +I6zppSSMEYCUWqKiuUnSwdzRp+0xESyeGabu4VXhwOrPDYTkF7eifKXeVSUG7szA +h1xA2syVP1XgNce4hL60Xc16gwFy7ofmXx2utYXGJt/mwZrpHgJHnyqobalbz+xF +d3+YJ5oyXSrjhO7FmGYvliAd3djDJ9ew+f7Zfc3Qn48LFFhRny+Lwzgt3uiP1o2H +pPVWQxaZLPSkVrQ0uGE3ycJYgBugl6H8WY3pEfbRD0tVNEYqi4Y7 +-----END CERTIFICATE----- + +# Issuer: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA Global Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA Global Root CA" +# Serial: 3262 +# MD5 Fingerprint: f9:03:7e:cf:e6:9e:3c:73:7a:2a:90:07:69:ff:2b:96 +# SHA1 Fingerprint: 9c:bb:48:53:f6:a4:f6:d3:52:a4:e8:32:52:55:60:13:f5:ad:af:65 +# SHA256 Fingerprint: 59:76:90:07:f7:68:5d:0f:cd:50:87:2f:9f:95:d5:75:5a:5b:2b:45:7d:81:f3:69:2b:61:0a:98:67:2f:0e:1b +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgICDL4wDQYJKoZIhvcNAQELBQAwUTELMAkGA1UEBhMCVFcx +EjAQBgNVBAoTCVRBSVdBTi1DQTEQMA4GA1UECxMHUm9vdCBDQTEcMBoGA1UEAxMT +VFdDQSBHbG9iYWwgUm9vdCBDQTAeFw0xMjA2MjcwNjI4MzNaFw0zMDEyMzExNTU5 +NTlaMFExCzAJBgNVBAYTAlRXMRIwEAYDVQQKEwlUQUlXQU4tQ0ExEDAOBgNVBAsT +B1Jvb3QgQ0ExHDAaBgNVBAMTE1RXQ0EgR2xvYmFsIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCwBdvI64zEbooh745NnHEKH1Jw7W2CnJfF +10xORUnLQEK1EjRsGcJ0pDFfhQKX7EMzClPSnIyOt7h52yvVavKOZsTuKwEHktSz +0ALfUPZVr2YOy+BHYC8rMjk1Ujoog/h7FsYYuGLWRyWRzvAZEk2tY/XTP3VfKfCh +MBwqoJimFb3u/Rk28OKRQ4/6ytYQJ0lM793B8YVwm8rqqFpD/G2Gb3PpN0Wp8DbH +zIh1HrtsBv+baz4X7GGqcXzGHaL3SekVtTzWoWH1EfcFbx39Eb7QMAfCKbAJTibc +46KokWofwpFFiFzlmLhxpRUZyXx1EcxwdE8tmx2RRP1WKKD+u4ZqyPpcC1jcxkt2 +yKsi2XMPpfRaAok/T54igu6idFMqPVMnaR1sjjIsZAAmY2E2TqNGtz99sy2sbZCi +laLOz9qC5wc0GZbpuCGqKX6mOL6OKUohZnkfs8O1CWfe1tQHRvMq2uYiN2DLgbYP +oA/pyJV/v1WRBXrPPRXAb94JlAGD1zQbzECl8LibZ9WYkTunhHiVJqRaCPgrdLQA +BDzfuBSO6N+pjWxnkjMdwLfS7JLIvgm/LCkFbwJrnu+8vyq8W8BQj0FwcYeyTbcE +qYSjMq+u7msXi7Kx/mzhkIyIqJdIzshNy/MGz19qCkKxHh53L46g5pIOBvwFItIm +4TFRfTLcDwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zANBgkqhkiG9w0BAQsFAAOCAgEAXzSBdu+WHdXltdkCY4QWwa6gcFGn90xHNcgL +1yg9iXHZqjNB6hQbbCEAwGxCGX6faVsgQt+i0trEfJdLjbDorMjupWkEmQqSpqsn +LhpNgb+E1HAerUf+/UqdM+DyucRFCCEK2mlpc3INvjT+lIutwx4116KD7+U4x6WF +H6vPNOw/KP4M8VeGTslV9xzU2KV9Bnpv1d8Q34FOIWWxtuEXeZVFBs5fzNxGiWNo +RI2T9GRwoD2dKAXDOXC4Ynsg/eTb6QihuJ49CcdP+yz4k3ZB3lLg4VfSnQO8d57+ +nile98FRYB/e2guyLXW3Q0iT5/Z5xoRdgFlglPx4mI88k1HtQJAH32RjJMtOcQWh +15QaiDLxInQirqWm2BJpTGCjAu4r7NRjkgtevi92a6O2JryPA9gK8kxkRr05YuWW +6zRjESjMlfGt7+/cgFhI6Uu46mWs6fyAtbXIRfmswZ/ZuepiiI7E8UuDEq3mi4TW +nsLrgxifarsbJGAzcMzs9zLzXNl5fe+epP7JI8Mk7hWSsT2RTyaGvWZzJBPqpK5j +wa19hAM8EHiGG3njxPPyBJUgriOCxLM6AGK/5jYk4Ve6xx6QddVfP5VhK8E7zeWz +aGHQRiapIVJpLesux+t3zqY6tQMzT3bR51xUAV3LePTJDL/PEo4XLSNolOer/qmy +KwbQBM0= +-----END CERTIFICATE----- + +# Issuer: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Subject: CN=TeliaSonera Root CA v1 O=TeliaSonera +# Label: "TeliaSonera Root CA v1" +# Serial: 199041966741090107964904287217786801558 +# MD5 Fingerprint: 37:41:49:1b:18:56:9a:26:f5:ad:c2:66:fb:40:a5:4c +# SHA1 Fingerprint: 43:13:bb:96:f1:d5:86:9b:c1:4e:6a:92:f6:cf:f6:34:69:87:82:37 +# SHA256 Fingerprint: dd:69:36:fe:21:f8:f0:77:c1:23:a1:a5:21:c1:22:24:f7:22:55:b7:3e:03:a7:26:06:93:e8:a2:4b:0f:a3:89 +-----BEGIN CERTIFICATE----- +MIIFODCCAyCgAwIBAgIRAJW+FqD3LkbxezmCcvqLzZYwDQYJKoZIhvcNAQEFBQAw +NzEUMBIGA1UECgwLVGVsaWFTb25lcmExHzAdBgNVBAMMFlRlbGlhU29uZXJhIFJv +b3QgQ0EgdjEwHhcNMDcxMDE4MTIwMDUwWhcNMzIxMDE4MTIwMDUwWjA3MRQwEgYD +VQQKDAtUZWxpYVNvbmVyYTEfMB0GA1UEAwwWVGVsaWFTb25lcmEgUm9vdCBDQSB2 +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMK+6yfwIaPzaSZVfp3F +VRaRXP3vIb9TgHot0pGMYzHw7CTww6XScnwQbfQ3t+XmfHnqjLWCi65ItqwA3GV1 +7CpNX8GH9SBlK4GoRz6JI5UwFpB/6FcHSOcZrr9FZ7E3GwYq/t75rH2D+1665I+X +Z75Ljo1kB1c4VWk0Nj0TSO9P4tNmHqTPGrdeNjPUtAa9GAH9d4RQAEX1jF3oI7x+ +/jXh7VB7qTCNGdMJjmhnXb88lxhTuylixcpecsHHltTbLaC0H2kD7OriUPEMPPCs +81Mt8Bz17Ww5OXOAFshSsCPN4D7c3TxHoLs1iuKYaIu+5b9y7tL6pe0S7fyYGKkm +dtwoSxAgHNN/Fnct7W+A90m7UwW7XWjH1Mh1Fj+JWov3F0fUTPHSiXk+TT2YqGHe +Oh7S+F4D4MHJHIzTjU3TlTazN19jY5szFPAtJmtTfImMMsJu7D0hADnJoWjiUIMu +sDor8zagrC/kb2HCUQk5PotTubtn2txTuXZZNp1D5SDgPTJghSJRt8czu90VL6R4 +pgd7gUY2BIbdeTXHlSw7sKMXNeVzH7RcWe/a6hBle3rQf5+ztCo3O3CLm1u5K7fs +slESl1MpWtTwEhDcTwK7EpIvYtQ/aUN8Ddb8WHUBiJ1YFkveupD/RwGJBmr2X7KQ +arMCpgKIv7NHfirZ1fpoeDVNAgMBAAGjPzA9MA8GA1UdEwEB/wQFMAMBAf8wCwYD +VR0PBAQDAgEGMB0GA1UdDgQWBBTwj1k4ALP1j5qWDNXr+nuqF+gTEjANBgkqhkiG +9w0BAQUFAAOCAgEAvuRcYk4k9AwI//DTDGjkk0kiP0Qnb7tt3oNmzqjMDfz1mgbl +dxSR651Be5kqhOX//CHBXfDkH1e3damhXwIm/9fH907eT/j3HEbAek9ALCI18Bmx +0GtnLLCo4MBANzX2hFxc469CeP6nyQ1Q6g2EdvZR74NTxnr/DlZJLo961gzmJ1Tj +TQpgcmLNkQfWpb/ImWvtxBnmq0wROMVvMeJuScg/doAmAyYp4Db29iBT4xdwNBed +Y2gea+zDTYa4EzAvXUYNR0PVG6pZDrlcjQZIrXSHX8f8MVRBE+LHIQ6e4B4N4cB7 +Q4WQxYpYxmUKeFfyxiMPAdkgS94P+5KFdSpcc41teyWRyu5FrgZLAMzTsVlQ2jqI +OylDRl6XK1TOU2+NSueW+r9xDkKLfP0ooNBIytrEgUy7onOTJsjrDNYmiLbAJM+7 +vVvrdX3pCI6GMyx5dwlppYn8s3CQh3aP0yK7Qs69cwsgJirQmz1wHiRszYd2qReW +t88NkvuOGKmYSdGe/mBEciG5Ge3C9THxOUiIkCR1VBatzvT4aRRkOfujuLpwQMcn +HL/EVlP6Y2XQ8xwOFvVrhlhNGNTkDY6lnVuR3HYkUD/GKvvZt5y11ubQ2egZixVx +SK236thZiNSQvxaz2emsWWFUyBy6ysHK4bkgTI86k4mloMy/0/Z1pHWWbVY= +-----END CERTIFICATE----- + +# Issuer: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Subject: CN=T-TeleSec GlobalRoot Class 2 O=T-Systems Enterprise Services GmbH OU=T-Systems Trust Center +# Label: "T-TeleSec GlobalRoot Class 2" +# Serial: 1 +# MD5 Fingerprint: 2b:9b:9e:e4:7b:6c:1f:00:72:1a:cc:c1:77:79:df:6a +# SHA1 Fingerprint: 59:0d:2d:7d:88:4f:40:2e:61:7e:a5:62:32:17:65:cf:17:d8:94:e9 +# SHA256 Fingerprint: 91:e2:f5:78:8d:58:10:eb:a7:ba:58:73:7d:e1:54:8a:8e:ca:cd:01:45:98:bc:0b:14:3e:04:1b:17:05:25:52 +-----BEGIN CERTIFICATE----- +MIIDwzCCAqugAwIBAgIBATANBgkqhkiG9w0BAQsFADCBgjELMAkGA1UEBhMCREUx +KzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnByaXNlIFNlcnZpY2VzIEdtYkgxHzAd +BgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50ZXIxJTAjBgNVBAMMHFQtVGVsZVNl +YyBHbG9iYWxSb290IENsYXNzIDIwHhcNMDgxMDAxMTA0MDE0WhcNMzMxMDAxMjM1 +OTU5WjCBgjELMAkGA1UEBhMCREUxKzApBgNVBAoMIlQtU3lzdGVtcyBFbnRlcnBy +aXNlIFNlcnZpY2VzIEdtYkgxHzAdBgNVBAsMFlQtU3lzdGVtcyBUcnVzdCBDZW50 +ZXIxJTAjBgNVBAMMHFQtVGVsZVNlYyBHbG9iYWxSb290IENsYXNzIDIwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqX9obX+hzkeXaXPSi5kfl82hVYAUd +AqSzm1nzHoqvNK38DcLZSBnuaY/JIPwhqgcZ7bBcrGXHX+0CfHt8LRvWurmAwhiC +FoT6ZrAIxlQjgeTNuUk/9k9uN0goOA/FvudocP05l03Sx5iRUKrERLMjfTlH6VJi +1hKTXrcxlkIF+3anHqP1wvzpesVsqXFP6st4vGCvx9702cu+fjOlbpSD8DT6Iavq +jnKgP6TeMFvvhk1qlVtDRKgQFRzlAVfFmPHmBiiRqiDFt1MmUUOyCxGVWOHAD3bZ +wI18gfNycJ5v/hqO2V81xrJvNHy+SE/iWjnX2J14np+GPgNeGYtEotXHAgMBAAGj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS/ +WSA2AHmgoCJrjNXyYdK4LMuCSjANBgkqhkiG9w0BAQsFAAOCAQEAMQOiYQsfdOhy +NsZt+U2e+iKo4YFWz827n+qrkRk4r6p8FU3ztqONpfSO9kSpp+ghla0+AGIWiPAC +uvxhI+YzmzB6azZie60EI4RYZeLbK4rnJVM3YlNfvNoBYimipidx5joifsFvHZVw +IEoHNN/q/xWA5brXethbdXwFeilHfkCoMRN3zUA7tFFHei4R40cR3p1m0IvVVGb6 +g1XqfMIpiRvpb7PO4gWEyS8+eIVibslfwXhjdFjASBgMmTnrpMwatXlajRWc2BQN +9noHV8cigwUtPJslJj0Ys6lDfMjIq2SPDqO/nBudMNva0Bkuqjzx+zOAduTNrRlP +BSeOE6Fuwg== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot 2011 O=Atos +# Subject: CN=Atos TrustedRoot 2011 O=Atos +# Label: "Atos TrustedRoot 2011" +# Serial: 6643877497813316402 +# MD5 Fingerprint: ae:b9:c4:32:4b:ac:7f:5d:66:cc:77:94:bb:2a:77:56 +# SHA1 Fingerprint: 2b:b1:f5:3e:55:0c:1d:c5:f1:d4:e6:b7:6a:46:4b:55:06:02:ac:21 +# SHA256 Fingerprint: f3:56:be:a2:44:b7:a9:1e:b3:5d:53:ca:9a:d7:86:4a:ce:01:8e:2d:35:d5:f8:f9:6d:df:68:a6:f4:1a:a4:74 +-----BEGIN CERTIFICATE----- +MIIDdzCCAl+gAwIBAgIIXDPLYixfszIwDQYJKoZIhvcNAQELBQAwPDEeMBwGA1UE +AwwVQXRvcyBUcnVzdGVkUm9vdCAyMDExMQ0wCwYDVQQKDARBdG9zMQswCQYDVQQG +EwJERTAeFw0xMTA3MDcxNDU4MzBaFw0zMDEyMzEyMzU5NTlaMDwxHjAcBgNVBAMM +FUF0b3MgVHJ1c3RlZFJvb3QgMjAxMTENMAsGA1UECgwEQXRvczELMAkGA1UEBhMC +REUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVhTuXbyo7LjvPpvMp +Nb7PGKw+qtn4TaA+Gke5vJrf8v7MPkfoepbCJI419KkM/IL9bcFyYie96mvr54rM +VD6QUM+A1JX76LWC1BTFtqlVJVfbsVD2sGBkWXppzwO3bw2+yj5vdHLqqjAqc2K+ +SZFhyBH+DgMq92og3AIVDV4VavzjgsG1xZ1kCWyjWZgHJ8cblithdHFsQ/H3NYkQ +4J7sVaE3IqKHBAUsR320HLliKWYoyrfhk/WklAOZuXCFteZI6o1Q/NnezG8HDt0L +cp2AMBYHlT8oDv3FdU9T1nSatCQujgKRz3bFmx5VdJx4IbHwLfELn8LVlhgf8FQi +eowHAgMBAAGjfTB7MB0GA1UdDgQWBBSnpQaxLKYJYO7Rl+lwrrw7GWzbITAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFKelBrEspglg7tGX6XCuvDsZbNshMBgG +A1UdIAQRMA8wDQYLKwYBBAGwLQMEAQEwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3 +DQEBCwUAA4IBAQAmdzTblEiGKkGdLD4GkGDEjKwLVLgfuXvTBznk+j57sj1O7Z8j +vZfza1zv7v1Apt+hk6EKhqzvINB5Ab149xnYJDE0BAGmuhWawyfc2E8PzBhj/5kP +DpFrdRbhIfzYJsdHt6bPWHJxfrrhTZVHO8mvbaG0weyJ9rQPOLXiZNwlz6bb65pc +maHFCN795trV1lpFDMS3wrUU77QR/w4VtfX128a961qn8FYiqTxlVMYVqL2Gns2D +lmh6cYGJ4Qvh6hEbaAjMaZ7snkGeRDImeuKHCnE96+RapNLbxc3G3mB/ufNPRJLv +KrcYPqcZ2Qt9sTdBQrC6YB3y/gkRsPCHe6ed +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 1 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 1 G3" +# Serial: 687049649626669250736271037606554624078720034195 +# MD5 Fingerprint: a4:bc:5b:3f:fe:37:9a:fa:64:f0:e2:fa:05:3d:0b:ab +# SHA1 Fingerprint: 1b:8e:ea:57:96:29:1a:c9:39:ea:b8:0a:81:1a:73:73:c0:93:79:67 +# SHA256 Fingerprint: 8a:86:6f:d1:b2:76:b5:7e:57:8e:92:1c:65:82:8a:2b:ed:58:e9:f2:f2:88:05:41:34:b7:f1:f4:bf:c9:cc:74 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIUeFhfLq0sGUvjNwc1NBMotZbUZZMwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMSBHMzAeFw0xMjAxMTIxNzI3NDRaFw00 +MjAxMTIxNzI3NDRaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDEgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCgvlAQjunybEC0BJyFuTHK3C3kEakEPBtV +wedYMB0ktMPvhd6MLOHBPd+C5k+tR4ds7FtJwUrVu4/sh6x/gpqG7D0DmVIB0jWe +rNrwU8lmPNSsAgHaJNM7qAJGr6Qc4/hzWHa39g6QDbXwz8z6+cZM5cOGMAqNF341 +68Xfuw6cwI2H44g4hWf6Pser4BOcBRiYz5P1sZK0/CPTz9XEJ0ngnjybCKOLXSoh +4Pw5qlPafX7PGglTvF0FBM+hSo+LdoINofjSxxR3W5A2B4GbPgb6Ul5jxaYA/qXp +UhtStZI5cgMJYr2wYBZupt0lwgNm3fME0UDiTouG9G/lg6AnhF4EwfWQvTA9xO+o +abw4m6SkltFi2mnAAZauy8RRNOoMqv8hjlmPSlzkYZqn0ukqeI1RPToV7qJZjqlc +3sX5kCLliEVx3ZGZbHqfPT2YfF72vhZooF6uCyP8Wg+qInYtyaEQHeTTRCOQiJ/G +KubX9ZqzWB4vMIkIG1SitZgj7Ah3HJVdYdHLiZxfokqRmu8hqkkWCKi9YSgxyXSt +hfbZxbGL0eUQMk1fiyA6PEkfM4VZDdvLCXVDaXP7a3F98N/ETH3Goy7IlXnLc6KO +Tk0k+17kBL5yG6YnLUlamXrXXAkgt3+UuU/xDRxeiEIbEbfnkduebPRq34wGmAOt +zCjvpUfzUwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUo5fW816iEOGrRZ88F2Q87gFwnMwwDQYJKoZIhvcNAQELBQAD +ggIBABj6W3X8PnrHX3fHyt/PX8MSxEBd1DKquGrX1RUVRpgjpeaQWxiZTOOtQqOC +MTaIzen7xASWSIsBx40Bz1szBpZGZnQdT+3Btrm0DWHMY37XLneMlhwqI2hrhVd2 +cDMT/uFPpiN3GPoajOi9ZcnPP/TJF9zrx7zABC4tRi9pZsMbj/7sPtPKlL92CiUN +qXsCHKnQO18LwIE6PWThv6ctTr1NxNgpxiIY0MWscgKCP6o6ojoilzHdCGPDdRS5 +YCgtW2jgFqlmgiNR9etT2DGbe+m3nUvriBbP+V04ikkwj+3x6xn0dxoxGE1nVGwv +b2X52z3sIexe9PSLymBlVNFxZPT5pqOBMzYzcfCkeF9OrYMh3jRJjehZrJ3ydlo2 +8hP0r+AJx2EqbPfgna67hkooby7utHnNkDPDs3b69fBsnQGQ+p6Q9pxyz0fawx/k +NSBT8lTR32GDpgLiJTjehTItXnOQUl1CxM49S+H5GYQd1aJQzEH7QRTDvdbJWqNj +ZgKAvQU6O0ec7AAmTPWIUb+oI38YB7AL7YsmoWTTYUrrXJ/es69nA7Mf3W1daWhp +q1467HxpvMc7hU6eFbm0FU/DlXpY18ls6Wy58yljXrQs8C097Vpl4KlbQMJImYFt +nh8GKjwStIsPm6Ik8KaN1nrgS7ZklmOVhMJKzRwuJIczYOXD +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 2 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 2 G3" +# Serial: 390156079458959257446133169266079962026824725800 +# MD5 Fingerprint: af:0c:86:6e:bf:40:2d:7f:0b:3e:12:50:ba:12:3d:06 +# SHA1 Fingerprint: 09:3c:61:f3:8b:8b:dc:7d:55:df:75:38:02:05:00:e1:25:f5:c8:36 +# SHA256 Fingerprint: 8f:e4:fb:0a:f9:3a:4d:0d:67:db:0b:eb:b2:3e:37:c7:1b:f3:25:dc:bc:dd:24:0e:a0:4d:af:58:b4:7e:18:40 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIURFc0JFuBiZs18s64KztbpybwdSgwDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMiBHMzAeFw0xMjAxMTIxODU5MzJaFw00 +MjAxMTIxODU5MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDIgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQChriWyARjcV4g/Ruv5r+LrI3HimtFhZiFf +qq8nUeVuGxbULX1QsFN3vXg6YOJkApt8hpvWGo6t/x8Vf9WVHhLL5hSEBMHfNrMW +n4rjyduYNM7YMxcoRvynyfDStNVNCXJJ+fKH46nafaF9a7I6JaltUkSs+L5u+9ym +c5GQYaYDFCDy54ejiK2toIz/pgslUiXnFgHVy7g1gQyjO/Dh4fxaXc6AcW34Sas+ +O7q414AB+6XrW7PFXmAqMaCvN+ggOp+oMiwMzAkd056OXbxMmO7FGmh77FOm6RQ1 +o9/NgJ8MSPsc9PG/Srj61YxxSscfrf5BmrODXfKEVu+lV0POKa2Mq1W/xPtbAd0j +IaFYAI7D0GoT7RPjEiuA3GfmlbLNHiJuKvhB1PLKFAeNilUSxmn1uIZoL1NesNKq +IcGY5jDjZ1XHm26sGahVpkUG0CM62+tlXSoREfA7T8pt9DTEceT/AFr2XK4jYIVz +8eQQsSWu1ZK7E8EM4DnatDlXtas1qnIhO4M15zHfeiFuuDIIfR0ykRVKYnLP43eh +vNURG3YBZwjgQQvD6xVu+KQZ2aKrr+InUlYrAoosFCT5v0ICvybIxo/gbjh9Uy3l +7ZizlWNof/k19N+IxWA1ksB8aRxhlRbQ694Lrz4EEEVlWFA4r0jyWbYW8jwNkALG +cC4BrTwV1wIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQU7edvdlq/YOxJW8ald7tyFnGbxD0wDQYJKoZIhvcNAQELBQAD +ggIBAJHfgD9DCX5xwvfrs4iP4VGyvD11+ShdyLyZm3tdquXK4Qr36LLTn91nMX66 +AarHakE7kNQIXLJgapDwyM4DYvmL7ftuKtwGTTwpD4kWilhMSA/ohGHqPHKmd+RC +roijQ1h5fq7KpVMNqT1wvSAZYaRsOPxDMuHBR//47PERIjKWnML2W2mWeyAMQ0Ga +W/ZZGYjeVYg3UQt4XAoeo0L9x52ID8DyeAIkVJOviYeIyUqAHerQbj5hLja7NQ4n +lv1mNDthcnPxFlxHBlRJAHpYErAK74X9sbgzdWqTHBLmYF5vHX/JHyPLhGGfHoJE ++V+tYlUkmlKY7VHnoX6XOuYvHxHaU4AshZ6rNRDbIl9qxV6XU/IyAgkwo1jwDQHV +csaxfGl7w/U2Rcxhbl5MlMVerugOXou/983g7aEOGzPuVBj+D77vfoRrQ+NwmNtd +dbINWQeFFSM51vHfqSYP1kjHs6Yi9TM3WpVHn3u6GBVv/9YUZINJ0gpnIdsPNWNg +KCLjsZWDzYWm3S8P52dSbrsvhXz1SnPnxT7AvSESBT/8twNJAlvIJebiVDj1eYeM +HVOyToV7BjjHLPj4sHKNJeV3UvQDHEimUF+IIDBu8oJDqz2XhOdT+yHBTw8imoa4 +WSr2Rz0ZiC3oheGe7IUIarFsNMkd7EgrO3jtZsSOeWmD3n+M +-----END CERTIFICATE----- + +# Issuer: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Subject: CN=QuoVadis Root CA 3 G3 O=QuoVadis Limited +# Label: "QuoVadis Root CA 3 G3" +# Serial: 268090761170461462463995952157327242137089239581 +# MD5 Fingerprint: df:7d:b9:ad:54:6f:68:a1:df:89:57:03:97:43:b0:d7 +# SHA1 Fingerprint: 48:12:bd:92:3c:a8:c4:39:06:e7:30:6d:27:96:e6:a4:cf:22:2e:7d +# SHA256 Fingerprint: 88:ef:81:de:20:2e:b0:18:45:2e:43:f8:64:72:5c:ea:5f:bd:1f:c2:d9:d2:05:73:07:09:c5:d8:b8:69:0f:46 +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIULvWbAiin23r/1aOp7r0DoM8Sah0wDQYJKoZIhvcNAQEL +BQAwSDELMAkGA1UEBhMCQk0xGTAXBgNVBAoTEFF1b1ZhZGlzIExpbWl0ZWQxHjAc +BgNVBAMTFVF1b1ZhZGlzIFJvb3QgQ0EgMyBHMzAeFw0xMjAxMTIyMDI2MzJaFw00 +MjAxMTIyMDI2MzJaMEgxCzAJBgNVBAYTAkJNMRkwFwYDVQQKExBRdW9WYWRpcyBM +aW1pdGVkMR4wHAYDVQQDExVRdW9WYWRpcyBSb290IENBIDMgRzMwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCzyw4QZ47qFJenMioKVjZ/aEzHs286IxSR +/xl/pcqs7rN2nXrpixurazHb+gtTTK/FpRp5PIpM/6zfJd5O2YIyC0TeytuMrKNu +FoM7pmRLMon7FhY4futD4tN0SsJiCnMK3UmzV9KwCoWdcTzeo8vAMvMBOSBDGzXR +U7Ox7sWTaYI+FrUoRqHe6okJ7UO4BUaKhvVZR74bbwEhELn9qdIoyhA5CcoTNs+c +ra1AdHkrAj80//ogaX3T7mH1urPnMNA3I4ZyYUUpSFlob3emLoG+B01vr87ERROR +FHAGjx+f+IdpsQ7vw4kZ6+ocYfx6bIrc1gMLnia6Et3UVDmrJqMz6nWB2i3ND0/k +A9HvFZcba5DFApCTZgIhsUfei5pKgLlVj7WiL8DWM2fafsSntARE60f75li59wzw +eyuxwHApw0BiLTtIadwjPEjrewl5qW3aqDCYz4ByA4imW0aucnl8CAMhZa634Ryl +sSqiMd5mBPfAdOhx3v89WcyWJhKLhZVXGqtrdQtEPREoPHtht+KPZ0/l7DxMYIBp +VzgeAVuNVejH38DMdyM0SXV89pgR6y3e7UEuFAUCf+D+IOs15xGsIs5XPd7JMG0Q +A4XN8f+MFrXBsj6IbGB/kE+V9/YtrQE5BwT6dYB9v0lQ7e/JxHwc64B+27bQ3RP+ +ydOc17KXqQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +BjAdBgNVHQ4EFgQUxhfQvKjqAkPyGwaZXSuQILnXnOQwDQYJKoZIhvcNAQELBQAD +ggIBADRh2Va1EodVTd2jNTFGu6QHcrxfYWLopfsLN7E8trP6KZ1/AvWkyaiTt3px +KGmPc+FSkNrVvjrlt3ZqVoAh313m6Tqe5T72omnHKgqwGEfcIHB9UqM+WXzBusnI +FUBhynLWcKzSt/Ac5IYp8M7vaGPQtSCKFWGafoaYtMnCdvvMujAWzKNhxnQT5Wvv +oxXqA/4Ti2Tk08HS6IT7SdEQTXlm66r99I0xHnAUrdzeZxNMgRVhvLfZkXdxGYFg +u/BYpbWcC/ePIlUnwEsBbTuZDdQdm2NnL9DuDcpmvJRPpq3t/O5jrFc/ZSXPsoaP +0Aj/uHYUbt7lJ+yreLVTubY/6CD50qi+YUbKh4yE8/nxoGibIh6BJpsQBJFxwAYf +3KDTuVan45gtf4Od34wrnDKOMpTwATwiKp9Dwi7DmDkHOHv8XgBCH/MyJnmDhPbl +8MFREsALHgQjDFSlTC9JxUrRtm5gDWv8a4uFJGS3iQ6rJUdbPM9+Sb3H6QrG2vd+ +DhcI00iX0HGS8A85PjRqHH3Y8iKuu2n0M7SmSFXRDw4m6Oy2Cy2nhTXN/VnIn9HN +PlopNLk9hM6xZdRZkZFWdSHBd575euFgndOtBBj0fOtek49TSiIp+EgrPk2GrFt/ +ywaZWWDYWGWVjUTR939+J399roD1B0y2PpxxVJkES/1Y+Zj0 +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G2" +# Serial: 15385348160840213938643033620894905419 +# MD5 Fingerprint: 92:38:b9:f8:63:24:82:65:2c:57:33:e6:fe:81:8f:9d +# SHA1 Fingerprint: a1:4b:48:d9:43:ee:0a:0e:40:90:4f:3c:e0:a4:c0:91:93:51:5d:3f +# SHA256 Fingerprint: 7d:05:eb:b6:82:33:9f:8c:94:51:ee:09:4e:eb:fe:fa:79:53:a1:14:ed:b2:f4:49:49:45:2f:ab:7d:2f:c1:85 +-----BEGIN CERTIFICATE----- +MIIDljCCAn6gAwIBAgIQC5McOtY5Z+pnI7/Dr5r0SzANBgkqhkiG9w0BAQsFADBl +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJv +b3QgRzIwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQG +EwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNl +cnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzIwggEi +MA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDZ5ygvUj82ckmIkzTz+GoeMVSA +n61UQbVH35ao1K+ALbkKz3X9iaV9JPrjIgwrvJUXCzO/GU1BBpAAvQxNEP4Htecc +biJVMWWXvdMX0h5i89vqbFCMP4QMls+3ywPgym2hFEwbid3tALBSfK+RbLE4E9Hp +EgjAALAcKxHad3A2m67OeYfcgnDmCXRwVWmvo2ifv922ebPynXApVfSr/5Vh88lA +bx3RvpO704gqu52/clpWcTs/1PPRCv4o76Pu2ZmvA9OPYLfykqGxvYmJHzDNw6Yu +YjOuFgJ3RFrngQo8p0Quebg/BLxcoIfhG69Rjs3sLPr4/m3wOnyqi+RnlTGNAgMB +AAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQW +BBTOw0q5mVXyuNtgv6l+vVa1lzan1jANBgkqhkiG9w0BAQsFAAOCAQEAyqVVjOPI +QW5pJ6d1Ee88hjZv0p3GeDgdaZaikmkuOGybfQTUiaWxMTeKySHMq2zNixya1r9I +0jJmwYrA8y8678Dj1JGG0VDjA9tzd29KOVPt3ibHtX2vK0LRdWLjSisCx1BL4Gni +lmwORGYQRI+tBev4eaymG+g3NJ1TyWGqolKvSnAWhsI6yLETcDbYz+70CjTVW0z9 +B5yiutkBclzzTcHdDrEcDcRjvq30FPuJ7KJBDkzMyFdA0G4Dqs0MjomZmWzwPDCv +ON9vvKO+KSAnq3T/EyJ43pdSVR6DtVQgA+6uwE9W3jfMw3+qBCe703e4YtsXfJwo +IhNzbM8m9Yop5w== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Assured ID Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Assured ID Root G3" +# Serial: 15459312981008553731928384953135426796 +# MD5 Fingerprint: 7c:7f:65:31:0c:81:df:8d:ba:3e:99:e2:5c:ad:6e:fb +# SHA1 Fingerprint: f5:17:a2:4f:9a:48:c6:c9:f8:a2:00:26:9f:dc:0f:48:2c:ab:30:89 +# SHA256 Fingerprint: 7e:37:cb:8b:4c:47:09:0c:ab:36:55:1b:a6:f4:5d:b8:40:68:0f:ba:16:6a:95:2d:b1:00:71:7f:43:05:3f:c2 +-----BEGIN CERTIFICATE----- +MIICRjCCAc2gAwIBAgIQC6Fa+h3foLVJRK/NJKBs7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3Qg +RzMwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBlMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgRzMwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAAQZ57ysRGXtzbg/WPuNsVepRC0FFfLvC/8QdJ+1YlJf +Zn4f5dwbRXkLzMZTCp2NXQLZqVneAlr2lSoOjThKiknGvMYDOAdfVdp+CW7if17Q +RSAPWXYQ1qAk8C3eNvJsKTmjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/ +BAQDAgGGMB0GA1UdDgQWBBTL0L2p4ZgFUaFNN6KDec6NHSrkhDAKBggqhkjOPQQD +AwNnADBkAjAlpIFFAmsSS3V0T8gj43DydXLefInwz5FyYZ5eEJJZVrmDxxDnOOlY +JjZ91eQ0hjkCMHw2U/Aw5WJjOpnitqM7mzT6HtoQknFekROn3aRukswy1vUhZscv +6pZjamVFkpUBtA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G2 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G2" +# Serial: 4293743540046975378534879503202253541 +# MD5 Fingerprint: e4:a6:8a:c8:54:ac:52:42:46:0a:fd:72:48:1b:2a:44 +# SHA1 Fingerprint: df:3c:24:f9:bf:d6:66:76:1b:26:80:73:fe:06:d1:cc:8d:4f:82:a4 +# SHA256 Fingerprint: cb:3c:cb:b7:60:31:e5:e0:13:8f:8d:d3:9a:23:f9:de:47:ff:c3:5e:43:c1:14:4c:ea:27:d4:6a:5a:b1:cb:5f +-----BEGIN CERTIFICATE----- +MIIDjjCCAnagAwIBAgIQAzrx5qcRqaC7KGSxHQn65TANBgkqhkiG9w0BAQsFADBh +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBH +MjAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVT +MRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5j +b20xIDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEcyMIIBIjANBgkqhkiG +9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuzfNNNx7a8myaJCtSnX/RrohCgiN9RlUyfuI +2/Ou8jqJkTx65qsGGmvPrC3oXgkkRLpimn7Wo6h+4FR1IAWsULecYxpsMNzaHxmx +1x7e/dfgy5SDN67sH0NO3Xss0r0upS/kqbitOtSZpLYl6ZtrAGCSYP9PIUkY92eQ +q2EGnI/yuum06ZIya7XzV+hdG82MHauVBJVJ8zUtluNJbd134/tJS7SsVQepj5Wz +tCO7TG1F8PapspUwtP1MVYwnSlcUfIKdzXOS0xZKBgyMUNGPHgm+F6HmIcr9g+UQ +vIOlCsRnKPZzFBQ9RnbDhxSJITRNrw9FDKZJobq7nMWxM4MphQIDAQABo0IwQDAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAdBgNVHQ4EFgQUTiJUIBiV +5uNu5g/6+rkS7QYXjzkwDQYJKoZIhvcNAQELBQADggEBAGBnKJRvDkhj6zHd6mcY +1Yl9PMWLSn/pvtsrF9+wX3N3KjITOYFnQoQj8kVnNeyIv/iPsGEMNKSuIEyExtv4 +NeF22d+mQrvHRAiGfzZ0JFrabA0UWTW98kndth/Jsw1HKj2ZL7tcu7XUIOGZX1NG +Fdtom/DzMNU+MeKNhJ7jitralj41E6Vf8PlwUHBHQRFXGU7Aj64GxJUTFy8bJZ91 +8rGOmaFvE7FBcf6IKshPECBV1/MUReXgRPTqh5Uykw7+U0b6LJ3/iyK5S9kJRaTe +pLiaWN0bfVKfjllDiIGknibVb63dDcY3fe0Dkhvld1927jyNxF1WW6LZZm6zNTfl +MrY= +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Global Root G3 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Global Root G3" +# Serial: 7089244469030293291760083333884364146 +# MD5 Fingerprint: f5:5d:a4:50:a5:fb:28:7e:1e:0f:0d:cc:96:57:56:ca +# SHA1 Fingerprint: 7e:04:de:89:6a:3e:66:6d:00:e6:87:d3:3f:fa:d9:3b:e8:3d:34:9e +# SHA256 Fingerprint: 31:ad:66:48:f8:10:41:38:c7:38:f3:9e:a4:32:01:33:39:3e:3a:18:cc:02:29:6e:f9:7c:2a:c9:ef:67:31:d0 +-----BEGIN CERTIFICATE----- +MIICPzCCAcWgAwIBAgIQBVVWvPJepDU1w6QP1atFcjAKBggqhkjOPQQDAzBhMQsw +CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu +ZGlnaWNlcnQuY29tMSAwHgYDVQQDExdEaWdpQ2VydCBHbG9iYWwgUm9vdCBHMzAe +Fw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGExCzAJBgNVBAYTAlVTMRUw +EwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20x +IDAeBgNVBAMTF0RpZ2lDZXJ0IEdsb2JhbCBSb290IEczMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAE3afZu4q4C/sLfyHS8L6+c/MzXRq8NOrexpu80JX28MzQC7phW1FG +fp4tn+6OYwwX7Adw9c+ELkCDnOg/QW07rdOkFFk2eJ0DQ+4QE2xy3q6Ip6FrtUPO +Z9wj/wMco+I+o0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBhjAd +BgNVHQ4EFgQUs9tIpPmhxdiuNkHMEWNpYim8S8YwCgYIKoZIzj0EAwMDaAAwZQIx +AK288mw/EkrRLTnDCgmXc/SINoyIJ7vmiI1Qhadj+Z4y3maTD/HMsQmP3Wyr+mt/ +oAIwOWZbwmSNuJ5Q3KjVSaLtx9zRSX8XAbjIho9OjIgrqJqpisXRAL34VOKa5Vt8 +sycX +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Subject: CN=DigiCert Trusted Root G4 O=DigiCert Inc OU=www.digicert.com +# Label: "DigiCert Trusted Root G4" +# Serial: 7451500558977370777930084869016614236 +# MD5 Fingerprint: 78:f2:fc:aa:60:1f:2f:b4:eb:c9:37:ba:53:2e:75:49 +# SHA1 Fingerprint: dd:fb:16:cd:49:31:c9:73:a2:03:7d:3f:c8:3a:4d:7d:77:5d:05:e4 +# SHA256 Fingerprint: 55:2f:7b:dc:f1:a7:af:9e:6c:e6:72:01:7f:4f:12:ab:f7:72:40:c7:8e:76:1a:c2:03:d1:d9:d2:0a:c8:99:88 +-----BEGIN CERTIFICATE----- +MIIFkDCCA3igAwIBAgIQBZsbV56OITLiOQe9p3d1XDANBgkqhkiG9w0BAQwFADBi +MQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3 +d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3Qg +RzQwHhcNMTMwODAxMTIwMDAwWhcNMzgwMTE1MTIwMDAwWjBiMQswCQYDVQQGEwJV +UzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQu +Y29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3y +ithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1If +xp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDV +ySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiO +DCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQ +jdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/ +CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCi +EhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADM +fRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QY +uKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXK +chYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t +9dmpsh3lGwIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIB +hjAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wDQYJKoZIhvcNAQEMBQAD +ggIBALth2X2pbL4XxJEbw6GiAI3jZGgPVs93rnD5/ZpKmbnJeFwMDF/k5hQpVgs2 +SV1EY+CtnJYYZhsjDT156W1r1lT40jzBQ0CuHVD1UvyQO7uYmWlrx8GnqGikJ9yd ++SeuMIW59mdNOj6PWTkiU0TryF0Dyu1Qen1iIQqAyHNm0aAFYF/opbSnr6j3bTWc +fFqK1qI4mfN4i/RN0iAL3gTujJtHgXINwBQy7zBZLq7gcfJW5GqXb5JQbZaNaHqa +sjYUegbyJLkJEVDXCLG4iXqEI2FCKeWjzaIgQdfRnGTZ6iahixTXTBmyUEFxPT9N +cCOGDErcgdLMMpSEDQgJlxxPwO5rIHQw0uA5NBCFIRUBCOhVMt5xSdkoF1BN5r5N +0XWs0Mr7QbhDparTwwVETyw2m+L64kW4I1NsBm9nVX9GtUw/bihaeSbSpKhil9Ie +4u1Ki7wb/UdKDd9nZn6yW0HQO+T0O/QEY+nvwlQAUaCKKsnOeMzV6ocEGLPOr0mI +r/OSmbaz5mEP0oUA51Aa5BuVnRmhuZyxm7EAHu/QD09CbMkKvO5D+jpxpchNJqU1 +/YldvIViHTLSoCtU7ZpXwdv6EM8Zt4tKG48BtieVU+i2iW1bvGjUI+iLUaJW+fCm +gKDWHrO8Dw9TdSmq6hN35N6MgSGtBxBHEa2HPQfRdbzP82Z+ +-----END CERTIFICATE----- + +# Issuer: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Subject: CN=COMODO RSA Certification Authority O=COMODO CA Limited +# Label: "COMODO RSA Certification Authority" +# Serial: 101909084537582093308941363524873193117 +# MD5 Fingerprint: 1b:31:b0:71:40:36:cc:14:36:91:ad:c4:3e:fd:ec:18 +# SHA1 Fingerprint: af:e5:d2:44:a8:d1:19:42:30:ff:47:9f:e2:f8:97:bb:cd:7a:8c:b4 +# SHA256 Fingerprint: 52:f0:e1:c4:e5:8e:c6:29:29:1b:60:31:7f:07:46:71:b8:5d:7e:a8:0d:5b:07:27:34:63:53:4b:32:b4:02:34 +-----BEGIN CERTIFICATE----- +MIIF2DCCA8CgAwIBAgIQTKr5yttjb+Af907YWwOGnTANBgkqhkiG9w0BAQwFADCB +hTELMAkGA1UEBhMCR0IxGzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4G +A1UEBxMHU2FsZm9yZDEaMBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxKzApBgNV +BAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMTE5 +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBhTELMAkGA1UEBhMCR0IxGzAZBgNVBAgT +EkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEaMBgGA1UEChMR +Q09NT0RPIENBIExpbWl0ZWQxKzApBgNVBAMTIkNPTU9ETyBSU0EgQ2VydGlmaWNh +dGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCR +6FSS0gpWsawNJN3Fz0RndJkrN6N9I3AAcbxT38T6KhKPS38QVr2fcHK3YX/JSw8X +pz3jsARh7v8Rl8f0hj4K+j5c+ZPmNHrZFGvnnLOFoIJ6dq9xkNfs/Q36nGz637CC +9BR++b7Epi9Pf5l/tfxnQ3K9DADWietrLNPtj5gcFKt+5eNu/Nio5JIk2kNrYrhV +/erBvGy2i/MOjZrkm2xpmfh4SDBF1a3hDTxFYPwyllEnvGfDyi62a+pGx8cgoLEf +Zd5ICLqkTqnyg0Y3hOvozIFIQ2dOciqbXL1MGyiKXCJ7tKuY2e7gUYPDCUZObT6Z ++pUX2nwzV0E8jVHtC7ZcryxjGt9XyD+86V3Em69FmeKjWiS0uqlWPc9vqv9JWL7w +qP/0uK3pN/u6uPQLOvnoQ0IeidiEyxPx2bvhiWC4jChWrBQdnArncevPDt09qZah +SL0896+1DSJMwBGB7FY79tOi4lu3sgQiUpWAk2nojkxl8ZEDLXB0AuqLZxUpaVIC +u9ffUGpVRr+goyhhf3DQw6KqLCGqR84onAZFdr+CGCe01a60y1Dma/RMhnEw6abf +Fobg2P9A3fvQQoh/ozM6LlweQRGBY84YcWsr7KaKtzFcOmpH4MN5WdYgGq/yapiq +crxXStJLnbsQ/LBMQeXtHT1eKJ2czL+zUdqnR+WEUwIDAQABo0IwQDAdBgNVHQ4E +FgQUu69+Aj36pvE8hI6t7jiY7NkyMtQwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB +/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAArx1UaEt65Ru2yyTUEUAJNMnMvl +wFTPoCWOAvn9sKIN9SCYPBMtrFaisNZ+EZLpLrqeLppysb0ZRGxhNaKatBYSaVqM +4dc+pBroLwP0rmEdEBsqpIt6xf4FpuHA1sj+nq6PK7o9mfjYcwlYRm6mnPTXJ9OV +2jeDchzTc+CiR5kDOF3VSXkAKRzH7JsgHAckaVd4sjn8OoSgtZx8jb8uk2Intzna +FxiuvTwJaP+EmzzV1gsD41eeFPfR60/IvYcjt7ZJQ3mFXLrrkguhxuhoqEwWsRqZ +CuhTLJK7oQkYdQxlqHvLI7cawiiFwxv/0Cti76R7CZGYZ4wUAc1oBmpjIXUDgIiK +boHGhfKppC3n9KUkEEeDys30jXlYsQab5xoq2Z0B15R97QNKyvDb6KkBPvVWmcke +jkk9u+UJueBPSZI9FoJAzMxZxuY67RIuaTxslbH9qh17f4a+Hg4yRvv7E491f0yL +S0Zj/gA0QHDBw7mh3aZw4gSzQbzpgJHqZJx64SIDqZxubw5lT2yHh17zbqD5daWb +QOhTsiedSrnAdyGN/4fy3ryM7xfft0kL0fJuMAsaDk527RH89elWsn2/x20Kk4yl +0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB +NVOFBkpdn627G190 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust RSA Certification Authority O=The USERTRUST Network +# Label: "USERTrust RSA Certification Authority" +# Serial: 2645093764781058787591871645665788717 +# MD5 Fingerprint: 1b:fe:69:d1:91:b7:19:33:a3:72:a8:0f:e1:55:e5:b5 +# SHA1 Fingerprint: 2b:8f:1b:57:33:0d:bb:a2:d0:7a:6c:51:f7:0e:e9:0d:da:b9:ad:8e +# SHA256 Fingerprint: e7:93:c9:b0:2f:d8:aa:13:e2:1c:31:22:8a:cc:b0:81:19:64:3b:74:9c:89:89:64:b1:74:6d:46:c3:d4:cb:d2 +-----BEGIN CERTIFICATE----- +MIIF3jCCA8agAwIBAgIQAf1tMPyjylGoG7xkDjUDLTANBgkqhkiG9w0BAQwFADCB +iDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0pl +cnNleSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNV +BAMTJVVTRVJUcnVzdCBSU0EgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAw +MjAxMDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNV +BAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVU +aGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBSU0EgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCAEmUXNg7D2wiz0KxXDXbtzSfTTK1Qg2HiqiBNCS1kCdzOiZ/MPans9s/B +3PHTsdZ7NygRK0faOca8Ohm0X6a9fZ2jY0K2dvKpOyuR+OJv0OwWIJAJPuLodMkY +tJHUYmTbf6MG8YgYapAiPLz+E/CHFHv25B+O1ORRxhFnRghRy4YUVD+8M/5+bJz/ +Fp0YvVGONaanZshyZ9shZrHUm3gDwFA66Mzw3LyeTP6vBZY1H1dat//O+T23LLb2 +VN3I5xI6Ta5MirdcmrS3ID3KfyI0rn47aGYBROcBTkZTmzNg95S+UzeQc0PzMsNT +79uq/nROacdrjGCT3sTHDN/hMq7MkztReJVni+49Vv4M0GkPGw/zJSZrM233bkf6 +c0Plfg6lZrEpfDKEY1WJxA3Bk1QwGROs0303p+tdOmw1XNtB1xLaqUkL39iAigmT +Yo61Zs8liM2EuLE/pDkP2QKe6xJMlXzzawWpXhaDzLhn4ugTncxbgtNMs+1b/97l +c6wjOy0AvzVVdAlJ2ElYGn+SNuZRkg7zJn0cTRe8yexDJtC/QV9AqURE9JnnV4ee +UB9XVKg+/XRjL7FQZQnmWEIuQxpMtPAlR1n6BB6T1CZGSlCBst6+eLf8ZxXhyVeE +Hg9j1uliutZfVS7qXMYoCAQlObgOK6nyTJccBz8NUvXt7y+CDwIDAQABo0IwQDAd +BgNVHQ4EFgQUU3m/WqorSs9UgOHYm8Cd8rIDZsswDgYDVR0PAQH/BAQDAgEGMA8G +A1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAFzUfA3P9wF9QZllDHPF +Up/L+M+ZBn8b2kMVn54CVVeWFPFSPCeHlCjtHzoBN6J2/FNQwISbxmtOuowhT6KO +VWKR82kV2LyI48SqC/3vqOlLVSoGIG1VeCkZ7l8wXEskEVX/JJpuXior7gtNn3/3 +ATiUFJVDBwn7YKnuHKsSjKCaXqeYalltiz8I+8jRRa8YFWSQEg9zKC7F4iRO/Fjs +8PRF/iKz6y+O0tlFYQXBl2+odnKPi4w2r78NBc5xjeambx9spnFixdjQg3IM8WcR +iQycE0xyNN+81XHfqnHd4blsjDwSXWXavVcStkNr/+XeTWYRUc+ZruwXtuhxkYze +Sf7dNXGiFSeUHM9h4ya7b6NnJSFd5t0dCy5oGzuCr+yDZ4XUmFF0sbmZgIn/f3gZ +XHlKYC6SQK5MNyosycdiyA5d9zZbyuAlJQG03RoHnHcAP9Dc1ew91Pq7P8yF1m9/ +qS3fuQL39ZeatTXaw2ewh0qpKJ4jjv9cJ2vhsE/zB+4ALtRZh8tSQZXq9EfX7mRB +VXyNWQKV3WKdwrnuWih0hKWbt5DHDAff9Yk2dDLWKMGwsAvgnEzDHNb842m1R0aB +L6KCq9NjRHDEjf8tM7qtj3u1cIiuPhnPQCjY/MiQu12ZIvVS5ljFH4gxQ+6IHdfG +jjxDah2nGN59PRbxYvnKkKj9 +-----END CERTIFICATE----- + +# Issuer: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Subject: CN=USERTrust ECC Certification Authority O=The USERTRUST Network +# Label: "USERTrust ECC Certification Authority" +# Serial: 123013823720199481456569720443997572134 +# MD5 Fingerprint: fa:68:bc:d9:b5:7f:ad:fd:c9:1d:06:83:28:cc:24:c1 +# SHA1 Fingerprint: d1:cb:ca:5d:b2:d5:2a:7f:69:3b:67:4d:e5:f0:5a:1d:0c:95:7d:f0 +# SHA256 Fingerprint: 4f:f4:60:d5:4b:9c:86:da:bf:bc:fc:57:12:e0:40:0d:2b:ed:3f:bc:4d:4f:bd:aa:86:e0:6a:dc:d2:a9:ad:7a +-----BEGIN CERTIFICATE----- +MIICjzCCAhWgAwIBAgIQXIuZxVqUxdJxVt7NiYDMJjAKBggqhkjOPQQDAzCBiDEL +MAkGA1UEBhMCVVMxEzARBgNVBAgTCk5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNl +eSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMT +JVVTRVJUcnVzdCBFQ0MgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTAwMjAx +MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT +Ck5ldyBKZXJzZXkxFDASBgNVBAcTC0plcnNleSBDaXR5MR4wHAYDVQQKExVUaGUg +VVNFUlRSVVNUIE5ldHdvcmsxLjAsBgNVBAMTJVVTRVJUcnVzdCBFQ0MgQ2VydGlm +aWNhdGlvbiBBdXRob3JpdHkwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQarFRaqflo +I+d61SRvU8Za2EurxtW20eZzca7dnNYMYf3boIkDuAUU7FfO7l0/4iGzzvfUinng +o4N+LZfQYcTxmdwlkWOrfzCjtHDix6EznPO/LlxTsV+zfTJ/ijTjeXmjQjBAMB0G +A1UdDgQWBBQ64QmG1M8ZwpZ2dEl23OA1xmNjmjAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjA2Z6EWCNzklwBBHU6+4WMB +zzuqQhFkoJ2UOQIReVx7Hfpkue4WQrO/isIJxOzksU0CMQDpKmFHjFJKS04YcPbW +RNZu9YO6bVi9JNlWSOrvxKJGgYhqOkbRqZtNyWHa0V1Xahg= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R5 +# Label: "GlobalSign ECC Root CA - R5" +# Serial: 32785792099990507226680698011560947931244 +# MD5 Fingerprint: 9f:ad:3b:1c:02:1e:8a:ba:17:74:38:81:0c:a2:bc:08 +# SHA1 Fingerprint: 1f:24:c6:30:cd:a4:18:ef:20:69:ff:ad:4f:dd:5f:46:3a:1b:69:aa +# SHA256 Fingerprint: 17:9f:bc:14:8a:3d:d0:0f:d2:4e:a1:34:58:cc:43:bf:a7:f5:9c:81:82:d7:83:a5:13:f6:eb:ec:10:0c:89:24 +-----BEGIN CERTIFICATE----- +MIICHjCCAaSgAwIBAgIRYFlJ4CYuu1X5CneKcflK2GwwCgYIKoZIzj0EAwMwUDEk +MCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBDQSAtIFI1MRMwEQYDVQQKEwpH +bG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWduMB4XDTEyMTExMzAwMDAwMFoX +DTM4MDExOTAzMTQwN1owUDEkMCIGA1UECxMbR2xvYmFsU2lnbiBFQ0MgUm9vdCBD +QSAtIFI1MRMwEQYDVQQKEwpHbG9iYWxTaWduMRMwEQYDVQQDEwpHbG9iYWxTaWdu +MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAER0UOlvt9Xb/pOdEh+J8LttV7HpI6SFkc +8GIxLcB6KP4ap1yztsyX50XUWPrRd21DosCHZTQKH3rd6zwzocWdTaRvQZU4f8ke +hOvRnkmSh5SHDDqFSmafnVmTTZdhBoZKo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUPeYpSJvqB8ohREom3m7e0oPQn1kwCgYI +KoZIzj0EAwMDaAAwZQIxAOVpEslu28YxuglB4Zf4+/2a4n0Sye18ZNPLBSWLVtmg +515dTguDnFt2KaAJJiFqYgIwcdK1j1zqO+F4CYWodZI7yFz9SO8NdCKoCOJuxUnO +xwy8p2Fp8fc74SrL+SvzZpA3 +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Commercial Root CA 1 O=IdenTrust +# Label: "IdenTrust Commercial Root CA 1" +# Serial: 13298821034946342390520003877796839426 +# MD5 Fingerprint: b3:3e:77:73:75:ee:a0:d3:e3:7e:49:63:49:59:bb:c7 +# SHA1 Fingerprint: df:71:7e:aa:4a:d9:4e:c9:55:84:99:60:2d:48:de:5f:bc:f0:3a:25 +# SHA256 Fingerprint: 5d:56:49:9b:e4:d2:e0:8b:cf:ca:d0:8a:3e:38:72:3d:50:50:3b:de:70:69:48:e4:2f:55:60:30:19:e5:28:ae +-----BEGIN CERTIFICATE----- +MIIFYDCCA0igAwIBAgIQCgFCgAAAAUUjyES1AAAAAjANBgkqhkiG9w0BAQsFADBK +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScwJQYDVQQDEx5JZGVu +VHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwHhcNMTQwMTE2MTgxMjIzWhcNMzQw +MTE2MTgxMjIzWjBKMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MScw +JQYDVQQDEx5JZGVuVHJ1c3QgQ29tbWVyY2lhbCBSb290IENBIDEwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQCnUBneP5k91DNG8W9RYYKyqU+PZ4ldhNlT +3Qwo2dfw/66VQ3KZ+bVdfIrBQuExUHTRgQ18zZshq0PirK1ehm7zCYofWjK9ouuU ++ehcCuz/mNKvcbO0U59Oh++SvL3sTzIwiEsXXlfEU8L2ApeN2WIrvyQfYo3fw7gp +S0l4PJNgiCL8mdo2yMKi1CxUAGc1bnO/AljwpN3lsKImesrgNqUZFvX9t++uP0D1 +bVoE/c40yiTcdCMbXTMTEl3EASX2MN0CXZ/g1Ue9tOsbobtJSdifWwLziuQkkORi +T0/Br4sOdBeo0XKIanoBScy0RnnGF7HamB4HWfp1IYVl3ZBWzvurpWCdxJ35UrCL +vYf5jysjCiN2O/cz4ckA82n5S6LgTrx+kzmEB/dEcH7+B1rlsazRGMzyNeVJSQjK +Vsk9+w8YfYs7wRPCTY/JTw436R+hDmrfYi7LNQZReSzIJTj0+kuniVyc0uMNOYZK +dHzVWYfCP04MXFL0PfdSgvHqo6z9STQaKPNBiDoT7uje/5kdX7rL6B7yuVBgwDHT +c+XvvqDtMwt0viAgxGds8AgDelWAf0ZOlqf0Hj7h9tgJ4TNkK2PXMl6f+cB7D3hv +l7yTmvmcEpB4eoCHFddydJxVdHixuuFucAS6T6C6aMN7/zHwcz09lCqxC0EOoP5N +iGVreTO01wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB +/zAdBgNVHQ4EFgQU7UQZwNPwBovupHu+QucmVMiONnYwDQYJKoZIhvcNAQELBQAD +ggIBAA2ukDL2pkt8RHYZYR4nKM1eVO8lvOMIkPkp165oCOGUAFjvLi5+U1KMtlwH +6oi6mYtQlNeCgN9hCQCTrQ0U5s7B8jeUeLBfnLOic7iPBZM4zY0+sLj7wM+x8uwt +LRvM7Kqas6pgghstO8OEPVeKlh6cdbjTMM1gCIOQ045U8U1mwF10A0Cj7oV+wh93 +nAbowacYXVKV7cndJZ5t+qntozo00Fl72u1Q8zW/7esUTTHHYPTa8Yec4kjixsU3 ++wYQ+nVZZjFHKdp2mhzpgq7vmrlR94gjmmmVYjzlVYA211QC//G5Xc7UI2/YRYRK +W2XviQzdFKcgyxilJbQN+QHwotL0AMh0jqEqSI5l2xPE4iUXfeu+h1sXIFRRk0pT +AwvsXcoz7WL9RccvW9xYoIA55vrX/hMUpu09lEpCdNTDd1lzzY9GvlU47/rokTLq +l1gEIt44w8y8bckzOmoKaT+gyOpyj4xjhiO9bTyWnpXgSUyqorkqG5w2gXjtw+hG +4iZZRHUe2XWJUc0QhJ1hYMtd+ZciTY6Y5uN/9lu7rs3KSoFrXgvzUeF0K+l+J6fZ +mUlO+KWA2yUPHGNiiskzZ2s8EIPGrd6ozRaOjfAHN3Gf8qv8QfXBi+wAN10J5U6A +7/qxXDgGpRtK4dw4LTzcqx+QGtVKnO7RcGzM7vRX+Bi6hG6H +-----END CERTIFICATE----- + +# Issuer: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Subject: CN=IdenTrust Public Sector Root CA 1 O=IdenTrust +# Label: "IdenTrust Public Sector Root CA 1" +# Serial: 13298821034946342390521976156843933698 +# MD5 Fingerprint: 37:06:a5:b0:fc:89:9d:ba:f4:6b:8c:1a:64:cd:d5:ba +# SHA1 Fingerprint: ba:29:41:60:77:98:3f:f4:f3:ef:f2:31:05:3b:2e:ea:6d:4d:45:fd +# SHA256 Fingerprint: 30:d0:89:5a:9a:44:8a:26:20:91:63:55:22:d1:f5:20:10:b5:86:7a:ca:e1:2c:78:ef:95:8f:d4:f4:38:9f:2f +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCgFCgAAAAUUjz0Z8AAAAAjANBgkqhkiG9w0BAQsFADBN +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0MSowKAYDVQQDEyFJZGVu +VHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwHhcNMTQwMTE2MTc1MzMyWhcN +MzQwMTE2MTc1MzMyWjBNMQswCQYDVQQGEwJVUzESMBAGA1UEChMJSWRlblRydXN0 +MSowKAYDVQQDEyFJZGVuVHJ1c3QgUHVibGljIFNlY3RvciBSb290IENBIDEwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC2IpT8pEiv6EdrCvsnduTyP4o7 +ekosMSqMjbCpwzFrqHd2hCa2rIFCDQjrVVi7evi8ZX3yoG2LqEfpYnYeEe4IFNGy +RBb06tD6Hi9e28tzQa68ALBKK0CyrOE7S8ItneShm+waOh7wCLPQ5CQ1B5+ctMlS +bdsHyo+1W/CD80/HLaXIrcuVIKQxKFdYWuSNG5qrng0M8gozOSI5Cpcu81N3uURF +/YTLNiCBWS2ab21ISGHKTN9T0a9SvESfqy9rg3LvdYDaBjMbXcjaY8ZNzaxmMc3R +3j6HEDbhuaR672BQssvKplbgN6+rNBM5Jeg5ZuSYeqoSmJxZZoY+rfGwyj4GD3vw +EUs3oERte8uojHH01bWRNszwFcYr3lEXsZdMUD2xlVl8BX0tIdUAvwFnol57plzy +9yLxkA2T26pEUWbMfXYD62qoKjgZl3YNa4ph+bz27nb9cCvdKTz4Ch5bQhyLVi9V +GxyhLrXHFub4qjySjmm2AcG1hp2JDws4lFTo6tyePSW8Uybt1as5qsVATFSrsrTZ +2fjXctscvG29ZV/viDUqZi/u9rNl8DONfJhBaUYPQxxp+pu10GFqzcpL2UyQRqsV +WaFHVCkugyhfHMKiq3IXAAaOReyL4jM9f9oZRORicsPfIsbyVtTdX5Vy7W1f90gD +W/3FKqD2cyOEEBsB5wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQU43HgntinQtnbcZFrlJPrw6PRFKMwDQYJKoZIhvcN +AQELBQADggIBAEf63QqwEZE4rU1d9+UOl1QZgkiHVIyqZJnYWv6IAcVYpZmxI1Qj +t2odIFflAWJBF9MJ23XLblSQdf4an4EKwt3X9wnQW3IV5B4Jaj0z8yGa5hV+rVHV +DRDtfULAj+7AmgjVQdZcDiFpboBhDhXAuM/FSRJSzL46zNQuOAXeNf0fb7iAaJg9 +TaDKQGXSc3z1i9kKlT/YPyNtGtEqJBnZhbMX73huqVjRI9PHE+1yJX9dsXNw0H8G +lwmEKYBhHfpe/3OsoOOJuBxxFcbeMX8S3OFtm6/n6J91eEyrRjuazr8FGF1NFTwW +mhlQBJqymm9li1JfPFgEKCXAZmExfrngdbkaqIHWchezxQMxNRF4eKLg6TCMf4Df +WN88uieW4oA0beOY02QnrEh+KHdcxiVhJfiFDGX6xDIvpZgF5PgLZxYWxoK4Mhn5 ++bl53B/N66+rDt0b20XkeucC4pVd/GnwU2lhlXV5C15V5jgclKlZM57IcXR5f1GJ +tshquDDIajjDbp7hNxbqBWJMWxJH7ae0s1hWx0nzfxJoCTFx8G34Tkf71oXuxVhA +GaQdp/lLQzfcaFpPz+vCZHTetBXZ9FRUGi8c15dxVJCO2SCdUyt/q4/i6jC8UDfv +8Ue1fXwsBOxonbRJRBD0ckscZOf85muQ3Wl9af0AVqW3rLatt8o+Ae+c +-----END CERTIFICATE----- + +# Issuer: CN=CFCA EV ROOT O=China Financial Certification Authority +# Subject: CN=CFCA EV ROOT O=China Financial Certification Authority +# Label: "CFCA EV ROOT" +# Serial: 407555286 +# MD5 Fingerprint: 74:e1:b6:ed:26:7a:7a:44:30:33:94:ab:7b:27:81:30 +# SHA1 Fingerprint: e2:b8:29:4b:55:84:ab:6b:58:c2:90:46:6c:ac:3f:b8:39:8f:84:83 +# SHA256 Fingerprint: 5c:c3:d7:8e:4e:1d:5e:45:54:7a:04:e6:87:3e:64:f9:0c:f9:53:6d:1c:cc:2e:f8:00:f3:55:c4:c5:fd:70:fd +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIEGErM1jANBgkqhkiG9w0BAQsFADBWMQswCQYDVQQGEwJD +TjEwMC4GA1UECgwnQ2hpbmEgRmluYW5jaWFsIENlcnRpZmljYXRpb24gQXV0aG9y +aXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJPT1QwHhcNMTIwODA4MDMwNzAxWhcNMjkx +MjMxMDMwNzAxWjBWMQswCQYDVQQGEwJDTjEwMC4GA1UECgwnQ2hpbmEgRmluYW5j +aWFsIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MRUwEwYDVQQDDAxDRkNBIEVWIFJP +T1QwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDXXWvNED8fBVnVBU03 +sQ7smCuOFR36k0sXgiFxEFLXUWRwFsJVaU2OFW2fvwwbwuCjZ9YMrM8irq93VCpL +TIpTUnrD7i7es3ElweldPe6hL6P3KjzJIx1qqx2hp/Hz7KDVRM8Vz3IvHWOX6Jn5 +/ZOkVIBMUtRSqy5J35DNuF++P96hyk0g1CXohClTt7GIH//62pCfCqktQT+x8Rgp +7hZZLDRJGqgG16iI0gNyejLi6mhNbiyWZXvKWfry4t3uMCz7zEasxGPrb382KzRz +EpR/38wmnvFyXVBlWY9ps4deMm/DGIq1lY+wejfeWkU7xzbh72fROdOXW3NiGUgt +hxwG+3SYIElz8AXSG7Ggo7cbcNOIabla1jj0Ytwli3i/+Oh+uFzJlU9fpy25IGvP +a931DfSCt/SyZi4QKPaXWnuWFo8BGS1sbn85WAZkgwGDg8NNkt0yxoekN+kWzqot +aK8KgWU6cMGbrU1tVMoqLUuFG7OA5nBFDWteNfB/O7ic5ARwiRIlk9oKmSJgamNg +TnYGmE69g60dWIolhdLHZR4tjsbftsbhf4oEIRUpdPA+nJCdDC7xij5aqgwJHsfV +PKPtl8MeNPo4+QgO48BdK4PRVmrJtqhUUy54Mmc9gn900PvhtgVguXDbjgv5E1hv +cWAQUhC5wUEJ73IfZzF4/5YFjQIDAQABo2MwYTAfBgNVHSMEGDAWgBTj/i39KNAL +tbq2osS/BqoFjJP7LzAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAd +BgNVHQ4EFgQU4/4t/SjQC7W6tqLEvwaqBYyT+y8wDQYJKoZIhvcNAQELBQADggIB +ACXGumvrh8vegjmWPfBEp2uEcwPenStPuiB/vHiyz5ewG5zz13ku9Ui20vsXiObT +ej/tUxPQ4i9qecsAIyjmHjdXNYmEwnZPNDatZ8POQQaIxffu2Bq41gt/UP+TqhdL +jOztUmCypAbqTuv0axn96/Ua4CUqmtzHQTb3yHQFhDmVOdYLO6Qn+gjYXB74BGBS +ESgoA//vU2YApUo0FmZ8/Qmkrp5nGm9BC2sGE5uPhnEFtC+NiWYzKXZUmhH4J/qy +P5Hgzg0b8zAarb8iXRvTvyUFTeGSGn+ZnzxEk8rUQElsgIfXBDrDMlI1Dlb4pd19 +xIsNER9Tyx6yF7Zod1rg1MvIB671Oi6ON7fQAUtDKXeMOZePglr4UeWJoBjnaH9d +Ci77o0cOPaYjesYBx4/IXr9tgFa+iiS6M+qf4TIRnvHST4D2G0CvOJ4RUHlzEhLN +5mydLIhyPDCBBpEi6lmt2hkuIsKNuYyH4Ga8cyNfIWRjgEj1oDwYPZTISEEdQLpe +/v5WOaHIz16eGWRGENoXkbcFgKyLmZJ956LYBws2J+dIeWCKw9cTXPhyQN9Ky8+Z +AAoACxGV2lZFA4gKn2fQ1XmxqI1AbQ3CekD6819kR5LLU7m7Wc5P/dAVUwHY3+vZ +5nbv0CO7O6l5s9UCKc2Jo5YPSjXnTkLAdc0Hz+Ys63su +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GB CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GB CA" +# Serial: 157768595616588414422159278966750757568 +# MD5 Fingerprint: a4:eb:b9:61:28:2e:b7:2f:98:b0:35:26:90:99:51:1d +# SHA1 Fingerprint: 0f:f9:40:76:18:d3:d7:6a:4b:98:f0:a8:35:9e:0c:fd:27:ac:cc:ed +# SHA256 Fingerprint: 6b:9c:08:e8:6e:b0:f7:67:cf:ad:65:cd:98:b6:21:49:e5:49:4a:67:f5:84:5e:7b:d1:ed:01:9f:27:b8:6b:d6 +-----BEGIN CERTIFICATE----- +MIIDtTCCAp2gAwIBAgIQdrEgUnTwhYdGs/gjGvbCwDANBgkqhkiG9w0BAQsFADBt +MQswCQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUg +Rm91bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9i +YWwgUm9vdCBHQiBDQTAeFw0xNDEyMDExNTAwMzJaFw0zOTEyMDExNTEwMzFaMG0x +CzAJBgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBG +b3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2Jh +bCBSb290IEdCIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2Be3 +HEokKtaXscriHvt9OO+Y9bI5mE4nuBFde9IllIiCFSZqGzG7qFshISvYD06fWvGx +WuR51jIjK+FTzJlFXHtPrby/h0oLS5daqPZI7H17Dc0hBt+eFf1Biki3IPShehtX +1F1Q/7pn2COZH8g/497/b1t3sWtuuMlk9+HKQUYOKXHQuSP8yYFfTvdv37+ErXNk +u7dCjmn21HYdfp2nuFeKUWdy19SouJVUQHMD9ur06/4oQnc/nSMbsrY9gBQHTC5P +99UKFg29ZkM3fiNDecNAhvVMKdqOmq0NpQSHiB6F4+lT1ZvIiwNjeOvgGUpuuy9r +M2RYk61pv48b74JIxwIDAQABo1EwTzALBgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUNQ/INmNe4qPs+TtmFc5RUuORmj0wEAYJKwYBBAGCNxUB +BAMCAQAwDQYJKoZIhvcNAQELBQADggEBAEBM+4eymYGQfp3FsLAmzYh7KzKNbrgh +cViXfa43FK8+5/ea4n32cZiZBKpDdHij40lhPnOMTZTg+XHEthYOU3gf1qKHLwI5 +gSk8rxWYITD+KJAAjNHhy/peyP34EEY7onhCkRd0VQreUGdNZtGn//3ZwLWoo4rO +ZvUPQ82nK1d7Y0Zqqi5S2PTt4W2tKZB4SLrhI6qjiey1q5bAtEuiHZeeevJuQHHf +aPFlTc58Bd9TZaml8LGXBHAVRgOY1NK/VLSgWH1Sb9pWJmLU2NuJMW8c8CLC02Ic +Nc1MaRVUGpCY3useX8p3x8uOPUNpnJpY0CQ73xtAln41rYHHTnG6iBM= +-----END CERTIFICATE----- + +# Issuer: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Subject: CN=SZAFIR ROOT CA2 O=Krajowa Izba Rozliczeniowa S.A. +# Label: "SZAFIR ROOT CA2" +# Serial: 357043034767186914217277344587386743377558296292 +# MD5 Fingerprint: 11:64:c1:89:b0:24:b1:8c:b1:07:7e:89:9e:51:9e:99 +# SHA1 Fingerprint: e2:52:fa:95:3f:ed:db:24:60:bd:6e:28:f3:9c:cc:cf:5e:b3:3f:de +# SHA256 Fingerprint: a1:33:9d:33:28:1a:0b:56:e5:57:d3:d3:2b:1c:e7:f9:36:7e:b0:94:bd:5f:a7:2a:7e:50:04:c8:de:d7:ca:fe +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUPopdB+xV0jLVt+O2XwHrLdzk1uQwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCUEwxKDAmBgNVBAoMH0tyYWpvd2EgSXpiYSBSb3psaWN6 +ZW5pb3dhIFMuQS4xGDAWBgNVBAMMD1NaQUZJUiBST09UIENBMjAeFw0xNTEwMTkw +NzQzMzBaFw0zNTEwMTkwNzQzMzBaMFExCzAJBgNVBAYTAlBMMSgwJgYDVQQKDB9L +cmFqb3dhIEl6YmEgUm96bGljemVuaW93YSBTLkEuMRgwFgYDVQQDDA9TWkFGSVIg +Uk9PVCBDQTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC3vD5QqEvN +QLXOYeeWyrSh2gwisPq1e3YAd4wLz32ohswmUeQgPYUM1ljj5/QqGJ3a0a4m7utT +3PSQ1hNKDJA8w/Ta0o4NkjrcsbH/ON7Dui1fgLkCvUqdGw+0w8LBZwPd3BucPbOw +3gAeqDRHu5rr/gsUvTaE2g0gv/pby6kWIK05YO4vdbbnl5z5Pv1+TW9NL++IDWr6 +3fE9biCloBK0TXC5ztdyO4mTp4CEHCdJckm1/zuVnsHMyAHs6A6KCpbns6aH5db5 +BSsNl0BwPLqsdVqc1U2dAgrSS5tmS0YHF2Wtn2yIANwiieDhZNRnvDF5YTy7ykHN +XGoAyDw4jlivAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBQuFqlKGLXLzPVvUPMjX/hd56zwyDANBgkqhkiG9w0BAQsF +AAOCAQEAtXP4A9xZWx126aMqe5Aosk3AM0+qmrHUuOQn/6mWmc5G4G18TKI4pAZw +8PRBEew/R40/cof5O/2kbytTAOD/OblqBw7rHRz2onKQy4I9EYKL0rufKq8h5mOG +nXkZ7/e7DDWQw4rtTw/1zBLZpD67oPwglV9PJi8RI4NOdQcPv5vRtB3pEAT+ymCP +oky4rc/hkA/NrgrHXXu3UNLUYfrVFdvXn4dRVOul4+vJhaAlIDf7js4MNIThPIGy +d05DpYhfhmehPea0XGG2Ptv+tyjFogeutcrKjSoS75ftwjCkySp6+/NNIxuZMzSg +LvWpCz/UXeHPhJ/iGcJfitYgHuNztw== +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Network CA 2 O=Unizeto Technologies S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Network CA 2" +# Serial: 44979900017204383099463764357512596969 +# MD5 Fingerprint: 6d:46:9e:d9:25:6d:08:23:5b:5e:74:7d:1e:27:db:f2 +# SHA1 Fingerprint: d3:dd:48:3e:2b:bf:4c:05:e8:af:10:f5:fa:76:26:cf:d3:dc:30:92 +# SHA256 Fingerprint: b6:76:f2:ed:da:e8:77:5c:d3:6c:b0:f6:3c:d1:d4:60:39:61:f4:9e:62:65:ba:01:3a:2f:03:07:b6:d0:b8:04 +-----BEGIN CERTIFICATE----- +MIIF0jCCA7qgAwIBAgIQIdbQSk8lD8kyN/yqXhKN6TANBgkqhkiG9w0BAQ0FADCB +gDELMAkGA1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMu +QS4xJzAlBgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIG +A1UEAxMbQ2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMCIYDzIwMTExMDA2MDgz +OTU2WhgPMjA0NjEwMDYwODM5NTZaMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ +VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp +ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 +b3JrIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC9+Xj45tWA +DGSdhhuWZGc/IjoedQF97/tcZ4zJzFxrqZHmuULlIEub2pt7uZld2ZuAS9eEQCsn +0+i6MLs+CRqnSZXvK0AkwpfHp+6bJe+oCgCXhVqqndwpyeI1B+twTUrWwbNWuKFB +OJvR+zF/j+Bf4bE/D44WSWDXBo0Y+aomEKsq09DRZ40bRr5HMNUuctHFY9rnY3lE +fktjJImGLjQ/KUxSiyqnwOKRKIm5wFv5HdnnJ63/mgKXwcZQkpsCLL2puTRZCr+E +Sv/f/rOf69me4Jgj7KZrdxYq28ytOxykh9xGc14ZYmhFV+SQgkK7QtbwYeDBoz1m +o130GO6IyY0XRSmZMnUCMe4pJshrAua1YkV/NxVaI2iJ1D7eTiew8EAMvE0Xy02i +sx7QBlrd9pPPV3WZ9fqGGmd4s7+W/jTcvedSVuWz5XV710GRBdxdaeOVDUO5/IOW +OZV7bIBaTxNyxtd9KXpEulKkKtVBRgkg/iKgtlswjbyJDNXXcPiHUv3a76xRLgez +Tv7QCdpw75j6VuZt27VXS9zlLCUVyJ4ueE742pyehizKV/Ma5ciSixqClnrDvFAS +adgOWkaLOusm+iPJtrCBvkIApPjW/jAux9JG9uWOdf3yzLnQh1vMBhBgu4M1t15n +3kfsmUjxpKEV/q2MYo45VU85FrmxY53/twIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBS2oVQ5AsOgP46KvPrU+Bym0ToO/TAOBgNVHQ8BAf8EBAMC +AQYwDQYJKoZIhvcNAQENBQADggIBAHGlDs7k6b8/ONWJWsQCYftMxRQXLYtPU2sQ +F/xlhMcQSZDe28cmk4gmb3DWAl45oPePq5a1pRNcgRRtDoGCERuKTsZPpd1iHkTf +CVn0W3cLN+mLIMb4Ck4uWBzrM9DPhmDJ2vuAL55MYIR4PSFk1vtBHxgP58l1cb29 +XN40hz5BsA72udY/CROWFC/emh1auVbONTqwX3BNXuMp8SMoclm2q8KMZiYcdywm +djWLKKdpoPk79SPdhRB0yZADVpHnr7pH1BKXESLjokmUbOe3lEu6LaTaM4tMpkT/ +WjzGHWTYtTHkpjx6qFcL2+1hGsvxznN3Y6SHb0xRONbkX8eftoEq5IVIeVheO/jb +AoJnwTnbw3RLPTYe+SmTiGhbqEQZIfCn6IENLOiTNrQ3ssqwGyZ6miUfmpqAnksq +P/ujmv5zMnHCnsZy4YpoJ/HkD7TETKVhk/iXEAcqMCWpuchxuO9ozC1+9eB+D4Ko +b7a6bINDd82Kkhehnlt4Fj1F4jNy3eFmypnTycUm/Q1oBEauttmbjL4ZvrHG8hnj +XALKLNhvSgfZyTXaQHXyxKcZb55CEJh15pWLYLztxRLXis7VmFxWlgPF7ncGNf/P +5O4/E2Hu29othfDNrp2yGAlFw5Khchf8R7agCyzxxN5DaAhqXzvwdmP7zAYspsbi +DrW5viSP +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: ca:ff:e2:db:03:d9:cb:4b:e9:0f:ad:84:fd:7b:18:ce +# SHA1 Fingerprint: 01:0c:06:95:a6:98:19:14:ff:bf:5f:c6:b0:b6:95:ea:29:e9:12:a6 +# SHA256 Fingerprint: a0:40:92:9a:02:ce:53:b4:ac:f4:f2:ff:c6:98:1c:e4:49:6f:75:5e:6d:45:fe:0b:2a:69:2b:cd:52:52:3f:36 +-----BEGIN CERTIFICATE----- +MIIGCzCCA/OgAwIBAgIBADANBgkqhkiG9w0BAQsFADCBpjELMAkGA1UEBhMCR1Ix +DzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5k +IFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxQDA+BgNVBAMT +N0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgUm9v +dENBIDIwMTUwHhcNMTUwNzA3MTAxMTIxWhcNNDAwNjMwMTAxMTIxWjCBpjELMAkG +A1UEBhMCR1IxDzANBgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNh +ZGVtaWMgYW5kIFJlc2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkx +QDA+BgNVBAMTN0hlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1 +dGlvbnMgUm9vdENBIDIwMTUwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC +AQDC+Kk/G4n8PDwEXT2QNrCROnk8ZlrvbTkBSRq0t89/TSNTt5AA4xMqKKYx8ZEA +4yjsriFBzh/a/X0SWwGDD7mwX5nh8hKDgE0GPt+sr+ehiGsxr/CL0BgzuNtFajT0 +AoAkKAoCFZVedioNmToUW/bLy1O8E00BiDeUJRtCvCLYjqOWXjrZMts+6PAQZe10 +4S+nfK8nNLspfZu2zwnI5dMK/IhlZXQK3HMcXM1AsRzUtoSMTFDPaI6oWa7CJ06C +ojXdFPQf/7J31Ycvqm59JCfnxssm5uX+Zwdj2EUN3TpZZTlYepKZcj2chF6IIbjV +9Cz82XBST3i4vTwri5WY9bPRaM8gFH5MXF/ni+X1NYEZN9cRCLdmvtNKzoNXADrD +gfgXy5I2XdGj2HUb4Ysn6npIQf1FGQatJ5lOwXBH3bWfgVMS5bGMSF0xQxfjjMZ6 +Y5ZLKTBOhE5iGV48zpeQpX8B653g+IuJ3SWYPZK2fu/Z8VFRfS0myGlZYeCsargq +NhEEelC9MoS+L9xy1dcdFkfkR2YgP/SWxa+OAXqlD3pk9Q0Yh9muiNX6hME6wGko +LfINaFGq46V3xqSQDqE3izEjR8EJCOtu93ib14L8hCCZSRm2Ekax+0VVFqmjZayc +Bw/qa9wfLgZy7IaIEuQt218FL+TwA9MmM+eAws1CoRc0CwIDAQABo0IwQDAPBgNV +HRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUcRVnyMjJvXVd +ctA4GGqd83EkVAswDQYJKoZIhvcNAQELBQADggIBAHW7bVRLqhBYRjTyYtcWNl0I +XtVsyIe9tC5G8jH4fOpCtZMWVdyhDBKg2mF+D1hYc2Ryx+hFjtyp8iY/xnmMsVMI +M4GwVhO+5lFc2JsKT0ucVlMC6U/2DWDqTUJV6HwbISHTGzrMd/K4kPFox/la/vot +9L/J9UUbzjgQKjeKeaO04wlshYaT/4mWJ3iBj2fjRnRUjtkNaeJK9E10A/+yd+2V +Z5fkscWrv2oj6NSU4kQoYsRL4vDY4ilrGnB+JGGTe08DMiUNRSQrlrRGar9KC/ea +j8GsGsVn82800vpzY4zvFrCopEYq+OsS7HK07/grfoxSwIuEVPkvPuNVqNxmsdnh +X9izjFk0WaSrT2y7HxjbdavYy5LNlDhhDgcGH0tGEPEVvo2FXDtKK4F5D7Rpn0lQ +l033DlZdwJVqwjbDG2jJ9SrcR5q+ss7FJej6A7na+RZukYT1HCjI/CbM1xyQVqdf +bzoEvM14iQuODy+jqk+iGxI9FghAD/FGTNeqewjBCvVtJ94Cj8rDtSvK6evIIVM4 +pcw72Hc3MKJP2W/R8kCtQXoXxdZKNYm3QdV8hn9VTYNKpXMgwDqvkPGaJI7ZjnHK +e7iG2rKPmT4dEw0SEe7Uq/DpFXYC5ODfqiAeW2GFZECpkJcNrVPSWh2HagCXZWK0 +vm9qp/UsQu0yrbYhnr68 +-----END CERTIFICATE----- + +# Issuer: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Subject: CN=Hellenic Academic and Research Institutions ECC RootCA 2015 O=Hellenic Academic and Research Institutions Cert. Authority +# Label: "Hellenic Academic and Research Institutions ECC RootCA 2015" +# Serial: 0 +# MD5 Fingerprint: 81:e5:b4:17:eb:c2:f5:e1:4b:0d:41:7b:49:92:fe:ef +# SHA1 Fingerprint: 9f:f1:71:8d:92:d5:9a:f3:7d:74:97:b4:bc:6f:84:68:0b:ba:b6:66 +# SHA256 Fingerprint: 44:b5:45:aa:8a:25:e6:5a:73:ca:15:dc:27:fc:36:d2:4c:1c:b9:95:3a:06:65:39:b1:15:82:dc:48:7b:48:33 +-----BEGIN CERTIFICATE----- +MIICwzCCAkqgAwIBAgIBADAKBggqhkjOPQQDAjCBqjELMAkGA1UEBhMCR1IxDzAN +BgNVBAcTBkF0aGVuczFEMEIGA1UEChM7SGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDZXJ0LiBBdXRob3JpdHkxRDBCBgNVBAMTO0hl +bGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgRUNDIFJv +b3RDQSAyMDE1MB4XDTE1MDcwNzEwMzcxMloXDTQwMDYzMDEwMzcxMlowgaoxCzAJ +BgNVBAYTAkdSMQ8wDQYDVQQHEwZBdGhlbnMxRDBCBgNVBAoTO0hlbGxlbmljIEFj +YWRlbWljIGFuZCBSZXNlYXJjaCBJbnN0aXR1dGlvbnMgQ2VydC4gQXV0aG9yaXR5 +MUQwQgYDVQQDEztIZWxsZW5pYyBBY2FkZW1pYyBhbmQgUmVzZWFyY2ggSW5zdGl0 +dXRpb25zIEVDQyBSb290Q0EgMjAxNTB2MBAGByqGSM49AgEGBSuBBAAiA2IABJKg +QehLgoRc4vgxEZmGZE4JJS+dQS8KrjVPdJWyUWRrjWvmP3CV8AVER6ZyOFB2lQJa +jq4onvktTpnvLEhvTCUp6NFxW98dwXU3tNf6e3pCnGoKVlp8aQuqgAkkbH7BRqNC +MEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFLQi +C4KZJAEOnLvkDv2/+5cgk5kqMAoGCCqGSM49BAMCA2cAMGQCMGfOFmI4oqxiRaep +lSTAGiecMjvAwNW6qef4BENThe5SId6d9SWDPp5YSy/XZxMOIQIwBeF1Ad5o7Sof +TUwJCA3sS61kFyjndc5FZXIhF8siQQ6ME5g4mlRtm8rifOoCWCKR +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X1 O=Internet Security Research Group +# Subject: CN=ISRG Root X1 O=Internet Security Research Group +# Label: "ISRG Root X1" +# Serial: 172886928669790476064670243504169061120 +# MD5 Fingerprint: 0c:d2:f9:e0:da:17:73:e9:ed:86:4d:a5:e3:70:e7:4e +# SHA1 Fingerprint: ca:bd:2a:79:a1:07:6a:31:f2:1d:25:36:35:cb:03:9d:43:29:a5:e8 +# SHA256 Fingerprint: 96:bc:ec:06:26:49:76:f3:74:60:77:9a:cf:28:c5:a7:cf:e8:a3:c0:aa:e1:1a:8f:fc:ee:05:c0:bd:df:08:c6 +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- + +# Issuer: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Subject: O=FNMT-RCM OU=AC RAIZ FNMT-RCM +# Label: "AC RAIZ FNMT-RCM" +# Serial: 485876308206448804701554682760554759 +# MD5 Fingerprint: e2:09:04:b4:d3:bd:d1:a0:14:fd:1a:d2:47:c4:57:1d +# SHA1 Fingerprint: ec:50:35:07:b2:15:c4:95:62:19:e2:a8:9a:5b:42:99:2c:4c:2c:20 +# SHA256 Fingerprint: eb:c5:57:0c:29:01:8c:4d:67:b1:aa:12:7b:af:12:f7:03:b4:61:1e:bc:17:b7:da:b5:57:38:94:17:9b:93:fa +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIPXZONMGc2yAYdGsdUhGkHMA0GCSqGSIb3DQEBCwUAMDsx +CzAJBgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJ +WiBGTk1ULVJDTTAeFw0wODEwMjkxNTU5NTZaFw0zMDAxMDEwMDAwMDBaMDsxCzAJ +BgNVBAYTAkVTMREwDwYDVQQKDAhGTk1ULVJDTTEZMBcGA1UECwwQQUMgUkFJWiBG +Tk1ULVJDTTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALpxgHpMhm5/ +yBNtwMZ9HACXjywMI7sQmkCpGreHiPibVmr75nuOi5KOpyVdWRHbNi63URcfqQgf +BBckWKo3Shjf5TnUV/3XwSyRAZHiItQDwFj8d0fsjz50Q7qsNI1NOHZnjrDIbzAz +WHFctPVrbtQBULgTfmxKo0nRIBnuvMApGGWn3v7v3QqQIecaZ5JCEJhfTzC8PhxF +tBDXaEAUwED653cXeuYLj2VbPNmaUtu1vZ5Gzz3rkQUCwJaydkxNEJY7kvqcfw+Z +374jNUUeAlz+taibmSXaXvMiwzn15Cou08YfxGyqxRxqAQVKL9LFwag0Jl1mpdIC +IfkYtwb1TplvqKtMUejPUBjFd8g5CSxJkjKZqLsXF3mwWsXmo8RZZUc1g16p6DUL +mbvkzSDGm0oGObVo/CK67lWMK07q87Hj/LaZmtVC+nFNCM+HHmpxffnTtOmlcYF7 +wk5HlqX2doWjKI/pgG6BU6VtX7hI+cL5NqYuSf+4lsKMB7ObiFj86xsc3i1w4peS +MKGJ47xVqCfWS+2QrYv6YyVZLag13cqXM7zlzced0ezvXg5KkAYmY6252TUtB7p2 +ZSysV4999AeU14ECll2jB0nVetBX+RvnU0Z1qrB5QstocQjpYL05ac70r8NWQMet +UqIJ5G+GR4of6ygnXYMgrwTJbFaai0b1AgMBAAGjgYMwgYAwDwYDVR0TAQH/BAUw +AwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFPd9xf3E6Jobd2Sn9R2gzL+H +YJptMD4GA1UdIAQ3MDUwMwYEVR0gADArMCkGCCsGAQUFBwIBFh1odHRwOi8vd3d3 +LmNlcnQuZm5tdC5lcy9kcGNzLzANBgkqhkiG9w0BAQsFAAOCAgEAB5BK3/MjTvDD +nFFlm5wioooMhfNzKWtN/gHiqQxjAb8EZ6WdmF/9ARP67Jpi6Yb+tmLSbkyU+8B1 +RXxlDPiyN8+sD8+Nb/kZ94/sHvJwnvDKuO+3/3Y3dlv2bojzr2IyIpMNOmqOFGYM +LVN0V2Ue1bLdI4E7pWYjJ2cJj+F3qkPNZVEI7VFY/uY5+ctHhKQV8Xa7pO6kO8Rf +77IzlhEYt8llvhjho6Tc+hj507wTmzl6NLrTQfv6MooqtyuGC2mDOL7Nii4LcK2N +JpLuHvUBKwrZ1pebbuCoGRw6IYsMHkCtA+fdZn71uSANA+iW+YJF1DngoABd15jm +fZ5nc8OaKveri6E6FO80vFIOiZiaBECEHX5FaZNXzuvO+FB8TxxuBEOb+dY7Ixjp +6o7RTUaN8Tvkasq6+yO3m/qZASlaWFot4/nUbQ4mrcFuNLwy+AwF+mWj2zs3gyLp +1txyM/1d8iC9djwj2ij3+RvrWWTV3F9yfiD8zYm1kGdNYno/Tq0dwzn+evQoFt9B +9kiABdcPUXmsEKvU7ANm5mqwujGSQkBqvjrTcuFqN1W8rB2Vt2lh8kORdOag0wok +RqEIr9baRRmW1FMdW4R58MD3R++Lj8UGrp1MYp3/RgT408m2ECVAdf4WqslKYIYv +uu8wd+RU4riEmViAqhOLUTpPSPaLtrM= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 1 O=Amazon +# Subject: CN=Amazon Root CA 1 O=Amazon +# Label: "Amazon Root CA 1" +# Serial: 143266978916655856878034712317230054538369994 +# MD5 Fingerprint: 43:c6:bf:ae:ec:fe:ad:2f:18:c6:88:68:30:fc:c8:e6 +# SHA1 Fingerprint: 8d:a7:f9:65:ec:5e:fc:37:91:0f:1c:6e:59:fd:c1:cc:6a:6e:de:16 +# SHA256 Fingerprint: 8e:cd:e6:88:4f:3d:87:b1:12:5b:a3:1a:c3:fc:b1:3d:70:16:de:7f:57:cc:90:4f:e1:cb:97:c6:ae:98:19:6e +-----BEGIN CERTIFICATE----- +MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj +ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM +9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw +IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 +VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L +93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm +jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC +AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA +A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI +U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs +N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv +o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU +5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy +rqXRfboQnoZsG4q5WTP468SQvvG5 +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 2 O=Amazon +# Subject: CN=Amazon Root CA 2 O=Amazon +# Label: "Amazon Root CA 2" +# Serial: 143266982885963551818349160658925006970653239 +# MD5 Fingerprint: c8:e5:8d:ce:a8:42:e2:7a:c0:2a:5c:7c:9e:26:bf:66 +# SHA1 Fingerprint: 5a:8c:ef:45:d7:a6:98:59:76:7a:8c:8b:44:96:b5:78:cf:47:4b:1a +# SHA256 Fingerprint: 1b:a5:b2:aa:8c:65:40:1a:82:96:01:18:f8:0b:ec:4f:62:30:4d:83:ce:c4:71:3a:19:c3:9c:01:1e:a4:6d:b4 +-----BEGIN CERTIFICATE----- +MIIFQTCCAymgAwIBAgITBmyf0pY1hp8KD+WGePhbJruKNzANBgkqhkiG9w0BAQwF +ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 +b24gUm9vdCBDQSAyMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTEL +MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv +b3QgQ0EgMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK2Wny2cSkxK +gXlRmeyKy2tgURO8TW0G/LAIjd0ZEGrHJgw12MBvIITplLGbhQPDW9tK6Mj4kHbZ +W0/jTOgGNk3Mmqw9DJArktQGGWCsN0R5hYGCrVo34A3MnaZMUnbqQ523BNFQ9lXg +1dKmSYXpN+nKfq5clU1Imj+uIFptiJXZNLhSGkOQsL9sBbm2eLfq0OQ6PBJTYv9K +8nu+NQWpEjTj82R0Yiw9AElaKP4yRLuH3WUnAnE72kr3H9rN9yFVkE8P7K6C4Z9r +2UXTu/Bfh+08LDmG2j/e7HJV63mjrdvdfLC6HM783k81ds8P+HgfajZRRidhW+me +z/CiVX18JYpvL7TFz4QuK/0NURBs+18bvBt+xa47mAExkv8LV/SasrlX6avvDXbR +8O70zoan4G7ptGmh32n2M8ZpLpcTnqWHsFcQgTfJU7O7f/aS0ZzQGPSSbtqDT6Zj +mUyl+17vIWR6IF9sZIUVyzfpYgwLKhbcAS4y2j5L9Z469hdAlO+ekQiG+r5jqFoz +7Mt0Q5X5bGlSNscpb/xVA1wf+5+9R+vnSUeVC06JIglJ4PVhHvG/LopyboBZ/1c6 ++XUyo05f7O0oYtlNc/LMgRdg7c3r3NunysV+Ar3yVAhU/bQtCSwXVEqY0VThUWcI +0u1ufm8/0i2BWSlmy5A5lREedCf+3euvAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSwDPBMMPQFWAJI/TPlUq9LhONm +UjANBgkqhkiG9w0BAQwFAAOCAgEAqqiAjw54o+Ci1M3m9Zh6O+oAA7CXDpO8Wqj2 +LIxyh6mx/H9z/WNxeKWHWc8w4Q0QshNabYL1auaAn6AFC2jkR2vHat+2/XcycuUY ++gn0oJMsXdKMdYV2ZZAMA3m3MSNjrXiDCYZohMr/+c8mmpJ5581LxedhpxfL86kS +k5Nrp+gvU5LEYFiwzAJRGFuFjWJZY7attN6a+yb3ACfAXVU3dJnJUH/jWS5E4ywl +7uxMMne0nxrpS10gxdr9HIcWxkPo1LsmmkVwXqkLN1PiRnsn/eBG8om3zEK2yygm +btmlyTrIQRNg91CMFa6ybRoVGld45pIq2WWQgj9sAq+uEjonljYE1x2igGOpm/Hl +urR8FLBOybEfdF849lHqm/osohHUqS0nGkWxr7JOcQ3AWEbWaQbLU8uz/mtBzUF+ +fUwPfHJ5elnNXkoOrJupmHN5fLT0zLm4BwyydFy4x2+IoZCn9Kr5v2c69BoVYh63 +n749sSmvZ6ES8lgQGVMDMBu4Gon2nL2XA46jCfMdiyHxtN/kHNGfZQIG6lzWE7OE +76KlXIx3KadowGuuQNKotOrN8I1LOJwZmhsoVLiJkO/KdYE+HvJkJMcYr07/R54H +9jVlpNMKVv/1F2Rs76giJUmTtt8AF9pYfl3uxRuw0dFfIRDH+fO6AgonB8Xx1sfT +4PsJYGw= +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 3 O=Amazon +# Subject: CN=Amazon Root CA 3 O=Amazon +# Label: "Amazon Root CA 3" +# Serial: 143266986699090766294700635381230934788665930 +# MD5 Fingerprint: a0:d4:ef:0b:f7:b5:d8:49:95:2a:ec:f5:c4:fc:81:87 +# SHA1 Fingerprint: 0d:44:dd:8c:3c:8c:1a:1a:58:75:64:81:e9:0f:2e:2a:ff:b3:d2:6e +# SHA256 Fingerprint: 18:ce:6c:fe:7b:f1:4e:60:b2:e3:47:b8:df:e8:68:cb:31:d0:2e:bb:3a:da:27:15:69:f5:03:43:b4:6d:b3:a4 +-----BEGIN CERTIFICATE----- +MIIBtjCCAVugAwIBAgITBmyf1XSXNmY/Owua2eiedgPySjAKBggqhkjOPQQDAjA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSAzMB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgMzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABCmXp8ZBf8ANm+gBG1bG8lKl +ui2yEujSLtf6ycXYqm0fc4E7O5hrOXwzpcVOho6AF2hiRVd9RFgdszflZwjrZt6j +QjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMB0GA1UdDgQWBBSr +ttvXBp43rDCGB5Fwx5zEGbF4wDAKBggqhkjOPQQDAgNJADBGAiEA4IWSoxe3jfkr +BqWTrBqYaGFy+uGh0PsceGCmQ5nFuMQCIQCcAu/xlJyzlvnrxir4tiz+OpAUFteM +YyRIHN8wfdVoOw== +-----END CERTIFICATE----- + +# Issuer: CN=Amazon Root CA 4 O=Amazon +# Subject: CN=Amazon Root CA 4 O=Amazon +# Label: "Amazon Root CA 4" +# Serial: 143266989758080763974105200630763877849284878 +# MD5 Fingerprint: 89:bc:27:d5:eb:17:8d:06:6a:69:d5:fd:89:47:b4:cd +# SHA1 Fingerprint: f6:10:84:07:d6:f8:bb:67:98:0c:c2:e2:44:c2:eb:ae:1c:ef:63:be +# SHA256 Fingerprint: e3:5d:28:41:9e:d0:20:25:cf:a6:90:38:cd:62:39:62:45:8d:a5:c6:95:fb:de:a3:c2:2b:0b:fb:25:89:70:92 +-----BEGIN CERTIFICATE----- +MIIB8jCCAXigAwIBAgITBmyf18G7EEwpQ+Vxe3ssyBrBDjAKBggqhkjOPQQDAzA5 +MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6b24g +Um9vdCBDQSA0MB4XDTE1MDUyNjAwMDAwMFoXDTQwMDUyNjAwMDAwMFowOTELMAkG +A1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJvb3Qg +Q0EgNDB2MBAGByqGSM49AgEGBSuBBAAiA2IABNKrijdPo1MN/sGKe0uoe0ZLY7Bi +9i0b2whxIdIA6GO9mif78DluXeo9pcmBqqNbIJhFXRbb/egQbeOc4OO9X4Ri83Bk +M6DLJC9wuoihKqB1+IGuYgbEgds5bimwHvouXKNCMEAwDwYDVR0TAQH/BAUwAwEB +/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0OBBYEFNPsxzplbszh2naaVvuc84ZtV+WB +MAoGCCqGSM49BAMDA2gAMGUCMDqLIfG9fhGt0O9Yli/W651+kI0rz2ZVwyzjKKlw +CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW +1KyLa2tJElMzrdfkviT8tQp21KW8EA== +-----END CERTIFICATE----- + +# Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM +# Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" +# Serial: 1 +# MD5 Fingerprint: dc:00:81:dc:69:2f:3e:2f:b0:3b:f6:3d:5a:91:8e:49 +# SHA1 Fingerprint: 31:43:64:9b:ec:ce:27:ec:ed:3a:3f:0b:8f:0d:e4:e8:91:dd:ee:ca +# SHA256 Fingerprint: 46:ed:c3:68:90:46:d5:3a:45:3f:b3:10:4a:b8:0d:ca:ec:65:8b:26:60:ea:16:29:dd:7e:86:79:90:64:87:16 +-----BEGIN CERTIFICATE----- +MIIEYzCCA0ugAwIBAgIBATANBgkqhkiG9w0BAQsFADCB0jELMAkGA1UEBhMCVFIx +GDAWBgNVBAcTD0dlYnplIC0gS29jYWVsaTFCMEAGA1UEChM5VHVya2l5ZSBCaWxp +bXNlbCB2ZSBUZWtub2xvamlrIEFyYXN0aXJtYSBLdXJ1bXUgLSBUVUJJVEFLMS0w +KwYDVQQLEyRLYW11IFNlcnRpZmlrYXN5b24gTWVya2V6aSAtIEthbXUgU00xNjA0 +BgNVBAMTLVRVQklUQUsgS2FtdSBTTSBTU0wgS29rIFNlcnRpZmlrYXNpIC0gU3Vy +dW0gMTAeFw0xMzExMjUwODI1NTVaFw00MzEwMjUwODI1NTVaMIHSMQswCQYDVQQG +EwJUUjEYMBYGA1UEBxMPR2ViemUgLSBLb2NhZWxpMUIwQAYDVQQKEzlUdXJraXll +IEJpbGltc2VsIHZlIFRla25vbG9qaWsgQXJhc3Rpcm1hIEt1cnVtdSAtIFRVQklU +QUsxLTArBgNVBAsTJEthbXUgU2VydGlmaWthc3lvbiBNZXJrZXppIC0gS2FtdSBT +TTE2MDQGA1UEAxMtVFVCSVRBSyBLYW11IFNNIFNTTCBLb2sgU2VydGlmaWthc2kg +LSBTdXJ1bSAxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr3UwM6q7 +a9OZLBI3hNmNe5eA027n/5tQlT6QlVZC1xl8JoSNkvoBHToP4mQ4t4y86Ij5iySr +LqP1N+RAjhgleYN1Hzv/bKjFxlb4tO2KRKOrbEz8HdDc72i9z+SqzvBV96I01INr +N3wcwv61A+xXzry0tcXtAA9TNypN9E8Mg/uGz8v+jE69h/mniyFXnHrfA2eJLJ2X +YacQuFWQfw4tJzh03+f92k4S400VIgLI4OD8D62K18lUUMw7D8oWgITQUVbDjlZ/ +iSIzL+aFCr2lqBs23tPcLG07xxO9WSMs5uWk99gL7eqQQESolbuT1dCANLZGeA4f +AJNG4e7p+exPFwIDAQABo0IwQDAdBgNVHQ4EFgQUZT/HiobGPN08VFw1+DrtUgxH +V8gwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBACo/4fEyjq7hmFxLXs9rHmoJ0iKpEsdeV31zVmSAhHqT5Am5EM2fKifh +AHe+SMg1qIGf5LgsyX8OsNJLN13qudULXjS99HMpw+0mFZx+CFOKWI3QSyjfwbPf +IPP54+M638yclNhOT8NrF7f3cuitZjO1JVOr4PhMqZ398g26rrnZqsZr+ZO7rqu4 +lzwDGrpDxpa5RXI4s6ehlj2Re37AIVNMh+3yC1SVUZPVIqUNivGTDj5UDrDYyU7c +8jEyVupk+eq1nRZmQnLzf9OxMUP8pI4X8W0jq5Rm+K37DwhuJi1/FwcJsoz7UMCf +lo3Ptv0AnVoUmr8CRPXBwp8iXqIPoeM= +-----END CERTIFICATE----- + +# Issuer: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Subject: CN=GDCA TrustAUTH R5 ROOT O=GUANG DONG CERTIFICATE AUTHORITY CO.,LTD. +# Label: "GDCA TrustAUTH R5 ROOT" +# Serial: 9009899650740120186 +# MD5 Fingerprint: 63:cc:d9:3d:34:35:5c:6f:53:a3:e2:08:70:48:1f:b4 +# SHA1 Fingerprint: 0f:36:38:5b:81:1a:25:c3:9b:31:4e:83:ca:e9:34:66:70:cc:74:b4 +# SHA256 Fingerprint: bf:ff:8f:d0:44:33:48:7d:6a:8a:a6:0c:1a:29:76:7a:9f:c2:bb:b0:5e:42:0f:71:3a:13:b9:92:89:1d:38:93 +-----BEGIN CERTIFICATE----- +MIIFiDCCA3CgAwIBAgIIfQmX/vBH6nowDQYJKoZIhvcNAQELBQAwYjELMAkGA1UE +BhMCQ04xMjAwBgNVBAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZ +IENPLixMVEQuMR8wHQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMB4XDTE0 +MTEyNjA1MTMxNVoXDTQwMTIzMTE1NTk1OVowYjELMAkGA1UEBhMCQ04xMjAwBgNV +BAoMKUdVQU5HIERPTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZIENPLixMVEQuMR8w +HQYDVQQDDBZHRENBIFRydXN0QVVUSCBSNSBST09UMIICIjANBgkqhkiG9w0BAQEF +AAOCAg8AMIICCgKCAgEA2aMW8Mh0dHeb7zMNOwZ+Vfy1YI92hhJCfVZmPoiC7XJj +Dp6L3TQsAlFRwxn9WVSEyfFrs0yw6ehGXTjGoqcuEVe6ghWinI9tsJlKCvLriXBj +TnnEt1u9ol2x8kECK62pOqPseQrsXzrj/e+APK00mxqriCZ7VqKChh/rNYmDf1+u +KU49tm7srsHwJ5uu4/Ts765/94Y9cnrrpftZTqfrlYwiOXnhLQiPzLyRuEH3FMEj +qcOtmkVEs7LXLM3GKeJQEK5cy4KOFxg2fZfmiJqwTTQJ9Cy5WmYqsBebnh52nUpm +MUHfP/vFBu8btn4aRjb3ZGM74zkYI+dndRTVdVeSN72+ahsmUPI2JgaQxXABZG12 +ZuGR224HwGGALrIuL4xwp9E7PLOR5G62xDtw8mySlwnNR30YwPO7ng/Wi64HtloP +zgsMR6flPri9fcebNaBhlzpBdRfMK5Z3KpIhHtmVdiBnaM8Nvd/WHwlqmuLMc3Gk +L30SgLdTMEZeS1SZD2fJpcjyIMGC7J0R38IC+xo70e0gmu9lZJIQDSri3nDxGGeC +jGHeuLzRL5z7D9Ar7Rt2ueQ5Vfj4oR24qoAATILnsn8JuLwwoC8N9VKejveSswoA +HQBUlwbgsQfZxw9cZX08bVlX5O2ljelAU58VS6Bx9hoh49pwBiFYFIeFd3mqgnkC +AwEAAaNCMEAwHQYDVR0OBBYEFOLJQJ9NzuiaoXzPDj9lxSmIahlRMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQDRSVfg +p8xoWLoBDysZzY2wYUWsEe1jUGn4H3++Fo/9nesLqjJHdtJnJO29fDMylyrHBYZm +DRd9FBUb1Ov9H5r2XpdptxolpAqzkT9fNqyL7FeoPueBihhXOYV0GkLH6VsTX4/5 +COmSdI31R9KrO9b7eGZONn356ZLpBN79SWP8bfsUcZNnL0dKt7n/HipzcEYwv1ry +L3ml4Y0M2fmyYzeMN2WFcGpcWwlyua1jPLHd+PwyvzeG5LuOmCd+uh8W4XAR8gPf +JWIyJyYYMoSf/wA6E7qaTfRPuBRwIrHKK5DOKcFw9C+df/KQHtZa37dG/OaG+svg +IHZ6uqbL9XzeYqWxi+7egmaKTjowHz+Ay60nugxe19CxVsp3cbK1daFQqUBDF8Io +2c9Si1vIY9RCPqAzekYu9wogRlR+ak8x8YF+QnQ4ZXMn7sZ8uI7XpTrXmKGcjBBV +09tL7ECQ8s1uV9JiDnxXk7Gnbc2dg7sq5+W2O3FYrf3RRbxake5TFW/TRQl1brqQ +XR4EzzffHqhmsYzmIGrv/EhOdJhCrylvLmrH+33RZjEizIYAfmaDDEL0vTSSwxrq +T8p+ck0LcIymSLumoRT2+1hEmRSuqguTaaApJUqlyyvdimYHFngVV3Eb7PVHhPOe +MTd61X8kreS8/f3MboPoDKi3QWwH3b08hpcv0g== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority RSA O=SSL Corporation +# Label: "SSL.com Root Certification Authority RSA" +# Serial: 8875640296558310041 +# MD5 Fingerprint: 86:69:12:c0:70:f1:ec:ac:ac:c2:d5:bc:a5:5b:a1:29 +# SHA1 Fingerprint: b7:ab:33:08:d1:ea:44:77:ba:14:80:12:5a:6f:bd:a9:36:49:0c:bb +# SHA256 Fingerprint: 85:66:6a:56:2e:e0:be:5c:e9:25:c1:d8:89:0a:6f:76:a8:7e:c1:6d:4d:7d:5f:29:ea:74:19:cf:20:12:3b:69 +-----BEGIN CERTIFICATE----- +MIIF3TCCA8WgAwIBAgIIeyyb0xaAMpkwDQYJKoZIhvcNAQELBQAwfDELMAkGA1UE +BhMCVVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQK +DA9TU0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBSU0EwHhcNMTYwMjEyMTczOTM5WhcNNDEwMjEyMTcz +OTM5WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNv +bSBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IFJTQTCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAPkP3aMrfcvQKv7sZ4Wm5y4bunfh4/WvpOz6Sl2R +xFdHaxh3a3by/ZPkPQ/CFp4LZsNWlJ4Xg4XOVu/yFv0AYvUiCVToZRdOQbngT0aX +qhvIuG5iXmmxX9sqAn78bMrzQdjt0Oj8P2FI7bADFB0QDksZ4LtO7IZl/zbzXmcC +C52GVWH9ejjt/uIZALdvoVBidXQ8oPrIJZK0bnoix/geoeOy3ZExqysdBP+lSgQ3 +6YWkMyv94tZVNHwZpEpox7Ko07fKoZOI68GXvIz5HdkihCR0xwQ9aqkpk8zruFvh +/l8lqjRYyMEjVJ0bmBHDOJx+PYZspQ9AhnwC9FwCTyjLrnGfDzrIM/4RJTXq/LrF +YD3ZfBjVsqnTdXgDciLKOsMf7yzlLqn6niy2UUb9rwPW6mBo6oUWNmuF6R7As93E +JNyAKoFBbZQ+yODJgUEAnl6/f8UImKIYLEJAs/lvOCdLToD0PYFH4Ih86hzOtXVc +US4cK38acijnALXRdMbX5J+tB5O2UzU1/Dfkw/ZdFr4hc96SCvigY2q8lpJqPvi8 +ZVWb3vUNiSYE/CUapiVpy8JtynziWV+XrOvvLsi81xtZPCvM8hnIk2snYxnP/Okm ++Mpxm3+T/jRnhE6Z6/yzeAkzcLpmpnbtG3PrGqUNxCITIJRWCk4sbE6x/c+cCbqi +M+2HAgMBAAGjYzBhMB0GA1UdDgQWBBTdBAkHovV6fVJTEpKV7jiAJQ2mWTAPBgNV +HRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFN0ECQei9Xp9UlMSkpXuOIAlDaZZMA4G +A1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAIBgRlCn7Jp0cHh5wYfGV +cpNxJK1ok1iOMq8bs3AD/CUrdIWQPXhq9LmLpZc7tRiRux6n+UBbkflVma8eEdBc +Hadm47GUBwwyOabqG7B52B2ccETjit3E+ZUfijhDPwGFpUenPUayvOUiaPd7nNgs +PgohyC0zrL/FgZkxdMF1ccW+sfAjRfSda/wZY52jvATGGAslu1OJD7OAUN5F7kR/ +q5R4ZJjT9ijdh9hwZXT7DrkT66cPYakylszeu+1jTBi7qUD3oFRuIIhxdRjqerQ0 +cuAjJ3dctpDqhiVAq+8zD8ufgr6iIPv2tS0a5sKFsXQP+8hlAqRSAUfdSSLBv9jr +a6x+3uxjMxW3IwiPxg+NQVrdjsW5j+VFP3jbutIbQLH+cU0/4IGiul607BXgk90I +H37hVZkLId6Tngr75qNJvTYw/ud3sqB1l7UtgYgXZSD32pAAn8lSzDLKNXz1PQ/Y +K9f1JmzJBjSWFupwWRoyeXkLtoh/D1JIPb9s2KJELtFOt3JY04kTlf5Eq/jXixtu +nLwsoFvVagCvXzfh1foQC5ichucmj87w7G6KVwuA406ywKBjYZC6VWg3dGq2ktuf +oYYitmUnDuy2n0Jg5GfCtdpBC8TTi2EbvPofkSvXRAdeuims2cXp71NIWuuA8ShY +Ic2wBlX7Jz9TkHCpBB5XJ7k= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com Root Certification Authority ECC" +# Serial: 8495723813297216424 +# MD5 Fingerprint: 2e:da:e4:39:7f:9c:8f:37:d1:70:9f:26:17:51:3a:8e +# SHA1 Fingerprint: c3:19:7c:39:24:e6:54:af:1b:c4:ab:20:95:7a:e2:c3:0e:13:02:6a +# SHA256 Fingerprint: 34:17:bb:06:cc:60:07:da:1b:96:1c:92:0b:8a:b4:ce:3f:ad:82:0e:4a:a3:0b:9a:cb:c4:a7:4e:bd:ce:bc:65 +-----BEGIN CERTIFICATE----- +MIICjTCCAhSgAwIBAgIIdebfy8FoW6gwCgYIKoZIzj0EAwIwfDELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xMTAvBgNVBAMMKFNTTC5jb20gUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNDAzWhcNNDEwMjEyMTgxNDAz +WjB8MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hvdXN0 +b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjExMC8GA1UEAwwoU1NMLmNvbSBS +b290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABEVuqVDEpiM2nl8ojRfLliJkP9x6jh3MCLOicSS6jkm5BBtHllirLZXI +7Z4INcgn64mMU1jrYor+8FsPazFSY0E7ic3s7LaNGdM0B9y7xgZ/wkWV7Mt/qCPg +CemB+vNH06NjMGEwHQYDVR0OBBYEFILRhXMw5zUE044CkvvlpNHEIejNMA8GA1Ud +EwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUgtGFczDnNQTTjgKS++Wk0cQh6M0wDgYD +VR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2cAMGQCMG/n61kRpGDPYbCWe+0F+S8T +kdzt5fxQaxFGRrMcIQBiu77D5+jNB5n5DQtdcj7EqgIwH7y6C+IwJPt8bYBVCpk+ +gA0z5Wajs6O7pdWLjwkspl1+4vAHCGht0nxpbl/f5Wpl +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority RSA R2 O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority RSA R2" +# Serial: 6248227494352943350 +# MD5 Fingerprint: e1:1e:31:58:1a:ae:54:53:02:f6:17:6a:11:7b:4d:95 +# SHA1 Fingerprint: 74:3a:f0:52:9b:d0:32:a0:f4:4a:83:cd:d4:ba:a9:7b:7c:2e:c4:9a +# SHA256 Fingerprint: 2e:7b:f1:6c:c2:24:85:a7:bb:e2:aa:86:96:75:07:61:b0:ae:39:be:3b:2f:e9:d0:cc:6d:4e:f7:34:91:42:5c +-----BEGIN CERTIFICATE----- +MIIF6zCCA9OgAwIBAgIIVrYpzTS8ePYwDQYJKoZIhvcNAQELBQAwgYIxCzAJBgNV +BAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4GA1UEBwwHSG91c3RvbjEYMBYGA1UE +CgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQDDC5TU0wuY29tIEVWIFJvb3QgQ2Vy +dGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIyMB4XDTE3MDUzMTE4MTQzN1oXDTQy +MDUzMDE4MTQzN1owgYIxCzAJBgNVBAYTAlVTMQ4wDAYDVQQIDAVUZXhhczEQMA4G +A1UEBwwHSG91c3RvbjEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMTcwNQYDVQQD +DC5TU0wuY29tIEVWIFJvb3QgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkgUlNBIFIy +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAjzZlQOHWTcDXtOlG2mvq +M0fNTPl9fb69LT3w23jhhqXZuglXaO1XPqDQCEGD5yhBJB/jchXQARr7XnAjssuf +OePPxU7Gkm0mxnu7s9onnQqG6YE3Bf7wcXHswxzpY6IXFJ3vG2fThVUCAtZJycxa +4bH3bzKfydQ7iEGonL3Lq9ttewkfokxykNorCPzPPFTOZw+oz12WGQvE43LrrdF9 +HSfvkusQv1vrO6/PgN3B0pYEW3p+pKk8OHakYo6gOV7qd89dAFmPZiw+B6KjBSYR +aZfqhbcPlgtLyEDhULouisv3D5oi53+aNxPN8k0TayHRwMwi8qFG9kRpnMphNQcA +b9ZhCBHqurj26bNg5U257J8UZslXWNvNh2n4ioYSA0e/ZhN2rHd9NCSFg83XqpyQ +Gp8hLH94t2S42Oim9HizVcuE0jLEeK6jj2HdzghTreyI/BXkmg3mnxp3zkyPuBQV +PWKchjgGAGYS5Fl2WlPAApiiECtoRHuOec4zSnaqW4EWG7WK2NAAe15itAnWhmMO +pgWVSbooi4iTsjQc2KRVbrcc0N6ZVTsj9CLg+SlmJuwgUHfbSguPvuUCYHBBXtSu +UDkiFCbLsjtzdFVHB3mBOagwE0TlBIqulhMlQg+5U8Sb/M3kHN48+qvWBkofZ6aY +MBzdLNvcGJVXZsb/XItW9XcCAwEAAaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNV +HSMEGDAWgBT5YLvU49U09rj1BoAlp3PbRmmonjAdBgNVHQ4EFgQU+WC71OPVNPa4 +9QaAJadz20ZpqJ4wDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEBCwUAA4ICAQBW +s47LCp1Jjr+kxJG7ZhcFUZh1++VQLHqe8RT6q9OKPv+RKY9ji9i0qVQBDb6Thi/5 +Sm3HXvVX+cpVHBK+Rw82xd9qt9t1wkclf7nxY/hoLVUE0fKNsKTPvDxeH3jnpaAg +cLAExbf3cqfeIg29MyVGjGSSJuM+LmOW2puMPfgYCdcDzH2GguDKBAdRUNf/ktUM +79qGn5nX67evaOI5JpS6aLe/g9Pqemc9YmeuJeVy6OLk7K4S9ksrPJ/psEDzOFSz +/bdoyNrGj1E8svuR3Bznm53htw1yj+KkxKl4+esUrMZDBcJlOSgYAsOCsp0FvmXt +ll9ldDz7CTUue5wT/RsPXcdtgTpWD8w74a8CLyKsRspGPKAcTNZEtF4uXBVmCeEm +Kf7GUmG6sXP/wwyc5WxqlD8UykAWlYTzWamsX0xhk23RO8yilQwipmdnRC652dKK +QbNmC1r7fSOl8hqw/96bg5Qu0T/fkreRrwU7ZcegbLHNYhLDkBvjJc40vG93drEQ +w/cFGsDWr3RiSBd3kmmQYRzelYB0VI8YHMPzA9C/pEN1hlMYegouCRw2n5H9gooi +S9EOUCXdywMMF8mDAAhONU2Ki+3wApRmLER/y5UnlhetCTCstnEXbosX9hwJ1C07 +mKVx01QT2WDz9UtmT/rx7iASjbSsV7FFY6GsdqnC+w== +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Subject: CN=SSL.com EV Root Certification Authority ECC O=SSL Corporation +# Label: "SSL.com EV Root Certification Authority ECC" +# Serial: 3182246526754555285 +# MD5 Fingerprint: 59:53:22:65:83:42:01:54:c0:ce:42:b9:5a:7c:f2:90 +# SHA1 Fingerprint: 4c:dd:51:a3:d1:f5:20:32:14:b0:c6:c5:32:23:03:91:c7:46:42:6d +# SHA256 Fingerprint: 22:a2:c1:f7:bd:ed:70:4c:c1:e7:01:b5:f4:08:c3:10:88:0f:e9:56:b5:de:2a:4a:44:f9:9c:87:3a:25:a7:c8 +-----BEGIN CERTIFICATE----- +MIIClDCCAhqgAwIBAgIILCmcWxbtBZUwCgYIKoZIzj0EAwIwfzELMAkGA1UEBhMC +VVMxDjAMBgNVBAgMBVRleGFzMRAwDgYDVQQHDAdIb3VzdG9uMRgwFgYDVQQKDA9T +U0wgQ29ycG9yYXRpb24xNDAyBgNVBAMMK1NTTC5jb20gRVYgUm9vdCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eSBFQ0MwHhcNMTYwMjEyMTgxNTIzWhcNNDEwMjEyMTgx +NTIzWjB/MQswCQYDVQQGEwJVUzEOMAwGA1UECAwFVGV4YXMxEDAOBgNVBAcMB0hv +dXN0b24xGDAWBgNVBAoMD1NTTCBDb3Jwb3JhdGlvbjE0MDIGA1UEAwwrU1NMLmNv +bSBFViBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IEVDQzB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABKoSR5CYG/vvw0AHgyBO8TCCogbR8pKGYfL2IWjKAMTH6kMA +VIbc/R/fALhBYlzccBYy3h+Z1MzFB8gIH2EWB1E9fVwHU+M1OIzfzZ/ZLg1Kthku +WnBaBu2+8KGwytAJKaNjMGEwHQYDVR0OBBYEFFvKXuXe0oGqzagtZFG22XKbl+ZP +MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUW8pe5d7SgarNqC1kUbbZcpuX +5k8wDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMCA2gAMGUCMQCK5kCJN+vp1RPZ +ytRrJPOwPYdGWBrssd9v+1a6cGvHOMzosYxPD/fxZ3YOg9AeUY8CMD32IygmTMZg +h5Mmm7I1HrrW9zzRHM76JTymGoEVW/MSD2zuZYrJh6j5B+BimoxcSg== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign Root CA - R6 +# Label: "GlobalSign Root CA - R6" +# Serial: 1417766617973444989252670301619537 +# MD5 Fingerprint: 4f:dd:07:e4:d4:22:64:39:1e:0c:37:42:ea:d1:c6:ae +# SHA1 Fingerprint: 80:94:64:0e:b5:a7:a1:ca:11:9c:1f:dd:d5:9f:81:02:63:a7:fb:d1 +# SHA256 Fingerprint: 2c:ab:ea:fe:37:d0:6c:a2:2a:ba:73:91:c0:03:3d:25:98:29:52:c4:53:64:73:49:76:3a:3a:b5:ad:6c:cf:69 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIORea7A4Mzw4VlSOb/RVEwDQYJKoZIhvcNAQEMBQAwTDEg +MB4GA1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjYxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTQxMjEwMDAwMDAwWhcNMzQx +MjEwMDAwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSNjET +MBEGA1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAJUH6HPKZvnsFMp7PPcNCPG0RQssgrRI +xutbPK6DuEGSMxSkb3/pKszGsIhrxbaJ0cay/xTOURQh7ErdG1rG1ofuTToVBu1k +ZguSgMpE3nOUTvOniX9PeGMIyBJQbUJmL025eShNUhqKGoC3GYEOfsSKvGRMIRxD +aNc9PIrFsmbVkJq3MQbFvuJtMgamHvm566qjuL++gmNQ0PAYid/kD3n16qIfKtJw +LnvnvJO7bVPiSHyMEAc4/2ayd2F+4OqMPKq0pPbzlUoSB239jLKJz9CgYXfIWHSw +1CM69106yqLbnQneXUQtkPGBzVeS+n68UARjNN9rkxi+azayOeSsJDa38O+2HBNX +k7besvjihbdzorg1qkXy4J02oW9UivFyVm4uiMVRQkQVlO6jxTiWm05OWgtH8wY2 +SXcwvHE35absIQh1/OZhFj931dmRl4QKbNQCTXTAFO39OfuD8l4UoQSwC+n+7o/h +bguyCLNhZglqsQY6ZZZZwPA1/cnaKI0aEYdwgQqomnUdnjqGBQCe24DWJfncBZ4n +WUx2OVvq+aWh2IMP0f/fMBH5hc8zSPXKbWQULHpYT9NLCEnFlWQaYw55PfWzjMpY +rZxCRXluDocZXFSxZba/jJvcE+kNb7gu3GduyYsRtYQUigAZcIN5kZeR1Bonvzce +MgfYFGM8KEyvAgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMBAf8EBTAD +AQH/MB0GA1UdDgQWBBSubAWjkxPioufi1xzWx/B/yGdToDAfBgNVHSMEGDAWgBSu +bAWjkxPioufi1xzWx/B/yGdToDANBgkqhkiG9w0BAQwFAAOCAgEAgyXt6NH9lVLN +nsAEoJFp5lzQhN7craJP6Ed41mWYqVuoPId8AorRbrcWc+ZfwFSY1XS+wc3iEZGt +Ixg93eFyRJa0lV7Ae46ZeBZDE1ZXs6KzO7V33EByrKPrmzU+sQghoefEQzd5Mr61 +55wsTLxDKZmOMNOsIeDjHfrYBzN2VAAiKrlNIC5waNrlU/yDXNOd8v9EDERm8tLj +vUYAGm0CuiVdjaExUd1URhxN25mW7xocBFymFe944Hn+Xds+qkxV/ZoVqW/hpvvf +cDDpw+5CRu3CkwWJ+n1jez/QcYF8AOiYrg54NMMl+68KnyBr3TsTjxKM4kEaSHpz +oHdpx7Zcf4LIHv5YGygrqGytXm3ABdJ7t+uA/iU3/gKbaKxCXcPu9czc8FB10jZp +nOZ7BN9uBmm23goJSFmH63sUYHpkqmlD75HHTOwY3WzvUy2MmeFe8nI+z1TIvWfs +pA9MRf/TuTAjB0yPEL+GltmZWrSZVxykzLsViVO6LAUP5MSeGbEYNNVMnbrt9x+v +JJUEeKgDu+6B5dpffItKoZB0JaezPkvILFa9x8jvOOJckvB595yEunQtYQEgfn7R +8k8HWV+LLUNS60YMlOH1Zkd5d9VUWx+tJDfLRVpOoERIyNiwmcUVhAn21klJwGW4 +5hpxbqCo8YLoRT5s1gLXCmeDBVrJpBA= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Subject: CN=OISTE WISeKey Global Root GC CA O=WISeKey OU=OISTE Foundation Endorsed +# Label: "OISTE WISeKey Global Root GC CA" +# Serial: 44084345621038548146064804565436152554 +# MD5 Fingerprint: a9:d6:b9:2d:2f:93:64:f8:a5:69:ca:91:e9:68:07:23 +# SHA1 Fingerprint: e0:11:84:5e:34:de:be:88:81:b9:9c:f6:16:26:d1:96:1f:c3:b9:31 +# SHA256 Fingerprint: 85:60:f9:1c:36:24:da:ba:95:70:b5:fe:a0:db:e3:6f:f1:1a:83:23:be:94:86:85:4f:b3:f3:4a:55:71:19:8d +-----BEGIN CERTIFICATE----- +MIICaTCCAe+gAwIBAgIQISpWDK7aDKtARb8roi066jAKBggqhkjOPQQDAzBtMQsw +CQYDVQQGEwJDSDEQMA4GA1UEChMHV0lTZUtleTEiMCAGA1UECxMZT0lTVEUgRm91 +bmRhdGlvbiBFbmRvcnNlZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwg +Um9vdCBHQyBDQTAeFw0xNzA1MDkwOTQ4MzRaFw00MjA1MDkwOTU4MzNaMG0xCzAJ +BgNVBAYTAkNIMRAwDgYDVQQKEwdXSVNlS2V5MSIwIAYDVQQLExlPSVNURSBGb3Vu +ZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBXSVNlS2V5IEdsb2JhbCBS +b290IEdDIENBMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAETOlQwMYPchi82PG6s4ni +eUqjFqdrVCTbUf/q9Akkwwsin8tqJ4KBDdLArzHkdIJuyiXZjHWd8dvQmqJLIX4W +p2OQ0jnUsYd4XxiWD1AbNTcPasbc2RNNpI6QN+a9WzGRo1QwUjAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUSIcUrOPDnpBgOtfKie7T +rYy0UGYwEAYJKwYBBAGCNxUBBAMCAQAwCgYIKoZIzj0EAwMDaAAwZQIwJsdpW9zV +57LnyAyMjMPdeYwbY9XJUpROTYJKcx6ygISpJcBMWm1JKWB4E+J+SOtkAjEA2zQg +Mgj/mkkCtojeFK9dbJlxjRo/i9fgojaGHAeCOnZT/cKi7e97sIBPWA9LUzm9 +-----END CERTIFICATE----- + +# Issuer: CN=UCA Global G2 Root O=UniTrust +# Subject: CN=UCA Global G2 Root O=UniTrust +# Label: "UCA Global G2 Root" +# Serial: 124779693093741543919145257850076631279 +# MD5 Fingerprint: 80:fe:f0:c4:4a:f0:5c:62:32:9f:1c:ba:78:a9:50:f8 +# SHA1 Fingerprint: 28:f9:78:16:19:7a:ff:18:25:18:aa:44:fe:c1:a0:ce:5c:b6:4c:8a +# SHA256 Fingerprint: 9b:ea:11:c9:76:fe:01:47:64:c1:be:56:a6:f9:14:b5:a5:60:31:7a:bd:99:88:39:33:82:e5:16:1a:a0:49:3c +-----BEGIN CERTIFICATE----- +MIIFRjCCAy6gAwIBAgIQXd+x2lqj7V2+WmUgZQOQ7zANBgkqhkiG9w0BAQsFADA9 +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxGzAZBgNVBAMMElVDQSBH +bG9iYWwgRzIgUm9vdDAeFw0xNjAzMTEwMDAwMDBaFw00MDEyMzEwMDAwMDBaMD0x +CzAJBgNVBAYTAkNOMREwDwYDVQQKDAhVbmlUcnVzdDEbMBkGA1UEAwwSVUNBIEds +b2JhbCBHMiBSb290MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxeYr +b3zvJgUno4Ek2m/LAfmZmqkywiKHYUGRO8vDaBsGxUypK8FnFyIdK+35KYmToni9 +kmugow2ifsqTs6bRjDXVdfkX9s9FxeV67HeToI8jrg4aA3++1NDtLnurRiNb/yzm +VHqUwCoV8MmNsHo7JOHXaOIxPAYzRrZUEaalLyJUKlgNAQLx+hVRZ2zA+te2G3/R +VogvGjqNO7uCEeBHANBSh6v7hn4PJGtAnTRnvI3HLYZveT6OqTwXS3+wmeOwcWDc +C/Vkw85DvG1xudLeJ1uK6NjGruFZfc8oLTW4lVYa8bJYS7cSN8h8s+1LgOGN+jIj +tm+3SJUIsUROhYw6AlQgL9+/V087OpAh18EmNVQg7Mc/R+zvWr9LesGtOxdQXGLY +D0tK3Cv6brxzks3sx1DoQZbXqX5t2Okdj4q1uViSukqSKwxW/YDrCPBeKW4bHAyv +j5OJrdu9o54hyokZ7N+1wxrrFv54NkzWbtA+FxyQF2smuvt6L78RHBgOLXMDj6Dl +NaBa4kx1HXHhOThTeEDMg5PXCp6dW4+K5OXgSORIskfNTip1KnvyIvbJvgmRlld6 +iIis7nCs+dwp4wwcOxJORNanTrAmyPPZGpeRaOrvjUYG0lZFWJo8DA+DuAUlwznP +O6Q0ibd5Ei9Hxeepl2n8pndntd978XplFeRhVmUCAwEAAaNCMEAwDgYDVR0PAQH/ +BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFIHEjMz15DD/pQwIX4wV +ZyF0Ad/fMA0GCSqGSIb3DQEBCwUAA4ICAQATZSL1jiutROTL/7lo5sOASD0Ee/oj +L3rtNtqyzm325p7lX1iPyzcyochltq44PTUbPrw7tgTQvPlJ9Zv3hcU2tsu8+Mg5 +1eRfB70VVJd0ysrtT7q6ZHafgbiERUlMjW+i67HM0cOU2kTC5uLqGOiiHycFutfl +1qnN3e92mI0ADs0b+gO3joBYDic/UvuUospeZcnWhNq5NXHzJsBPd+aBJ9J3O5oU +b3n09tDh05S60FdRvScFDcH9yBIw7m+NESsIndTUv4BFFJqIRNow6rSn4+7vW4LV +PtateJLbXDzz2K36uGt/xDYotgIVilQsnLAXc47QN6MUPJiVAAwpBVueSUmxX8fj +y88nZY41F7dXyDDZQVu5FLbowg+UMaeUmMxq67XhJ/UQqAHojhJi6IjMtX9Gl8Cb +EGY4GjZGXyJoPd/JxhMnq1MGrKI8hgZlb7F+sSlEmqO6SWkoaY/X5V+tBIZkbxqg +DMUIYs6Ao9Dz7GjevjPHF1t/gMRMTLGmhIrDO7gJzRSBuhjjVFc2/tsvfEehOjPI ++Vg7RE+xygKJBJYoaMVLuCaJu9YzL1DV/pqJuhgyklTGW+Cd+V7lDSKb9triyCGy +YiGqhkCyLmTTX8jjfhFnRR8F/uOi77Oos/N9j/gMHyIfLXC0uAE0djAA5SN4p1bX +UB+K+wb1whnw0A== +-----END CERTIFICATE----- + +# Issuer: CN=UCA Extended Validation Root O=UniTrust +# Subject: CN=UCA Extended Validation Root O=UniTrust +# Label: "UCA Extended Validation Root" +# Serial: 106100277556486529736699587978573607008 +# MD5 Fingerprint: a1:f3:5f:43:c6:34:9b:da:bf:8c:7e:05:53:ad:96:e2 +# SHA1 Fingerprint: a3:a1:b0:6f:24:61:23:4a:e3:36:a5:c2:37:fc:a6:ff:dd:f0:d7:3a +# SHA256 Fingerprint: d4:3a:f9:b3:54:73:75:5c:96:84:fc:06:d7:d8:cb:70:ee:5c:28:e7:73:fb:29:4e:b4:1e:e7:17:22:92:4d:24 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgIQT9Irj/VkyDOeTzRYZiNwYDANBgkqhkiG9w0BAQsFADBH +MQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNVBAMMHFVDQSBF +eHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwHhcNMTUwMzEzMDAwMDAwWhcNMzgxMjMx +MDAwMDAwWjBHMQswCQYDVQQGEwJDTjERMA8GA1UECgwIVW5pVHJ1c3QxJTAjBgNV +BAMMHFVDQSBFeHRlbmRlZCBWYWxpZGF0aW9uIFJvb3QwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCpCQcoEwKwmeBkqh5DFnpzsZGgdT6o+uM4AHrsiWog +D4vFsJszA1qGxliG1cGFu0/GnEBNyr7uaZa4rYEwmnySBesFK5pI0Lh2PpbIILvS +sPGP2KxFRv+qZ2C0d35qHzwaUnoEPQc8hQ2E0B92CvdqFN9y4zR8V05WAT558aop +O2z6+I9tTcg1367r3CTueUWnhbYFiN6IXSV8l2RnCdm/WhUFhvMJHuxYMjMR83dk +sHYf5BA1FxvyDrFspCqjc/wJHx4yGVMR59mzLC52LqGj3n5qiAno8geK+LLNEOfi +c0CTuwjRP+H8C5SzJe98ptfRr5//lpr1kXuYC3fUfugH0mK1lTnj8/FtDw5lhIpj +VMWAtuCeS31HJqcBCF3RiJ7XwzJE+oJKCmhUfzhTA8ykADNkUVkLo4KRel7sFsLz +KuZi2irbWWIQJUoqgQtHB0MGcIfS+pMRKXpITeuUx3BNr2fVUbGAIAEBtHoIppB/ +TuDvB0GHr2qlXov7z1CymlSvw4m6WC31MJixNnI5fkkE/SmnTHnkBVfblLkWU41G +sx2VYVdWf6/wFlthWG82UBEL2KwrlRYaDh8IzTY0ZRBiZtWAXxQgXy0MoHgKaNYs +1+lvK9JKBZP8nm9rZ/+I8U6laUpSNwXqxhaN0sSZ0YIrO7o1dfdRUVjzyAfd5LQD +fwIDAQABo0IwQDAdBgNVHQ4EFgQU2XQ65DA9DfcS3H5aBZ8eNJr34RQwDwYDVR0T +AQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwDQYJKoZIhvcNAQELBQADggIBADaN +l8xCFWQpN5smLNb7rhVpLGsaGvdftvkHTFnq88nIua7Mui563MD1sC3AO6+fcAUR +ap8lTwEpcOPlDOHqWnzcSbvBHiqB9RZLcpHIojG5qtr8nR/zXUACE/xOHAbKsxSQ +VBcZEhrxH9cMaVr2cXj0lH2RC47skFSOvG+hTKv8dGT9cZr4QQehzZHkPJrgmzI5 +c6sq1WnIeJEmMX3ixzDx/BR4dxIOE/TdFpS/S2d7cFOFyrC78zhNLJA5wA3CXWvp +4uXViI3WLL+rG761KIcSF3Ru/H38j9CHJrAb+7lsq+KePRXBOy5nAliRn+/4Qh8s +t2j1da3Ptfb/EX3C8CSlrdP6oDyp+l3cpaDvRKS+1ujl5BOWF3sGPjLtx7dCvHaj +2GU4Kzg1USEODm8uNBNA4StnDG1KQTAYI1oyVZnJF+A83vbsea0rWBmirSwiGpWO +vpaQXUJXxPkUAzUrHC1RVwinOt4/5Mi0A3PCwSaAuwtCH60NryZy2sy+s6ODWA2C +xR9GUeOcGMyNm43sSet1UNWMKFnKdDTajAshqx7qG+XH/RU+wBeq+yNuJkbL+vmx +cmtpzyKEC2IPrNkZAJSidjzULZrtBJ4tBmIQN1IchXIbJ+XMxjHsN+xjWZsLHXbM +fjKaiJUINlK73nZfdklJrX+9ZSCyycErdhh2n1ax +-----END CERTIFICATE----- + +# Issuer: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Subject: CN=Certigna Root CA O=Dhimyotis OU=0002 48146308100036 +# Label: "Certigna Root CA" +# Serial: 269714418870597844693661054334862075617 +# MD5 Fingerprint: 0e:5c:30:62:27:eb:5b:bc:d7:ae:62:ba:e9:d5:df:77 +# SHA1 Fingerprint: 2d:0d:52:14:ff:9e:ad:99:24:01:74:20:47:6e:6c:85:27:27:f5:43 +# SHA256 Fingerprint: d4:8d:3d:23:ee:db:50:a4:59:e5:51:97:60:1c:27:77:4b:9d:7b:18:c9:4d:5a:05:95:11:a1:02:50:b9:31:68 +-----BEGIN CERTIFICATE----- +MIIGWzCCBEOgAwIBAgIRAMrpG4nxVQMNo+ZBbcTjpuEwDQYJKoZIhvcNAQELBQAw +WjELMAkGA1UEBhMCRlIxEjAQBgNVBAoMCURoaW15b3RpczEcMBoGA1UECwwTMDAw +MiA0ODE0NjMwODEwMDAzNjEZMBcGA1UEAwwQQ2VydGlnbmEgUm9vdCBDQTAeFw0x +MzEwMDEwODMyMjdaFw0zMzEwMDEwODMyMjdaMFoxCzAJBgNVBAYTAkZSMRIwEAYD +VQQKDAlEaGlteW90aXMxHDAaBgNVBAsMEzAwMDIgNDgxNDYzMDgxMDAwMzYxGTAX +BgNVBAMMEENlcnRpZ25hIFJvb3QgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAw +ggIKAoICAQDNGDllGlmx6mQWDoyUJJV8g9PFOSbcDO8WV43X2KyjQn+Cyu3NW9sO +ty3tRQgXstmzy9YXUnIo245Onoq2C/mehJpNdt4iKVzSs9IGPjA5qXSjklYcoW9M +CiBtnyN6tMbaLOQdLNyzKNAT8kxOAkmhVECe5uUFoC2EyP+YbNDrihqECB63aCPu +I9Vwzm1RaRDuoXrC0SIxwoKF0vJVdlB8JXrJhFwLrN1CTivngqIkicuQstDuI7pm +TLtipPlTWmR7fJj6o0ieD5Wupxj0auwuA0Wv8HT4Ks16XdG+RCYyKfHx9WzMfgIh +C59vpD++nVPiz32pLHxYGpfhPTc3GGYo0kDFUYqMwy3OU4gkWGQwFsWq4NYKpkDf +ePb1BHxpE4S80dGnBs8B92jAqFe7OmGtBIyT46388NtEbVncSVmurJqZNjBBe3Yz +IoejwpKGbvlw7q6Hh5UbxHq9MfPU0uWZ/75I7HX1eBYdpnDBfzwboZL7z8g81sWT +Co/1VTp2lc5ZmIoJlXcymoO6LAQ6l73UL77XbJuiyn1tJslV1c/DeVIICZkHJC1k +JWumIWmbat10TWuXekG9qxf5kBdIjzb5LdXF2+6qhUVB+s06RbFo5jZMm5BX7CO5 +hwjCxAnxl4YqKE3idMDaxIzb3+KhF1nOJFl0Mdp//TBt2dzhauH8XwIDAQABo4IB +GjCCARYwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYE +FBiHVuBud+4kNTxOc5of1uHieX4rMB8GA1UdIwQYMBaAFBiHVuBud+4kNTxOc5of +1uHieX4rMEQGA1UdIAQ9MDswOQYEVR0gADAxMC8GCCsGAQUFBwIBFiNodHRwczov +L3d3d3cuY2VydGlnbmEuZnIvYXV0b3JpdGVzLzBtBgNVHR8EZjBkMC+gLaArhilo +dHRwOi8vY3JsLmNlcnRpZ25hLmZyL2NlcnRpZ25hcm9vdGNhLmNybDAxoC+gLYYr +aHR0cDovL2NybC5kaGlteW90aXMuY29tL2NlcnRpZ25hcm9vdGNhLmNybDANBgkq +hkiG9w0BAQsFAAOCAgEAlLieT/DjlQgi581oQfccVdV8AOItOoldaDgvUSILSo3L +6btdPrtcPbEo/uRTVRPPoZAbAh1fZkYJMyjhDSSXcNMQH+pkV5a7XdrnxIxPTGRG +HVyH41neQtGbqH6mid2PHMkwgu07nM3A6RngatgCdTer9zQoKJHyBApPNeNgJgH6 +0BGM+RFq7q89w1DTj18zeTyGqHNFkIwgtnJzFyO+B2XleJINugHA64wcZr+shncB +lA2c5uk5jR+mUYyZDDl34bSb+hxnV29qao6pK0xXeXpXIs/NX2NGjVxZOob4Mkdi +o2cNGJHc+6Zr9UhhcyNZjgKnvETq9Emd8VRY+WCv2hikLyhF3HqgiIZd8zvn/yk1 +gPxkQ5Tm4xxvvq0OKmOZK8l+hfZx6AYDlf7ej0gcWtSS6Cvu5zHbugRqh5jnxV/v +faci9wHYTfmJ0A6aBVmknpjZbyvKcL5kwlWj9Omvw5Ip3IgWJJk8jSaYtlu3zM63 +Nwf9JtmYhST/WSMDmu2dnajkXjjO11INb9I/bbEFa0nOipFGc/T2L/Coc3cOZayh +jWZSaX5LaAzHHjcng6WMxwLkFM1JAbBzs/3GkDpv0mztO+7skb6iQ12LAEpmJURw +3kAP+HwV96LOPNdeE4yBFxgX0b3xdxA61GU5wSesVywlVP+i2k+KYTlerj1KjL0= +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign Root CA - G1 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign Root CA - G1" +# Serial: 235931866688319308814040 +# MD5 Fingerprint: 9c:42:84:57:dd:cb:0b:a7:2e:95:ad:b6:f3:da:bc:ac +# SHA1 Fingerprint: 8a:c7:ad:8f:73:ac:4e:c1:b5:75:4d:a5:40:f4:fc:cf:7c:b5:8e:8c +# SHA256 Fingerprint: 40:f6:af:03:46:a9:9a:a1:cd:1d:55:5a:4e:9c:ce:62:c7:f9:63:46:03:ee:40:66:15:83:3d:c8:c8:d0:03:67 +-----BEGIN CERTIFICATE----- +MIIDlDCCAnygAwIBAgIKMfXkYgxsWO3W2DANBgkqhkiG9w0BAQsFADBnMQswCQYD +VQQGEwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBU +ZWNobm9sb2dpZXMgTGltaXRlZDEcMBoGA1UEAxMTZW1TaWduIFJvb3QgQ0EgLSBH +MTAeFw0xODAyMTgxODMwMDBaFw00MzAyMTgxODMwMDBaMGcxCzAJBgNVBAYTAklO +MRMwEQYDVQQLEwplbVNpZ24gUEtJMSUwIwYDVQQKExxlTXVkaHJhIFRlY2hub2xv +Z2llcyBMaW1pdGVkMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEcxMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAk0u76WaK7p1b1TST0Bsew+eeuGQz +f2N4aLTNLnF115sgxk0pvLZoYIr3IZpWNVrzdr3YzZr/k1ZLpVkGoZM0Kd0WNHVO +8oG0x5ZOrRkVUkr+PHB1cM2vK6sVmjM8qrOLqs1D/fXqcP/tzxE7lM5OMhbTI0Aq +d7OvPAEsbO2ZLIvZTmmYsvePQbAyeGHWDV/D+qJAkh1cF+ZwPjXnorfCYuKrpDhM +tTk1b+oDafo6VGiFbdbyL0NVHpENDtjVaqSW0RM8LHhQ6DqS0hdW5TUaQBw+jSzt +Od9C4INBdN+jzcKGYEho42kLVACL5HZpIQ15TjQIXhTCzLG3rdd8cIrHhQIDAQAB +o0IwQDAdBgNVHQ4EFgQU++8Nhp6w492pufEhF38+/PB3KxowDgYDVR0PAQH/BAQD +AgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAFn/8oz1h31x +PaOfG1vR2vjTnGs2vZupYeveFix0PZ7mddrXuqe8QhfnPZHr5X3dPpzxz5KsbEjM +wiI/aTvFthUvozXGaCocV685743QNcMYDHsAVhzNixl03r4PEuDQqqE/AjSxcM6d +GNYIAwlG7mDgfrbESQRRfXBgvKqy/3lyeqYdPV8q+Mri/Tm3R7nrft8EI6/6nAYH +6ftjk4BAtcZsCjEozgyfz7MjNYBBjWzEN3uBL4ChQEKF6dk4jeihU80Bv2noWgby +RQuQ+q7hv53yrlc8pa6yVvSLZUDp/TGBLPQ5Cdjua6e0ph0VpZj3AYHYhX3zUVxx +iN66zB+Afko= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Subject: CN=emSign ECC Root CA - G3 O=eMudhra Technologies Limited OU=emSign PKI +# Label: "emSign ECC Root CA - G3" +# Serial: 287880440101571086945156 +# MD5 Fingerprint: ce:0b:72:d1:9f:88:8e:d0:50:03:e8:e3:b8:8b:67:40 +# SHA1 Fingerprint: 30:43:fa:4f:f2:57:dc:a0:c3:80:ee:2e:58:ea:78:b2:3f:e6:bb:c1 +# SHA256 Fingerprint: 86:a1:ec:ba:08:9c:4a:8d:3b:be:27:34:c6:12:ba:34:1d:81:3e:04:3c:f9:e8:a8:62:cd:5c:57:a3:6b:be:6b +-----BEGIN CERTIFICATE----- +MIICTjCCAdOgAwIBAgIKPPYHqWhwDtqLhDAKBggqhkjOPQQDAzBrMQswCQYDVQQG +EwJJTjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNo +bm9sb2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0g +RzMwHhcNMTgwMjE4MTgzMDAwWhcNNDMwMjE4MTgzMDAwWjBrMQswCQYDVQQGEwJJ +TjETMBEGA1UECxMKZW1TaWduIFBLSTElMCMGA1UEChMcZU11ZGhyYSBUZWNobm9s +b2dpZXMgTGltaXRlZDEgMB4GA1UEAxMXZW1TaWduIEVDQyBSb290IENBIC0gRzMw +djAQBgcqhkjOPQIBBgUrgQQAIgNiAAQjpQy4LRL1KPOxst3iAhKAnjlfSU2fySU0 +WXTsuwYc58Byr+iuL+FBVIcUqEqy6HyC5ltqtdyzdc6LBtCGI79G1Y4PPwT01xyS +fvalY8L1X44uT6EYGQIrMgqCZH0Wk9GjQjBAMB0GA1UdDgQWBBR8XQKEE9TMipuB +zhccLikenEhjQjAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggq +hkjOPQQDAwNpADBmAjEAvvNhzwIQHWSVB7gYboiFBS+DCBeQyh+KTOgNG3qxrdWB +CUfvO6wIBHxcmbHtRwfSAjEAnbpV/KlK6O3t5nYBQnvI+GDZjVGLVTv7jHvrZQnD ++JbNR6iC8hZVdyR+EhCVBCyj +-----END CERTIFICATE----- + +# Issuer: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign Root CA - C1 O=eMudhra Inc OU=emSign PKI +# Label: "emSign Root CA - C1" +# Serial: 825510296613316004955058 +# MD5 Fingerprint: d8:e3:5d:01:21:fa:78:5a:b0:df:ba:d2:ee:2a:5f:68 +# SHA1 Fingerprint: e7:2e:f1:df:fc:b2:09:28:cf:5d:d4:d5:67:37:b1:51:cb:86:4f:01 +# SHA256 Fingerprint: 12:56:09:aa:30:1d:a0:a2:49:b9:7a:82:39:cb:6a:34:21:6f:44:dc:ac:9f:39:54:b1:42:92:f2:e8:c8:60:8f +-----BEGIN CERTIFICATE----- +MIIDczCCAlugAwIBAgILAK7PALrEzzL4Q7IwDQYJKoZIhvcNAQELBQAwVjELMAkG +A1UEBhMCVVMxEzARBgNVBAsTCmVtU2lnbiBQS0kxFDASBgNVBAoTC2VNdWRocmEg +SW5jMRwwGgYDVQQDExNlbVNpZ24gUm9vdCBDQSAtIEMxMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowVjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMRwwGgYDVQQDExNlbVNpZ24gUm9v +dCBDQSAtIEMxMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAz+upufGZ +BczYKCFK83M0UYRWEPWgTywS4/oTmifQz/l5GnRfHXk5/Fv4cI7gklL35CX5VIPZ +HdPIWoU/Xse2B+4+wM6ar6xWQio5JXDWv7V7Nq2s9nPczdcdioOl+yuQFTdrHCZH +3DspVpNqs8FqOp099cGXOFgFixwR4+S0uF2FHYP+eF8LRWgYSKVGczQ7/g/IdrvH +GPMF0Ybzhe3nudkyrVWIzqa2kbBPrH4VI5b2P/AgNBbeCsbEBEV5f6f9vtKppa+c +xSMq9zwhbL2vj07FOrLzNBL834AaSaTUqZX3noleoomslMuoaJuvimUnzYnu3Yy1 +aylwQ6BpC+S5DwIDAQABo0IwQDAdBgNVHQ4EFgQU/qHgcB4qAzlSWkK+XJGFehiq +TbUwDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL +BQADggEBAMJKVvoVIXsoounlHfv4LcQ5lkFMOycsxGwYFYDGrK9HWS8mC+M2sO87 +/kOXSTKZEhVb3xEp/6tT+LvBeA+snFOvV71ojD1pM/CjoCNjO2RnIkSt1XHLVip4 +kqNPEjE2NuLe/gDEo2APJ62gsIq1NnpSob0n9CAnYuhNlCQT5AoE6TyrLshDCUrG +YQTlSTR+08TI9Q/Aqum6VF7zYytPT1DU/rl7mYw9wC68AivTxEDkigcxHpvOJpkT ++xHqmiIMERnHXhuBUDDIlhJu58tBf5E7oke3VIAb3ADMmpDqw8NQBmIMMMAVSKeo +WXzhriKi4gp6D/piq1JM4fHfyr6DDUI= +-----END CERTIFICATE----- + +# Issuer: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Subject: CN=emSign ECC Root CA - C3 O=eMudhra Inc OU=emSign PKI +# Label: "emSign ECC Root CA - C3" +# Serial: 582948710642506000014504 +# MD5 Fingerprint: 3e:53:b3:a3:81:ee:d7:10:f8:d3:b0:1d:17:92:f5:d5 +# SHA1 Fingerprint: b6:af:43:c2:9b:81:53:7d:f6:ef:6b:c3:1f:1f:60:15:0c:ee:48:66 +# SHA256 Fingerprint: bc:4d:80:9b:15:18:9d:78:db:3e:1d:8c:f4:f9:72:6a:79:5d:a1:64:3c:a5:f1:35:8e:1d:db:0e:dc:0d:7e:b3 +-----BEGIN CERTIFICATE----- +MIICKzCCAbGgAwIBAgIKe3G2gla4EnycqDAKBggqhkjOPQQDAzBaMQswCQYDVQQG +EwJVUzETMBEGA1UECxMKZW1TaWduIFBLSTEUMBIGA1UEChMLZU11ZGhyYSBJbmMx +IDAeBgNVBAMTF2VtU2lnbiBFQ0MgUm9vdCBDQSAtIEMzMB4XDTE4MDIxODE4MzAw +MFoXDTQzMDIxODE4MzAwMFowWjELMAkGA1UEBhMCVVMxEzARBgNVBAsTCmVtU2ln +biBQS0kxFDASBgNVBAoTC2VNdWRocmEgSW5jMSAwHgYDVQQDExdlbVNpZ24gRUND +IFJvb3QgQ0EgLSBDMzB2MBAGByqGSM49AgEGBSuBBAAiA2IABP2lYa57JhAd6bci +MK4G9IGzsUJxlTm801Ljr6/58pc1kjZGDoeVjbk5Wum739D+yAdBPLtVb4Ojavti +sIGJAnB9SMVK4+kiVCJNk7tCDK93nCOmfddhEc5lx/h//vXyqaNCMEAwHQYDVR0O +BBYEFPtaSNCAIEDyqOkAB2kZd6fmw/TPMA4GA1UdDwEB/wQEAwIBBjAPBgNVHRMB +Af8EBTADAQH/MAoGCCqGSM49BAMDA2gAMGUCMQC02C8Cif22TGK6Q04ThHK1rt0c +3ta13FaPWEBaLd4gTCKDypOofu4SQMfWh0/434UCMBwUZOR8loMRnLDRWmFLpg9J +0wD8ofzkpf9/rdcw0Md3f76BB1UwUCAU9Vc4CqgxUQ== +-----END CERTIFICATE----- + +# Issuer: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Subject: CN=Hongkong Post Root CA 3 O=Hongkong Post +# Label: "Hongkong Post Root CA 3" +# Serial: 46170865288971385588281144162979347873371282084 +# MD5 Fingerprint: 11:fc:9f:bd:73:30:02:8a:fd:3f:f3:58:b9:cb:20:f0 +# SHA1 Fingerprint: 58:a2:d0:ec:20:52:81:5b:c1:f3:f8:64:02:24:4e:c2:8e:02:4b:02 +# SHA256 Fingerprint: 5a:2f:c0:3f:0c:83:b0:90:bb:fa:40:60:4b:09:88:44:6c:76:36:18:3d:f9:84:6e:17:10:1a:44:7f:b8:ef:d6 +-----BEGIN CERTIFICATE----- +MIIFzzCCA7egAwIBAgIUCBZfikyl7ADJk0DfxMauI7gcWqQwDQYJKoZIhvcNAQEL +BQAwbzELMAkGA1UEBhMCSEsxEjAQBgNVBAgTCUhvbmcgS29uZzESMBAGA1UEBxMJ +SG9uZyBLb25nMRYwFAYDVQQKEw1Ib25na29uZyBQb3N0MSAwHgYDVQQDExdIb25n +a29uZyBQb3N0IFJvb3QgQ0EgMzAeFw0xNzA2MDMwMjI5NDZaFw00MjA2MDMwMjI5 +NDZaMG8xCzAJBgNVBAYTAkhLMRIwEAYDVQQIEwlIb25nIEtvbmcxEjAQBgNVBAcT +CUhvbmcgS29uZzEWMBQGA1UEChMNSG9uZ2tvbmcgUG9zdDEgMB4GA1UEAxMXSG9u +Z2tvbmcgUG9zdCBSb290IENBIDMwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK +AoICAQCziNfqzg8gTr7m1gNt7ln8wlffKWihgw4+aMdoWJwcYEuJQwy51BWy7sFO +dem1p+/l6TWZ5Mwc50tfjTMwIDNT2aa71T4Tjukfh0mtUC1Qyhi+AViiE3CWu4mI +VoBc+L0sPOFMV4i707mV78vH9toxdCim5lSJ9UExyuUmGs2C4HDaOym71QP1mbpV +9WTRYA6ziUm4ii8F0oRFKHyPaFASePwLtVPLwpgchKOesL4jpNrcyCse2m5FHomY +2vkALgbpDDtw1VAliJnLzXNg99X/NWfFobxeq81KuEXryGgeDQ0URhLj0mRiikKY +vLTGCAj4/ahMZJx2Ab0vqWwzD9g/KLg8aQFChn5pwckGyuV6RmXpwtZQQS4/t+Tt +bNe/JgERohYpSms0BpDsE9K2+2p20jzt8NYt3eEV7KObLyzJPivkaTv/ciWxNoZb +x39ri1UbSsUgYT2uy1DhCDq+sI9jQVMwCFk8mB13umOResoQUGC/8Ne8lYePl8X+ +l2oBlKN8W4UdKjk60FSh0Tlxnf0h+bV78OLgAo9uliQlLKAeLKjEiafv7ZkGL7YK +TE/bosw3Gq9HhS2KX8Q0NEwA/RiTZxPRN+ZItIsGxVd7GYYKecsAyVKvQv83j+Gj +Hno9UKtjBucVtT+2RTeUN7F+8kjDf8V1/peNRY8apxpyKBpADwIDAQABo2MwYTAP +BgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBQXnc0e +i9Y5K3DTXNSguB+wAPzFYTAdBgNVHQ4EFgQUF53NHovWOStw01zUoLgfsAD8xWEw +DQYJKoZIhvcNAQELBQADggIBAFbVe27mIgHSQpsY1Q7XZiNc4/6gx5LS6ZStS6LG +7BJ8dNVI0lkUmcDrudHr9EgwW62nV3OZqdPlt9EuWSRY3GguLmLYauRwCy0gUCCk +MpXRAJi70/33MvJJrsZ64Ee+bs7Lo3I6LWldy8joRTnU+kLBEUx3XZL7av9YROXr +gZ6voJmtvqkBZss4HTzfQx/0TW60uhdG/H39h4F5ag0zD/ov+BS5gLNdTaqX4fnk +GMX41TiMJjz98iji7lpJiCzfeT2OnpA8vUFKOt1b9pq0zj8lMH8yfaIDlNDceqFS +3m6TjRgm/VWsvY+b0s+v54Ysyx8Jb6NvqYTUc79NoXQbTiNg8swOqn+knEwlqLJm +Ozj/2ZQw9nKEvmhVEA/GcywWaZMH/rFF7buiVWqw2rVKAiUnhde3t4ZEFolsgCs+ +l6mc1X5VTMbeRRAc6uk7nwNT7u56AQIWeNTowr5GdogTPyK7SBIdUgC0An4hGh6c +JfTzPV4e0hz5sy229zdcxsshTrD3mUcYhcErulWuBurQB7Lcq9CClnXO0lD+mefP +L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa +LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG +mpv0 +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft ECC Root Certificate Authority 2017" +# Serial: 136839042543790627607696632466672567020 +# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 +# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 +# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft RSA Root Certificate Authority 2017" +# Serial: 40975477897264996090493496164228220339 +# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 +# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 +# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Label: "e-Szigno Root CA 2017" +# Serial: 411379200276854331539784714 +# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 +# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 +# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Label: "certSIGN Root CA G2" +# Serial: 313609486401300475190 +# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 +# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 +# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global Certification Authority" +# Serial: 1846098327275375458322922162 +# MD5 Fingerprint: f8:1c:18:2d:2f:ba:5f:6d:a1:6c:bc:c7:ab:91:c7:0e +# SHA1 Fingerprint: 2f:8f:36:4f:e1:58:97:44:21:59:87:a5:2a:9a:d0:69:95:26:7f:b5 +# SHA256 Fingerprint: 97:55:20:15:f5:dd:fc:3c:87:88:c0:06:94:45:55:40:88:94:45:00:84:f1:00:86:70:86:bc:1a:2b:b5:8d:c8 +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global ECC P256 Certification Authority" +# Serial: 4151900041497450638097112925 +# MD5 Fingerprint: 5b:44:e3:8d:5d:36:86:26:e8:0d:05:d2:59:a7:83:54 +# SHA1 Fingerprint: b4:90:82:dd:45:0c:be:8b:5b:b1:66:d3:e2:a4:08:26:cd:ed:42:cf +# SHA256 Fingerprint: 94:5b:bc:82:5e:a5:54:f4:89:d1:fd:51:a7:3d:df:2e:a6:24:ac:70:19:a0:52:05:22:5c:22:a7:8c:cf:a8:b4 +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global ECC P384 Certification Authority" +# Serial: 2704997926503831671788816187 +# MD5 Fingerprint: ea:cf:60:c4:3b:b9:15:29:40:a1:97:ed:78:27:93:d6 +# SHA1 Fingerprint: e7:f3:a3:c8:cf:6f:c3:04:2e:6d:0e:67:32:c5:9e:68:95:0d:5e:d2 +# SHA256 Fingerprint: 55:90:38:59:c8:c0:c3:eb:b8:75:9e:ce:4e:25:57:22:5f:f5:75:8b:bd:38:eb:d4:82:76:60:1e:1b:d5:80:97 +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE----- + +# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Label: "NAVER Global Root Certification Authority" +# Serial: 9013692873798656336226253319739695165984492813 +# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b +# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 +# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- + +# Issuer: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Subject: CN=AC RAIZ FNMT-RCM SERVIDORES SEGUROS O=FNMT-RCM OU=Ceres +# Label: "AC RAIZ FNMT-RCM SERVIDORES SEGUROS" +# Serial: 131542671362353147877283741781055151509 +# MD5 Fingerprint: 19:36:9c:52:03:2f:d2:d1:bb:23:cc:dd:1e:12:55:bb +# SHA1 Fingerprint: 62:ff:d9:9e:c0:65:0d:03:ce:75:93:d2:ed:3f:2d:32:c9:e3:e5:4a +# SHA256 Fingerprint: 55:41:53:b1:3d:2c:f9:dd:b7:53:bf:be:1a:4e:0a:e0:8d:0a:a4:18:70:58:fe:60:a2:b8:62:b2:e4:b8:7b:cb +-----BEGIN CERTIFICATE----- +MIICbjCCAfOgAwIBAgIQYvYybOXE42hcG2LdnC6dlTAKBggqhkjOPQQDAzB4MQsw +CQYDVQQGEwJFUzERMA8GA1UECgwIRk5NVC1SQ00xDjAMBgNVBAsMBUNlcmVzMRgw +FgYDVQRhDA9WQVRFUy1RMjgyNjAwNEoxLDAqBgNVBAMMI0FDIFJBSVogRk5NVC1S +Q00gU0VSVklET1JFUyBTRUdVUk9TMB4XDTE4MTIyMDA5MzczM1oXDTQzMTIyMDA5 +MzczM1oweDELMAkGA1UEBhMCRVMxETAPBgNVBAoMCEZOTVQtUkNNMQ4wDAYDVQQL +DAVDZXJlczEYMBYGA1UEYQwPVkFURVMtUTI4MjYwMDRKMSwwKgYDVQQDDCNBQyBS +QUlaIEZOTVQtUkNNIFNFUlZJRE9SRVMgU0VHVVJPUzB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPa6V1PIyqvfNkpSIeSX0oNnnvBlUdBeh8dHsVnyV0ebAAKTRBdp20LH +sbI6GA60XYyzZl2hNPk2LEnb80b8s0RpRBNm/dfF/a82Tc4DTQdxz69qBdKiQ1oK +Um8BA06Oi6NCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFAG5L++/EYZg8k/QQW6rcx/n0m5JMAoGCCqGSM49BAMDA2kAMGYCMQCu +SuMrQMN0EfKVrRYj3k4MGuZdpSRea0R7/DjiT8ucRRcRTBQnJlU5dUoDzBOQn5IC +MQD6SmxgiHPz7riYYqnOK8LZiqZwMR2vsJRM60/G49HzYqc8/5MuB1xJAWdpEgJy +v+c= +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root R46 O=GlobalSign nv-sa +# Label: "GlobalSign Root R46" +# Serial: 1552617688466950547958867513931858518042577 +# MD5 Fingerprint: c4:14:30:e4:fa:66:43:94:2a:6a:1b:24:5f:19:d0:ef +# SHA1 Fingerprint: 53:a2:b0:4b:ca:6b:d6:45:e6:39:8a:8e:c4:0d:d2:bf:77:c3:a2:90 +# SHA256 Fingerprint: 4f:a3:12:6d:8d:3a:11:d1:c4:85:5a:4f:80:7c:ba:d6:cf:91:9d:3a:5a:88:b0:3b:ea:2c:63:72:d9:3c:40:c9 +-----BEGIN CERTIFICATE----- +MIIFWjCCA0KgAwIBAgISEdK7udcjGJ5AXwqdLdDfJWfRMA0GCSqGSIb3DQEBDAUA +MEYxCzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYD +VQQDExNHbG9iYWxTaWduIFJvb3QgUjQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMy +MDAwMDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYt +c2ExHDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCsrHQy6LNl5brtQyYdpokNRbopiLKkHWPd08EsCVeJ +OaFV6Wc0dwxu5FUdUiXSE2te4R2pt32JMl8Nnp8semNgQB+msLZ4j5lUlghYruQG +vGIFAha/r6gjA7aUD7xubMLL1aa7DOn2wQL7Id5m3RerdELv8HQvJfTqa1VbkNud +316HCkD7rRlr+/fKYIje2sGP1q7Vf9Q8g+7XFkyDRTNrJ9CG0Bwta/OrffGFqfUo +0q3v84RLHIf8E6M6cqJaESvWJ3En7YEtbWaBkoe0G1h6zD8K+kZPTXhc+CtI4wSE +y132tGqzZfxCnlEmIyDLPRT5ge1lFgBPGmSXZgjPjHvjK8Cd+RTyG/FWaha/LIWF +zXg4mutCagI0GIMXTpRW+LaCtfOW3T3zvn8gdz57GSNrLNRyc0NXfeD412lPFzYE ++cCQYDdF3uYM2HSNrpyibXRdQr4G9dlkbgIQrImwTDsHTUB+JMWKmIJ5jqSngiCN +I/onccnfxkF0oE32kRbcRoxfKWMxWXEM2G/CtjJ9++ZdU6Z+Ffy7dXxd7Pj2Fxzs +x2sZy/N78CsHpdlseVR2bJ0cpm4O6XkMqCNqo98bMDGfsVR7/mrLZqrcZdCinkqa +ByFrgY/bxFn63iLABJzjqls2k+g9vXqhnQt2sQvHnf3PmKgGwvgqo6GDoLclcqUC +4wIDAQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQUA1yrc4GHqMywptWU4jaWSf8FmSwwDQYJKoZIhvcNAQEMBQADggIBAHx4 +7PYCLLtbfpIrXTncvtgdokIzTfnvpCo7RGkerNlFo048p9gkUbJUHJNOxO97k4Vg +JuoJSOD1u8fpaNK7ajFxzHmuEajwmf3lH7wvqMxX63bEIaZHU1VNaL8FpO7XJqti +2kM3S+LGteWygxk6x9PbTZ4IevPuzz5i+6zoYMzRx6Fcg0XERczzF2sUyQQCPtIk +pnnpHs6i58FZFZ8d4kuaPp92CC1r2LpXFNqD6v6MVenQTqnMdzGxRBF6XLE+0xRF +FRhiJBPSy03OXIPBNvIQtQ6IbbjhVp+J3pZmOUdkLG5NrmJ7v2B0GbhWrJKsFjLt +rWhV/pi60zTe9Mlhww6G9kuEYO4Ne7UyWHmRVSyBQ7N0H3qqJZ4d16GLuc1CLgSk +ZoNNiTW2bKg2SnkheCLQQrzRQDGQob4Ez8pn7fXwgNNgyYMqIgXQBztSvwyeqiv5 +u+YfjyW6hY0XHgL+XVAEV8/+LbzvXMAaq7afJMbfc2hIkCwU9D9SGuTSyxTDYWnP +4vkYxboznxSjBF25cfe1lNj2M8FawTSLfJvdkzrnE6JwYZ+vj+vYxXX4M2bUdGc6 +N3ec592kD3ZDZopD8p/7DEJ4Y9HiD2971KE9dJeFt0g5QdYg/NA6s/rob8SKunE3 +vouXsXgxT7PntgMTzlSdriVZzH81Xwj3QEUxeCp6 +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Subject: CN=GlobalSign Root E46 O=GlobalSign nv-sa +# Label: "GlobalSign Root E46" +# Serial: 1552617690338932563915843282459653771421763 +# MD5 Fingerprint: b5:b8:66:ed:de:08:83:e3:c9:e2:01:34:06:ac:51:6f +# SHA1 Fingerprint: 39:b4:6c:d5:fe:80:06:eb:e2:2f:4a:bb:08:33:a0:af:db:b9:dd:84 +# SHA256 Fingerprint: cb:b9:c4:4d:84:b8:04:3e:10:50:ea:31:a6:9f:51:49:55:d7:bf:d2:e2:c6:b4:93:01:01:9a:d6:1d:9f:50:58 +-----BEGIN CERTIFICATE----- +MIICCzCCAZGgAwIBAgISEdK7ujNu1LzmJGjFDYQdmOhDMAoGCCqGSM49BAMDMEYx +CzAJBgNVBAYTAkJFMRkwFwYDVQQKExBHbG9iYWxTaWduIG52LXNhMRwwGgYDVQQD +ExNHbG9iYWxTaWduIFJvb3QgRTQ2MB4XDTE5MDMyMDAwMDAwMFoXDTQ2MDMyMDAw +MDAwMFowRjELMAkGA1UEBhMCQkUxGTAXBgNVBAoTEEdsb2JhbFNpZ24gbnYtc2Ex +HDAaBgNVBAMTE0dsb2JhbFNpZ24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAScDrHPt+ieUnd1NPqlRqetMhkytAepJ8qUuwzSChDH2omwlwxwEwkBjtjq +R+q+soArzfwoDdusvKSGN+1wCAB16pMLey5SnCNoIwZD7JIvU4Tb+0cUB+hflGdd +yXqBPCCjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1Ud +DgQWBBQxCpCPtsad0kRLgLWi5h+xEk8blTAKBggqhkjOPQQDAwNoADBlAjEA31SQ +7Zvvi5QCkxeCmb6zniz2C5GMn0oUsfZkvLtoURMMA/cVi4RguYv/Uo7njLwcAjA8 ++RHUjE7AwWHCFUyqqx0LMV87HOIAl0Qx5v5zli/altP+CAezNIm8BZ/3Hobui3A= +-----END CERTIFICATE----- + +# Issuer: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Subject: CN=ANF Secure Server Root CA O=ANF Autoridad de Certificacion OU=ANF CA Raiz +# Label: "ANF Secure Server Root CA" +# Serial: 996390341000653745 +# MD5 Fingerprint: 26:a6:44:5a:d9:af:4e:2f:b2:1d:b6:65:b0:4e:e8:96 +# SHA1 Fingerprint: 5b:6e:68:d0:cc:15:b6:a0:5f:1e:c1:5f:ae:02:fc:6b:2f:5d:6f:74 +# SHA256 Fingerprint: fb:8f:ec:75:91:69:b9:10:6b:1e:51:16:44:c6:18:c5:13:04:37:3f:6c:06:43:08:8d:8b:ef:fd:1b:99:75:99 +-----BEGIN CERTIFICATE----- +MIIF7zCCA9egAwIBAgIIDdPjvGz5a7EwDQYJKoZIhvcNAQELBQAwgYQxEjAQBgNV +BAUTCUc2MzI4NzUxMDELMAkGA1UEBhMCRVMxJzAlBgNVBAoTHkFORiBBdXRvcmlk +YWQgZGUgQ2VydGlmaWNhY2lvbjEUMBIGA1UECxMLQU5GIENBIFJhaXoxIjAgBgNV +BAMTGUFORiBTZWN1cmUgU2VydmVyIFJvb3QgQ0EwHhcNMTkwOTA0MTAwMDM4WhcN +MzkwODMwMTAwMDM4WjCBhDESMBAGA1UEBRMJRzYzMjg3NTEwMQswCQYDVQQGEwJF +UzEnMCUGA1UEChMeQU5GIEF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uMRQwEgYD +VQQLEwtBTkYgQ0EgUmFpejEiMCAGA1UEAxMZQU5GIFNlY3VyZSBTZXJ2ZXIgUm9v +dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANvrayvmZFSVgpCj +cqQZAZ2cC4Ffc0m6p6zzBE57lgvsEeBbphzOG9INgxwruJ4dfkUyYA8H6XdYfp9q +yGFOtibBTI3/TO80sh9l2Ll49a2pcbnvT1gdpd50IJeh7WhM3pIXS7yr/2WanvtH +2Vdy8wmhrnZEE26cLUQ5vPnHO6RYPUG9tMJJo8gN0pcvB2VSAKduyK9o7PQUlrZX +H1bDOZ8rbeTzPvY1ZNoMHKGESy9LS+IsJJ1tk0DrtSOOMspvRdOoiXsezx76W0OL +zc2oD2rKDF65nkeP8Nm2CgtYZRczuSPkdxl9y0oukntPLxB3sY0vaJxizOBQ+OyR +p1RMVwnVdmPF6GUe7m1qzwmd+nxPrWAI/VaZDxUse6mAq4xhj0oHdkLePfTdsiQz +W7i1o0TJrH93PB0j7IKppuLIBkwC/qxcmZkLLxCKpvR/1Yd0DVlJRfbwcVw5Kda/ +SiOL9V8BY9KHcyi1Swr1+KuCLH5zJTIdC2MKF4EA/7Z2Xue0sUDKIbvVgFHlSFJn +LNJhiQcND85Cd8BEc5xEUKDbEAotlRyBr+Qc5RQe8TZBAQIvfXOn3kLMTOmJDVb3 +n5HUA8ZsyY/b2BzgQJhdZpmYgG4t/wHFzstGH6wCxkPmrqKEPMVOHj1tyRRM4y5B +u8o5vzY8KhmqQYdOpc5LMnndkEl/AgMBAAGjYzBhMB8GA1UdIwQYMBaAFJxf0Gxj +o1+TypOYCK2Mh6UsXME3MB0GA1UdDgQWBBScX9BsY6Nfk8qTmAitjIelLFzBNzAO +BgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOC +AgEATh65isagmD9uw2nAalxJUqzLK114OMHVVISfk/CHGT0sZonrDUL8zPB1hT+L +9IBdeeUXZ701guLyPI59WzbLWoAAKfLOKyzxj6ptBZNscsdW699QIyjlRRA96Gej +rw5VD5AJYu9LWaL2U/HANeQvwSS9eS9OICI7/RogsKQOLHDtdD+4E5UGUcjohybK +pFtqFiGS3XNgnhAY3jyB6ugYw3yJ8otQPr0R4hUDqDZ9MwFsSBXXiJCZBMXM5gf0 +vPSQ7RPi6ovDj6MzD8EpTBNO2hVWcXNyglD2mjN8orGoGjR0ZVzO0eurU+AagNjq +OknkJjCb5RyKqKkVMoaZkgoQI1YS4PbOTOK7vtuNknMBZi9iPrJyJ0U27U1W45eZ +/zo1PqVUSlJZS2Db7v54EX9K3BR5YLZrZAPbFYPhor72I5dQ8AkzNqdxliXzuUJ9 +2zg/LFis6ELhDtjTO0wugumDLmsx2d1Hhk9tl5EuT+IocTUW0fJz/iUrB0ckYyfI ++PbZa/wSMVYIwFNCr5zQM378BvAxRAMU8Vjq8moNqRGyg77FGr8H6lnco4g175x2 +MjxNBiLOFeXdntiP2t7SxDnlF4HPOEfrf4htWRvfn0IUrn7PqLBmZdo3r5+qPeoo +tt7VMVgWglvquxl1AnMaykgaIZOQCo6ThKd9OyMYkomgjaw= +-----END CERTIFICATE----- + +# Issuer: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum EC-384 CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum EC-384 CA" +# Serial: 160250656287871593594747141429395092468 +# MD5 Fingerprint: b6:65:b3:96:60:97:12:a1:ec:4e:e1:3d:a3:c6:c9:f1 +# SHA1 Fingerprint: f3:3e:78:3c:ac:df:f4:a2:cc:ac:67:55:69:56:d7:e5:16:3c:e1:ed +# SHA256 Fingerprint: 6b:32:80:85:62:53:18:aa:50:d1:73:c9:8d:8b:da:09:d5:7e:27:41:3d:11:4c:f7:87:a0:f5:d0:6c:03:0c:f6 +-----BEGIN CERTIFICATE----- +MIICZTCCAeugAwIBAgIQeI8nXIESUiClBNAt3bpz9DAKBggqhkjOPQQDAzB0MQsw +CQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMScw +JQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAXBgNVBAMT +EENlcnR1bSBFQy0zODQgQ0EwHhcNMTgwMzI2MDcyNDU0WhcNNDMwMzI2MDcyNDU0 +WjB0MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT +LkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxGTAX +BgNVBAMTEENlcnR1bSBFQy0zODQgQ0EwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATE +KI6rGFtqvm5kN2PkzeyrOvfMobgOgknXhimfoZTy42B4mIF4Bk3y7JoOV2CDn7Tm +Fy8as10CW4kjPMIRBSqniBMY81CE1700LCeJVf/OTOffph8oxPBUw7l8t1Ot68Kj +QjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFI0GZnQkdjrzife81r1HfS+8 +EF9LMA4GA1UdDwEB/wQEAwIBBjAKBggqhkjOPQQDAwNoADBlAjADVS2m5hjEfO/J +UG7BJw+ch69u1RsIGL2SKcHvlJF40jocVYli5RsJHrpka/F2tNQCMQC0QoSZ/6vn +nvuRlydd3LBbMHHOXjgaatkl5+r3YZJW+OraNsKHZZYuciUvf9/DE8k= +-----END CERTIFICATE----- + +# Issuer: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Subject: CN=Certum Trusted Root CA O=Asseco Data Systems S.A. OU=Certum Certification Authority +# Label: "Certum Trusted Root CA" +# Serial: 40870380103424195783807378461123655149 +# MD5 Fingerprint: 51:e1:c2:e7:fe:4c:84:af:59:0e:2f:f4:54:6f:ea:29 +# SHA1 Fingerprint: c8:83:44:c0:18:ae:9f:cc:f1:87:b7:8f:22:d1:c5:d7:45:84:ba:e5 +# SHA256 Fingerprint: fe:76:96:57:38:55:77:3e:37:a9:5e:7a:d4:d9:cc:96:c3:01:57:c1:5d:31:76:5b:a9:b1:57:04:e1:ae:78:fd +-----BEGIN CERTIFICATE----- +MIIFwDCCA6igAwIBAgIQHr9ZULjJgDdMBvfrVU+17TANBgkqhkiG9w0BAQ0FADB6 +MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEu +MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxHzAdBgNV +BAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwHhcNMTgwMzE2MTIxMDEzWhcNNDMw +MzE2MTIxMDEzWjB6MQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEg +U3lzdGVtcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRo +b3JpdHkxHzAdBgNVBAMTFkNlcnR1bSBUcnVzdGVkIFJvb3QgQ0EwggIiMA0GCSqG +SIb3DQEBAQUAA4ICDwAwggIKAoICAQDRLY67tzbqbTeRn06TpwXkKQMlzhyC93yZ +n0EGze2jusDbCSzBfN8pfktlL5On1AFrAygYo9idBcEq2EXxkd7fO9CAAozPOA/q +p1x4EaTByIVcJdPTsuclzxFUl6s1wB52HO8AU5853BSlLCIls3Jy/I2z5T4IHhQq +NwuIPMqw9MjCoa68wb4pZ1Xi/K1ZXP69VyywkI3C7Te2fJmItdUDmj0VDT06qKhF +8JVOJVkdzZhpu9PMMsmN74H+rX2Ju7pgE8pllWeg8xn2A1bUatMn4qGtg/BKEiJ3 +HAVz4hlxQsDsdUaakFjgao4rpUYwBI4Zshfjvqm6f1bxJAPXsiEodg42MEx51UGa +mqi4NboMOvJEGyCI98Ul1z3G4z5D3Yf+xOr1Uz5MZf87Sst4WmsXXw3Hw09Omiqi +7VdNIuJGmj8PkTQkfVXjjJU30xrwCSss0smNtA0Aq2cpKNgB9RkEth2+dv5yXMSF +ytKAQd8FqKPVhJBPC/PgP5sZ0jeJP/J7UhyM9uH3PAeXjA6iWYEMspA90+NZRu0P +qafegGtaqge2Gcu8V/OXIXoMsSt0Puvap2ctTMSYnjYJdmZm/Bo/6khUHL4wvYBQ +v3y1zgD2DGHZ5yQD4OMBgQ692IU0iL2yNqh7XAjlRICMb/gv1SHKHRzQ+8S1h9E6 +Tsd2tTVItQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSM+xx1 +vALTn04uSNn5YFSqxLNP+jAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQENBQAD +ggIBAEii1QALLtA/vBzVtVRJHlpr9OTy4EA34MwUe7nJ+jW1dReTagVphZzNTxl4 +WxmB82M+w85bj/UvXgF2Ez8sALnNllI5SW0ETsXpD4YN4fqzX4IS8TrOZgYkNCvo +zMrnadyHncI013nR03e4qllY/p0m+jiGPp2Kh2RX5Rc64vmNueMzeMGQ2Ljdt4NR +5MTMI9UGfOZR0800McD2RrsLrfw9EAUqO0qRJe6M1ISHgCq8CYyqOhNf6DR5UMEQ +GfnTKB7U0VEwKbOukGfWHwpjscWpxkIxYxeU72nLL/qMFH3EQxiJ2fAyQOaA4kZf +5ePBAFmo+eggvIksDkc0C+pXwlM2/KfUrzHN/gLldfq5Jwn58/U7yn2fqSLLiMmq +0Uc9NneoWWRrJ8/vJ8HjJLWG965+Mk2weWjROeiQWMODvA8s1pfrzgzhIMfatz7D +P78v3DSk+yshzWePS/Tj6tQ/50+6uaWTRRxmHyH6ZF5v4HaUMst19W7l9o/HuKTM +qJZ9ZPskWkoDbGs4xugDQ5r3V7mzKWmTOPQD8rv7gmsHINFSH5pkAnuYZttcTVoP +0ISVoDwUQwbKytu4QTbaakRnh6+v40URFWkIsr4WOZckbxJF0WddCajJFdr60qZf +E2Efv4WstK2tBZQIgx51F9NxO5NQI1mg7TyRVJ12AMXDuDjb +-----END CERTIFICATE----- + +# Issuer: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Subject: CN=TunTrust Root CA O=Agence Nationale de Certification Electronique +# Label: "TunTrust Root CA" +# Serial: 108534058042236574382096126452369648152337120275 +# MD5 Fingerprint: 85:13:b9:90:5b:36:5c:b6:5e:b8:5a:f8:e0:31:57:b4 +# SHA1 Fingerprint: cf:e9:70:84:0f:e0:73:0f:9d:f6:0c:7f:2c:4b:ee:20:46:34:9c:bb +# SHA256 Fingerprint: 2e:44:10:2a:b5:8c:b8:54:19:45:1c:8e:19:d9:ac:f3:66:2c:af:bc:61:4b:6a:53:96:0a:30:f7:d0:e2:eb:41 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIUEwLV4kBMkkaGFmddtLu7sms+/BMwDQYJKoZIhvcNAQEL +BQAwYTELMAkGA1UEBhMCVE4xNzA1BgNVBAoMLkFnZW5jZSBOYXRpb25hbGUgZGUg +Q2VydGlmaWNhdGlvbiBFbGVjdHJvbmlxdWUxGTAXBgNVBAMMEFR1blRydXN0IFJv +b3QgQ0EwHhcNMTkwNDI2MDg1NzU2WhcNNDQwNDI2MDg1NzU2WjBhMQswCQYDVQQG +EwJUTjE3MDUGA1UECgwuQWdlbmNlIE5hdGlvbmFsZSBkZSBDZXJ0aWZpY2F0aW9u +IEVsZWN0cm9uaXF1ZTEZMBcGA1UEAwwQVHVuVHJ1c3QgUm9vdCBDQTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAMPN0/y9BFPdDCA61YguBUtB9YOCfvdZ +n56eY+hz2vYGqU8ftPkLHzmMmiDQfgbU7DTZhrx1W4eI8NLZ1KMKsmwb60ksPqxd +2JQDoOw05TDENX37Jk0bbjBU2PWARZw5rZzJJQRNmpA+TkBuimvNKWfGzC3gdOgF +VwpIUPp6Q9p+7FuaDmJ2/uqdHYVy7BG7NegfJ7/Boce7SBbdVtfMTqDhuazb1YMZ +GoXRlJfXyqNlC/M4+QKu3fZnz8k/9YosRxqZbwUN/dAdgjH8KcwAWJeRTIAAHDOF +li/LQcKLEITDCSSJH7UP2dl3RxiSlGBcx5kDPP73lad9UKGAwqmDrViWVSHbhlnU +r8a83YFuB9tgYv7sEG7aaAH0gxupPqJbI9dkxt/con3YS7qC0lH4Zr8GRuR5KiY2 +eY8fTpkdso8MDhz/yV3A/ZAQprE38806JG60hZC/gLkMjNWb1sjxVj8agIl6qeIb +MlEsPvLfe/ZdeikZjuXIvTZxi11Mwh0/rViizz1wTaZQmCXcI/m4WEEIcb9PuISg +jwBUFfyRbVinljvrS5YnzWuioYasDXxU5mZMZl+QviGaAkYt5IPCgLnPSz7ofzwB +7I9ezX/SKEIBlYrilz0QIX32nRzFNKHsLA4KUiwSVXAkPcvCFDVDXSdOvsC9qnyW +5/yeYa1E0wCXAgMBAAGjYzBhMB0GA1UdDgQWBBQGmpsfU33x9aTI04Y+oXNZtPdE +ITAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFAaamx9TffH1pMjThj6hc1m0 +90QhMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAqgVutt0Vyb+z +xiD2BkewhpMl0425yAA/l/VSJ4hxyXT968pk21vvHl26v9Hr7lxpuhbI87mP0zYu +QEkHDVneixCwSQXi/5E/S7fdAo74gShczNxtr18UnH1YeA32gAm56Q6XKRm4t+v4 +FstVEuTGfbvE7Pi1HE4+Z7/FXxttbUcoqgRYYdZ2vyJ/0Adqp2RT8JeNnYA/u8EH +22Wv5psymsNUk8QcCMNE+3tjEUPRahphanltkE8pjkcFwRJpadbGNjHh/PqAulxP +xOu3Mqz4dWEX1xAZufHSCe96Qp1bWgvUxpVOKs7/B9dPfhgGiPEZtdmYu65xxBzn +dFlY7wyJz4sfdZMaBBSSSFCp61cpABbjNhzI+L/wM9VBD8TMPN3pM0MBkRArHtG5 +Xc0yGYuPjCB31yLEQtyEFpslbei0VXF/sHyz03FJuc9SpAQ/3D2gu68zngowYI7b +nV2UqL1g52KAdoGDDIzMMEZJ4gzSqK/rYXHv5yJiqfdcZGyfFoxnNidF9Ql7v/YQ +CvGwjVRDjAS6oz/v4jXH+XTgbzRB0L9zZVcg+ZtnemZoJE6AZb0QmQZZ8mWvuMZH +u/2QeItBcy6vVR/cO5JyboTT0GFMDcx2V+IthSIVNg3rAZ3r2OvEhJn7wAzMMujj +d9qDRIueVSjAi1jTkD5OGwDxFa2DK5o= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS RSA Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS RSA Root CA 2021" +# Serial: 76817823531813593706434026085292783742 +# MD5 Fingerprint: 65:47:9b:58:86:dd:2c:f0:fc:a2:84:1f:1e:96:c4:91 +# SHA1 Fingerprint: 02:2d:05:82:fa:88:ce:14:0c:06:79:de:7f:14:10:e9:45:d7:a5:6d +# SHA256 Fingerprint: d9:5d:0e:8e:da:79:52:5b:f9:be:b1:1b:14:d2:10:0d:32:94:98:5f:0c:62:d9:fa:bd:9c:d9:99:ec:cb:7b:1d +-----BEGIN CERTIFICATE----- +MIIFpDCCA4ygAwIBAgIQOcqTHO9D88aOk8f0ZIk4fjANBgkqhkiG9w0BAQsFADBs +MQswCQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJl +c2VhcmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBSU0Eg +Um9vdCBDQSAyMDIxMB4XDTIxMDIxOTEwNTUzOFoXDTQ1MDIxMzEwNTUzN1owbDEL +MAkGA1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNl +YXJjaCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgUlNBIFJv +b3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAIvC569l +mwVnlskNJLnQDmT8zuIkGCyEf3dRywQRNrhe7Wlxp57kJQmXZ8FHws+RFjZiPTgE +4VGC/6zStGndLuwRo0Xua2s7TL+MjaQenRG56Tj5eg4MmOIjHdFOY9TnuEFE+2uv +a9of08WRiFukiZLRgeaMOVig1mlDqa2YUlhu2wr7a89o+uOkXjpFc5gH6l8Cct4M +pbOfrqkdtx2z/IpZ525yZa31MJQjB/OCFks1mJxTuy/K5FrZx40d/JiZ+yykgmvw +Kh+OC19xXFyuQnspiYHLA6OZyoieC0AJQTPb5lh6/a6ZcMBaD9YThnEvdmn8kN3b +LW7R8pv1GmuebxWMevBLKKAiOIAkbDakO/IwkfN4E8/BPzWr8R0RI7VDIp4BkrcY +AuUR0YLbFQDMYTfBKnya4dC6s1BG7oKsnTH4+yPiAwBIcKMJJnkVU2DzOFytOOqB +AGMUuTNe3QvboEUHGjMJ+E20pwKmafTCWQWIZYVWrkvL4N48fS0ayOn7H6NhStYq +E613TBoYm5EPWNgGVMWX+Ko/IIqmhaZ39qb8HOLubpQzKoNQhArlT4b4UEV4AIHr +W2jjJo3Me1xR9BQsQL4aYB16cmEdH2MtiKrOokWQCPxrvrNQKlr9qEgYRtaQQJKQ +CoReaDH46+0N0x3GfZkYVVYnZS6NRcUk7M7jAgMBAAGjQjBAMA8GA1UdEwEB/wQF +MAMBAf8wHQYDVR0OBBYEFApII6ZgpJIKM+qTW8VX6iVNvRLuMA4GA1UdDwEB/wQE +AwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAPpBIqm5iFSVmewzVjIuJndftTgfvnNAU +X15QvWiWkKQUEapobQk1OUAJ2vQJLDSle1mESSmXdMgHHkdt8s4cUCbjnj1AUz/3 +f5Z2EMVGpdAgS1D0NTsY9FVqQRtHBmg8uwkIYtlfVUKqrFOFrJVWNlar5AWMxaja +H6NpvVMPxP/cyuN+8kyIhkdGGvMA9YCRotxDQpSbIPDRzbLrLFPCU3hKTwSUQZqP +JzLB5UkZv/HywouoCjkxKLR9YjYsTewfM7Z+d21+UPCfDtcRj88YxeMn/ibvBZ3P +zzfF0HvaO7AWhAw6k9a+F9sPPg4ZeAnHqQJyIkv3N3a6dcSFA1pj1bF1BcK5vZSt +jBWZp5N99sXzqnTPBIWUmAD04vnKJGW/4GKvyMX6ssmeVkjaef2WdhW+o45WxLM0 +/L5H9MG0qPzVMIho7suuyWPEdr6sOBjhXlzPrjoiUevRi7PzKzMHVIf6tLITe7pT +BGIBnfHAT+7hOtSLIBD6Alfm78ELt5BGnBkpjNxvoEppaZS3JGWg/6w/zgH7IS79 +aPib8qXPMThcFarmlwDB31qlpzmq6YR/PFGoOtmUW4y/Twhx5duoXNTSpv4Ao8YW +xw/ogM4cKGR0GQjTQuPOAF1/sdwTsOEFy9EgqoZ0njnnkf3/W9b3raYvAwtt41dU +63ZTGI0RmLo= +-----END CERTIFICATE----- + +# Issuer: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Subject: CN=HARICA TLS ECC Root CA 2021 O=Hellenic Academic and Research Institutions CA +# Label: "HARICA TLS ECC Root CA 2021" +# Serial: 137515985548005187474074462014555733966 +# MD5 Fingerprint: ae:f7:4c:e5:66:35:d1:b7:9b:8c:22:93:74:d3:4b:b0 +# SHA1 Fingerprint: bc:b0:c1:9d:e9:98:92:70:19:38:57:e9:8d:a7:b4:5d:6e:ee:01:48 +# SHA256 Fingerprint: 3f:99:cc:47:4a:cf:ce:4d:fe:d5:87:94:66:5e:47:8d:15:47:73:9f:2e:78:0f:1b:b4:ca:9b:13:30:97:d4:01 +-----BEGIN CERTIFICATE----- +MIICVDCCAdugAwIBAgIQZ3SdjXfYO2rbIvT/WeK/zjAKBggqhkjOPQQDAzBsMQsw +CQYDVQQGEwJHUjE3MDUGA1UECgwuSGVsbGVuaWMgQWNhZGVtaWMgYW5kIFJlc2Vh +cmNoIEluc3RpdHV0aW9ucyBDQTEkMCIGA1UEAwwbSEFSSUNBIFRMUyBFQ0MgUm9v +dCBDQSAyMDIxMB4XDTIxMDIxOTExMDExMFoXDTQ1MDIxMzExMDEwOVowbDELMAkG +A1UEBhMCR1IxNzA1BgNVBAoMLkhlbGxlbmljIEFjYWRlbWljIGFuZCBSZXNlYXJj +aCBJbnN0aXR1dGlvbnMgQ0ExJDAiBgNVBAMMG0hBUklDQSBUTFMgRUNDIFJvb3Qg +Q0EgMjAyMTB2MBAGByqGSM49AgEGBSuBBAAiA2IABDgI/rGgltJ6rK9JOtDA4MM7 +KKrxcm1lAEeIhPyaJmuqS7psBAqIXhfyVYf8MLA04jRYVxqEU+kw2anylnTDUR9Y +STHMmE5gEYd103KUkE+bECUqqHgtvpBBWJAVcqeht6NCMEAwDwYDVR0TAQH/BAUw +AwEB/zAdBgNVHQ4EFgQUyRtTgRL+BNUW0aq8mm+3oJUZbsowDgYDVR0PAQH/BAQD +AgGGMAoGCCqGSM49BAMDA2cAMGQCMBHervjcToiwqfAircJRQO9gcS3ujwLEXQNw +SaSS6sUUiHCm0w2wqsosQJz76YJumgIwK0eaB8bRwoF8yguWGEEbo/QwCZ61IygN +nxS2PFOiTAZpffpskcYqSUXm7LcT4Tps +-----END CERTIFICATE----- + +# Issuer: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Subject: CN=Autoridad de Certificacion Firmaprofesional CIF A62634068 +# Label: "Autoridad de Certificacion Firmaprofesional CIF A62634068" +# Serial: 1977337328857672817 +# MD5 Fingerprint: 4e:6e:9b:54:4c:ca:b7:fa:48:e4:90:b1:15:4b:1c:a3 +# SHA1 Fingerprint: 0b:be:c2:27:22:49:cb:39:aa:db:35:5c:53:e3:8c:ae:78:ff:b6:fe +# SHA256 Fingerprint: 57:de:05:83:ef:d2:b2:6e:03:61:da:99:da:9d:f4:64:8d:ef:7e:e8:44:1c:3b:72:8a:fa:9b:cd:e0:f9:b2:6a +-----BEGIN CERTIFICATE----- +MIIGFDCCA/ygAwIBAgIIG3Dp0v+ubHEwDQYJKoZIhvcNAQELBQAwUTELMAkGA1UE +BhMCRVMxQjBABgNVBAMMOUF1dG9yaWRhZCBkZSBDZXJ0aWZpY2FjaW9uIEZpcm1h +cHJvZmVzaW9uYWwgQ0lGIEE2MjYzNDA2ODAeFw0xNDA5MjMxNTIyMDdaFw0zNjA1 +MDUxNTIyMDdaMFExCzAJBgNVBAYTAkVTMUIwQAYDVQQDDDlBdXRvcmlkYWQgZGUg +Q2VydGlmaWNhY2lvbiBGaXJtYXByb2Zlc2lvbmFsIENJRiBBNjI2MzQwNjgwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKlmuO6vj78aI14H9M2uDDUtd9 +thDIAl6zQyrET2qyyhxdKJp4ERppWVevtSBC5IsP5t9bpgOSL/UR5GLXMnE42QQM +cas9UX4PB99jBVzpv5RvwSmCwLTaUbDBPLutN0pcyvFLNg4kq7/DhHf9qFD0sefG +L9ItWY16Ck6WaVICqjaY7Pz6FIMMNx/Jkjd/14Et5cS54D40/mf0PmbR0/RAz15i +NA9wBj4gGFrO93IbJWyTdBSTo3OxDqqHECNZXyAFGUftaI6SEspd/NYrspI8IM/h +X68gvqB2f3bl7BqGYTM+53u0P6APjqK5am+5hyZvQWyIplD9amML9ZMWGxmPsu2b +m8mQ9QEM3xk9Dz44I8kvjwzRAv4bVdZO0I08r0+k8/6vKtMFnXkIoctXMbScyJCy +Z/QYFpM6/EfY0XiWMR+6KwxfXZmtY4laJCB22N/9q06mIqqdXuYnin1oKaPnirja +EbsXLZmdEyRG98Xi2J+Of8ePdG1asuhy9azuJBCtLxTa/y2aRnFHvkLfuwHb9H/T +KI8xWVvTyQKmtFLKbpf7Q8UIJm+K9Lv9nyiqDdVF8xM6HdjAeI9BZzwelGSuewvF +6NkBiDkal4ZkQdU7hwxu+g/GvUgUvzlN1J5Bto+WHWOWk9mVBngxaJ43BjuAiUVh +OSPHG0SjFeUc+JIwuwIDAQABo4HvMIHsMB0GA1UdDgQWBBRlzeurNR4APn7VdMAc +tHNHDhpkLzASBgNVHRMBAf8ECDAGAQH/AgEBMIGmBgNVHSAEgZ4wgZswgZgGBFUd +IAAwgY8wLwYIKwYBBQUHAgEWI2h0dHA6Ly93d3cuZmlybWFwcm9mZXNpb25hbC5j +b20vY3BzMFwGCCsGAQUFBwICMFAeTgBQAGEAcwBlAG8AIABkAGUAIABsAGEAIABC +AG8AbgBhAG4AbwB2AGEAIAA0ADcAIABCAGEAcgBjAGUAbABvAG4AYQAgADAAOAAw +ADEANzAOBgNVHQ8BAf8EBAMCAQYwDQYJKoZIhvcNAQELBQADggIBAHSHKAIrdx9m +iWTtj3QuRhy7qPj4Cx2Dtjqn6EWKB7fgPiDL4QjbEwj4KKE1soCzC1HA01aajTNF +Sa9J8OA9B3pFE1r/yJfY0xgsfZb43aJlQ3CTkBW6kN/oGbDbLIpgD7dvlAceHabJ +hfa9NPhAeGIQcDq+fUs5gakQ1JZBu/hfHAsdCPKxsIl68veg4MSPi3i1O1ilI45P +Vf42O+AMt8oqMEEgtIDNrvx2ZnOorm7hfNoD6JQg5iKj0B+QXSBTFCZX2lSX3xZE +EAEeiGaPcjiT3SC3NL7X8e5jjkd5KAb881lFJWAiMxujX6i6KtoaPc1A6ozuBRWV +1aUsIC+nmCjuRfzxuIgALI9C2lHVnOUTaHFFQ4ueCyE8S1wF3BqfmI7avSKecs2t +CsvMo2ebKHTEm9caPARYpoKdrcd7b/+Alun4jWq9GJAd/0kakFI3ky88Al2CdgtR +5xbHV/g4+afNmyJU72OwFW1TZQNKXkqgsqeOSQBZONXH9IBk9W6VULgRfhVwOEqw +f9DEMnDAGf/JOC0ULGb0QkTmVXYbgBVX/8Cnp6o5qtjTcNAuuuuUavpfNIbnYrX9 +ivAwhZTJryQCL2/W3Wf+47BVTwSYT6RBVuKT0Gro1vP7ZeDOdcQxWQzugsgMYDNK +GbqEZycPvEJdvSRUDewdcAZfpLz6IHxV +-----END CERTIFICATE----- + +# Issuer: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus ECC Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus ECC Root CA" +# Serial: 630369271402956006249506845124680065938238527194 +# MD5 Fingerprint: de:4b:c1:f5:52:8c:9b:43:e1:3e:8f:55:54:17:8d:85 +# SHA1 Fingerprint: f6:9c:db:b0:fc:f6:02:13:b6:52:32:a6:a3:91:3f:16:70:da:c3:e1 +# SHA256 Fingerprint: 30:fb:ba:2c:32:23:8e:2a:98:54:7a:f9:79:31:e5:50:42:8b:9b:3f:1c:8e:eb:66:33:dc:fa:86:c5:b2:7d:d3 +-----BEGIN CERTIFICATE----- +MIICDzCCAZWgAwIBAgIUbmq8WapTvpg5Z6LSa6Q75m0c1towCgYIKoZIzj0EAwMw +RzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4xGjAY +BgNVBAMTEXZUcnVzIEVDQyBSb290IENBMB4XDTE4MDczMTA3MjY0NFoXDTQzMDcz +MTA3MjY0NFowRzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28u +LEx0ZC4xGjAYBgNVBAMTEXZUcnVzIEVDQyBSb290IENBMHYwEAYHKoZIzj0CAQYF +K4EEACIDYgAEZVBKrox5lkqqHAjDo6LN/llWQXf9JpRCux3NCNtzslt188+cToL0 +v/hhJoVs1oVbcnDS/dtitN9Ti72xRFhiQgnH+n9bEOf+QP3A2MMrMudwpremIFUd +e4BdS49nTPEQo0IwQDAdBgNVHQ4EFgQUmDnNvtiyjPeyq+GtJK97fKHbH88wDwYD +VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwCgYIKoZIzj0EAwMDaAAwZQIw +V53dVvHH4+m4SVBrm2nDb+zDfSXkV5UTQJtS0zvzQBm8JsctBp61ezaf9SXUY2sA +AjEA6dPGnlaaKsyh2j/IZivTWJwghfqrkYpwcBE4YGQLYgmRWAD5Tfs0aNoJrSEG +GJTO +-----END CERTIFICATE----- + +# Issuer: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Subject: CN=vTrus Root CA O=iTrusChina Co.,Ltd. +# Label: "vTrus Root CA" +# Serial: 387574501246983434957692974888460947164905180485 +# MD5 Fingerprint: b8:c9:37:df:fa:6b:31:84:64:c5:ea:11:6a:1b:75:fc +# SHA1 Fingerprint: 84:1a:69:fb:f5:cd:1a:25:34:13:3d:e3:f8:fc:b8:99:d0:c9:14:b7 +# SHA256 Fingerprint: 8a:71:de:65:59:33:6f:42:6c:26:e5:38:80:d0:0d:88:a1:8d:a4:c6:a9:1f:0d:cb:61:94:e2:06:c5:c9:63:87 +-----BEGIN CERTIFICATE----- +MIIFVjCCAz6gAwIBAgIUQ+NxE9izWRRdt86M/TX9b7wFjUUwDQYJKoZIhvcNAQEL +BQAwQzELMAkGA1UEBhMCQ04xHDAaBgNVBAoTE2lUcnVzQ2hpbmEgQ28uLEx0ZC4x +FjAUBgNVBAMTDXZUcnVzIFJvb3QgQ0EwHhcNMTgwNzMxMDcyNDA1WhcNNDMwNzMx +MDcyNDA1WjBDMQswCQYDVQQGEwJDTjEcMBoGA1UEChMTaVRydXNDaGluYSBDby4s +THRkLjEWMBQGA1UEAxMNdlRydXMgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQAD +ggIPADCCAgoCggIBAL1VfGHTuB0EYgWgrmy3cLRB6ksDXhA/kFocizuwZotsSKYc +IrrVQJLuM7IjWcmOvFjai57QGfIvWcaMY1q6n6MLsLOaXLoRuBLpDLvPbmyAhykU +AyyNJJrIZIO1aqwTLDPxn9wsYTwaP3BVm60AUn/PBLn+NvqcwBauYv6WTEN+VRS+ +GrPSbcKvdmaVayqwlHeFXgQPYh1jdfdr58tbmnDsPmcF8P4HCIDPKNsFxhQnL4Z9 +8Cfe/+Z+M0jnCx5Y0ScrUw5XSmXX+6KAYPxMvDVTAWqXcoKv8R1w6Jz1717CbMdH +flqUhSZNO7rrTOiwCcJlwp2dCZtOtZcFrPUGoPc2BX70kLJrxLT5ZOrpGgrIDajt +J8nU57O5q4IikCc9Kuh8kO+8T/3iCiSn3mUkpF3qwHYw03dQ+A0Em5Q2AXPKBlim +0zvc+gRGE1WKyURHuFE5Gi7oNOJ5y1lKCn+8pu8fA2dqWSslYpPZUxlmPCdiKYZN +pGvu/9ROutW04o5IWgAZCfEF2c6Rsffr6TlP9m8EQ5pV9T4FFL2/s1m02I4zhKOQ +UqqzApVg+QxMaPnu1RcN+HFXtSXkKe5lXa/R7jwXC1pDxaWG6iSe4gUH3DRCEpHW +OXSuTEGC2/KmSNGzm/MzqvOmwMVO9fSddmPmAsYiS8GVP1BkLFTltvA8Kc9XAgMB +AAGjQjBAMB0GA1UdDgQWBBRUYnBj8XWEQ1iO0RYgscasGrz2iTAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAKbqSSaet +8PFww+SX8J+pJdVrnjT+5hpk9jprUrIQeBqfTNqK2uwcN1LgQkv7bHbKJAs5EhWd +nxEt/Hlk3ODg9d3gV8mlsnZwUKT+twpw1aA08XXXTUm6EdGz2OyC/+sOxL9kLX1j +bhd47F18iMjrjld22VkE+rxSH0Ws8HqA7Oxvdq6R2xCOBNyS36D25q5J08FsEhvM +Kar5CKXiNxTKsbhm7xqC5PD48acWabfbqWE8n/Uxy+QARsIvdLGx14HuqCaVvIiv +TDUHKgLKeBRtRytAVunLKmChZwOgzoy8sHJnxDHO2zTlJQNgJXtxmOTAGytfdELS +S8VZCAeHvsXDf+eW2eHcKJfWjwXj9ZtOyh1QRwVTsMo554WgicEFOwE30z9J4nfr +I8iIZjs9OXYhRvHsXyO466JmdXTBQPfYaJqT4i2pLr0cox7IdMakLXogqzu4sEb9 +b91fUlV1YvCXoHzXOP0l382gmxDPi7g4Xl7FtKYCNqEeXxzP4padKar9mK5S4fNB +UvupLnKWnyfjqnN9+BojZns7q2WwMgFLFT49ok8MKzWixtlnEjUwzXYuFrOZnk1P +Ti07NEPhmg4NpGaXutIcSkwsKouLgU9xGqndXHt7CMUADTdA43x7VF8vhV929ven +sBxXVsFy6K2ir40zSbofitzmdHxghm+Hl3s= +-----END CERTIFICATE----- + +# Issuer: CN=ISRG Root X2 O=Internet Security Research Group +# Subject: CN=ISRG Root X2 O=Internet Security Research Group +# Label: "ISRG Root X2" +# Serial: 87493402998870891108772069816698636114 +# MD5 Fingerprint: d3:9e:c4:1e:23:3c:a6:df:cf:a3:7e:6d:e0:14:e6:e5 +# SHA1 Fingerprint: bd:b1:b9:3c:d5:97:8d:45:c6:26:14:55:f8:db:95:c7:5a:d1:53:af +# SHA256 Fingerprint: 69:72:9b:8e:15:a8:6e:fc:17:7a:57:af:b7:17:1d:fc:64:ad:d2:8c:2f:ca:8c:f1:50:7e:34:45:3c:cb:14:70 +-----BEGIN CERTIFICATE----- +MIICGzCCAaGgAwIBAgIQQdKd0XLq7qeAwSxs6S+HUjAKBggqhkjOPQQDAzBPMQsw +CQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJuZXQgU2VjdXJpdHkgUmVzZWFyY2gg +R3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBYMjAeFw0yMDA5MDQwMDAwMDBaFw00 +MDA5MTcxNjAwMDBaME8xCzAJBgNVBAYTAlVTMSkwJwYDVQQKEyBJbnRlcm5ldCBT +ZWN1cml0eSBSZXNlYXJjaCBHcm91cDEVMBMGA1UEAxMMSVNSRyBSb290IFgyMHYw +EAYHKoZIzj0CAQYFK4EEACIDYgAEzZvVn4CDCuwJSvMWSj5cz3es3mcFDR0HttwW ++1qLFNvicWDEukWVEYmO6gbf9yoWHKS5xcUy4APgHoIYOIvXRdgKam7mAHf7AlF9 +ItgKbppbd9/w+kHsOdx1ymgHDB/qo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0T +AQH/BAUwAwEB/zAdBgNVHQ4EFgQUfEKWrt5LSDv6kviejM9ti6lyN5UwCgYIKoZI +zj0EAwMDaAAwZQIwe3lORlCEwkSHRhtFcP9Ymd70/aTSVaYgLXTWNLxBo1BfASdW +tL4ndQavEi51mI38AjEAi/V3bNTIZargCyzuFJ0nN6T5U6VR5CmD1/iQMVtCnwr1 +/q4AaOeMSQ+2b1tbFfLn +-----END CERTIFICATE----- + +# Issuer: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Subject: CN=HiPKI Root CA - G1 O=Chunghwa Telecom Co., Ltd. +# Label: "HiPKI Root CA - G1" +# Serial: 60966262342023497858655262305426234976 +# MD5 Fingerprint: 69:45:df:16:65:4b:e8:68:9a:8f:76:5f:ff:80:9e:d3 +# SHA1 Fingerprint: 6a:92:e4:a8:ee:1b:ec:96:45:37:e3:29:57:49:cd:96:e3:e5:d2:60 +# SHA256 Fingerprint: f0:15:ce:3c:c2:39:bf:ef:06:4b:e9:f1:d2:c4:17:e1:a0:26:4a:0a:94:be:1f:0c:8d:12:18:64:eb:69:49:cc +-----BEGIN CERTIFICATE----- +MIIFajCCA1KgAwIBAgIQLd2szmKXlKFD6LDNdmpeYDANBgkqhkiG9w0BAQsFADBP +MQswCQYDVQQGEwJUVzEjMCEGA1UECgwaQ2h1bmdod2EgVGVsZWNvbSBDby4sIEx0 +ZC4xGzAZBgNVBAMMEkhpUEtJIFJvb3QgQ0EgLSBHMTAeFw0xOTAyMjIwOTQ2MDRa +Fw0zNzEyMzExNTU5NTlaME8xCzAJBgNVBAYTAlRXMSMwIQYDVQQKDBpDaHVuZ2h3 +YSBUZWxlY29tIENvLiwgTHRkLjEbMBkGA1UEAwwSSGlQS0kgUm9vdCBDQSAtIEcx +MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA9B5/UnMyDHPkvRN0o9Qw +qNCuS9i233VHZvR85zkEHmpwINJaR3JnVfSl6J3VHiGh8Ge6zCFovkRTv4354twv +Vcg3Px+kwJyz5HdcoEb+d/oaoDjq7Zpy3iu9lFc6uux55199QmQ5eiY29yTw1S+6 +lZgRZq2XNdZ1AYDgr/SEYYwNHl98h5ZeQa/rh+r4XfEuiAU+TCK72h8q3VJGZDnz +Qs7ZngyzsHeXZJzA9KMuH5UHsBffMNsAGJZMoYFL3QRtU6M9/Aes1MU3guvklQgZ +KILSQjqj2FPseYlgSGDIcpJQ3AOPgz+yQlda22rpEZfdhSi8MEyr48KxRURHH+CK +FgeW0iEPU8DtqX7UTuybCeyvQqww1r/REEXgphaypcXTT3OUM3ECoWqj1jOXTyFj +HluP2cFeRXF3D4FdXyGarYPM+l7WjSNfGz1BryB1ZlpK9p/7qxj3ccC2HTHsOyDr +y+K49a6SsvfhhEvyovKTmiKe0xRvNlS9H15ZFblzqMF8b3ti6RZsR1pl8w4Rm0bZ +/W3c1pzAtH2lsN0/Vm+h+fbkEkj9Bn8SV7apI09bA8PgcSojt/ewsTu8mL3WmKgM +a/aOEmem8rJY5AIJEzypuxC00jBF8ez3ABHfZfjcK0NVvxaXxA/VLGGEqnKG/uY6 +fsI/fe78LxQ+5oXdUG+3Se0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNV +HQ4EFgQU8ncX+l6o/vY9cdVouslGDDjYr7AwDgYDVR0PAQH/BAQDAgGGMA0GCSqG +SIb3DQEBCwUAA4ICAQBQUfB13HAE4/+qddRxosuej6ip0691x1TPOhwEmSKsxBHi +7zNKpiMdDg1H2DfHb680f0+BazVP6XKlMeJ45/dOlBhbQH3PayFUhuaVevvGyuqc +SE5XCV0vrPSltJczWNWseanMX/mF+lLFjfiRFOs6DRfQUsJ748JzjkZ4Bjgs6Fza +ZsT0pPBWGTMpWmWSBUdGSquEwx4noR8RkpkndZMPvDY7l1ePJlsMu5wP1G4wB9Tc +XzZoZjmDlicmisjEOf6aIW/Vcobpf2Lll07QJNBAsNB1CI69aO4I1258EHBGG3zg +iLKecoaZAeO/n0kZtCW+VmWuF2PlHt/o/0elv+EmBYTksMCv5wiZqAxeJoBF1Pho +L5aPruJKHJwWDBNvOIf2u8g0X5IDUXlwpt/L9ZlNec1OvFefQ05rLisY+GpzjLrF +Ne85akEez3GoorKGB1s6yeHvP2UEgEcyRHCVTjFnanRbEEV16rCf0OY1/k6fi8wr +kkVbbiVghUbN0aqwdmaTd5a+g744tiROJgvM7XpWGuDpWsZkrUx6AEhEL7lAuxM+ +vhV4nYWBSipX3tUZQ9rbyltHhoMLP7YNdnhzeSJesYAfz77RP1YQmCuVh6EfnWQU +YDksswBVLuT1sw5XxJFBAJw/6KXf6vb/yPCtbVKoF6ubYfwSUTXkJf2vqmqGOQ== +-----END CERTIFICATE----- + +# Issuer: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Subject: CN=GlobalSign O=GlobalSign OU=GlobalSign ECC Root CA - R4 +# Label: "GlobalSign ECC Root CA - R4" +# Serial: 159662223612894884239637590694 +# MD5 Fingerprint: 26:29:f8:6d:e1:88:bf:a2:65:7f:aa:c4:cd:0f:7f:fc +# SHA1 Fingerprint: 6b:a0:b0:98:e1:71:ef:5a:ad:fe:48:15:80:77:10:f4:bd:6f:0b:28 +# SHA256 Fingerprint: b0:85:d7:0b:96:4f:19:1a:73:e4:af:0d:54:ae:7a:0e:07:aa:fd:af:9b:71:dd:08:62:13:8a:b7:32:5a:24:a2 +-----BEGIN CERTIFICATE----- +MIIB3DCCAYOgAwIBAgINAgPlfvU/k/2lCSGypjAKBggqhkjOPQQDAjBQMSQwIgYD +VQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0gUjQxEzARBgNVBAoTCkdsb2Jh +bFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMTIxMTEzMDAwMDAwWhcNMzgw +MTE5MDMxNDA3WjBQMSQwIgYDVQQLExtHbG9iYWxTaWduIEVDQyBSb290IENBIC0g +UjQxEzARBgNVBAoTCkdsb2JhbFNpZ24xEzARBgNVBAMTCkdsb2JhbFNpZ24wWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAAS4xnnTj2wlDp8uORkcA6SumuU5BwkWymOx +uYb4ilfBV85C+nOh92VC/x7BALJucw7/xyHlGKSq2XE/qNS5zowdo0IwQDAOBgNV +HQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUVLB7rUW44kB/ ++wpu+74zyTyjhNUwCgYIKoZIzj0EAwIDRwAwRAIgIk90crlgr/HmnKAWBVBfw147 +bmF0774BxL4YSFlhgjICICadVGNA3jdgUM/I2O2dgq43mLyjj0xMqTQrbO/7lZsm +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R1 O=Google Trust Services LLC +# Subject: CN=GTS Root R1 O=Google Trust Services LLC +# Label: "GTS Root R1" +# Serial: 159662320309726417404178440727 +# MD5 Fingerprint: 05:fe:d0:bf:71:a8:a3:76:63:da:01:e0:d8:52:dc:40 +# SHA1 Fingerprint: e5:8c:1c:c4:91:3b:38:63:4b:e9:10:6e:e3:ad:8e:6b:9d:d9:81:4a +# SHA256 Fingerprint: d9:47:43:2a:bd:e7:b7:fa:90:fc:2e:6b:59:10:1b:12:80:e0:e1:c7:e4:e4:0f:a3:c6:88:7f:ff:57:a7:f4:cf +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlk28xsBNJiGuiFzANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjEwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjEwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQC2EQKLHuOhd5s73L+UPreVp0A8of2C+X0yBoJx9vaMf/vo +27xqLpeXo4xL+Sv2sfnOhB2x+cWX3u+58qPpvBKJXqeqUqv4IyfLpLGcY9vXmX7w +Cl7raKb0xlpHDU0QM+NOsROjyBhsS+z8CZDfnWQpJSMHobTSPS5g4M/SCYe7zUjw +TcLCeoiKu7rPWRnWr4+wB7CeMfGCwcDfLqZtbBkOtdh+JhpFAz2weaSUKK0Pfybl +qAj+lug8aJRT7oM6iCsVlgmy4HqMLnXWnOunVmSPlk9orj2XwoSPwLxAwAtcvfaH +szVsrBhQf4TgTM2S0yDpM7xSma8ytSmzJSq0SPly4cpk9+aCEI3oncKKiPo4Zor8 +Y/kB+Xj9e1x3+naH+uzfsQ55lVe0vSbv1gHR6xYKu44LtcXFilWr06zqkUspzBmk +MiVOKvFlRNACzqrOSbTqn3yDsEB750Orp2yjj32JgfpMpf/VjsPOS+C12LOORc92 +wO1AK/1TD7Cn1TsNsYqiA94xrcx36m97PtbfkSIS5r762DL8EGMUUXLeXdYWk70p +aDPvOmbsB4om3xPXV2V4J95eSRQAogB/mqghtqmxlbCluQ0WEdrHbEg8QOB+DVrN +VjzRlwW5y0vtOUucxD/SVRNuJLDWcfr0wbrM7Rv1/oFB2ACYPTrIrnqYNxgFlQID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQU5K8rJnEaK0gnhS9SZizv8IkTcT4wDQYJKoZIhvcNAQEMBQADggIBAJ+qQibb +C5u+/x6Wki4+omVKapi6Ist9wTrYggoGxval3sBOh2Z5ofmmWJyq+bXmYOfg6LEe +QkEzCzc9zolwFcq1JKjPa7XSQCGYzyI0zzvFIoTgxQ6KfF2I5DUkzps+GlQebtuy +h6f88/qBVRRiClmpIgUxPoLW7ttXNLwzldMXG+gnoot7TiYaelpkttGsN/H9oPM4 +7HLwEXWdyzRSjeZ2axfG34arJ45JK3VmgRAhpuo+9K4l/3wV3s6MJT/KYnAK9y8J +ZgfIPxz88NtFMN9iiMG1D53Dn0reWVlHxYciNuaCp+0KueIHoI17eko8cdLiA6Ef +MgfdG+RCzgwARWGAtQsgWSl4vflVy2PFPEz0tv/bal8xa5meLMFrUKTX5hgUvYU/ +Z6tGn6D/Qqc6f1zLXbBwHSs09dR2CQzreExZBfMzQsNhFRAbd03OIozUhfJFfbdT +6u9AWpQKXCBfTkBdYiJ23//OYb2MI3jSNwLgjt7RETeJ9r/tSQdirpLsQBqvFAnZ +0E6yove+7u7Y/9waLd64NnHi/Hm3lCXRSHNboTXns5lndcEZOitHTtNCjv0xyBZm +2tIMPNuzjsmhDYAPexZ3FL//2wmUspO8IFgV6dtxQ/PeEMMA3KgqlbbC1j+Qa3bb +bP6MvPJwNQzcmRk13NfIRmPVNnGuV/u3gm3c +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R2 O=Google Trust Services LLC +# Subject: CN=GTS Root R2 O=Google Trust Services LLC +# Label: "GTS Root R2" +# Serial: 159662449406622349769042896298 +# MD5 Fingerprint: 1e:39:c0:53:e6:1e:29:82:0b:ca:52:55:36:5d:57:dc +# SHA1 Fingerprint: 9a:44:49:76:32:db:de:fa:d0:bc:fb:5a:7b:17:bd:9e:56:09:24:94 +# SHA256 Fingerprint: 8d:25:cd:97:22:9d:bf:70:35:6b:da:4e:b3:cc:73:40:31:e2:4c:f0:0f:af:cf:d3:2d:c7:6e:b5:84:1c:7e:a8 +-----BEGIN CERTIFICATE----- +MIIFVzCCAz+gAwIBAgINAgPlrsWNBCUaqxElqjANBgkqhkiG9w0BAQwFADBHMQsw +CQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEU +MBIGA1UEAxMLR1RTIFJvb3QgUjIwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAw +MDAwWjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZp +Y2VzIExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjIwggIiMA0GCSqGSIb3DQEBAQUA +A4ICDwAwggIKAoICAQDO3v2m++zsFDQ8BwZabFn3GTXd98GdVarTzTukk3LvCvpt +nfbwhYBboUhSnznFt+4orO/LdmgUud+tAWyZH8QiHZ/+cnfgLFuv5AS/T3KgGjSY +6Dlo7JUle3ah5mm5hRm9iYz+re026nO8/4Piy33B0s5Ks40FnotJk9/BW9BuXvAu +MC6C/Pq8tBcKSOWIm8Wba96wyrQD8Nr0kLhlZPdcTK3ofmZemde4wj7I0BOdre7k +RXuJVfeKH2JShBKzwkCX44ofR5GmdFrS+LFjKBC4swm4VndAoiaYecb+3yXuPuWg +f9RhD1FLPD+M2uFwdNjCaKH5wQzpoeJ/u1U8dgbuak7MkogwTZq9TwtImoS1mKPV ++3PBV2HdKFZ1E66HjucMUQkQdYhMvI35ezzUIkgfKtzra7tEscszcTJGr61K8Yzo +dDqs5xoic4DSMPclQsciOzsSrZYuxsN2B6ogtzVJV+mSSeh2FnIxZyuWfoqjx5RW +Ir9qS34BIbIjMt/kmkRtWVtd9QCgHJvGeJeNkP+byKq0rxFROV7Z+2et1VsRnTKa +G73VululycslaVNVJ1zgyjbLiGH7HrfQy+4W+9OmTN6SpdTi3/UGVN4unUu0kzCq +gc7dGtxRcw1PcOnlthYhGXmy5okLdWTK1au8CcEYof/UVKGFPP0UJAOyh9OktwID +AQABo0IwQDAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4E +FgQUu//KjiOfT5nK2+JopqUVJxce2Q4wDQYJKoZIhvcNAQEMBQADggIBAB/Kzt3H +vqGf2SdMC9wXmBFqiN495nFWcrKeGk6c1SuYJF2ba3uwM4IJvd8lRuqYnrYb/oM8 +0mJhwQTtzuDFycgTE1XnqGOtjHsB/ncw4c5omwX4Eu55MaBBRTUoCnGkJE+M3DyC +B19m3H0Q/gxhswWV7uGugQ+o+MePTagjAiZrHYNSVc61LwDKgEDg4XSsYPWHgJ2u +NmSRXbBoGOqKYcl3qJfEycel/FVL8/B/uWU9J2jQzGv6U53hkRrJXRqWbTKH7QMg +yALOWr7Z6v2yTcQvG99fevX4i8buMTolUVVnjWQye+mew4K6Ki3pHrTgSAai/Gev +HyICc/sgCq+dVEuhzf9gR7A/Xe8bVr2XIZYtCtFenTgCR2y59PYjJbigapordwj6 +xLEokCZYCDzifqrXPW+6MYgKBesntaFJ7qBFVHvmJ2WZICGoo7z7GJa7Um8M7YNR +TOlZ4iBgxcJlkoKM8xAfDoqXvneCbT+PHV28SSe9zE8P4c52hgQjxcCMElv924Sg +JPFI/2R80L5cFtHvma3AH/vLrrw4IgYmZNralw4/KBVEqE8AyvCazM90arQ+POuV +7LXTWtiBmelDGDfrs7vRWGJB82bSj6p4lVQgw1oudCvV0b4YacCs1aTPObpRhANl +6WLAYv7YTVWW4tAR+kg0Eeye7QUd5MjWHYbL +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R3 O=Google Trust Services LLC +# Subject: CN=GTS Root R3 O=Google Trust Services LLC +# Label: "GTS Root R3" +# Serial: 159662495401136852707857743206 +# MD5 Fingerprint: 3e:e7:9d:58:02:94:46:51:94:e5:e0:22:4a:8b:e7:73 +# SHA1 Fingerprint: ed:e5:71:80:2b:c8:92:b9:5b:83:3c:d2:32:68:3f:09:cd:a0:1e:46 +# SHA256 Fingerprint: 34:d8:a7:3e:e2:08:d9:bc:db:0d:95:65:20:93:4b:4e:40:e6:94:82:59:6e:8b:6f:73:c8:42:6b:01:0a:6f:48 +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPluILrIPglJ209ZjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjMwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjMwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AAQfTzOHMymKoYTey8chWEGJ6ladK0uFxh1MJ7x/JlFyb+Kf1qPKzEUURout736G +jOyxfi//qXGdGIRFBEFVbivqJn+7kAHjSxm65FSWRQmx1WyRRK2EE46ajA2ADDL2 +4CejQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBTB8Sa6oC2uhYHP0/EqEr24Cmf9vDAKBggqhkjOPQQDAwNpADBmAjEA9uEglRR7 +VKOQFhG/hMjqb2sXnh5GmCCbn9MN2azTL818+FsuVbu/3ZL3pAzcMeGiAjEA/Jdm +ZuVDFhOD3cffL74UOO0BzrEXGhF16b0DjyZ+hOXJYKaV11RZt+cRLInUue4X +-----END CERTIFICATE----- + +# Issuer: CN=GTS Root R4 O=Google Trust Services LLC +# Subject: CN=GTS Root R4 O=Google Trust Services LLC +# Label: "GTS Root R4" +# Serial: 159662532700760215368942768210 +# MD5 Fingerprint: 43:96:83:77:19:4d:76:b3:9d:65:52:e4:1d:22:a5:e8 +# SHA1 Fingerprint: 77:d3:03:67:b5:e0:0c:15:f6:0c:38:61:df:7c:e1:3b:92:46:4d:47 +# SHA256 Fingerprint: 34:9d:fa:40:58:c5:e2:63:12:3b:39:8a:e7:95:57:3c:4e:13:13:c8:3f:e6:8f:93:55:6c:d5:e8:03:1b:3c:7d +-----BEGIN CERTIFICATE----- +MIICCTCCAY6gAwIBAgINAgPlwGjvYxqccpBQUjAKBggqhkjOPQQDAzBHMQswCQYD +VQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2VzIExMQzEUMBIG +A1UEAxMLR1RTIFJvb3QgUjQwHhcNMTYwNjIyMDAwMDAwWhcNMzYwNjIyMDAwMDAw +WjBHMQswCQYDVQQGEwJVUzEiMCAGA1UEChMZR29vZ2xlIFRydXN0IFNlcnZpY2Vz +IExMQzEUMBIGA1UEAxMLR1RTIFJvb3QgUjQwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AATzdHOnaItgrkO4NcWBMHtLSZ37wWHO5t5GvWvVYRg1rkDdc/eJkTBa6zzuhXyi +QHY7qca4R9gq55KRanPpsXI5nymfopjTX15YhmUPoYRlBtHci8nHc8iMai/lxKvR +HYqjQjBAMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW +BBSATNbrdP9JNqPV2Py1PsVq8JQdjDAKBggqhkjOPQQDAwNpADBmAjEA6ED/g94D +9J+uHXqnLrmvT/aDHQ4thQEd0dlq7A/Cr8deVl5c1RxYIigL9zC2L7F8AjEA8GE8 +p/SgguMh1YQdc4acLa/KNJvxn7kjNuK8YAOdgLOaVsjh4rsUecrNIdSUtUlD +-----END CERTIFICATE----- + +# Issuer: CN=Telia Root CA v2 O=Telia Finland Oyj +# Subject: CN=Telia Root CA v2 O=Telia Finland Oyj +# Label: "Telia Root CA v2" +# Serial: 7288924052977061235122729490515358 +# MD5 Fingerprint: 0e:8f:ac:aa:82:df:85:b1:f4:dc:10:1c:fc:99:d9:48 +# SHA1 Fingerprint: b9:99:cd:d1:73:50:8a:c4:47:05:08:9c:8c:88:fb:be:a0:2b:40:cd +# SHA256 Fingerprint: 24:2b:69:74:2f:cb:1e:5b:2a:bf:98:89:8b:94:57:21:87:54:4e:5b:4d:99:11:78:65:73:62:1f:6a:74:b8:2c +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIPAWdfJ9b+euPkrL4JWwWeMA0GCSqGSIb3DQEBCwUAMEQx +CzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZMBcGA1UE +AwwQVGVsaWEgUm9vdCBDQSB2MjAeFw0xODExMjkxMTU1NTRaFw00MzExMjkxMTU1 +NTRaMEQxCzAJBgNVBAYTAkZJMRowGAYDVQQKDBFUZWxpYSBGaW5sYW5kIE95ajEZ +MBcGA1UEAwwQVGVsaWEgUm9vdCBDQSB2MjCCAiIwDQYJKoZIhvcNAQEBBQADggIP +ADCCAgoCggIBALLQPwe84nvQa5n44ndp586dpAO8gm2h/oFlH0wnrI4AuhZ76zBq +AMCzdGh+sq/H1WKzej9Qyow2RCRj0jbpDIX2Q3bVTKFgcmfiKDOlyzG4OiIjNLh9 +vVYiQJ3q9HsDrWj8soFPmNB06o3lfc1jw6P23pLCWBnglrvFxKk9pXSW/q/5iaq9 +lRdU2HhE8Qx3FZLgmEKnpNaqIJLNwaCzlrI6hEKNfdWV5Nbb6WLEWLN5xYzTNTOD +n3WhUidhOPFZPY5Q4L15POdslv5e2QJltI5c0BE0312/UqeBAMN/mUWZFdUXyApT +7GPzmX3MaRKGwhfwAZ6/hLzRUssbkmbOpFPlob/E2wnW5olWK8jjfN7j/4nlNW4o +6GwLI1GpJQXrSPjdscr6bAhR77cYbETKJuFzxokGgeWKrLDiKca5JLNrRBH0pUPC +TEPlcDaMtjNXepUugqD0XBCzYYP2AgWGLnwtbNwDRm41k9V6lS/eINhbfpSQBGq6 +WT0EBXWdN6IOLj3rwaRSg/7Qa9RmjtzG6RJOHSpXqhC8fF6CfaamyfItufUXJ63R +DolUK5X6wK0dmBR4M0KGCqlztft0DbcbMBnEWg4cJ7faGND/isgFuvGqHKI3t+ZI +pEYslOqodmJHixBTB0hXbOKSTbauBcvcwUpej6w9GU7C7WB1K9vBykLVAgMBAAGj +YzBhMB8GA1UdIwQYMBaAFHKs5DN5qkWH9v2sHZ7Wxy+G2CQ5MB0GA1UdDgQWBBRy +rOQzeapFh/b9rB2e1scvhtgkOTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUw +AwEB/zANBgkqhkiG9w0BAQsFAAOCAgEAoDtZpwmUPjaE0n4vOaWWl/oRrfxn83EJ +8rKJhGdEr7nv7ZbsnGTbMjBvZ5qsfl+yqwE2foH65IRe0qw24GtixX1LDoJt0nZi +0f6X+J8wfBj5tFJ3gh1229MdqfDBmgC9bXXYfef6xzijnHDoRnkDry5023X4blMM +A8iZGok1GTzTyVR8qPAs5m4HeW9q4ebqkYJpCh3DflminmtGFZhb069GHWLIzoBS +SRE/yQQSwxN8PzuKlts8oB4KtItUsiRnDe+Cy748fdHif64W1lZYudogsYMVoe+K +TTJvQS8TUoKU1xrBeKJR3Stwbbca+few4GeXVtt8YVMJAygCQMez2P2ccGrGKMOF +6eLtGpOg3kuYooQ+BXcBlj37tCAPnHICehIv1aO6UXivKitEZU61/Qrowc15h2Er +3oBXRb9n8ZuRXqWk7FlIEA04x7D6w0RtBPV4UBySllva9bguulvP5fBqnUsvWHMt +Ty3EHD70sz+rFQ47GUGKpMFXEmZxTPpT41frYpUJnlTd0cI8Vzy9OK2YZLe4A5pT +VmBds9hCG1xLEooc6+t9xnppxyd/pPiL8uSUZodL6ZQHCRJ5irLrdATczvREWeAW +ysUsWNc8e89ihmpQfTU2Zqf7N+cox9jQraVplI/owd8k+BsHMYeB2F326CjYSlKA +rBPuUBQemMc= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 1 2020" +# Serial: 165870826978392376648679885835942448534 +# MD5 Fingerprint: b5:aa:4b:d5:ed:f7:e3:55:2e:8f:72:0a:f3:75:b8:ed +# SHA1 Fingerprint: 1f:5b:98:f0:e3:b5:f7:74:3c:ed:e6:b0:36:7d:32:cd:f4:09:41:67 +# SHA256 Fingerprint: e5:9a:aa:81:60:09:c2:2b:ff:5b:25:ba:d3:7d:f3:06:f0:49:79:7c:1f:81:d8:5a:b0:89:e6:57:bd:8f:00:44 +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQfMmPK4TX3+oPyWWa00tNljAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEJSIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTA5NDUwMFoXDTM1MDIxMTA5 +NDQ1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABMbLxyjR+4T1mu9CFCDhQ2tuda38KwOE1HaTJddZO0Flax7mNCq7dPYS +zuht56vkPE4/RAiLzRZxy7+SmfSk1zxQVFKQhYN4lGdnoxwJGT11NIXe7WB9xwy0 +QVK5buXuQqOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFHOREKv/ +VbNafAkl1bK6CKBrqx9tMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2JyX3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwQlIlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAlJAtE/rhY/hhY+ithXhUkZy4kzg+GkHaQBZTQgjKL47xPoFW +wKrY7RjEsK70PvomAjEA8yjixtsrmfu3Ubgko6SUeho/5jbiA1czijDLgsfWFBHV +dWNbFJWcHwHP2NVypw87 +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 1 2020 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 1 2020" +# Serial: 126288379621884218666039612629459926992 +# MD5 Fingerprint: 8c:2d:9d:70:9f:48:99:11:06:11:fb:e9:cb:30:c0:6e +# SHA1 Fingerprint: 61:db:8c:21:59:69:03:90:d8:7c:9c:12:86:54:cf:9d:3d:f4:dd:07 +# SHA256 Fingerprint: 08:17:0d:1a:a3:64:53:90:1a:2f:95:92:45:e3:47:db:0c:8d:37:ab:aa:bc:56:b8:1a:a1:00:dc:95:89:70:db +-----BEGIN CERTIFICATE----- +MIIC2zCCAmCgAwIBAgIQXwJB13qHfEwDo6yWjfv/0DAKBggqhkjOPQQDAzBIMQsw +CQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlELVRS +VVNUIEVWIFJvb3QgQ0EgMSAyMDIwMB4XDTIwMDIxMTEwMDAwMFoXDTM1MDIxMTA5 +NTk1OVowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEiMCAG +A1UEAxMZRC1UUlVTVCBFViBSb290IENBIDEgMjAyMDB2MBAGByqGSM49AgEGBSuB +BAAiA2IABPEL3YZDIBnfl4XoIkqbz52Yv7QFJsnL46bSj8WeeHsxiamJrSc8ZRCC +/N/DnU7wMyPE0jL1HLDfMxddxfCxivnvubcUyilKwg+pf3VlSSowZ/Rk99Yad9rD +wpdhQntJraOCAQ0wggEJMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFH8QARY3 +OqQo5FD4pPfsazK2/umLMA4GA1UdDwEB/wQEAwIBBjCBxgYDVR0fBIG+MIG7MD6g +PKA6hjhodHRwOi8vY3JsLmQtdHJ1c3QubmV0L2NybC9kLXRydXN0X2V2X3Jvb3Rf +Y2FfMV8yMDIwLmNybDB5oHegdYZzbGRhcDovL2RpcmVjdG9yeS5kLXRydXN0Lm5l +dC9DTj1ELVRSVVNUJTIwRVYlMjBSb290JTIwQ0ElMjAxJTIwMjAyMCxPPUQtVHJ1 +c3QlMjBHbWJILEM9REU/Y2VydGlmaWNhdGVyZXZvY2F0aW9ubGlzdDAKBggqhkjO +PQQDAwNpADBmAjEAyjzGKnXCXnViOTYAYFqLwZOZzNnbQTs7h5kXO9XMT8oi96CA +y/m0sRtW9XLS/BnRAjEAkfcwkz8QRitxpNA7RJvAKQIFskF3UfN5Wp6OFKBOQtJb +gfM0agPnIjhQW+0ZT0MW +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS ECC P384 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS ECC P384 Root G5" +# Serial: 13129116028163249804115411775095713523 +# MD5 Fingerprint: d3:71:04:6a:43:1c:db:a6:59:e1:a8:a3:aa:c5:71:ed +# SHA1 Fingerprint: 17:f3:de:5e:9f:0f:19:e9:8e:f6:1f:32:26:6e:20:c4:07:ae:30:ee +# SHA256 Fingerprint: 01:8e:13:f0:77:25:32:cf:80:9b:d1:b1:72:81:86:72:83:fc:48:c6:e1:3b:e9:c6:98:12:85:4a:49:0c:1b:05 +-----BEGIN CERTIFICATE----- +MIICGTCCAZ+gAwIBAgIQCeCTZaz32ci5PhwLBCou8zAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJjAkBgNVBAMTHURp +Z2lDZXJ0IFRMUyBFQ0MgUDM4NCBSb290IEc1MB4XDTIxMDExNTAwMDAwMFoXDTQ2 +MDExNDIzNTk1OVowTjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ +bmMuMSYwJAYDVQQDEx1EaWdpQ2VydCBUTFMgRUNDIFAzODQgUm9vdCBHNTB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABMFEoc8Rl1Ca3iOCNQfN0MsYndLxf3c1TzvdlHJS +7cI7+Oz6e2tYIOyZrsn8aLN1udsJ7MgT9U7GCh1mMEy7H0cKPGEQQil8pQgO4CLp +0zVozptjn4S1mU1YoI71VOeVyaNCMEAwHQYDVR0OBBYEFMFRRVBZqz7nLFr6ICIS +B4CIfBFqMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MAoGCCqGSM49 +BAMDA2gAMGUCMQCJao1H5+z8blUD2WdsJk6Dxv3J+ysTvLd6jLRl0mlpYxNjOyZQ +LgGheQaRnUi/wr4CMEfDFXuxoJGZSZOoPHzoRgaLLPIxAJSdYsiJvRmEFOml+wG4 +DXZDjC5Ty3zfDBeWUA== +-----END CERTIFICATE----- + +# Issuer: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Subject: CN=DigiCert TLS RSA4096 Root G5 O=DigiCert, Inc. +# Label: "DigiCert TLS RSA4096 Root G5" +# Serial: 11930366277458970227240571539258396554 +# MD5 Fingerprint: ac:fe:f7:34:96:a9:f2:b3:b4:12:4b:e4:27:41:6f:e1 +# SHA1 Fingerprint: a7:88:49:dc:5d:7c:75:8c:8c:de:39:98:56:b3:aa:d0:b2:a5:71:35 +# SHA256 Fingerprint: 37:1a:00:dc:05:33:b3:72:1a:7e:eb:40:e8:41:9e:70:79:9d:2b:0a:0f:2c:1d:80:69:31:65:f7:ce:c4:ad:75 +-----BEGIN CERTIFICATE----- +MIIFZjCCA06gAwIBAgIQCPm0eKj6ftpqMzeJ3nzPijANBgkqhkiG9w0BAQwFADBN +MQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xJTAjBgNVBAMT +HERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwHhcNMjEwMTE1MDAwMDAwWhcN +NDYwMTE0MjM1OTU5WjBNMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs +IEluYy4xJTAjBgNVBAMTHERpZ2lDZXJ0IFRMUyBSU0E0MDk2IFJvb3QgRzUwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCz0PTJeRGd/fxmgefM1eS87IE+ +ajWOLrfn3q/5B03PMJ3qCQuZvWxX2hhKuHisOjmopkisLnLlvevxGs3npAOpPxG0 +2C+JFvuUAT27L/gTBaF4HI4o4EXgg/RZG5Wzrn4DReW+wkL+7vI8toUTmDKdFqgp +wgscONyfMXdcvyej/Cestyu9dJsXLfKB2l2w4SMXPohKEiPQ6s+d3gMXsUJKoBZM +pG2T6T867jp8nVid9E6P/DsjyG244gXazOvswzH016cpVIDPRFtMbzCe88zdH5RD +nU1/cHAN1DrRN/BsnZvAFJNY781BOHW8EwOVfH/jXOnVDdXifBBiqmvwPXbzP6Po +sMH976pXTayGpxi0KcEsDr9kvimM2AItzVwv8n/vFfQMFawKsPHTDU9qTXeXAaDx +Zre3zu/O7Oyldcqs4+Fj97ihBMi8ez9dLRYiVu1ISf6nL3kwJZu6ay0/nTvEF+cd +Lvvyz6b84xQslpghjLSR6Rlgg/IwKwZzUNWYOwbpx4oMYIwo+FKbbuH2TbsGJJvX +KyY//SovcfXWJL5/MZ4PbeiPT02jP/816t9JXkGPhvnxd3lLG7SjXi/7RgLQZhNe +XoVPzthwiHvOAbWWl9fNff2C+MIkwcoBOU+NosEUQB+cZtUMCUbW8tDRSHZWOkPL +tgoRObqME2wGtZ7P6wIDAQABo0IwQDAdBgNVHQ4EFgQUUTMc7TZArxfTJc1paPKv +TiM+s0EwDgYDVR0PAQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcN +AQEMBQADggIBAGCmr1tfV9qJ20tQqcQjNSH/0GEwhJG3PxDPJY7Jv0Y02cEhJhxw +GXIeo8mH/qlDZJY6yFMECrZBu8RHANmfGBg7sg7zNOok992vIGCukihfNudd5N7H +PNtQOa27PShNlnx2xlv0wdsUpasZYgcYQF+Xkdycx6u1UQ3maVNVzDl92sURVXLF +O4uJ+DQtpBflF+aZfTCIITfNMBc9uPK8qHWgQ9w+iUuQrm0D4ByjoJYJu32jtyoQ +REtGBzRj7TG5BO6jm5qu5jF49OokYTurWGT/u4cnYiWB39yhL/btp/96j1EuMPik +AdKFOV8BmZZvWltwGUb+hmA+rYAQCd05JS9Yf7vSdPD3Rh9GOUrYU9DzLjtxpdRv +/PNn5AeP3SYZ4Y1b+qOTEZvpyDrDVWiakuFSdjjo4bq9+0/V77PnSIMx8IIh47a+ +p6tv75/fTM8BuGJqIz3nCU2AG3swpMPdB380vqQmsvZB6Akd4yCYqjdP//fx4ilw +MUc/dNAUFvohigLVigmUdy7yWSiLfFCSCmZ4OIN1xLVaqBHG5cGdZlXPU8Sv13WF +qUITVuwhd4GTWgzqltlJyqEI8pc7bZsEGCREjnwB8twl2F6GmrE52/WRMmrRpnCK +ovfepEWFJqgejF0pW8hL2JpqA15w8oVPbEtoL8pU9ozaMv7Da4M/OMZ+ +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root R1 O=Certainly +# Subject: CN=Certainly Root R1 O=Certainly +# Label: "Certainly Root R1" +# Serial: 188833316161142517227353805653483829216 +# MD5 Fingerprint: 07:70:d4:3e:82:87:a0:fa:33:36:13:f4:fa:33:e7:12 +# SHA1 Fingerprint: a0:50:ee:0f:28:71:f4:27:b2:12:6d:6f:50:96:25:ba:cc:86:42:af +# SHA256 Fingerprint: 77:b8:2c:d8:64:4c:43:05:f7:ac:c5:cb:15:6b:45:67:50:04:03:3d:51:c6:0c:62:02:a8:e0:c3:34:67:d3:a0 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIRAI4P+UuQcWhlM1T01EQ5t+AwDQYJKoZIhvcNAQELBQAw +PTELMAkGA1UEBhMCVVMxEjAQBgNVBAoTCUNlcnRhaW5seTEaMBgGA1UEAxMRQ2Vy +dGFpbmx5IFJvb3QgUjEwHhcNMjEwNDAxMDAwMDAwWhcNNDYwNDAxMDAwMDAwWjA9 +MQswCQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0 +YWlubHkgUm9vdCBSMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANA2 +1B/q3avk0bbm+yLA3RMNansiExyXPGhjZjKcA7WNpIGD2ngwEc/csiu+kr+O5MQT +vqRoTNoCaBZ0vrLdBORrKt03H2As2/X3oXyVtwxwhi7xOu9S98zTm/mLvg7fMbed +aFySpvXl8wo0tf97ouSHocavFwDvA5HtqRxOcT3Si2yJ9HiG5mpJoM610rCrm/b0 +1C7jcvk2xusVtyWMOvwlDbMicyF0yEqWYZL1LwsYpfSt4u5BvQF5+paMjRcCMLT5 +r3gajLQ2EBAHBXDQ9DGQilHFhiZ5shGIXsXwClTNSaa/ApzSRKft43jvRl5tcdF5 +cBxGX1HpyTfcX35pe0HfNEXgO4T0oYoKNp43zGJS4YkNKPl6I7ENPT2a/Z2B7yyQ +wHtETrtJ4A5KVpK8y7XdeReJkd5hiXSSqOMyhb5OhaRLWcsrxXiOcVTQAjeZjOVJ +6uBUcqQRBi8LjMFbvrWhsFNunLhgkR9Za/kt9JQKl7XsxXYDVBtlUrpMklZRNaBA +2CnbrlJ2Oy0wQJuK0EJWtLeIAaSHO1OWzaMWj/Nmqhexx2DgwUMFDO6bW2BvBlyH +Wyf5QBGenDPBt+U1VwV/J84XIIwc/PH72jEpSe31C4SnT8H2TsIonPru4K8H+zMR +eiFPCyEQtkA6qyI6BJyLm4SGcprSp6XEtHWRqSsjAgMBAAGjQjBAMA4GA1UdDwEB +/wQEAwIBBjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTgqj8ljZ9EXME66C6u +d0yEPmcM9DANBgkqhkiG9w0BAQsFAAOCAgEAuVevuBLaV4OPaAszHQNTVfSVcOQr +PbA56/qJYv331hgELyE03fFo8NWWWt7CgKPBjcZq91l3rhVkz1t5BXdm6ozTaw3d +8VkswTOlMIAVRQdFGjEitpIAq5lNOo93r6kiyi9jyhXWx8bwPWz8HA2YEGGeEaIi +1wrykXprOQ4vMMM2SZ/g6Q8CRFA3lFV96p/2O7qUpUzpvD5RtOjKkjZUbVwlKNrd +rRT90+7iIgXr0PK3aBLXWopBGsaSpVo7Y0VPv+E6dyIvXL9G+VoDhRNCX8reU9di +taY1BMJH/5n9hN9czulegChB8n3nHpDYT3Y+gjwN/KUD+nsa2UUeYNrEjvn8K8l7 +lcUq/6qJ34IxD3L/DCfXCh5WAFAeDJDBlrXYFIW7pw0WwfgHJBu6haEaBQmAupVj +yTrsJZ9/nbqkRxWbRHDxakvWOF5D8xh+UG7pWijmZeZ3Gzr9Hb4DJqPb1OG7fpYn +Kx3upPvaJVQTA945xsMfTZDsjxtK0hzthZU4UHlG1sGQUDGpXJpuHfUzVounmdLy +yCwzk5Iwx06MZTMQZBf9JBeW0Y3COmor6xOLRPIh80oat3df1+2IpHLlOR+Vnb5n +wXARPbv0+Em34yaXOp/SX3z7wJl8OSngex2/DaeP0ik0biQVy96QXr8axGbqwua6 +OV+KmalBWQewLK8= +-----END CERTIFICATE----- + +# Issuer: CN=Certainly Root E1 O=Certainly +# Subject: CN=Certainly Root E1 O=Certainly +# Label: "Certainly Root E1" +# Serial: 8168531406727139161245376702891150584 +# MD5 Fingerprint: 0a:9e:ca:cd:3e:52:50:c6:36:f3:4b:a3:ed:a7:53:e9 +# SHA1 Fingerprint: f9:e1:6d:dc:01:89:cf:d5:82:45:63:3e:c5:37:7d:c2:eb:93:6f:2b +# SHA256 Fingerprint: b4:58:5f:22:e4:ac:75:6a:4e:86:12:a1:36:1c:5d:9d:03:1a:93:fd:84:fe:bb:77:8f:a3:06:8b:0f:c4:2d:c2 +-----BEGIN CERTIFICATE----- +MIIB9zCCAX2gAwIBAgIQBiUzsUcDMydc+Y2aub/M+DAKBggqhkjOPQQDAzA9MQsw +CQYDVQQGEwJVUzESMBAGA1UEChMJQ2VydGFpbmx5MRowGAYDVQQDExFDZXJ0YWlu +bHkgUm9vdCBFMTAeFw0yMTA0MDEwMDAwMDBaFw00NjA0MDEwMDAwMDBaMD0xCzAJ +BgNVBAYTAlVTMRIwEAYDVQQKEwlDZXJ0YWlubHkxGjAYBgNVBAMTEUNlcnRhaW5s +eSBSb290IEUxMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAE3m/4fxzf7flHh4axpMCK ++IKXgOqPyEpeKn2IaKcBYhSRJHpcnqMXfYqGITQYUBsQ3tA3SybHGWCA6TS9YBk2 +QNYphwk8kXr2vBMj3VlOBF7PyAIcGFPBMdjaIOlEjeR2o0IwQDAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU8ygYy2R17ikq6+2uI1g4 +hevIIgcwCgYIKoZIzj0EAwMDaAAwZQIxALGOWiDDshliTd6wT99u0nCK8Z9+aozm +ut6Dacpps6kFtZaSF4fC0urQe87YQVt8rgIwRt7qy12a7DLCZRawTDBcMPPaTnOG +BtjOiQRINzf43TNRnXCve1XYAS59BWQOhriR +-----END CERTIFICATE----- + +# Issuer: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Subject: CN=Security Communication ECC RootCA1 O=SECOM Trust Systems CO.,LTD. +# Label: "Security Communication ECC RootCA1" +# Serial: 15446673492073852651 +# MD5 Fingerprint: 7e:43:b0:92:68:ec:05:43:4c:98:ab:5d:35:2e:7e:86 +# SHA1 Fingerprint: b8:0e:26:a9:bf:d2:b2:3b:c0:ef:46:c9:ba:c7:bb:f6:1d:0d:41:41 +# SHA256 Fingerprint: e7:4f:bd:a5:5b:d5:64:c4:73:a3:6b:44:1a:a7:99:c8:a6:8e:07:74:40:e8:28:8b:9f:a1:e5:0e:4b:ba:ca:11 +-----BEGIN CERTIFICATE----- +MIICODCCAb6gAwIBAgIJANZdm7N4gS7rMAoGCCqGSM49BAMDMGExCzAJBgNVBAYT +AkpQMSUwIwYDVQQKExxTRUNPTSBUcnVzdCBTeXN0ZW1zIENPLixMVEQuMSswKQYD +VQQDEyJTZWN1cml0eSBDb21tdW5pY2F0aW9uIEVDQyBSb290Q0ExMB4XDTE2MDYx +NjA1MTUyOFoXDTM4MDExODA1MTUyOFowYTELMAkGA1UEBhMCSlAxJTAjBgNVBAoT +HFNFQ09NIFRydXN0IFN5c3RlbXMgQ08uLExURC4xKzApBgNVBAMTIlNlY3VyaXR5 +IENvbW11bmljYXRpb24gRUNDIFJvb3RDQTEwdjAQBgcqhkjOPQIBBgUrgQQAIgNi +AASkpW9gAwPDvTH00xecK4R1rOX9PVdu12O/5gSJko6BnOPpR27KkBLIE+Cnnfdl +dB9sELLo5OnvbYUymUSxXv3MdhDYW72ixvnWQuRXdtyQwjWpS4g8EkdtXP9JTxpK +ULGjQjBAMB0GA1UdDgQWBBSGHOf+LaVKiwj+KBH6vqNm+GBZLzAOBgNVHQ8BAf8E +BAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNoADBlAjAVXUI9/Lbu +9zuxNuie9sRGKEkz0FhDKmMpzE2xtHqiuQ04pV1IKv3LsnNdo4gIxwwCMQDAqy0O +be0YottT6SXbVQjgUMzfRGEWgqtJsLKB7HOHeLRMsmIbEvoWTSVLY70eN9k= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA1 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA1" +# Serial: 113562791157148395269083148143378328608 +# MD5 Fingerprint: 42:32:99:76:43:33:36:24:35:07:82:9b:28:f9:d0:90 +# SHA1 Fingerprint: d5:ec:8d:7b:4c:ba:79:f4:e7:e8:cb:9d:6b:ae:77:83:10:03:21:6a +# SHA256 Fingerprint: f3:89:6f:88:fe:7c:0a:88:27:66:a7:fa:6a:d2:74:9f:b5:7a:7f:3e:98:fb:76:9c:1f:a7:b0:9c:2c:44:d5:ae +-----BEGIN CERTIFICATE----- +MIIFdDCCA1ygAwIBAgIQVW9l47TZkGobCdFsPsBsIDANBgkqhkiG9w0BAQsFADBU +MQswCQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRI +T1JJVFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0ExMB4XDTE5MTIxOTAz +MTYxN1oXDTQ0MTIxMjAzMTYxN1owVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJF +SUpJTkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2Jh +bCBSb290IENBMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPFmCL3Z +xRVhy4QEQaVpN3cdwbB7+sN3SJATcmTRuHyQNZ0YeYjjlwE8R4HyDqKYDZ4/N+AZ +spDyRhySsTphzvq3Rp4Dhtczbu33RYx2N95ulpH3134rhxfVizXuhJFyV9xgw8O5 +58dnJCNPYwpj9mZ9S1WnP3hkSWkSl+BMDdMJoDIwOvqfwPKcxRIqLhy1BDPapDgR +at7GGPZHOiJBhyL8xIkoVNiMpTAK+BcWyqw3/XmnkRd4OJmtWO2y3syJfQOcs4ll +5+M7sSKGjwZteAf9kRJ/sGsciQ35uMt0WwfCyPQ10WRjeulumijWML3mG90Vr4Tq +nMfK9Q7q8l0ph49pczm+LiRvRSGsxdRpJQaDrXpIhRMsDQa4bHlW/KNnMoH1V6XK +V0Jp6VwkYe/iMBhORJhVb3rCk9gZtt58R4oRTklH2yiUAguUSiz5EtBP6DF+bHq/ +pj+bOT0CFqMYs2esWz8sgytnOYFcuX6U1WTdno9uruh8W7TXakdI136z1C2OVnZO +z2nxbkRs1CTqjSShGL+9V/6pmTW12xB3uD1IutbB5/EjPtffhZ0nPNRAvQoMvfXn +jSXWgXSHRtQpdaJCbPdzied9v3pKH9MiyRVVz99vfFXQpIsHETdfg6YmV6YBW37+ +WGgHqel62bno/1Afq8K0wM7o6v0PvY1NuLxxAgMBAAGjQjBAMB0GA1UdDgQWBBTF +7+3M2I0hxkjk49cULqcWk+WYATAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQE +AwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAUoKsITQfI/Ki2Pm4rzc2IInRNwPWaZ+4 +YRC6ojGYWUfo0Q0lHhVBDOAqVdVXUsv45Mdpox1NcQJeXyFFYEhcCY5JEMEE3Kli +awLwQ8hOnThJdMkycFRtwUf8jrQ2ntScvd0g1lPJGKm1Vrl2i5VnZu69mP6u775u ++2D2/VnGKhs/I0qUJDAnyIm860Qkmss9vk/Ves6OF8tiwdneHg56/0OGNFK8YT88 +X7vZdrRTvJez/opMEi4r89fO4aL/3Xtw+zuhTaRjAv04l5U/BXCga99igUOLtFkN +SoxUnMW7gZ/NfaXvCyUeOiDbHPwfmGcCCtRzRBPbUYQaVQNW4AB+dAb/OMRyHdOo +P2gxXdMJxy6MW2Pg6Nwe0uxhHvLe5e/2mXZgLR6UcnHGCyoyx5JO1UbXHfmpGQrI ++pXObSOYqgs4rZpWDW+N8TEAiMEXnM0ZNjX+VVOg4DwzX5Ze4jLp3zO7Bkqp2IRz +znfSxqxx4VyjHQy7Ct9f4qNx2No3WqB4K/TUfet27fJhcKVlmtOJNBir+3I+17Q9 +eVzYH6Eze9mCUAyTF6ps3MKCuwJXNq+YJyo5UOGwifUll35HaBC07HPKs5fRJNz2 +YqAo07WjuGS3iGJCz51TzZm+ZGiPTx4SSPfSKcOYKMryMguTjClPPGAyzQWWYezy +r/6zcCwupvI= +-----END CERTIFICATE----- + +# Issuer: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Subject: CN=BJCA Global Root CA2 O=BEIJING CERTIFICATE AUTHORITY +# Label: "BJCA Global Root CA2" +# Serial: 58605626836079930195615843123109055211 +# MD5 Fingerprint: 5e:0a:f6:47:5f:a6:14:e8:11:01:95:3f:4d:01:eb:3c +# SHA1 Fingerprint: f4:27:86:eb:6e:b8:6d:88:31:67:02:fb:ba:66:a4:53:00:aa:7a:a6 +# SHA256 Fingerprint: 57:4d:f6:93:1e:27:80:39:66:7b:72:0a:fd:c1:60:0f:c2:7e:b6:6d:d3:09:29:79:fb:73:85:64:87:21:28:82 +-----BEGIN CERTIFICATE----- +MIICJTCCAaugAwIBAgIQLBcIfWQqwP6FGFkGz7RK6zAKBggqhkjOPQQDAzBUMQsw +CQYDVQQGEwJDTjEmMCQGA1UECgwdQkVJSklORyBDRVJUSUZJQ0FURSBBVVRIT1JJ +VFkxHTAbBgNVBAMMFEJKQ0EgR2xvYmFsIFJvb3QgQ0EyMB4XDTE5MTIxOTAzMTgy +MVoXDTQ0MTIxMjAzMTgyMVowVDELMAkGA1UEBhMCQ04xJjAkBgNVBAoMHUJFSUpJ +TkcgQ0VSVElGSUNBVEUgQVVUSE9SSVRZMR0wGwYDVQQDDBRCSkNBIEdsb2JhbCBS +b290IENBMjB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ3LgJGNU2e1uVCxA/jlSR9B +IgmwUVJY1is0j8USRhTFiy8shP8sbqjV8QnjAyEUxEM9fMEsxEtqSs3ph+B99iK+ ++kpRuDCK/eHeGBIK9ke35xe/J4rUQUyWPGCWwf0VHKNCMEAwHQYDVR0OBBYEFNJK +sVF/BvDRgh9Obl+rg/xI1LCRMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMAoGCCqGSM49BAMDA2gAMGUCMBq8W9f+qdJUDkpd0m2xQNz0Q9XSSpkZElaA +94M04TVOSG0ED1cxMDAtsaqdAzjbBgIxAMvMh1PLet8gUXOQwKhbYdDFUDn9hf7B +43j4ptZLvZuHjw/l1lOWqzzIQNph91Oj9w== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root E46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root E46" +# Serial: 88989738453351742415770396670917916916 +# MD5 Fingerprint: 28:23:f8:b2:98:5c:37:16:3b:3e:46:13:4e:b0:b3:01 +# SHA1 Fingerprint: ec:8a:39:6c:40:f0:2e:bc:42:75:d4:9f:ab:1c:1a:5b:67:be:d2:9a +# SHA256 Fingerprint: c9:0f:26:f0:fb:1b:40:18:b2:22:27:51:9b:5c:a2:b5:3e:2c:a5:b3:be:5c:f1:8e:fe:1b:ef:47:38:0c:53:83 +-----BEGIN CERTIFICATE----- +MIICOjCCAcGgAwIBAgIQQvLM2htpN0RfFf51KBC49DAKBggqhkjOPQQDAzBfMQsw +CQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1T +ZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwHhcN +MjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEYMBYG +A1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1YmxpYyBT +ZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBFNDYwdjAQBgcqhkjOPQIBBgUrgQQA +IgNiAAR2+pmpbiDt+dd34wc7qNs9Xzjoq1WmVk/WSOrsfy2qw7LFeeyZYX8QeccC +WvkEN/U0NSt3zn8gj1KjAIns1aeibVvjS5KToID1AZTc8GgHHs3u/iVStSBDHBv+ +6xnOQ6OjQjBAMB0GA1UdDgQWBBTRItpMWfFLXyY4qp3W7usNw/upYTAOBgNVHQ8B +Af8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAKBggqhkjOPQQDAwNnADBkAjAn7qRa +qCG76UeXlImldCBteU/IvZNeWBj7LRoAasm4PdCkT0RHlAFWovgzJQxC36oCMB3q +4S6ILuH5px0CMk7yn2xVdOOurvulGu7t0vzCAxHrRVxgED1cf5kDW21USAGKcw== +-----END CERTIFICATE----- + +# Issuer: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Subject: CN=Sectigo Public Server Authentication Root R46 O=Sectigo Limited +# Label: "Sectigo Public Server Authentication Root R46" +# Serial: 156256931880233212765902055439220583700 +# MD5 Fingerprint: 32:10:09:52:00:d5:7e:6c:43:df:15:c0:b1:16:93:e5 +# SHA1 Fingerprint: ad:98:f9:f3:e4:7d:75:3b:65:d4:82:b3:a4:52:17:bb:6e:f5:e4:38 +# SHA256 Fingerprint: 7b:b6:47:a6:2a:ee:ac:88:bf:25:7a:a5:22:d0:1f:fe:a3:95:e0:ab:45:c7:3f:93:f6:56:54:ec:38:f2:5a:06 +-----BEGIN CERTIFICATE----- +MIIFijCCA3KgAwIBAgIQdY39i658BwD6qSWn4cetFDANBgkqhkiG9w0BAQwFADBf +MQswCQYDVQQGEwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQD +Ey1TZWN0aWdvIFB1YmxpYyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYw +HhcNMjEwMzIyMDAwMDAwWhcNNDYwMzIxMjM1OTU5WjBfMQswCQYDVQQGEwJHQjEY +MBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMTYwNAYDVQQDEy1TZWN0aWdvIFB1Ymxp +YyBTZXJ2ZXIgQXV0aGVudGljYXRpb24gUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEB +AQUAA4ICDwAwggIKAoICAQCTvtU2UnXYASOgHEdCSe5jtrch/cSV1UgrJnwUUxDa +ef0rty2k1Cz66jLdScK5vQ9IPXtamFSvnl0xdE8H/FAh3aTPaE8bEmNtJZlMKpnz +SDBh+oF8HqcIStw+KxwfGExxqjWMrfhu6DtK2eWUAtaJhBOqbchPM8xQljeSM9xf +iOefVNlI8JhD1mb9nxc4Q8UBUQvX4yMPFF1bFOdLvt30yNoDN9HWOaEhUTCDsG3X +ME6WW5HwcCSrv0WBZEMNvSE6Lzzpng3LILVCJ8zab5vuZDCQOc2TZYEhMbUjUDM3 +IuM47fgxMMxF/mL50V0yeUKH32rMVhlATc6qu/m1dkmU8Sf4kaWD5QazYw6A3OAS +VYCmO2a0OYctyPDQ0RTp5A1NDvZdV3LFOxxHVp3i1fuBYYzMTYCQNFu31xR13NgE +SJ/AwSiItOkcyqex8Va3e0lMWeUgFaiEAin6OJRpmkkGj80feRQXEgyDet4fsZfu ++Zd4KKTIRJLpfSYFplhym3kT2BFfrsU4YjRosoYwjviQYZ4ybPUHNs2iTG7sijbt +8uaZFURww3y8nDnAtOFr94MlI1fZEoDlSfB1D++N6xybVCi0ITz8fAr/73trdf+L +HaAZBav6+CuBQug4urv7qv094PPK306Xlynt8xhW6aWWrL3DkJiy4Pmi1KZHQ3xt +zwIDAQABo0IwQDAdBgNVHQ4EFgQUVnNYZJX5khqwEioEYnmhQBWIIUkwDgYDVR0P +AQH/BAQDAgGGMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEMBQADggIBAC9c +mTz8Bl6MlC5w6tIyMY208FHVvArzZJ8HXtXBc2hkeqK5Duj5XYUtqDdFqij0lgVQ +YKlJfp/imTYpE0RHap1VIDzYm/EDMrraQKFz6oOht0SmDpkBm+S8f74TlH7Kph52 +gDY9hAaLMyZlbcp+nv4fjFg4exqDsQ+8FxG75gbMY/qB8oFM2gsQa6H61SilzwZA +Fv97fRheORKkU55+MkIQpiGRqRxOF3yEvJ+M0ejf5lG5Nkc/kLnHvALcWxxPDkjB +JYOcCj+esQMzEhonrPcibCTRAUH4WAP+JWgiH5paPHxsnnVI84HxZmduTILA7rpX +DhjvLpr3Etiga+kFpaHpaPi8TD8SHkXoUsCjvxInebnMMTzD9joiFgOgyY9mpFui +TdaBJQbpdqQACj7LzTWb4OE4y2BThihCQRxEV+ioratF4yUQvNs+ZUH7G6aXD+u5 +dHn5HrwdVw1Hr8Mvn4dGp+smWg9WY7ViYG4A++MnESLn/pmPNPW56MORcr3Ywx65 +LvKRRFHQV80MNNVIIb/bE/FmJUNS0nAiNs2fxBx1IK1jcmMGDw4nztJqDby1ORrp +0XZ60Vzk50lJLVU3aPAaOpg+VBeHVOmmJ1CJeyAvP/+/oYtKR5j/K3tJPsMpRmAY +QqszKbrAKbkTidOIijlBO8n9pu0f9GBj39ItVQGL +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS RSA Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS RSA Root CA 2022" +# Serial: 148535279242832292258835760425842727825 +# MD5 Fingerprint: d8:4e:c6:59:30:d8:fe:a0:d6:7a:5a:2c:2c:69:78:da +# SHA1 Fingerprint: ec:2c:83:40:72:af:26:95:10:ff:0e:f2:03:ee:31:70:f6:78:9d:ca +# SHA256 Fingerprint: 8f:af:7d:2e:2c:b4:70:9b:b8:e0:b3:36:66:bf:75:a5:dd:45:b5:de:48:0f:8e:a8:d4:bf:e6:be:bc:17:f2:ed +-----BEGIN CERTIFICATE----- +MIIFiTCCA3GgAwIBAgIQb77arXO9CEDii02+1PdbkTANBgkqhkiG9w0BAQsFADBO +MQswCQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQD +DBxTU0wuY29tIFRMUyBSU0EgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzQyMloX +DTQ2MDgxOTE2MzQyMVowTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jw +b3JhdGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgUlNBIFJvb3QgQ0EgMjAyMjCC +AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANCkCXJPQIgSYT41I57u9nTP +L3tYPc48DRAokC+X94xI2KDYJbFMsBFMF3NQ0CJKY7uB0ylu1bUJPiYYf7ISf5OY +t6/wNr/y7hienDtSxUcZXXTzZGbVXcdotL8bHAajvI9AI7YexoS9UcQbOcGV0ins +S657Lb85/bRi3pZ7QcacoOAGcvvwB5cJOYF0r/c0WRFXCsJbwST0MXMwgsadugL3 +PnxEX4MN8/HdIGkWCVDi1FW24IBydm5MR7d1VVm0U3TZlMZBrViKMWYPHqIbKUBO +L9975hYsLfy/7PO0+r4Y9ptJ1O4Fbtk085zx7AGL0SDGD6C1vBdOSHtRwvzpXGk3 +R2azaPgVKPC506QVzFpPulJwoxJF3ca6TvvC0PeoUidtbnm1jPx7jMEWTO6Af77w +dr5BUxIzrlo4QqvXDz5BjXYHMtWrifZOZ9mxQnUjbvPNQrL8VfVThxc7wDNY8VLS ++YCk8OjwO4s4zKTGkH8PnP2L0aPP2oOnaclQNtVcBdIKQXTbYxE3waWglksejBYS +d66UNHsef8JmAOSqg+qKkK3ONkRN0VHpvB/zagX9wHQfJRlAUW7qglFA35u5CCoG +AtUjHBPW6dvbxrB6y3snm/vg1UYk7RBLY0ulBY+6uB0rpvqR4pJSvezrZ5dtmi2f +gTIFZzL7SAg/2SW4BCUvAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0j +BBgwFoAU+y437uOEeicuzRk1sTN8/9REQrkwHQYDVR0OBBYEFPsuN+7jhHonLs0Z +NbEzfP/UREK5MA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsFAAOCAgEAjYlt +hEUY8U+zoO9opMAdrDC8Z2awms22qyIZZtM7QbUQnRC6cm4pJCAcAZli05bg4vsM +QtfhWsSWTVTNj8pDU/0quOr4ZcoBwq1gaAafORpR2eCNJvkLTqVTJXojpBzOCBvf +R4iyrT7gJ4eLSYwfqUdYe5byiB0YrrPRpgqU+tvT5TgKa3kSM/tKWTcWQA673vWJ +DPFs0/dRa1419dvAJuoSc06pkZCmF8NsLzjUo3KUQyxi4U5cMj29TH0ZR6LDSeeW +P4+a0zvkEdiLA9z2tmBVGKaBUfPhqBVq6+AL8BQx1rmMRTqoENjwuSfr98t67wVy +lrXEj5ZzxOhWc5y8aVFjvO9nHEMaX3cZHxj4HCUp+UmZKbaSPaKDN7EgkaibMOlq +bLQjk2UEqxHzDh1TJElTHaE/nUiSEeJ9DU/1172iWD54nR4fK/4huxoTtrEoZP2w +AgDHbICivRZQIA9ygV/MlP+7mea6kMvq+cYMwq7FGc4zoWtcu358NFcXrfA/rs3q +r5nsLFR+jM4uElZI7xc7P0peYNLcdDa8pUNjyw9bowJWCZ4kLOGGgYz+qxcs+sji +Mho6/4UIyYOf8kpIEFR3N+2ivEC+5BB09+Rbu7nzifmPQdjH5FCQNYA+HLhNkNPU +98OwoX6EyneSMSy4kLGCenROmxMmtNVQZlR4rmA= +-----END CERTIFICATE----- + +# Issuer: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Subject: CN=SSL.com TLS ECC Root CA 2022 O=SSL Corporation +# Label: "SSL.com TLS ECC Root CA 2022" +# Serial: 26605119622390491762507526719404364228 +# MD5 Fingerprint: 99:d7:5c:f1:51:36:cc:e9:ce:d9:19:2e:77:71:56:c5 +# SHA1 Fingerprint: 9f:5f:d9:1a:54:6d:f5:0c:71:f0:ee:7a:bd:17:49:98:84:73:e2:39 +# SHA256 Fingerprint: c3:2f:fd:9f:46:f9:36:d1:6c:36:73:99:09:59:43:4b:9a:d6:0a:af:bb:9e:7c:f3:36:54:f1:44:cc:1b:a1:43 +-----BEGIN CERTIFICATE----- +MIICOjCCAcCgAwIBAgIQFAP1q/s3ixdAW+JDsqXRxDAKBggqhkjOPQQDAzBOMQsw +CQYDVQQGEwJVUzEYMBYGA1UECgwPU1NMIENvcnBvcmF0aW9uMSUwIwYDVQQDDBxT +U0wuY29tIFRMUyBFQ0MgUm9vdCBDQSAyMDIyMB4XDTIyMDgyNTE2MzM0OFoXDTQ2 +MDgxOTE2MzM0N1owTjELMAkGA1UEBhMCVVMxGDAWBgNVBAoMD1NTTCBDb3Jwb3Jh +dGlvbjElMCMGA1UEAwwcU1NMLmNvbSBUTFMgRUNDIFJvb3QgQ0EgMjAyMjB2MBAG +ByqGSM49AgEGBSuBBAAiA2IABEUpNXP6wrgjzhR9qLFNoFs27iosU8NgCTWyJGYm +acCzldZdkkAZDsalE3D07xJRKF3nzL35PIXBz5SQySvOkkJYWWf9lCcQZIxPBLFN +SeR7T5v15wj4A4j3p8OSSxlUgaNjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSME +GDAWgBSJjy+j6CugFFR781a4Jl9nOAuc0DAdBgNVHQ4EFgQUiY8vo+groBRUe/NW +uCZfZzgLnNAwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2gAMGUCMFXjIlbp +15IkWE8elDIPDAI2wv2sdDJO4fscgIijzPvX6yv/N33w7deedWo1dlJF4AIxAMeN +b0Igj762TVntd00pxCAgRWSGOlDGxK0tk/UYfXLtqc/ErFc2KAhl3zx5Zn6g6g== +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA ECC TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA ECC TLS 2021" +# Serial: 81873346711060652204712539181482831616 +# MD5 Fingerprint: 16:9f:ad:f1:70:ad:79:d6:ed:29:b4:d1:c5:79:70:a8 +# SHA1 Fingerprint: 9e:bc:75:10:42:b3:02:f3:81:f4:f7:30:62:d4:8f:c3:a7:51:b2:dd +# SHA256 Fingerprint: b2:fa:e5:3e:14:cc:d7:ab:92:12:06:47:01:ae:27:9c:1d:89:88:fa:cb:77:5f:a8:a0:08:91:4e:66:39:88:a8 +-----BEGIN CERTIFICATE----- +MIICFTCCAZugAwIBAgIQPZg7pmY9kGP3fiZXOATvADAKBggqhkjOPQQDAzBMMS4w +LAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgRUNDIFRMUyAyMDIxMQ0w +CwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTI2MjNaFw00MTA0 +MTcwOTI2MjJaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBDQSBF +Q0MgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMHYwEAYHKoZI +zj0CAQYFK4EEACIDYgAEloZYKDcKZ9Cg3iQZGeHkBQcfl+3oZIK59sRxUM6KDP/X +tXa7oWyTbIOiaG6l2b4siJVBzV3dscqDY4PMwL502eCdpO5KTlbgmClBk1IQ1SQ4 +AjJn8ZQSb+/Xxd4u/RmAo0IwQDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBR2 +KCXWfeBmmnoJsmo7jjPXNtNPojAOBgNVHQ8BAf8EBAMCAYYwCgYIKoZIzj0EAwMD +aAAwZQIwW5kp85wxtolrbNa9d+F851F+uDrNozZffPc8dz7kUK2o59JZDCaOMDtu +CCrCp1rIAjEAmeMM56PDr9NJLkaCI2ZdyQAUEv049OGYa3cpetskz2VAv9LcjBHo +9H1/IISpQuQo +-----END CERTIFICATE----- + +# Issuer: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Subject: CN=Atos TrustedRoot Root CA RSA TLS 2021 O=Atos +# Label: "Atos TrustedRoot Root CA RSA TLS 2021" +# Serial: 111436099570196163832749341232207667876 +# MD5 Fingerprint: d4:d3:46:b8:9a:c0:9c:76:5d:9e:3a:c3:b9:99:31:d2 +# SHA1 Fingerprint: 18:52:3b:0d:06:37:e4:d6:3a:df:23:e4:98:fb:5b:16:fb:86:74:48 +# SHA256 Fingerprint: 81:a9:08:8e:a5:9f:b3:64:c5:48:a6:f8:55:59:09:9b:6f:04:05:ef:bf:18:e5:32:4e:c9:f4:57:ba:00:11:2f +-----BEGIN CERTIFICATE----- +MIIFZDCCA0ygAwIBAgIQU9XP5hmTC/srBRLYwiqipDANBgkqhkiG9w0BAQwFADBM +MS4wLAYDVQQDDCVBdG9zIFRydXN0ZWRSb290IFJvb3QgQ0EgUlNBIFRMUyAyMDIx +MQ0wCwYDVQQKDARBdG9zMQswCQYDVQQGEwJERTAeFw0yMTA0MjIwOTIxMTBaFw00 +MTA0MTcwOTIxMDlaMEwxLjAsBgNVBAMMJUF0b3MgVHJ1c3RlZFJvb3QgUm9vdCBD +QSBSU0EgVExTIDIwMjExDTALBgNVBAoMBEF0b3MxCzAJBgNVBAYTAkRFMIICIjAN +BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAtoAOxHm9BYx9sKOdTSJNy/BBl01Z +4NH+VoyX8te9j2y3I49f1cTYQcvyAh5x5en2XssIKl4w8i1mx4QbZFc4nXUtVsYv +Ye+W/CBGvevUez8/fEc4BKkbqlLfEzfTFRVOvV98r61jx3ncCHvVoOX3W3WsgFWZ +kmGbzSoXfduP9LVq6hdKZChmFSlsAvFr1bqjM9xaZ6cF4r9lthawEO3NUDPJcFDs +GY6wx/J0W2tExn2WuZgIWWbeKQGb9Cpt0xU6kGpn8bRrZtkh68rZYnxGEFzedUln +nkL5/nWpo63/dgpnQOPF943HhZpZnmKaau1Fh5hnstVKPNe0OwANwI8f4UDErmwh +3El+fsqyjW22v5MvoVw+j8rtgI5Y4dtXz4U2OLJxpAmMkokIiEjxQGMYsluMWuPD +0xeqqxmjLBvk1cbiZnrXghmmOxYsL3GHX0WelXOTwkKBIROW1527k2gV+p2kHYzy +geBYBr3JtuP2iV2J+axEoctr+hbxx1A9JNr3w+SH1VbxT5Aw+kUJWdo0zuATHAR8 +ANSbhqRAvNncTFd+rrcztl524WWLZt+NyteYr842mIycg5kDcPOvdO3GDjbnvezB +c6eUWsuSZIKmAMFwoW4sKeFYV+xafJlrJaSQOoD0IJ2azsct+bJLKZWD6TWNp0lI +pw9MGZHQ9b8Q4HECAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU +dEmZ0f+0emhFdcN+tNzMzjkz2ggwDgYDVR0PAQH/BAQDAgGGMA0GCSqGSIb3DQEB +DAUAA4ICAQAjQ1MkYlxt/T7Cz1UAbMVWiLkO3TriJQ2VSpfKgInuKs1l+NsW4AmS +4BjHeJi78+xCUvuppILXTdiK/ORO/auQxDh1MoSf/7OwKwIzNsAQkG8dnK/haZPs +o0UvFJ/1TCplQ3IM98P4lYsU84UgYt1UU90s3BiVaU+DR3BAM1h3Egyi61IxHkzJ +qM7F78PRreBrAwA0JrRUITWXAdxfG/F851X6LWh3e9NpzNMOa7pNdkTWwhWaJuyw +xfW70Xp0wmzNxbVe9kzmWy2B27O3Opee7c9GslA9hGCZcbUztVdF5kJHdWoOsAgM +rr3e97sPWD2PAzHoPYJQyi9eDF20l74gNAf0xBLh7tew2VktafcxBPTy+av5EzH4 +AXcOPUIjJsyacmdRIXrMPIWo6iFqO9taPKU0nprALN+AnCng33eU0aKAQv9qTFsR +0PXNor6uzFFcw9VUewyu1rkGd4Di7wcaaMxZUa1+XGdrudviB0JbuAEFWDlN5LuY +o7Ey7Nmj1m+UI/87tyll5gfp77YZ6ufCOB0yiJA8EytuzO+rdwY0d4RPcuSBhPm5 +dDTedk+SKlOxJTnbPP/lPqYO5Wue/9vsL3SD3460s6neFE3/MaNFcyT6lSnMEpcE +oji2jbDwN/zIIX8/syQbPYtuzE2wFg2WHYMfRsCbvUOZ58SWLs5fyQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G3 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G3" +# Serial: 576386314500428537169965010905813481816650257167 +# MD5 Fingerprint: 30:42:1b:b7:bb:81:75:35:e4:16:4f:53:d2:94:de:04 +# SHA1 Fingerprint: 63:cf:b6:c1:27:2b:56:e4:88:8e:1c:23:9a:b6:2e:81:47:24:c3:c7 +# SHA256 Fingerprint: e0:d3:22:6a:eb:11:63:c2:e4:8f:f9:be:3b:50:b4:c6:43:1b:e7:bb:1e:ac:c5:c3:6b:5d:5e:c5:09:03:9a:08 +-----BEGIN CERTIFICATE----- +MIIFpTCCA42gAwIBAgIUZPYOZXdhaqs7tOqFhLuxibhxkw8wDQYJKoZIhvcNAQEM +BQAwWjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHMzAe +Fw0yMTA1MjAwMjEwMTlaFw00NjA1MTkwMjEwMTlaMFoxCzAJBgNVBAYTAkNOMSUw +IwYDVQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtU +cnVzdEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzMwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQDAMYJhkuSUGwoqZdC+BqmHO1ES6nBBruL7dOoKjbmzTNyPtxNS +T1QY4SxzlZHFZjtqz6xjbYdT8PfxObegQ2OwxANdV6nnRM7EoYNl9lA+sX4WuDqK +AtCWHwDNBSHvBm3dIZwZQ0WhxeiAysKtQGIXBsaqvPPW5vxQfmZCHzyLpnl5hkA1 +nyDvP+uLRx+PjsXUjrYsyUQE49RDdT/VP68czH5GX6zfZBCK70bwkPAPLfSIC7Ep +qq+FqklYqL9joDiR5rPmd2jE+SoZhLsO4fWvieylL1AgdB4SQXMeJNnKziyhWTXA +yB1GJ2Faj/lN03J5Zh6fFZAhLf3ti1ZwA0pJPn9pMRJpxx5cynoTi+jm9WAPzJMs +hH/x/Gr8m0ed262IPfN2dTPXS6TIi/n1Q1hPy8gDVI+lhXgEGvNz8teHHUGf59gX +zhqcD0r83ERoVGjiQTz+LISGNzzNPy+i2+f3VANfWdP3kXjHi3dqFuVJhZBFcnAv +kV34PmVACxmZySYgWmjBNb9Pp1Hx2BErW+Canig7CjoKH8GB5S7wprlppYiU5msT +f9FkPz2ccEblooV7WIQn3MSAPmeamseaMQ4w7OYXQJXZRe0Blqq/DPNL0WP3E1jA +uPP6Z92bfW1K/zJMtSU7/xxnD4UiWQWRkUF3gdCFTIcQcf+eQxuulXUtgQIDAQAB +o2MwYTAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFEDk5PIj7zjKsK5Xf/Ih +MBY027ySMB0GA1UdDgQWBBRA5OTyI+84yrCuV3/yITAWNNu8kjAOBgNVHQ8BAf8E +BAMCAQYwDQYJKoZIhvcNAQEMBQADggIBACY7UeFNOPMyGLS0XuFlXsSUT9SnYaP4 +wM8zAQLpw6o1D/GUE3d3NZ4tVlFEbuHGLige/9rsR82XRBf34EzC4Xx8MnpmyFq2 +XFNFV1pF1AWZLy4jVe5jaN/TG3inEpQGAHUNcoTpLrxaatXeL1nHo+zSh2bbt1S1 +JKv0Q3jbSwTEb93mPmY+KfJLaHEih6D4sTNjduMNhXJEIlU/HHzp/LgV6FL6qj6j +ITk1dImmasI5+njPtqzn59ZW/yOSLlALqbUHM/Q4X6RJpstlcHboCoWASzY9M/eV +VHUl2qzEc4Jl6VL1XP04lQJqaTDFHApXB64ipCz5xUG3uOyfT0gA+QEEVcys+TIx +xHWVBqB/0Y0n3bOppHKH/lmLmnp0Ft0WpWIp6zqW3IunaFnT63eROfjXy9mPX1on +AX1daBli2MjN9LdyR75bl87yraKZk62Uy5P2EgmVtqvXO9A/EcswFi55gORngS1d +7XB4tmBZrOFdRWOPyN9yaFvqHbgB8X7754qz41SgOAngPN5C8sLtLpvzHzW2Ntjj +gKGLzZlkD8Kqq7HK9W+eQ42EVJmzbsASZthwEPEGNTNDqJwuuhQxzhB/HIbjj9LV ++Hfsm6vxL2PZQl/gZ4FkkfGXL/xuJvYz+NO1+MRiqzFRJQJ6+N1rZdVtTTDIZbpo +FGWsJwt0ivKH +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia Global Root CA G4 O=TrustAsia Technologies, Inc. +# Label: "TrustAsia Global Root CA G4" +# Serial: 451799571007117016466790293371524403291602933463 +# MD5 Fingerprint: 54:dd:b2:d7:5f:d8:3e:ed:7c:e0:0b:2e:cc:ed:eb:eb +# SHA1 Fingerprint: 57:73:a5:61:5d:80:b2:e6:ac:38:82:fc:68:07:31:ac:9f:b5:92:5a +# SHA256 Fingerprint: be:4b:56:cb:50:56:c0:13:6a:52:6d:f4:44:50:8d:aa:36:a0:b5:4f:42:e4:ac:38:f7:2a:f4:70:e4:79:65:4c +-----BEGIN CERTIFICATE----- +MIICVTCCAdygAwIBAgIUTyNkuI6XY57GU4HBdk7LKnQV1tcwCgYIKoZIzj0EAwMw +WjELMAkGA1UEBhMCQ04xJTAjBgNVBAoMHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xJDAiBgNVBAMMG1RydXN0QXNpYSBHbG9iYWwgUm9vdCBDQSBHNDAeFw0y +MTA1MjAwMjEwMjJaFw00NjA1MTkwMjEwMjJaMFoxCzAJBgNVBAYTAkNOMSUwIwYD +VQQKDBxUcnVzdEFzaWEgVGVjaG5vbG9naWVzLCBJbmMuMSQwIgYDVQQDDBtUcnVz +dEFzaWEgR2xvYmFsIFJvb3QgQ0EgRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATx +s8045CVD5d4ZCbuBeaIVXxVjAd7Cq92zphtnS4CDr5nLrBfbK5bKfFJV4hrhPVbw +LxYI+hW8m7tH5j/uqOFMjPXTNvk4XatwmkcN4oFBButJ+bAp3TPsUKV/eSm4IJij +YzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAUpbtKl86zK3+kMd6Xg1mD +pm9xy94wHQYDVR0OBBYEFKW7SpfOsyt/pDHel4NZg6ZvccveMA4GA1UdDwEB/wQE +AwIBBjAKBggqhkjOPQQDAwNnADBkAjBe8usGzEkxn0AAbbd+NvBNEU/zy4k6LHiR +UKNbwMp1JvK/kF0LgoxgKJ/GcJpo5PECMFxYDlZ2z1jD1xCMuo6u47xkdUfFVZDj +/bpV6wfEU6s3qe4hsiFbYI89MvHVI5TWWA== +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS ECC Root 2020 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS ECC Root 2020" +# Serial: 72082518505882327255703894282316633856 +# MD5 Fingerprint: c1:ab:fe:6a:10:2c:03:8d:bc:1c:22:32:c0:85:a7:fd +# SHA1 Fingerprint: c0:f8:96:c5:a9:3b:01:06:21:07:da:18:42:48:bc:e9:9d:88:d5:ec +# SHA256 Fingerprint: 57:8a:f4:de:d0:85:3f:4e:59:98:db:4a:ea:f9:cb:ea:8d:94:5f:60:b6:20:a3:8d:1a:3c:13:b2:bc:7b:a8:e1 +-----BEGIN CERTIFICATE----- +MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQsw +CQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0eSBH +bWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBFQ0MgUm9vdCAyMDIw +MB4XDTIwMDgyNTA3NDgyMFoXDTQ1MDgyNTIzNTk1OVowYzELMAkGA1UEBhMCREUx +JzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkGA1UE +AwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgRUNDIFJvb3QgMjAyMDB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABM6//leov9Wq9xCazbzREaK9Z0LMkOsVGJDZos0MKiXrPk/O +tdKPD/M12kOLAoC+b1EkHQ9rK8qfwm9QMuU3ILYg/4gND21Ju9sGpIeQkpT0CdDP +f8iAC8GXs7s1J8nCG6NCMEAwHQYDVR0OBBYEFONyzG6VmUex5rNhTNHLq+O6zd6f +MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2cA +MGQCMHVSi7ekEE+uShCLsoRbQuHmKjYC2qBuGT8lv9pZMo7k+5Dck2TOrbRBR2Di +z6fLHgIwN0GMZt9Ba9aDAEH9L1r3ULRn0SyocddDypwnJJGDSA3PzfdUga/sf+Rn +27iQ7t0l +-----END CERTIFICATE----- + +# Issuer: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Subject: CN=Telekom Security TLS RSA Root 2023 O=Deutsche Telekom Security GmbH +# Label: "Telekom Security TLS RSA Root 2023" +# Serial: 44676229530606711399881795178081572759 +# MD5 Fingerprint: bf:5b:eb:54:40:cd:48:71:c4:20:8d:7d:de:0a:42:f2 +# SHA1 Fingerprint: 54:d3:ac:b3:bd:57:56:f6:85:9d:ce:e5:c3:21:e2:d4:ad:83:d0:93 +# SHA256 Fingerprint: ef:c6:5c:ad:bb:59:ad:b6:ef:e8:4d:a2:23:11:b3:56:24:b7:1b:3b:1e:a0:da:8b:66:55:17:4e:c8:97:86:46 +-----BEGIN CERTIFICATE----- +MIIFszCCA5ugAwIBAgIQIZxULej27HF3+k7ow3BXlzANBgkqhkiG9w0BAQwFADBj +MQswCQYDVQQGEwJERTEnMCUGA1UECgweRGV1dHNjaGUgVGVsZWtvbSBTZWN1cml0 +eSBHbWJIMSswKQYDVQQDDCJUZWxla29tIFNlY3VyaXR5IFRMUyBSU0EgUm9vdCAy +MDIzMB4XDTIzMDMyODEyMTY0NVoXDTQ4MDMyNzIzNTk1OVowYzELMAkGA1UEBhMC +REUxJzAlBgNVBAoMHkRldXRzY2hlIFRlbGVrb20gU2VjdXJpdHkgR21iSDErMCkG +A1UEAwwiVGVsZWtvbSBTZWN1cml0eSBUTFMgUlNBIFJvb3QgMjAyMzCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAO01oYGA88tKaVvC+1GDrib94W7zgRJ9 +cUD/h3VCKSHtgVIs3xLBGYSJwb3FKNXVS2xE1kzbB5ZKVXrKNoIENqil/Cf2SfHV +cp6R+SPWcHu79ZvB7JPPGeplfohwoHP89v+1VmLhc2o0mD6CuKyVU/QBoCcHcqMA +U6DksquDOFczJZSfvkgdmOGjup5czQRxUX11eKvzWarE4GC+j4NSuHUaQTXtvPM6 +Y+mpFEXX5lLRbtLevOP1Czvm4MS9Q2QTps70mDdsipWol8hHD/BeEIvnHRz+sTug +BTNoBUGCwQMrAcjnj02r6LX2zWtEtefdi+zqJbQAIldNsLGyMcEWzv/9FIS3R/qy +8XDe24tsNlikfLMR0cN3f1+2JeANxdKz+bi4d9s3cXFH42AYTyS2dTd4uaNir73J +co4vzLuu2+QVUhkHM/tqty1LkCiCc/4YizWN26cEar7qwU02OxY2kTLvtkCJkUPg +8qKrBC7m8kwOFjQgrIfBLX7JZkcXFBGk8/ehJImr2BrIoVyxo/eMbcgByU/J7MT8 +rFEz0ciD0cmfHdRHNCk+y7AO+oMLKFjlKdw/fKifybYKu6boRhYPluV75Gp6SG12 +mAWl3G0eQh5C2hrgUve1g8Aae3g1LDj1H/1Joy7SWWO/gLCMk3PLNaaZlSJhZQNg ++y+TS/qanIA7AgMBAAGjYzBhMA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUtqeX +gj10hZv3PJ+TmpV5dVKMbUcwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBS2 +p5eCPXSFm/c8n5OalXl1UoxtRzANBgkqhkiG9w0BAQwFAAOCAgEAqMxhpr51nhVQ +pGv7qHBFfLp+sVr8WyP6Cnf4mHGCDG3gXkaqk/QeoMPhk9tLrbKmXauw1GLLXrtm +9S3ul0A8Yute1hTWjOKWi0FpkzXmuZlrYrShF2Y0pmtjxrlO8iLpWA1WQdH6DErw +M807u20hOq6OcrXDSvvpfeWxm4bu4uB9tPcy/SKE8YXJN3nptT+/XOR0so8RYgDd +GGah2XsjX/GO1WfoVNpbOms2b/mBsTNHM3dA+VKq3dSDz4V4mZqTuXNnQkYRIer+ +CqkbGmVps4+uFrb2S1ayLfmlyOw7YqPta9BO1UAJpB+Y1zqlklkg5LB9zVtzaL1t +xKITDmcZuI1CfmwMmm6gJC3VRRvcxAIU/oVbZZfKTpBQCHpCNfnqwmbU+AGuHrS+ +w6jv/naaoqYfRvaE7fzbzsQCzndILIyy7MMAo+wsVRjBfhnu4S/yrYObnqsZ38aK +L4x35bcF7DvB7L6Gs4a8wPfc5+pbrrLMtTWGS9DiP7bY+A4A7l3j941Y/8+LN+lj +X273CXE2whJdV/LItM3z7gLfEdxquVeEHVlNjM7IDiPCtyaaEBRx/pOyiriA8A4Q +ntOoUAw3gi/q4Iqd4Sw5/7W0cwDk90imc6y/st53BIe0o82bNSQ3+pCTE4FCxpgm +dTdmQRCsu/WU48IxK63nI1bMNSWSs1A= +-----END CERTIFICATE----- + +# Issuer: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA +# Subject: CN=FIRMAPROFESIONAL CA ROOT-A WEB O=Firmaprofesional SA +# Label: "FIRMAPROFESIONAL CA ROOT-A WEB" +# Serial: 65916896770016886708751106294915943533 +# MD5 Fingerprint: 82:b2:ad:45:00:82:b0:66:63:f8:5f:c3:67:4e:ce:a3 +# SHA1 Fingerprint: a8:31:11:74:a6:14:15:0d:ca:77:dd:0e:e4:0c:5d:58:fc:a0:72:a5 +# SHA256 Fingerprint: be:f2:56:da:f2:6e:9c:69:bd:ec:16:02:35:97:98:f3:ca:f7:18:21:a0:3e:01:82:57:c5:3c:65:61:7f:3d:4a +-----BEGIN CERTIFICATE----- +MIICejCCAgCgAwIBAgIQMZch7a+JQn81QYehZ1ZMbTAKBggqhkjOPQQDAzBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwHhcNMjIwNDA2MDkwMTM2WhcNNDcwMzMxMDkwMTM2WjBuMQsw +CQYDVQQGEwJFUzEcMBoGA1UECgwTRmlybWFwcm9mZXNpb25hbCBTQTEYMBYGA1UE +YQwPVkFURVMtQTYyNjM0MDY4MScwJQYDVQQDDB5GSVJNQVBST0ZFU0lPTkFMIENB +IFJPT1QtQSBXRUIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARHU+osEaR3xyrq89Zf +e9MEkVz6iMYiuYMQYneEMy3pA4jU4DP37XcsSmDq5G+tbbT4TIqk5B/K6k84Si6C +cyvHZpsKjECcfIr28jlgst7L7Ljkb+qbXbdTkBgyVcUgt5SjYzBhMA8GA1UdEwEB +/wQFMAMBAf8wHwYDVR0jBBgwFoAUk+FDY1w8ndYn81LsF7Kpryz3dvgwHQYDVR0O +BBYEFJPhQ2NcPJ3WJ/NS7Beyqa8s93b4MA4GA1UdDwEB/wQEAwIBBjAKBggqhkjO +PQQDAwNoADBlAjAdfKR7w4l1M+E7qUW/Runpod3JIha3RxEL2Jq68cgLcFBTApFw +hVmpHqTm6iMxoAACMQD94vizrxa5HnPEluPBMBnYfubDl94cT7iJLzPrSA8Z94dG +XSaQpYXFuXqUPoeovQA= +-----END CERTIFICATE----- + +# Issuer: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Subject: CN=TWCA CYBER Root CA O=TAIWAN-CA OU=Root CA +# Label: "TWCA CYBER Root CA" +# Serial: 85076849864375384482682434040119489222 +# MD5 Fingerprint: 0b:33:a0:97:52:95:d4:a9:fd:bb:db:6e:a3:55:5b:51 +# SHA1 Fingerprint: f6:b1:1c:1a:83:38:e9:7b:db:b3:a8:c8:33:24:e0:2d:9c:7f:26:66 +# SHA256 Fingerprint: 3f:63:bb:28:14:be:17:4e:c8:b6:43:9c:f0:8d:6d:56:f0:b7:c4:05:88:3a:56:48:a3:34:42:4d:6b:3e:c5:58 +-----BEGIN CERTIFICATE----- +MIIFjTCCA3WgAwIBAgIQQAE0jMIAAAAAAAAAATzyxjANBgkqhkiG9w0BAQwFADBQ +MQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FOLUNBMRAwDgYDVQQLEwdSb290 +IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3QgQ0EwHhcNMjIxMTIyMDY1NDI5 +WhcNNDcxMTIyMTU1OTU5WjBQMQswCQYDVQQGEwJUVzESMBAGA1UEChMJVEFJV0FO +LUNBMRAwDgYDVQQLEwdSb290IENBMRswGQYDVQQDExJUV0NBIENZQkVSIFJvb3Qg +Q0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDG+Moe2Qkgfh1sTs6P +40czRJzHyWmqOlt47nDSkvgEs1JSHWdyKKHfi12VCv7qze33Kc7wb3+szT3vsxxF +avcokPFhV8UMxKNQXd7UtcsZyoC5dc4pztKFIuwCY8xEMCDa6pFbVuYdHNWdZsc/ +34bKS1PE2Y2yHer43CdTo0fhYcx9tbD47nORxc5zb87uEB8aBs/pJ2DFTxnk684i +JkXXYJndzk834H/nY62wuFm40AZoNWDTNq5xQwTxaWV4fPMf88oon1oglWa0zbfu +j3ikRRjpJi+NmykosaS3Om251Bw4ckVYsV7r8Cibt4LK/c/WMw+f+5eesRycnupf +Xtuq3VTpMCEobY5583WSjCb+3MX2w7DfRFlDo7YDKPYIMKoNM+HvnKkHIuNZW0CP +2oi3aQiotyMuRAlZN1vH4xfyIutuOVLF3lSnmMlLIJXcRolftBL5hSmO68gnFSDA +S9TMfAxsNAwmmyYxpjyn9tnQS6Jk/zuZQXLB4HCX8SS7K8R0IrGsayIyJNN4KsDA +oS/xUgXJP+92ZuJF2A09rZXIx4kmyA+upwMu+8Ff+iDhcK2wZSA3M2Cw1a/XDBzC +kHDXShi8fgGwsOsVHkQGzaRP6AzRwyAQ4VRlnrZR0Bp2a0JaWHY06rc3Ga4udfmW +5cFZ95RXKSWNOkyrTZpB0F8mAwIDAQABo2MwYTAOBgNVHQ8BAf8EBAMCAQYwDwYD +VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBSdhWEUfMFib5do5E83QOGt4A1WNzAd +BgNVHQ4EFgQUnYVhFHzBYm+XaORPN0DhreANVjcwDQYJKoZIhvcNAQEMBQADggIB +AGSPesRiDrWIzLjHhg6hShbNcAu3p4ULs3a2D6f/CIsLJc+o1IN1KriWiLb73y0t +tGlTITVX1olNc79pj3CjYcya2x6a4CD4bLubIp1dhDGaLIrdaqHXKGnK/nZVekZn +68xDiBaiA9a5F/gZbG0jAn/xX9AKKSM70aoK7akXJlQKTcKlTfjF/biBzysseKNn +TKkHmvPfXvt89YnNdJdhEGoHK4Fa0o635yDRIG4kqIQnoVesqlVYL9zZyvpoBJ7t +RCT5dEA7IzOrg1oYJkK2bVS1FmAwbLGg+LhBoF1JSdJlBTrq/p1hvIbZv97Tujqx +f36SNI7JAG7cmL3c7IAFrQI932XtCwP39xaEBDG6k5TY8hL4iuO/Qq+n1M0RFxbI +Qh0UqEL20kCGoE8jypZFVmAGzbdVAaYBlGX+bgUJurSkquLvWL69J1bY73NxW0Qz +8ppy6rBePm6pUlvscG21h483XjyMnM7k8M4MZ0HMzvaAq07MTFb1wWFZk7Q+ptq4 +NxKfKjLji7gh7MMrZQzvIt6IKTtM1/r+t+FHvpw+PoP7UV31aPcuIYXcv/Fa4nzX +xeSDwWrruoBa3lwtcHb4yOWHh8qgnaHlIhInD0Q9HWzq1MKLL295q39QpsQZp6F6 +t5b5wR9iWqJDB0BeJsas7a5wFsWqynKKTbDPAYsDP27X +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA12 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA12" +# Serial: 587887345431707215246142177076162061960426065942 +# MD5 Fingerprint: c6:89:ca:64:42:9b:62:08:49:0b:1e:7f:e9:07:3d:e8 +# SHA1 Fingerprint: 7a:22:1e:3d:de:1b:06:ac:9e:c8:47:70:16:8e:3c:e5:f7:6b:06:f4 +# SHA256 Fingerprint: 3f:03:4b:b5:70:4d:44:b2:d0:85:45:a0:20:57:de:93:eb:f3:90:5f:ce:72:1a:cb:c7:30:c0:6d:da:ee:90:4e +-----BEGIN CERTIFICATE----- +MIIDcjCCAlqgAwIBAgIUZvnHwa/swlG07VOX5uaCwysckBYwDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExMjAeFw0yMDA0MDgw +NTM2NDZaFw00MDA0MDgwNTM2NDZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC6OcE3emhF +KxS06+QT61d1I02PJC0W6K6OyX2kVzsqdiUzg2zqMoqUm048luT9Ub+ZyZN+v/mt +p7JIKwccJ/VMvHASd6SFVLX9kHrko+RRWAPNEHl57muTH2SOa2SroxPjcf59q5zd +J1M3s6oYwlkm7Fsf0uZlfO+TvdhYXAvA42VvPMfKWeP+bl+sg779XSVOKik71gur +FzJ4pOE+lEa+Ym6b3kaosRbnhW70CEBFEaCeVESE99g2zvVQR9wsMJvuwPWW0v4J +hscGWa5Pro4RmHvzC1KqYiaqId+OJTN5lxZJjfU+1UefNzFJM3IFTQy2VYzxV4+K +h9GtxRESOaCtAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQD +AgEGMB0GA1UdDgQWBBRXNPN0zwRL1SXm8UC2LEzZLemgrTANBgkqhkiG9w0BAQsF +AAOCAQEAPrvbFxbS8hQBICw4g0utvsqFepq2m2um4fylOqyttCg6r9cBg0krY6Ld +mmQOmFxv3Y67ilQiLUoT865AQ9tPkbeGGuwAtEGBpE/6aouIs3YIcipJQMPTw4WJ +mBClnW8Zt7vPemVV2zfrPIpyMpcemik+rY3moxtt9XUa5rBouVui7mlHJzWhhpmA +8zNL4WukJsPvdFlseqJkth5Ew1DgDzk9qTPxpfPSvWKErI4cqc1avTc7bgoitPQV +55FYxTpE05Uo2cBl6XLK0A+9H7MV2anjpEcJnuDLN/v9vZfVvhgaaaI5gdka9at/ +yOPiZwud9AzqVN/Ssq+xIvEg37xEHA== +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA14 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA14" +# Serial: 575790784512929437950770173562378038616896959179 +# MD5 Fingerprint: 71:0d:72:fa:92:19:65:5e:89:04:ac:16:33:f0:bc:d5 +# SHA1 Fingerprint: dd:50:c0:f7:79:b3:64:2e:74:a2:b8:9d:9f:d3:40:dd:bb:f0:f2:4f +# SHA256 Fingerprint: 4b:00:9c:10:34:49:4f:9a:b5:6b:ba:3b:a1:d6:27:31:fc:4d:20:d8:95:5a:dc:ec:10:a9:25:60:72:61:e3:38 +-----BEGIN CERTIFICATE----- +MIIFcjCCA1qgAwIBAgIUZNtaDCBO6Ncpd8hQJ6JaJ90t8sswDQYJKoZIhvcNAQEM +BQAwUTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28u +LCBMdGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNDAeFw0yMDA0MDgw +NzA2MTlaFw00NTA0MDgwNzA2MTlaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpD +eWJlcnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBS +b290IENBMTQwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDF0nqh1oq/ +FjHQmNE6lPxauG4iwWL3pwon71D2LrGeaBLwbCRjOfHw3xDG3rdSINVSW0KZnvOg +vlIfX8xnbacuUKLBl422+JX1sLrcneC+y9/3OPJH9aaakpUqYllQC6KxNedlsmGy +6pJxaeQp8E+BgQQ8sqVb1MWoWWd7VRxJq3qdwudzTe/NCcLEVxLbAQ4jeQkHO6Lo +/IrPj8BGJJw4J+CDnRugv3gVEOuGTgpa/d/aLIJ+7sr2KeH6caH3iGicnPCNvg9J +kdjqOvn90Ghx2+m1K06Ckm9mH+Dw3EzsytHqunQG+bOEkJTRX45zGRBdAuVwpcAQ +0BB8b8VYSbSwbprafZX1zNoCr7gsfXmPvkPx+SgojQlD+Ajda8iLLCSxjVIHvXib +y8posqTdDEx5YMaZ0ZPxMBoH064iwurO8YQJzOAUbn8/ftKChazcqRZOhaBgy/ac +18izju3Gm5h1DVXoX+WViwKkrkMpKBGk5hIwAUt1ax5mnXkvpXYvHUC0bcl9eQjs +0Wq2XSqypWa9a4X0dFbD9ed1Uigspf9mR6XU/v6eVL9lfgHWMI+lNpyiUBzuOIAB +SMbHdPTGrMNASRZhdCyvjG817XsYAFs2PJxQDcqSMxDxJklt33UkN4Ii1+iW/RVL +ApY+B3KVfqs9TC7XyvDf4Fg/LS8EmjijAQIDAQABo0IwQDAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUBpOjCl4oaTeqYR3r6/wtbyPk +86AwDQYJKoZIhvcNAQEMBQADggIBAJaAcgkGfpzMkwQWu6A6jZJOtxEaCnFxEM0E +rX+lRVAQZk5KQaID2RFPeje5S+LGjzJmdSX7684/AykmjbgWHfYfM25I5uj4V7Ib +ed87hwriZLoAymzvftAj63iP/2SbNDefNWWipAA9EiOWWF3KY4fGoweITedpdopT +zfFP7ELyk+OZpDc8h7hi2/DsHzc/N19DzFGdtfCXwreFamgLRB7lUe6TzktuhsHS +DCRZNhqfLJGP4xjblJUK7ZGqDpncllPjYYPGFrojutzdfhrGe0K22VoF3Jpf1d+4 +2kd92jjbrDnVHmtsKheMYc2xbXIBw8MgAGJoFjHVdqqGuw6qnsb58Nn4DSEC5MUo +FlkRudlpcyqSeLiSV5sI8jrlL5WwWLdrIBRtFO8KvH7YVdiI2i/6GaX7i+B/OfVy +K4XELKzvGUWSTLNhB9xNH27SgRNcmvMSZ4PPmz+Ln52kuaiWA3rF7iDeM9ovnhp6 +dB7h7sxaOgTdsxoEqBRjrLdHEoOabPXm6RUVkRqEGQ6UROcSjiVbgGcZ3GOTEAtl +Lor6CZpO2oYofaphNdgOpygau1LgePhsumywbrmHXumZNTfxPWQrqaA0k89jL9WB +365jJ6UeTo3cKXhZ+PmhIIynJkBugnLNeLLIjzwec+fBH7/PzqUqm9tEZDKgu39c +JRNItX+S +-----END CERTIFICATE----- + +# Issuer: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Subject: CN=SecureSign Root CA15 O=Cybertrust Japan Co., Ltd. +# Label: "SecureSign Root CA15" +# Serial: 126083514594751269499665114766174399806381178503 +# MD5 Fingerprint: 13:30:fc:c4:62:a6:a9:de:b5:c1:68:af:b5:d2:31:47 +# SHA1 Fingerprint: cb:ba:83:c8:c1:5a:5d:f1:f9:73:6f:ca:d7:ef:28:13:06:4a:07:7d +# SHA256 Fingerprint: e7:78:f0:f0:95:fe:84:37:29:cd:1a:00:82:17:9e:53:14:a9:c2:91:44:28:05:e1:fb:1d:8f:b6:b8:88:6c:3a +-----BEGIN CERTIFICATE----- +MIICIzCCAamgAwIBAgIUFhXHw9hJp75pDIqI7fBw+d23PocwCgYIKoZIzj0EAwMw +UTELMAkGA1UEBhMCSlAxIzAhBgNVBAoTGkN5YmVydHJ1c3QgSmFwYW4gQ28uLCBM +dGQuMR0wGwYDVQQDExRTZWN1cmVTaWduIFJvb3QgQ0ExNTAeFw0yMDA0MDgwODMy +NTZaFw00NTA0MDgwODMyNTZaMFExCzAJBgNVBAYTAkpQMSMwIQYDVQQKExpDeWJl +cnRydXN0IEphcGFuIENvLiwgTHRkLjEdMBsGA1UEAxMUU2VjdXJlU2lnbiBSb290 +IENBMTUwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAQLUHSNZDKZmbPSYAi4Io5GdCx4 +wCtELW1fHcmuS1Iggz24FG1Th2CeX2yF2wYUleDHKP+dX+Sq8bOLbe1PL0vJSpSR +ZHX+AezB2Ot6lHhWGENfa4HL9rzatAy2KZMIaY+jQjBAMA8GA1UdEwEB/wQFMAMB +Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTrQciu/NWeUUj1vYv0hyCTQSvT +9DAKBggqhkjOPQQDAwNoADBlAjEA2S6Jfl5OpBEHvVnCB96rMjhTKkZEBhd6zlHp +4P9mLQlO4E/0BdGF9jVg3PVys0Z9AjBEmEYagoUeYWmJSwdLZrWeqrqgHkHZAXQ6 +bkU6iYAZezKYVWOr62Nuk22rGwlgMU4= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST BR Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST BR Root CA 2 2023" +# Serial: 153168538924886464690566649552453098598 +# MD5 Fingerprint: e1:09:ed:d3:60:d4:56:1b:47:1f:b7:0c:5f:1b:5f:85 +# SHA1 Fingerprint: 2d:b0:70:ee:71:94:af:69:68:17:db:79:ce:58:9f:a0:6b:96:f7:87 +# SHA256 Fingerprint: 05:52:e6:f8:3f:df:65:e8:fa:96:70:e6:66:df:28:a4:e2:13:40:b5:10:cb:e5:25:66:f9:7c:4f:b9:4b:2b:d1 +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQczswBEhb2U14LnNLyaHcZjANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEJSIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA4NTYzMVoXDTM4MDUw +OTA4NTYzMFowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBCUiBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBAK7/CVmRgApKaOYkP7in5Mg6CjoWzckjYaCTcfKr +i3OPoGdlYNJUa2NRb0kz4HIHE304zQaSBylSa053bATTlfrdTIzZXcFhfUvnKLNE +gXtRr90zsWh81k5M/itoucpmacTsXld/9w3HnDY25QdgrMBM6ghs7wZ8T1soegj8 +k12b9py0i4a6Ibn08OhZWiihNIQaJZG2tY/vsvmA+vk9PBFy2OMvhnbFeSzBqZCT +Rphny4NqoFAjpzv2gTng7fC5v2Xx2Mt6++9zA84A9H3X4F07ZrjcjrqDy4d2A/wl +2ecjbwb9Z/Pg/4S8R7+1FhhGaRTMBffb00msa8yr5LULQyReS2tNZ9/WtT5PeB+U +cSTq3nD88ZP+npNa5JRal1QMNXtfbO4AHyTsA7oC9Xb0n9Sa7YUsOCIvx9gvdhFP +/Wxc6PWOJ4d/GUohR5AdeY0cW/jPSoXk7bNbjb7EZChdQcRurDhaTyN0dKkSw/bS +uREVMweR2Ds3OmMwBtHFIjYoYiMQ4EbMl6zWK11kJNXuHA7e+whadSr2Y23OC0K+ +0bpwHJwh5Q8xaRfX/Aq03u2AnMuStIv13lmiWAmlY0cL4UEyNEHZmrHZqLAbWt4N +DfTisl01gLmB1IRpkQLLddCNxbU9CZEJjxShFHR5PtbJFR2kWVki3PaKRT08EtY+ +XTIvAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUZ5Dw1t61 +GNVGKX5cq/ieCLxklRAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfYnJfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQA097N3U9swFrktpSHxQCF16+tI +FoE9c+CeJyrrd6kTpGoKWloUMz1oH4Guaf2Mn2VsNELZLdB/eBaxOqwjMa1ef67n +riv6uvw8l5VAk1/DLQOj7aRvU9f6QA4w9QAgLABMjDu0ox+2v5Eyq6+SmNMW5tTR +VFxDWy6u71cqqLRvpO8NVhTaIasgdp4D/Ca4nj8+AybmTNudX0KEPUUDAxxZiMrc +LmEkWqTqJwtzEr5SswrPMhfiHocaFpVIbVrg0M8JkiZmkdijYQ6qgYF/6FKC0ULn +4B0Y+qSFNueG4A3rvNTJ1jxD8V1Jbn6Bm2m1iWKPiFLY1/4nwSPFyysCu7Ff/vtD +hQNGvl3GyiEm/9cCnnRK3PgTFbGBVzbLZVzRHTF36SXDw7IyN9XxmAnkbWOACKsG +koHU6XCPpz+y7YaMgmo1yEJagtFSGkUPFaUA8JR7ZSdXOUPPfH/mvTWze/EZTN46 +ls/pdu4D58JDUjxqgejBWoC9EV2Ta/vH5mQ/u2kc6d0li690yVRAysuTEwrt+2aS +Ecr1wPrYg1UDfNPFIkZ1cGt5SAYqgpq/5usWDiJFAbzdNpQ0qTUmiteXue4Icr80 +knCDgKs4qllo3UCkGJCy89UDyibK79XH4I9TjvAA46jtn/mtd+ArY0+ew+43u3gJ +hJ65bvspmZDogNOfJA== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS ECC Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS ECC Root CA" +# Serial: 310892014698942880364840003424242768478804666567 +# MD5 Fingerprint: 09:48:04:77:d2:fc:65:93:71:66:b1:11:95:4f:06:8c +# SHA1 Fingerprint: b5:ec:39:f3:a1:66:37:ae:c3:05:94:57:e2:be:11:be:b7:a1:7f:36 +# SHA256 Fingerprint: c0:07:6b:9e:f0:53:1f:b1:a6:56:d6:7c:4e:be:97:cd:5d:ba:a4:1e:f4:45:98:ac:c2:48:98:78:c9:2d:87:11 +-----BEGIN CERTIFICATE----- +MIICMTCCAbegAwIBAgIUNnThTXxlE8msg1UloD5Sfi9QaMcwCgYIKoZIzj0EAwMw +WDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dpZXMs +IEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgRUNDIFJvb3QgQ0EwHhcNMjQw +NTE1MDU0MTU2WhcNNDQwNTE1MDU0MTU1WjBYMQswCQYDVQQGEwJDTjElMCMGA1UE +ChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1c3RB +c2lhIFRMUyBFQ0MgUm9vdCBDQTB2MBAGByqGSM49AgEGBSuBBAAiA2IABLh/pVs/ +AT598IhtrimY4ZtcU5nb9wj/1WrgjstEpvDBjL1P1M7UiFPoXlfXTr4sP/MSpwDp +guMqWzJ8S5sUKZ74LYO1644xST0mYekdcouJtgq7nDM1D9rs3qlKH8kzsaNCMEAw +DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQULIVTu7FDzTLqnqOH/qKYqKaT6RAw +DgYDVR0PAQH/BAQDAgEGMAoGCCqGSM49BAMDA2gAMGUCMFRH18MtYYZI9HlaVQ01 +L18N9mdsd0AaRuf4aFtOJx24mH1/k78ITcTaRTChD15KeAIxAKORh/IRM4PDwYqR +OkwrULG9IpRdNYlzg8WbGf60oenUoWa2AaU2+dhoYSi3dOGiMQ== +-----END CERTIFICATE----- + +# Issuer: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Subject: CN=TrustAsia TLS RSA Root CA O=TrustAsia Technologies, Inc. +# Label: "TrustAsia TLS RSA Root CA" +# Serial: 160405846464868906657516898462547310235378010780 +# MD5 Fingerprint: 3b:9e:c3:86:0f:34:3c:6b:c5:46:c4:8e:1d:e7:19:12 +# SHA1 Fingerprint: a5:46:50:c5:62:ea:95:9a:1a:a7:04:6f:17:58:c7:29:53:3d:03:fa +# SHA256 Fingerprint: 06:c0:8d:7d:af:d8:76:97:1e:b1:12:4f:e6:7f:84:7e:c0:c7:a1:58:d3:ea:53:cb:e9:40:e2:ea:97:91:f4:c3 +-----BEGIN CERTIFICATE----- +MIIFgDCCA2igAwIBAgIUHBjYz+VTPyI1RlNUJDxsR9FcSpwwDQYJKoZIhvcNAQEM +BQAwWDELMAkGA1UEBhMCQ04xJTAjBgNVBAoTHFRydXN0QXNpYSBUZWNobm9sb2dp +ZXMsIEluYy4xIjAgBgNVBAMTGVRydXN0QXNpYSBUTFMgUlNBIFJvb3QgQ0EwHhcN +MjQwNTE1MDU0MTU3WhcNNDQwNTE1MDU0MTU2WjBYMQswCQYDVQQGEwJDTjElMCMG +A1UEChMcVHJ1c3RBc2lhIFRlY2hub2xvZ2llcywgSW5jLjEiMCAGA1UEAxMZVHJ1 +c3RBc2lhIFRMUyBSU0EgUm9vdCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBAMMWuBtqpERz5dZO9LnPWwvB0ZqB9WOwj0PBuwhaGnrhB3YmH49pVr7+ +NmDQDIPNlOrnxS1cLwUWAp4KqC/lYCZUlviYQB2srp10Zy9U+5RjmOMmSoPGlbYJ +Q1DNDX3eRA5gEk9bNb2/mThtfWza4mhzH/kxpRkQcwUqwzIZheo0qt1CHjCNP561 +HmHVb70AcnKtEj+qpklz8oYVlQwQX1Fkzv93uMltrOXVmPGZLmzjyUT5tUMnCE32 +ft5EebuyjBza00tsLtbDeLdM1aTk2tyKjg7/D8OmYCYozza/+lcK7Fs/6TAWe8Tb +xNRkoDD75f0dcZLdKY9BWN4ArTr9PXwaqLEX8E40eFgl1oUh63kd0Nyrz2I8sMeX +i9bQn9P+PN7F4/w6g3CEIR0JwqH8uyghZVNgepBtljhb//HXeltt08lwSUq6HTrQ +UNoyIBnkiz/r1RYmNzz7dZ6wB3C4FGB33PYPXFIKvF1tjVEK2sUYyJtt3LCDs3+j +TnhMmCWr8n4uIF6CFabW2I+s5c0yhsj55NqJ4js+k8UTav/H9xj8Z7XvGCxUq0DT +bE3txci3OE9kxJRMT6DNrqXGJyV1J23G2pyOsAWZ1SgRxSHUuPzHlqtKZFlhaxP8 +S8ySpg+kUb8OWJDZgoM5pl+z+m6Ss80zDoWo8SnTq1mt1tve1CuBAgMBAAGjQjBA +MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLgHkXlcBvRG/XtZylomkadFK/hT +MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQwFAAOCAgEAIZtqBSBdGBanEqT3 +Rz/NyjuujsCCztxIJXgXbODgcMTWltnZ9r96nBO7U5WS/8+S4PPFJzVXqDuiGev4 +iqME3mmL5Dw8veWv0BIb5Ylrc5tvJQJLkIKvQMKtuppgJFqBTQUYo+IzeXoLH5Pt +7DlK9RME7I10nYEKqG/odv6LTytpEoYKNDbdgptvT+Bz3Ul/KD7JO6NXBNiT2Twp +2xIQaOHEibgGIOcberyxk2GaGUARtWqFVwHxtlotJnMnlvm5P1vQiJ3koP26TpUJ +g3933FEFlJ0gcXax7PqJtZwuhfG5WyRasQmr2soaB82G39tp27RIGAAtvKLEiUUj +pQ7hRGU+isFqMB3iYPg6qocJQrmBktwliJiJ8Xw18WLK7nn4GS/+X/jbh87qqA8M +pugLoDzga5SYnH+tBuYc6kIQX+ImFTw3OffXvO645e8D7r0i+yiGNFjEWn9hongP +XvPKnbwbPKfILfanIhHKA9jnZwqKDss1jjQ52MjqjZ9k4DewbNfFj8GQYSbbJIwe +SsCI3zWQzj8C9GRh3sfIB5XeMhg6j6JCQCTl1jNdfK7vsU1P1FeQNWrcrgSXSYk0 +ly4wBOeY99sLAZDBHwo/+ML+TvrbmnNzFrwFuHnYWa8G5z9nODmxfKuU4CkUpijy +323imttUQ/hHWKNddBWcwauwxzQ= +-----END CERTIFICATE----- + +# Issuer: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Subject: CN=D-TRUST EV Root CA 2 2023 O=D-Trust GmbH +# Label: "D-TRUST EV Root CA 2 2023" +# Serial: 139766439402180512324132425437959641711 +# MD5 Fingerprint: 96:b4:78:09:f0:09:cb:77:eb:bb:1b:4d:6f:36:bc:b6 +# SHA1 Fingerprint: a5:5b:d8:47:6c:8f:19:f7:4c:f4:6d:6b:b6:c2:79:82:22:df:54:8b +# SHA256 Fingerprint: 8e:82:21:b2:e7:d4:00:78:36:a1:67:2f:0d:cc:29:9c:33:bc:07:d3:16:f1:32:fa:1a:20:6d:58:71:50:f1:ce +-----BEGIN CERTIFICATE----- +MIIFqTCCA5GgAwIBAgIQaSYJfoBLTKCnjHhiU19abzANBgkqhkiG9w0BAQ0FADBI +MQswCQYDVQQGEwJERTEVMBMGA1UEChMMRC1UcnVzdCBHbWJIMSIwIAYDVQQDExlE +LVRSVVNUIEVWIFJvb3QgQ0EgMiAyMDIzMB4XDTIzMDUwOTA5MTAzM1oXDTM4MDUw +OTA5MTAzMlowSDELMAkGA1UEBhMCREUxFTATBgNVBAoTDEQtVHJ1c3QgR21iSDEi +MCAGA1UEAxMZRC1UUlVTVCBFViBSb290IENBIDIgMjAyMzCCAiIwDQYJKoZIhvcN +AQEBBQADggIPADCCAgoCggIBANiOo4mAC7JXUtypU0w3uX9jFxPvp1sjW2l1sJkK +F8GLxNuo4MwxusLyzV3pt/gdr2rElYfXR8mV2IIEUD2BCP/kPbOx1sWy/YgJ25yE +7CUXFId/MHibaljJtnMoPDT3mfd/06b4HEV8rSyMlD/YZxBTfiLNTiVR8CUkNRFe +EMbsh2aJgWi6zCudR3Mfvc2RpHJqnKIbGKBv7FD0fUDCqDDPvXPIEysQEx6Lmqg6 +lHPTGGkKSv/BAQP/eX+1SH977ugpbzZMlWGG2Pmic4ruri+W7mjNPU0oQvlFKzIb +RlUWaqZLKfm7lVa/Rh3sHZMdwGWyH6FDrlaeoLGPaxK3YG14C8qKXO0elg6DpkiV +jTujIcSuWMYAsoS0I6SWhjW42J7YrDRJmGOVxcttSEfi8i4YHtAxq9107PncjLgc +jmgjutDzUNzPZY9zOjLHfP7KgiJPvo5iR2blzYfi6NUPGJ/lBHJLRjwQ8kTCZFZx +TnXonMkmdMV9WdEKWw9t/p51HBjGGjp82A0EzM23RWV6sY+4roRIPrN6TagD4uJ+ +ARZZaBhDM7DS3LAaQzXupdqpRlyuhoFBAUp0JuyfBr/CBTdkdXgpaP3F9ev+R/nk +hbDhezGdpn9yo7nELC7MmVcOIQxFAZRl62UJxmMiCzNJkkg8/M3OsD6Onov4/knF +NXJHAgMBAAGjgY4wgYswDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUqvyREBuH +kV8Wub9PS5FeAByxMoAwDgYDVR0PAQH/BAQDAgEGMEkGA1UdHwRCMEAwPqA8oDqG +OGh0dHA6Ly9jcmwuZC10cnVzdC5uZXQvY3JsL2QtdHJ1c3RfZXZfcm9vdF9jYV8y +XzIwMjMuY3JsMA0GCSqGSIb3DQEBDQUAA4ICAQCTy6UfmRHsmg1fLBWTxj++EI14 +QvBukEdHjqOSMo1wj/Zbjb6JzkcBahsgIIlbyIIQbODnmaprxiqgYzWRaoUlrRc4 +pZt+UPJ26oUFKidBK7GB0aL2QHWpDsvxVUjY7NHss+jOFKE17MJeNRqrphYBBo7q +3C+jisosketSjl8MmxfPy3MHGcRqwnNU73xDUmPBEcrCRbH0O1P1aa4846XerOhU +t7KR/aypH/KH5BfGSah82ApB9PI+53c0BFLd6IHyTS9URZ0V4U/M5d40VxDJI3IX +cI1QcB9WbMy5/zpaT2N6w25lBx2Eof+pDGOJbbJAiDnXH3dotfyc1dZnaVuodNv8 +ifYbMvekJKZ2t0dT741Jj6m2g1qllpBFYfXeA08mD6iL8AOWsKwV0HFaanuU5nCT +2vFp4LJiTZ6P/4mdm13NRemUAiKN4DV/6PEEeXFsVIP4M7kFMhtYVRFP0OUnR3Hs +7dpn1mKmS00PaaLJvOwiS5THaJQXfuKOKD62xur1NGyfN4gHONuGcfrNlUhDbqNP +gofXNJhuS5N5YHVpD/Aa1VP6IQzCP+k/HxiMkl14p3ZnGbuy6n/pcAlWVqOwDAst +Nl7F6cTVg8uGF5csbBNvh1qvSaYd2804BC5f4ko1Di1L+KIkBI3Y4WNeApI02phh +XBxvWHZks/wCuPWdCg== +-----END CERTIFICATE----- + +# Issuer: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Subject: CN=SwissSign RSA TLS Root CA 2022 - 1 O=SwissSign AG +# Label: "SwissSign RSA TLS Root CA 2022 - 1" +# Serial: 388078645722908516278762308316089881486363258315 +# MD5 Fingerprint: 16:2e:e4:19:76:81:85:ba:8e:91:58:f1:15:ef:72:39 +# SHA1 Fingerprint: 81:34:0a:be:4c:cd:ce:cc:e7:7d:cc:8a:d4:57:e2:45:a0:77:5d:ce +# SHA256 Fingerprint: 19:31:44:f4:31:e0:fd:db:74:07:17:d4:de:92:6a:57:11:33:88:4b:43:60:d3:0e:27:29:13:cb:e6:60:ce:41 +-----BEGIN CERTIFICATE----- +MIIFkzCCA3ugAwIBAgIUQ/oMX04bgBhE79G0TzUfRPSA7cswDQYJKoZIhvcNAQEL +BQAwUTELMAkGA1UEBhMCQ0gxFTATBgNVBAoTDFN3aXNzU2lnbiBBRzErMCkGA1UE +AxMiU3dpc3NTaWduIFJTQSBUTFMgUm9vdCBDQSAyMDIyIC0gMTAeFw0yMjA2MDgx +MTA4MjJaFw00NzA2MDgxMTA4MjJaMFExCzAJBgNVBAYTAkNIMRUwEwYDVQQKEwxT +d2lzc1NpZ24gQUcxKzApBgNVBAMTIlN3aXNzU2lnbiBSU0EgVExTIFJvb3QgQ0Eg +MjAyMiAtIDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDLKmjiC8NX +vDVjvHClO/OMPE5Xlm7DTjak9gLKHqquuN6orx122ro10JFwB9+zBvKK8i5VUXu7 +LCTLf5ImgKO0lPaCoaTo+nUdWfMHamFk4saMla+ju45vVs9xzF6BYQ1t8qsCLqSX +5XH8irCRIFucdFJtrhUnWXjyCcplDn/L9Ovn3KlMd/YrFgSVrpxxpT8q2kFC5zyE +EPThPYxr4iuRR1VPuFa+Rd4iUU1OKNlfGUEGjw5NBuBwQCMBauTLE5tzrE0USJIt +/m2n+IdreXXhvhCxqohAWVTXz8TQm0SzOGlkjIHRI36qOTw7D59Ke4LKa2/KIj4x +0LDQKhySio/YGZxH5D4MucLNvkEM+KRHBdvBFzA4OmnczcNpI/2aDwLOEGrOyvi5 +KaM2iYauC8BPY7kGWUleDsFpswrzd34unYyzJ5jSmY0lpx+Gs6ZUcDj8fV3oT4MM +0ZPlEuRU2j7yrTrePjxF8CgPBrnh25d7mUWe3f6VWQQvdT/TromZhqwUtKiE+shd +OxtYk8EXlFXIC+OCeYSf8wCENO7cMdWP8vpPlkwGqnj73mSiI80fPsWMvDdUDrta +clXvyFu1cvh43zcgTFeRc5JzrBh3Q4IgaezprClG5QtO+DdziZaKHG29777YtvTK +wP1H8K4LWCDFyB02rpeNUIMmJCn3nTsPBQIDAQABo2MwYTAPBgNVHRMBAf8EBTAD +AQH/MA4GA1UdDwEB/wQEAwIBBjAfBgNVHSMEGDAWgBRvjmKLk0Ow4UD2p8P98Q+4 +DxU4pTAdBgNVHQ4EFgQUb45ii5NDsOFA9qfD/fEPuA8VOKUwDQYJKoZIhvcNAQEL +BQADggIBAKwsKUF9+lz1GpUYvyypiqkkVHX1uECry6gkUSsYP2OprphWKwVDIqO3 +10aewCoSPY6WlkDfDDOLazeROpW7OSltwAJsipQLBwJNGD77+3v1dj2b9l4wBlgz +Hqp41eZUBDqyggmNzhYzWUUo8aWjlw5DI/0LIICQ/+Mmz7hkkeUFjxOgdg3XNwwQ +iJb0Pr6VvfHDffCjw3lHC1ySFWPtUnWK50Zpy1FVCypM9fJkT6lc/2cyjlUtMoIc +gC9qkfjLvH4YoiaoLqNTKIftV+Vlek4ASltOU8liNr3CjlvrzG4ngRhZi0Rjn9UM +ZfQpZX+RLOV/fuiJz48gy20HQhFRJjKKLjpHE7iNvUcNCfAWpO2Whi4Z2L6MOuhF +LhG6rlrnub+xzI/goP+4s9GFe3lmozm1O2bYQL7Pt2eLSMkZJVX8vY3PXtpOpvJp +zv1/THfQwUY1mFwjmwJFQ5Ra3bxHrSL+ul4vkSkphnsh3m5kt8sNjzdbowhq6/Td +Ao9QAwKxuDdollDruF/UKIqlIgyKhPBZLtU30WHlQnNYKoH3dtvi4k0NX/a3vgW0 +rk4N3hY9A4GzJl5LuEsAz/+MF7psYC0nhzck5npgL7XTgwSqT0N1osGDsieYK7EO +gLrAhV5Cud+xYJHT6xh+cHiudoO+cVrQkOPKwRYlZ0rwtnu64ZzZ +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root ECC G1 O=OISTE Foundation +# Label: "OISTE Server Root ECC G1" +# Serial: 47819833811561661340092227008453318557 +# MD5 Fingerprint: 42:a7:d2:35:ae:02:92:db:19:76:08:de:2f:05:b4:d4 +# SHA1 Fingerprint: 3b:f6:8b:09:ae:2a:92:7b:ba:e3:8d:3f:11:95:d9:e6:44:0c:45:e2 +# SHA256 Fingerprint: ee:c9:97:c0:c3:0f:21:6f:7e:3b:8b:30:7d:2b:ae:42:41:2d:75:3f:c8:21:9d:af:d1:52:0b:25:72:85:0f:49 +-----BEGIN CERTIFICATE----- +MIICNTCCAbqgAwIBAgIQI/nD1jWvjyhLH/BU6n6XnTAKBggqhkjOPQQDAzBLMQsw +CQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UEAwwY +T0lTVEUgU2VydmVyIFJvb3QgRUNDIEcxMB4XDTIzMDUzMTE0NDIyOFoXDTQ4MDUy +NDE0NDIyN1owSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5kYXRp +b24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IEVDQyBHMTB2MBAGByqGSM49 +AgEGBSuBBAAiA2IABBcv+hK8rBjzCvRE1nZCnrPoH7d5qVi2+GXROiFPqOujvqQy +cvO2Ackr/XeFblPdreqqLiWStukhEaivtUwL85Zgmjvn6hp4LrQ95SjeHIC6XG4N +2xml4z+cKrhAS93mT6NjMGEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBQ3 +TYhlz/w9itWj8UnATgwQb0K0nDAdBgNVHQ4EFgQUN02IZc/8PYrVo/FJwE4MEG9C +tJwwDgYDVR0PAQH/BAQDAgGGMAoGCCqGSM49BAMDA2kAMGYCMQCpKjAd0MKfkFFR +QD6VVCHNFmb3U2wIFjnQEnx/Yxvf4zgAOdktUyBFCxxgZzFDJe0CMQCSia7pXGKD +YmH5LVerVrkR3SW+ak5KGoJr3M/TvEqzPNcum9v4KGm8ay3sMaE641c= +-----END CERTIFICATE----- + +# Issuer: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Subject: CN=OISTE Server Root RSA G1 O=OISTE Foundation +# Label: " OISTE Server Root RSA G1" +# Serial: 113845518112613905024960613408179309848 +# MD5 Fingerprint: 23:a7:9e:d4:70:b8:b9:14:57:41:8a:7e:44:59:e2:68 +# SHA1 Fingerprint: f7:00:34:25:94:88:68:31:e4:34:87:3f:70:fe:86:b3:86:9f:f0:6e +# SHA256 Fingerprint: 9a:e3:62:32:a5:18:9f:fd:db:35:3d:fd:26:52:0c:01:53:95:d2:27:77:da:c5:9d:b5:7b:98:c0:89:a6:51:e6 +-----BEGIN CERTIFICATE----- +MIIFgzCCA2ugAwIBAgIQVaXZZ5Qoxu0M+ifdWwFNGDANBgkqhkiG9w0BAQwFADBL +MQswCQYDVQQGEwJDSDEZMBcGA1UECgwQT0lTVEUgRm91bmRhdGlvbjEhMB8GA1UE +AwwYT0lTVEUgU2VydmVyIFJvb3QgUlNBIEcxMB4XDTIzMDUzMTE0MzcxNloXDTQ4 +MDUyNDE0MzcxNVowSzELMAkGA1UEBhMCQ0gxGTAXBgNVBAoMEE9JU1RFIEZvdW5k +YXRpb24xITAfBgNVBAMMGE9JU1RFIFNlcnZlciBSb290IFJTQSBHMTCCAiIwDQYJ +KoZIhvcNAQEBBQADggIPADCCAgoCggIBAKqu9KuCz/vlNwvn1ZatkOhLKdxVYOPM +vLO8LZK55KN68YG0nnJyQ98/qwsmtO57Gmn7KNByXEptaZnwYx4M0rH/1ow00O7b +rEi56rAUjtgHqSSY3ekJvqgiG1k50SeH3BzN+Puz6+mTeO0Pzjd8JnduodgsIUzk +ik/HEzxux9UTl7Ko2yRpg1bTacuCErudG/L4NPKYKyqOBGf244ehHa1uzjZ0Dl4z +O8vbUZeUapU8zhhabkvG/AePLhq5SvdkNCncpo1Q4Y2LS+VIG24ugBA/5J8bZT8R +tOpXaZ+0AOuFJJkk9SGdl6r7NH8CaxWQrbueWhl/pIzY+m0o/DjH40ytas7ZTpOS +jswMZ78LS5bOZmdTaMsXEY5Z96ycG7mOaES3GK/m5Q9l3JUJsJMStR8+lKXHiHUh +sd4JJCpM4rzsTGdHwimIuQq6+cF0zowYJmXa92/GjHtoXAvuY8BeS/FOzJ8vD+Ho +mnqT8eDI278n5mUpezbgMxVz8p1rhAhoKzYHKyfMeNhqhw5HdPSqoBNdZH702xSu ++zrkL8Fl47l6QGzwBrd7KJvX4V84c5Ss2XCTLdyEr0YconosP4EmQufU2MVshGYR +i3drVByjtdgQ8K4p92cIiBdcuJd5z+orKu5YM+Vt6SmqZQENghPsJQtdLEByFSnT +kCz3GkPVavBpAgMBAAGjYzBhMA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU +8snBDw1jALvsRQ5KH7WxszbNDo0wHQYDVR0OBBYEFPLJwQ8NYwC77EUOSh+1sbM2 +zQ6NMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQwFAAOCAgEANGd5sjrG5T33 +I3K5Ce+SrScfoE4KsvXaFwyihdJ+klH9FWXXXGtkFu6KRcoMQzZENdl//nk6HOjG +5D1rd9QhEOP28yBOqb6J8xycqd+8MDoX0TJD0KqKchxRKEzdNsjkLWd9kYccnbz8 +qyiWXmFcuCIzGEgWUOrKL+mlSdx/PKQZvDatkuK59EvV6wit53j+F8Bdh3foZ3dP +AGav9LEDOr4SfEE15fSmG0eLy3n31r8Xbk5l8PjaV8GUgeV6Vg27Rn9vkf195hfk +gSe7BYhW3SCl95gtkRlpMV+bMPKZrXJAlszYd2abtNUOshD+FKrDgHGdPY3ofRRs +YWSGRqbXVMW215AWRqWFyp464+YTFrYVI8ypKVL9AMb2kI5Wj4kI3Zaq5tNqqYY1 +9tVFeEJKRvwDyF7YZvZFZSS0vod7VSCd9521Kvy5YhnLbDuv0204bKt7ph6N/Ome +/msVuduCmsuY33OhkKCgxeDoAaijFJzIwZqsFVAzje18KotzlUBDJvyBpCpfOZC3 +J8tRd/iWkx7P8nd9H0aTolkelUTFLXVksNb54Dxp6gS1HAviRkRNQzuXSXERvSS2 +wq1yVAb+axj5d9spLFKebXd7Yv0PTY6YMjAwcRLWJTXjn/hvnLXrahut6hDTlhZy +BiElxky8j3C7DOReIoMt0r7+hVu05L0= +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/certifi/core.py b/venv/lib/python3.10/site-packages/certifi/core.py new file mode 100644 index 0000000..1c9661c --- /dev/null +++ b/venv/lib/python3.10/site-packages/certifi/core.py @@ -0,0 +1,83 @@ +""" +certifi.py +~~~~~~~~~~ + +This module returns the installation location of cacert.pem or its contents. +""" +import sys +import atexit + +def exit_cacert_ctx() -> None: + _CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr] + + +if sys.version_info >= (3, 11): + + from importlib.resources import as_file, files + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the file + # in cases where we're inside of a zipimport situation until someone + # actually calls where(), but we don't want to re-extract the file + # on every call of where(), so we'll do it once then store it in a + # global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you to + # manage the cleanup of this file, so it doesn't actually return a + # path, it returns a context manager that will give you the path + # when you enter it and will do any cleanup when you leave it. In + # the common case of not needing a temporary file, it will just + # return the file system location and the __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem")) + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii") + +else: + + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None + _CACERT_PATH = None + + def where() -> str: + # This is slightly terrible, but we want to delay extracting the + # file in cases where we're inside of a zipimport situation until + # someone actually calls where(), but we don't want to re-extract + # the file on every call of where(), so we'll do it once then store + # it in a global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you + # to manage the cleanup of this file, so it doesn't actually + # return a path, it returns a context manager that will give + # you the path when you enter it and will do any cleanup when + # you leave it. In the common case of not needing a temporary + # file, it will just return the file system location and the + # __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + atexit.register(exit_cacert_ctx) + + return _CACERT_PATH + + def contents() -> str: + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/venv/lib/python3.10/site-packages/certifi/py.typed b/venv/lib/python3.10/site-packages/certifi/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/METADATA b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/METADATA new file mode 100644 index 0000000..8d32edc --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/METADATA @@ -0,0 +1,764 @@ +Metadata-Version: 2.4 +Name: charset-normalizer +Version: 3.4.4 +Summary: The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet. +Author-email: "Ahmed R. TAHRI" +Maintainer-email: "Ahmed R. TAHRI" +License: MIT +Project-URL: Changelog, https://github.com/jawah/charset_normalizer/blob/master/CHANGELOG.md +Project-URL: Documentation, https://charset-normalizer.readthedocs.io/ +Project-URL: Code, https://github.com/jawah/charset_normalizer +Project-URL: Issue tracker, https://github.com/jawah/charset_normalizer/issues +Keywords: encoding,charset,charset-detector,detector,normalization,unicode,chardet,detect +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Text Processing :: Linguistic +Classifier: Topic :: Utilities +Classifier: Typing :: Typed +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +License-File: LICENSE +Provides-Extra: unicode-backport +Dynamic: license-file + +

Charset Detection, for Everyone 👋

+ +

+ The Real First Universal Charset Detector
+ + + + + Download Count Total + + + + +

+

+ Featured Packages
+ + Static Badge + + + Static Badge + +

+

+ In other language (unofficial port - by the community)
+ + Static Badge + +

+ +> A library that helps you read text from an unknown charset encoding.
Motivated by `chardet`, +> I'm trying to resolve the issue by taking a new approach. +> All IANA character set names for which the Python core library provides codecs are supported. + +

+ >>>>> 👉 Try Me Online Now, Then Adopt Me 👈 <<<<< +

+ +This project offers you an alternative to **Universal Charset Encoding Detector**, also known as **Chardet**. + +| Feature | [Chardet](https://github.com/chardet/chardet) | Charset Normalizer | [cChardet](https://github.com/PyYoshi/cChardet) | +|--------------------------------------------------|:---------------------------------------------:|:--------------------------------------------------------------------------------------------------:|:-----------------------------------------------:| +| `Fast` | ❌ | ✅ | ✅ | +| `Universal**` | ❌ | ✅ | ❌ | +| `Reliable` **without** distinguishable standards | ❌ | ✅ | ✅ | +| `Reliable` **with** distinguishable standards | ✅ | ✅ | ✅ | +| `License` | LGPL-2.1
_restrictive_ | MIT | MPL-1.1
_restrictive_ | +| `Native Python` | ✅ | ✅ | ❌ | +| `Detect spoken language` | ❌ | ✅ | N/A | +| `UnicodeDecodeError Safety` | ❌ | ✅ | ❌ | +| `Whl Size (min)` | 193.6 kB | 42 kB | ~200 kB | +| `Supported Encoding` | 33 | 🎉 [99](https://charset-normalizer.readthedocs.io/en/latest/user/support.html#supported-encodings) | 40 | + +

+Reading Normalized TextCat Reading Text +

+ +*\*\* : They are clearly using specific code for a specific encoding even if covering most of used one*
+ +## ⚡ Performance + +This package offer better performance than its counterpart Chardet. Here are some numbers. + +| Package | Accuracy | Mean per file (ms) | File per sec (est) | +|-----------------------------------------------|:--------:|:------------------:|:------------------:| +| [chardet](https://github.com/chardet/chardet) | 86 % | 63 ms | 16 file/sec | +| charset-normalizer | **98 %** | **10 ms** | 100 file/sec | + +| Package | 99th percentile | 95th percentile | 50th percentile | +|-----------------------------------------------|:---------------:|:---------------:|:---------------:| +| [chardet](https://github.com/chardet/chardet) | 265 ms | 71 ms | 7 ms | +| charset-normalizer | 100 ms | 50 ms | 5 ms | + +_updated as of december 2024 using CPython 3.12_ + +Chardet's performance on larger file (1MB+) are very poor. Expect huge difference on large payload. + +> Stats are generated using 400+ files using default parameters. More details on used files, see GHA workflows. +> And yes, these results might change at any time. The dataset can be updated to include more files. +> The actual delays heavily depends on your CPU capabilities. The factors should remain the same. +> Keep in mind that the stats are generous and that Chardet accuracy vs our is measured using Chardet initial capability +> (e.g. Supported Encoding) Challenge-them if you want. + +## ✨ Installation + +Using pip: + +```sh +pip install charset-normalizer -U +``` + +## 🚀 Basic Usage + +### CLI +This package comes with a CLI. + +``` +usage: normalizer [-h] [-v] [-a] [-n] [-m] [-r] [-f] [-t THRESHOLD] + file [file ...] + +The Real First Universal Charset Detector. Discover originating encoding used +on text file. Normalize text to unicode. + +positional arguments: + files File(s) to be analysed + +optional arguments: + -h, --help show this help message and exit + -v, --verbose Display complementary information about file if any. + Stdout will contain logs about the detection process. + -a, --with-alternative + Output complementary possibilities if any. Top-level + JSON WILL be a list. + -n, --normalize Permit to normalize input file. If not set, program + does not write anything. + -m, --minimal Only output the charset detected to STDOUT. Disabling + JSON output. + -r, --replace Replace file when trying to normalize it instead of + creating a new one. + -f, --force Replace file without asking if you are sure, use this + flag with caution. + -t THRESHOLD, --threshold THRESHOLD + Define a custom maximum amount of chaos allowed in + decoded content. 0. <= chaos <= 1. + --version Show version information and exit. +``` + +```bash +normalizer ./data/sample.1.fr.srt +``` + +or + +```bash +python -m charset_normalizer ./data/sample.1.fr.srt +``` + +🎉 Since version 1.4.0 the CLI produce easily usable stdout result in JSON format. + +```json +{ + "path": "/home/default/projects/charset_normalizer/data/sample.1.fr.srt", + "encoding": "cp1252", + "encoding_aliases": [ + "1252", + "windows_1252" + ], + "alternative_encodings": [ + "cp1254", + "cp1256", + "cp1258", + "iso8859_14", + "iso8859_15", + "iso8859_16", + "iso8859_3", + "iso8859_9", + "latin_1", + "mbcs" + ], + "language": "French", + "alphabets": [ + "Basic Latin", + "Latin-1 Supplement" + ], + "has_sig_or_bom": false, + "chaos": 0.149, + "coherence": 97.152, + "unicode_path": null, + "is_preferred": true +} +``` + +### Python +*Just print out normalized text* +```python +from charset_normalizer import from_path + +results = from_path('./my_subtitle.srt') + +print(str(results.best())) +``` + +*Upgrade your code without effort* +```python +from charset_normalizer import detect +``` + +The above code will behave the same as **chardet**. We ensure that we offer the best (reasonable) BC result possible. + +See the docs for advanced usage : [readthedocs.io](https://charset-normalizer.readthedocs.io/en/latest/) + +## 😇 Why + +When I started using Chardet, I noticed that it was not suited to my expectations, and I wanted to propose a +reliable alternative using a completely different method. Also! I never back down on a good challenge! + +I **don't care** about the **originating charset** encoding, because **two different tables** can +produce **two identical rendered string.** +What I want is to get readable text, the best I can. + +In a way, **I'm brute forcing text decoding.** How cool is that ? 😎 + +Don't confuse package **ftfy** with charset-normalizer or chardet. ftfy goal is to repair Unicode string whereas charset-normalizer to convert raw file in unknown encoding to unicode. + +## 🍰 How + + - Discard all charset encoding table that could not fit the binary content. + - Measure noise, or the mess once opened (by chunks) with a corresponding charset encoding. + - Extract matches with the lowest mess detected. + - Additionally, we measure coherence / probe for a language. + +**Wait a minute**, what is noise/mess and coherence according to **YOU ?** + +*Noise :* I opened hundred of text files, **written by humans**, with the wrong encoding table. **I observed**, then +**I established** some ground rules about **what is obvious** when **it seems like** a mess (aka. defining noise in rendered text). + I know that my interpretation of what is noise is probably incomplete, feel free to contribute in order to + improve or rewrite it. + +*Coherence :* For each language there is on earth, we have computed ranked letter appearance occurrences (the best we can). So I thought +that intel is worth something here. So I use those records against decoded text to check if I can detect intelligent design. + +## ⚡ Known limitations + + - Language detection is unreliable when text contains two or more languages sharing identical letters. (eg. HTML (english tags) + Turkish content (Sharing Latin characters)) + - Every charset detector heavily depends on sufficient content. In common cases, do not bother run detection on very tiny content. + +## ⚠️ About Python EOLs + +**If you are running:** + +- Python >=2.7,<3.5: Unsupported +- Python 3.5: charset-normalizer < 2.1 +- Python 3.6: charset-normalizer < 3.1 +- Python 3.7: charset-normalizer < 4.0 + +Upgrade your Python interpreter as soon as possible. + +## 👤 Contributing + +Contributions, issues and feature requests are very much welcome.
+Feel free to check [issues page](https://github.com/ousret/charset_normalizer/issues) if you want to contribute. + +## 📝 License + +Copyright © [Ahmed TAHRI @Ousret](https://github.com/Ousret).
+This project is [MIT](https://github.com/Ousret/charset_normalizer/blob/master/LICENSE) licensed. + +Characters frequencies used in this project © 2012 [Denny Vrandečić](http://simia.net/letters/) + +## 💼 For Enterprise + +Professional support for charset-normalizer is available as part of the [Tidelift +Subscription][1]. Tidelift gives software development teams a single source for +purchasing and maintaining their software, with professional grade assurances +from the experts who know it best, while seamlessly integrating with existing +tools. + +[1]: https://tidelift.com/subscription/pkg/pypi-charset-normalizer?utm_source=pypi-charset-normalizer&utm_medium=readme + +[![OpenSSF Best Practices](https://www.bestpractices.dev/projects/7297/badge)](https://www.bestpractices.dev/projects/7297) + +# Changelog +All notable changes to charset-normalizer will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [3.4.4](https://github.com/Ousret/charset_normalizer/compare/3.4.2...3.4.4) (2025-10-13) + +### Changed +- Bound `setuptools` to a specific constraint `setuptools>=68,<=81`. +- Raised upper bound of mypyc for the optional pre-built extension to v1.18.2 + +### Removed +- `setuptools-scm` as a build dependency. + +### Misc +- Enforced hashes in `dev-requirements.txt` and created `ci-requirements.txt` for security purposes. +- Additional pre-built wheels for riscv64, s390x, and armv7l architectures. +- Restore ` multiple.intoto.jsonl` in GitHub releases in addition to individual attestation file per wheel. + +## [3.4.3](https://github.com/Ousret/charset_normalizer/compare/3.4.2...3.4.3) (2025-08-09) + +### Changed +- mypy(c) is no longer a required dependency at build time if `CHARSET_NORMALIZER_USE_MYPYC` isn't set to `1`. (#595) (#583) +- automatically lower confidence on small bytes samples that are not Unicode in `detect` output legacy function. (#391) + +### Added +- Custom build backend to overcome inability to mark mypy as an optional dependency in the build phase. +- Support for Python 3.14 + +### Fixed +- sdist archive contained useless directories. +- automatically fallback on valid UTF-16 or UTF-32 even if the md says it's noisy. (#633) + +### Misc +- SBOM are automatically published to the relevant GitHub release to comply with regulatory changes. + Each published wheel comes with its SBOM. We choose CycloneDX as the format. +- Prebuilt optimized wheel are no longer distributed by default for CPython 3.7 due to a change in cibuildwheel. + +## [3.4.2](https://github.com/Ousret/charset_normalizer/compare/3.4.1...3.4.2) (2025-05-02) + +### Fixed +- Addressed the DeprecationWarning in our CLI regarding `argparse.FileType` by backporting the target class into the package. (#591) +- Improved the overall reliability of the detector with CJK Ideographs. (#605) (#587) + +### Changed +- Optional mypyc compilation upgraded to version 1.15 for Python >= 3.8 + +## [3.4.1](https://github.com/Ousret/charset_normalizer/compare/3.4.0...3.4.1) (2024-12-24) + +### Changed +- Project metadata are now stored using `pyproject.toml` instead of `setup.cfg` using setuptools as the build backend. +- Enforce annotation delayed loading for a simpler and consistent types in the project. +- Optional mypyc compilation upgraded to version 1.14 for Python >= 3.8 + +### Added +- pre-commit configuration. +- noxfile. + +### Removed +- `build-requirements.txt` as per using `pyproject.toml` native build configuration. +- `bin/integration.py` and `bin/serve.py` in favor of downstream integration test (see noxfile). +- `setup.cfg` in favor of `pyproject.toml` metadata configuration. +- Unused `utils.range_scan` function. + +### Fixed +- Converting content to Unicode bytes may insert `utf_8` instead of preferred `utf-8`. (#572) +- Deprecation warning "'count' is passed as positional argument" when converting to Unicode bytes on Python 3.13+ + +## [3.4.0](https://github.com/Ousret/charset_normalizer/compare/3.3.2...3.4.0) (2024-10-08) + +### Added +- Argument `--no-preemptive` in the CLI to prevent the detector to search for hints. +- Support for Python 3.13 (#512) + +### Fixed +- Relax the TypeError exception thrown when trying to compare a CharsetMatch with anything else than a CharsetMatch. +- Improved the general reliability of the detector based on user feedbacks. (#520) (#509) (#498) (#407) (#537) +- Declared charset in content (preemptive detection) not changed when converting to utf-8 bytes. (#381) + +## [3.3.2](https://github.com/Ousret/charset_normalizer/compare/3.3.1...3.3.2) (2023-10-31) + +### Fixed +- Unintentional memory usage regression when using large payload that match several encoding (#376) +- Regression on some detection case showcased in the documentation (#371) + +### Added +- Noise (md) probe that identify malformed arabic representation due to the presence of letters in isolated form (credit to my wife) + +## [3.3.1](https://github.com/Ousret/charset_normalizer/compare/3.3.0...3.3.1) (2023-10-22) + +### Changed +- Optional mypyc compilation upgraded to version 1.6.1 for Python >= 3.8 +- Improved the general detection reliability based on reports from the community + +## [3.3.0](https://github.com/Ousret/charset_normalizer/compare/3.2.0...3.3.0) (2023-09-30) + +### Added +- Allow to execute the CLI (e.g. normalizer) through `python -m charset_normalizer.cli` or `python -m charset_normalizer` +- Support for 9 forgotten encoding that are supported by Python but unlisted in `encoding.aliases` as they have no alias (#323) + +### Removed +- (internal) Redundant utils.is_ascii function and unused function is_private_use_only +- (internal) charset_normalizer.assets is moved inside charset_normalizer.constant + +### Changed +- (internal) Unicode code blocks in constants are updated using the latest v15.0.0 definition to improve detection +- Optional mypyc compilation upgraded to version 1.5.1 for Python >= 3.8 + +### Fixed +- Unable to properly sort CharsetMatch when both chaos/noise and coherence were close due to an unreachable condition in \_\_lt\_\_ (#350) + +## [3.2.0](https://github.com/Ousret/charset_normalizer/compare/3.1.0...3.2.0) (2023-06-07) + +### Changed +- Typehint for function `from_path` no longer enforce `PathLike` as its first argument +- Minor improvement over the global detection reliability + +### Added +- Introduce function `is_binary` that relies on main capabilities, and optimized to detect binaries +- Propagate `enable_fallback` argument throughout `from_bytes`, `from_path`, and `from_fp` that allow a deeper control over the detection (default True) +- Explicit support for Python 3.12 + +### Fixed +- Edge case detection failure where a file would contain 'very-long' camel cased word (Issue #289) + +## [3.1.0](https://github.com/Ousret/charset_normalizer/compare/3.0.1...3.1.0) (2023-03-06) + +### Added +- Argument `should_rename_legacy` for legacy function `detect` and disregard any new arguments without errors (PR #262) + +### Removed +- Support for Python 3.6 (PR #260) + +### Changed +- Optional speedup provided by mypy/c 1.0.1 + +## [3.0.1](https://github.com/Ousret/charset_normalizer/compare/3.0.0...3.0.1) (2022-11-18) + +### Fixed +- Multi-bytes cutter/chunk generator did not always cut correctly (PR #233) + +### Changed +- Speedup provided by mypy/c 0.990 on Python >= 3.7 + +## [3.0.0](https://github.com/Ousret/charset_normalizer/compare/2.1.1...3.0.0) (2022-10-20) + +### Added +- Extend the capability of explain=True when cp_isolation contains at most two entries (min one), will log in details of the Mess-detector results +- Support for alternative language frequency set in charset_normalizer.assets.FREQUENCIES +- Add parameter `language_threshold` in `from_bytes`, `from_path` and `from_fp` to adjust the minimum expected coherence ratio +- `normalizer --version` now specify if current version provide extra speedup (meaning mypyc compilation whl) + +### Changed +- Build with static metadata using 'build' frontend +- Make the language detection stricter +- Optional: Module `md.py` can be compiled using Mypyc to provide an extra speedup up to 4x faster than v2.1 + +### Fixed +- CLI with opt --normalize fail when using full path for files +- TooManyAccentuatedPlugin induce false positive on the mess detection when too few alpha character have been fed to it +- Sphinx warnings when generating the documentation + +### Removed +- Coherence detector no longer return 'Simple English' instead return 'English' +- Coherence detector no longer return 'Classical Chinese' instead return 'Chinese' +- Breaking: Method `first()` and `best()` from CharsetMatch +- UTF-7 will no longer appear as "detected" without a recognized SIG/mark (is unreliable/conflict with ASCII) +- Breaking: Class aliases CharsetDetector, CharsetDoctor, CharsetNormalizerMatch and CharsetNormalizerMatches +- Breaking: Top-level function `normalize` +- Breaking: Properties `chaos_secondary_pass`, `coherence_non_latin` and `w_counter` from CharsetMatch +- Support for the backport `unicodedata2` + +## [3.0.0rc1](https://github.com/Ousret/charset_normalizer/compare/3.0.0b2...3.0.0rc1) (2022-10-18) + +### Added +- Extend the capability of explain=True when cp_isolation contains at most two entries (min one), will log in details of the Mess-detector results +- Support for alternative language frequency set in charset_normalizer.assets.FREQUENCIES +- Add parameter `language_threshold` in `from_bytes`, `from_path` and `from_fp` to adjust the minimum expected coherence ratio + +### Changed +- Build with static metadata using 'build' frontend +- Make the language detection stricter + +### Fixed +- CLI with opt --normalize fail when using full path for files +- TooManyAccentuatedPlugin induce false positive on the mess detection when too few alpha character have been fed to it + +### Removed +- Coherence detector no longer return 'Simple English' instead return 'English' +- Coherence detector no longer return 'Classical Chinese' instead return 'Chinese' + +## [3.0.0b2](https://github.com/Ousret/charset_normalizer/compare/3.0.0b1...3.0.0b2) (2022-08-21) + +### Added +- `normalizer --version` now specify if current version provide extra speedup (meaning mypyc compilation whl) + +### Removed +- Breaking: Method `first()` and `best()` from CharsetMatch +- UTF-7 will no longer appear as "detected" without a recognized SIG/mark (is unreliable/conflict with ASCII) + +### Fixed +- Sphinx warnings when generating the documentation + +## [3.0.0b1](https://github.com/Ousret/charset_normalizer/compare/2.1.0...3.0.0b1) (2022-08-15) + +### Changed +- Optional: Module `md.py` can be compiled using Mypyc to provide an extra speedup up to 4x faster than v2.1 + +### Removed +- Breaking: Class aliases CharsetDetector, CharsetDoctor, CharsetNormalizerMatch and CharsetNormalizerMatches +- Breaking: Top-level function `normalize` +- Breaking: Properties `chaos_secondary_pass`, `coherence_non_latin` and `w_counter` from CharsetMatch +- Support for the backport `unicodedata2` + +## [2.1.1](https://github.com/Ousret/charset_normalizer/compare/2.1.0...2.1.1) (2022-08-19) + +### Deprecated +- Function `normalize` scheduled for removal in 3.0 + +### Changed +- Removed useless call to decode in fn is_unprintable (#206) + +### Fixed +- Third-party library (i18n xgettext) crashing not recognizing utf_8 (PEP 263) with underscore from [@aleksandernovikov](https://github.com/aleksandernovikov) (#204) + +## [2.1.0](https://github.com/Ousret/charset_normalizer/compare/2.0.12...2.1.0) (2022-06-19) + +### Added +- Output the Unicode table version when running the CLI with `--version` (PR #194) + +### Changed +- Re-use decoded buffer for single byte character sets from [@nijel](https://github.com/nijel) (PR #175) +- Fixing some performance bottlenecks from [@deedy5](https://github.com/deedy5) (PR #183) + +### Fixed +- Workaround potential bug in cpython with Zero Width No-Break Space located in Arabic Presentation Forms-B, Unicode 1.1 not acknowledged as space (PR #175) +- CLI default threshold aligned with the API threshold from [@oleksandr-kuzmenko](https://github.com/oleksandr-kuzmenko) (PR #181) + +### Removed +- Support for Python 3.5 (PR #192) + +### Deprecated +- Use of backport unicodedata from `unicodedata2` as Python is quickly catching up, scheduled for removal in 3.0 (PR #194) + +## [2.0.12](https://github.com/Ousret/charset_normalizer/compare/2.0.11...2.0.12) (2022-02-12) + +### Fixed +- ASCII miss-detection on rare cases (PR #170) + +## [2.0.11](https://github.com/Ousret/charset_normalizer/compare/2.0.10...2.0.11) (2022-01-30) + +### Added +- Explicit support for Python 3.11 (PR #164) + +### Changed +- The logging behavior have been completely reviewed, now using only TRACE and DEBUG levels (PR #163 #165) + +## [2.0.10](https://github.com/Ousret/charset_normalizer/compare/2.0.9...2.0.10) (2022-01-04) + +### Fixed +- Fallback match entries might lead to UnicodeDecodeError for large bytes sequence (PR #154) + +### Changed +- Skipping the language-detection (CD) on ASCII (PR #155) + +## [2.0.9](https://github.com/Ousret/charset_normalizer/compare/2.0.8...2.0.9) (2021-12-03) + +### Changed +- Moderating the logging impact (since 2.0.8) for specific environments (PR #147) + +### Fixed +- Wrong logging level applied when setting kwarg `explain` to True (PR #146) + +## [2.0.8](https://github.com/Ousret/charset_normalizer/compare/2.0.7...2.0.8) (2021-11-24) +### Changed +- Improvement over Vietnamese detection (PR #126) +- MD improvement on trailing data and long foreign (non-pure latin) data (PR #124) +- Efficiency improvements in cd/alphabet_languages from [@adbar](https://github.com/adbar) (PR #122) +- call sum() without an intermediary list following PEP 289 recommendations from [@adbar](https://github.com/adbar) (PR #129) +- Code style as refactored by Sourcery-AI (PR #131) +- Minor adjustment on the MD around european words (PR #133) +- Remove and replace SRTs from assets / tests (PR #139) +- Initialize the library logger with a `NullHandler` by default from [@nmaynes](https://github.com/nmaynes) (PR #135) +- Setting kwarg `explain` to True will add provisionally (bounded to function lifespan) a specific stream handler (PR #135) + +### Fixed +- Fix large (misleading) sequence giving UnicodeDecodeError (PR #137) +- Avoid using too insignificant chunk (PR #137) + +### Added +- Add and expose function `set_logging_handler` to configure a specific StreamHandler from [@nmaynes](https://github.com/nmaynes) (PR #135) +- Add `CHANGELOG.md` entries, format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) (PR #141) + +## [2.0.7](https://github.com/Ousret/charset_normalizer/compare/2.0.6...2.0.7) (2021-10-11) +### Added +- Add support for Kazakh (Cyrillic) language detection (PR #109) + +### Changed +- Further, improve inferring the language from a given single-byte code page (PR #112) +- Vainly trying to leverage PEP263 when PEP3120 is not supported (PR #116) +- Refactoring for potential performance improvements in loops from [@adbar](https://github.com/adbar) (PR #113) +- Various detection improvement (MD+CD) (PR #117) + +### Removed +- Remove redundant logging entry about detected language(s) (PR #115) + +### Fixed +- Fix a minor inconsistency between Python 3.5 and other versions regarding language detection (PR #117 #102) + +## [2.0.6](https://github.com/Ousret/charset_normalizer/compare/2.0.5...2.0.6) (2021-09-18) +### Fixed +- Unforeseen regression with the loss of the backward-compatibility with some older minor of Python 3.5.x (PR #100) +- Fix CLI crash when using --minimal output in certain cases (PR #103) + +### Changed +- Minor improvement to the detection efficiency (less than 1%) (PR #106 #101) + +## [2.0.5](https://github.com/Ousret/charset_normalizer/compare/2.0.4...2.0.5) (2021-09-14) +### Changed +- The project now comply with: flake8, mypy, isort and black to ensure a better overall quality (PR #81) +- The BC-support with v1.x was improved, the old staticmethods are restored (PR #82) +- The Unicode detection is slightly improved (PR #93) +- Add syntax sugar \_\_bool\_\_ for results CharsetMatches list-container (PR #91) + +### Removed +- The project no longer raise warning on tiny content given for detection, will be simply logged as warning instead (PR #92) + +### Fixed +- In some rare case, the chunks extractor could cut in the middle of a multi-byte character and could mislead the mess detection (PR #95) +- Some rare 'space' characters could trip up the UnprintablePlugin/Mess detection (PR #96) +- The MANIFEST.in was not exhaustive (PR #78) + +## [2.0.4](https://github.com/Ousret/charset_normalizer/compare/2.0.3...2.0.4) (2021-07-30) +### Fixed +- The CLI no longer raise an unexpected exception when no encoding has been found (PR #70) +- Fix accessing the 'alphabets' property when the payload contains surrogate characters (PR #68) +- The logger could mislead (explain=True) on detected languages and the impact of one MBCS match (PR #72) +- Submatch factoring could be wrong in rare edge cases (PR #72) +- Multiple files given to the CLI were ignored when publishing results to STDOUT. (After the first path) (PR #72) +- Fix line endings from CRLF to LF for certain project files (PR #67) + +### Changed +- Adjust the MD to lower the sensitivity, thus improving the global detection reliability (PR #69 #76) +- Allow fallback on specified encoding if any (PR #71) + +## [2.0.3](https://github.com/Ousret/charset_normalizer/compare/2.0.2...2.0.3) (2021-07-16) +### Changed +- Part of the detection mechanism has been improved to be less sensitive, resulting in more accurate detection results. Especially ASCII. (PR #63) +- According to the community wishes, the detection will fall back on ASCII or UTF-8 in a last-resort case. (PR #64) + +## [2.0.2](https://github.com/Ousret/charset_normalizer/compare/2.0.1...2.0.2) (2021-07-15) +### Fixed +- Empty/Too small JSON payload miss-detection fixed. Report from [@tseaver](https://github.com/tseaver) (PR #59) + +### Changed +- Don't inject unicodedata2 into sys.modules from [@akx](https://github.com/akx) (PR #57) + +## [2.0.1](https://github.com/Ousret/charset_normalizer/compare/2.0.0...2.0.1) (2021-07-13) +### Fixed +- Make it work where there isn't a filesystem available, dropping assets frequencies.json. Report from [@sethmlarson](https://github.com/sethmlarson). (PR #55) +- Using explain=False permanently disable the verbose output in the current runtime (PR #47) +- One log entry (language target preemptive) was not show in logs when using explain=True (PR #47) +- Fix undesired exception (ValueError) on getitem of instance CharsetMatches (PR #52) + +### Changed +- Public function normalize default args values were not aligned with from_bytes (PR #53) + +### Added +- You may now use charset aliases in cp_isolation and cp_exclusion arguments (PR #47) + +## [2.0.0](https://github.com/Ousret/charset_normalizer/compare/1.4.1...2.0.0) (2021-07-02) +### Changed +- 4x to 5 times faster than the previous 1.4.0 release. At least 2x faster than Chardet. +- Accent has been made on UTF-8 detection, should perform rather instantaneous. +- The backward compatibility with Chardet has been greatly improved. The legacy detect function returns an identical charset name whenever possible. +- The detection mechanism has been slightly improved, now Turkish content is detected correctly (most of the time) +- The program has been rewritten to ease the readability and maintainability. (+Using static typing)+ +- utf_7 detection has been reinstated. + +### Removed +- This package no longer require anything when used with Python 3.5 (Dropped cached_property) +- Removed support for these languages: Catalan, Esperanto, Kazakh, Baque, Volapük, Azeri, Galician, Nynorsk, Macedonian, and Serbocroatian. +- The exception hook on UnicodeDecodeError has been removed. + +### Deprecated +- Methods coherence_non_latin, w_counter, chaos_secondary_pass of the class CharsetMatch are now deprecated and scheduled for removal in v3.0 + +### Fixed +- The CLI output used the relative path of the file(s). Should be absolute. + +## [1.4.1](https://github.com/Ousret/charset_normalizer/compare/1.4.0...1.4.1) (2021-05-28) +### Fixed +- Logger configuration/usage no longer conflict with others (PR #44) + +## [1.4.0](https://github.com/Ousret/charset_normalizer/compare/1.3.9...1.4.0) (2021-05-21) +### Removed +- Using standard logging instead of using the package loguru. +- Dropping nose test framework in favor of the maintained pytest. +- Choose to not use dragonmapper package to help with gibberish Chinese/CJK text. +- Require cached_property only for Python 3.5 due to constraint. Dropping for every other interpreter version. +- Stop support for UTF-7 that does not contain a SIG. +- Dropping PrettyTable, replaced with pure JSON output in CLI. + +### Fixed +- BOM marker in a CharsetNormalizerMatch instance could be False in rare cases even if obviously present. Due to the sub-match factoring process. +- Not searching properly for the BOM when trying utf32/16 parent codec. + +### Changed +- Improving the package final size by compressing frequencies.json. +- Huge improvement over the larges payload. + +### Added +- CLI now produces JSON consumable output. +- Return ASCII if given sequences fit. Given reasonable confidence. + +## [1.3.9](https://github.com/Ousret/charset_normalizer/compare/1.3.8...1.3.9) (2021-05-13) + +### Fixed +- In some very rare cases, you may end up getting encode/decode errors due to a bad bytes payload (PR #40) + +## [1.3.8](https://github.com/Ousret/charset_normalizer/compare/1.3.7...1.3.8) (2021-05-12) + +### Fixed +- Empty given payload for detection may cause an exception if trying to access the `alphabets` property. (PR #39) + +## [1.3.7](https://github.com/Ousret/charset_normalizer/compare/1.3.6...1.3.7) (2021-05-12) + +### Fixed +- The legacy detect function should return UTF-8-SIG if sig is present in the payload. (PR #38) + +## [1.3.6](https://github.com/Ousret/charset_normalizer/compare/1.3.5...1.3.6) (2021-02-09) + +### Changed +- Amend the previous release to allow prettytable 2.0 (PR #35) + +## [1.3.5](https://github.com/Ousret/charset_normalizer/compare/1.3.4...1.3.5) (2021-02-08) + +### Fixed +- Fix error while using the package with a python pre-release interpreter (PR #33) + +### Changed +- Dependencies refactoring, constraints revised. + +### Added +- Add python 3.9 and 3.10 to the supported interpreters + +MIT License + +Copyright (c) 2025 TAHRI Ahmed R. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/RECORD b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/RECORD new file mode 100644 index 0000000..6767780 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/RECORD @@ -0,0 +1,35 @@ +../../../bin/normalizer,sha256=Qx4Ulr0_T8Ol7l5LG__4wa-BP8i2OhaXu4psgYBjtSA,266 +charset_normalizer-3.4.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +charset_normalizer-3.4.4.dist-info/METADATA,sha256=jVuUFBti8dav19YLvWissTihVdF2ozUY4KKMw7jdkBQ,37303 +charset_normalizer-3.4.4.dist-info/RECORD,, +charset_normalizer-3.4.4.dist-info/WHEEL,sha256=DZl4yYurviXJJQsilHH1qAzCRDsdz--oNtDZ-hUrZUk,190 +charset_normalizer-3.4.4.dist-info/entry_points.txt,sha256=ADSTKrkXZ3hhdOVFi6DcUEHQRS0xfxDIE_pEz4wLIXA,65 +charset_normalizer-3.4.4.dist-info/licenses/LICENSE,sha256=bQ1Bv-FwrGx9wkjJpj4lTQ-0WmDVCoJX0K-SxuJJuIc,1071 +charset_normalizer-3.4.4.dist-info/top_level.txt,sha256=7ASyzePr8_xuZWJsnqJjIBtyV8vhEo0wBCv1MPRRi3Q,19 +charset_normalizer/__init__.py,sha256=OKRxRv2Zhnqk00tqkN0c1BtJjm165fWXLydE52IKuHc,1590 +charset_normalizer/__main__.py,sha256=yzYxMR-IhKRHYwcSlavEv8oGdwxsR89mr2X09qXGdps,109 +charset_normalizer/__pycache__/__init__.cpython-310.pyc,, +charset_normalizer/__pycache__/__main__.cpython-310.pyc,, +charset_normalizer/__pycache__/api.cpython-310.pyc,, +charset_normalizer/__pycache__/cd.cpython-310.pyc,, +charset_normalizer/__pycache__/constant.cpython-310.pyc,, +charset_normalizer/__pycache__/legacy.cpython-310.pyc,, +charset_normalizer/__pycache__/md.cpython-310.pyc,, +charset_normalizer/__pycache__/models.cpython-310.pyc,, +charset_normalizer/__pycache__/utils.cpython-310.pyc,, +charset_normalizer/__pycache__/version.cpython-310.pyc,, +charset_normalizer/api.py,sha256=V07i8aVeCD8T2fSia3C-fn0i9t8qQguEBhsqszg32Ns,22668 +charset_normalizer/cd.py,sha256=WKTo1HDb-H9HfCDc3Bfwq5jzS25Ziy9SE2a74SgTq88,12522 +charset_normalizer/cli/__init__.py,sha256=D8I86lFk2-py45JvqxniTirSj_sFyE6sjaY_0-G1shc,136 +charset_normalizer/cli/__main__.py,sha256=dMaXG6IJXRvqq8z2tig7Qb83-BpWTln55ooiku5_uvg,12646 +charset_normalizer/cli/__pycache__/__init__.cpython-310.pyc,, +charset_normalizer/cli/__pycache__/__main__.cpython-310.pyc,, +charset_normalizer/constant.py,sha256=7UVY4ldYhmQMHUdgQ_sgZmzcQ0xxYxpBunqSZ-XJZ8U,42713 +charset_normalizer/legacy.py,sha256=sYBzSpzsRrg_wF4LP536pG64BItw7Tqtc3SMQAHvFLM,2731 +charset_normalizer/md.cpython-310-x86_64-linux-gnu.so,sha256=vmQPHNPc6Z0rbXxiiMeoCQxfjjh5z8umtRgKpBckk7E,15912 +charset_normalizer/md.py,sha256=-_oN3h3_X99nkFfqamD3yu45DC_wfk5odH0Tr_CQiXs,20145 +charset_normalizer/md__mypyc.cpython-310-x86_64-linux-gnu.so,sha256=mIEu_3ENI9UWZgXaH2Nbe2wh501Z-RpM1BR17EkEVWA,286264 +charset_normalizer/models.py,sha256=lKXhOnIPtiakbK3i__J9wpOfzx3JDTKj7Dn3Rg0VaRI,12394 +charset_normalizer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +charset_normalizer/utils.py,sha256=sTejPgrdlNsKNucZfJCxJ95lMTLA0ShHLLE3n5wpT9Q,12170 +charset_normalizer/version.py,sha256=nKE4qBNk5WA4LIJ_yIH_aSDfvtsyizkWMg-PUG-UZVk,115 diff --git a/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/WHEEL b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/WHEEL new file mode 100644 index 0000000..6d6bf82 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/WHEEL @@ -0,0 +1,7 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: false +Tag: cp310-cp310-manylinux_2_17_x86_64 +Tag: cp310-cp310-manylinux2014_x86_64 +Tag: cp310-cp310-manylinux_2_28_x86_64 + diff --git a/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/entry_points.txt b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/entry_points.txt new file mode 100644 index 0000000..65619e7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +normalizer = charset_normalizer.cli:cli_detect diff --git a/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/licenses/LICENSE b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/licenses/LICENSE new file mode 100644 index 0000000..9725772 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 TAHRI Ahmed R. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/top_level.txt b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/top_level.txt new file mode 100644 index 0000000..66958f0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer-3.4.4.dist-info/top_level.txt @@ -0,0 +1 @@ +charset_normalizer diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__init__.py b/venv/lib/python3.10/site-packages/charset_normalizer/__init__.py new file mode 100644 index 0000000..0d3a379 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/__init__.py @@ -0,0 +1,48 @@ +""" +Charset-Normalizer +~~~~~~~~~~~~~~ +The Real First Universal Charset Detector. +A library that helps you read text from an unknown charset encoding. +Motivated by chardet, This package is trying to resolve the issue by taking a new approach. +All IANA character set names for which the Python core library provides codecs are supported. + +Basic usage: + >>> from charset_normalizer import from_bytes + >>> results = from_bytes('Bсеки човек има право на образование. Oбразованието!'.encode('utf_8')) + >>> best_guess = results.best() + >>> str(best_guess) + 'Bсеки човек има право на образование. Oбразованието!' + +Others methods and usages are available - see the full documentation +at . +:copyright: (c) 2021 by Ahmed TAHRI +:license: MIT, see LICENSE for more details. +""" + +from __future__ import annotations + +import logging + +from .api import from_bytes, from_fp, from_path, is_binary +from .legacy import detect +from .models import CharsetMatch, CharsetMatches +from .utils import set_logging_handler +from .version import VERSION, __version__ + +__all__ = ( + "from_fp", + "from_path", + "from_bytes", + "is_binary", + "detect", + "CharsetMatch", + "CharsetMatches", + "__version__", + "VERSION", + "set_logging_handler", +) + +# Attach a NullHandler to the top level logger by default +# https://docs.python.org/3.3/howto/logging.html#configuring-logging-for-a-library + +logging.getLogger("charset_normalizer").addHandler(logging.NullHandler()) diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__main__.py b/venv/lib/python3.10/site-packages/charset_normalizer/__main__.py new file mode 100644 index 0000000..e0e76f7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/__main__.py @@ -0,0 +1,6 @@ +from __future__ import annotations + +from .cli import cli_detect + +if __name__ == "__main__": + cli_detect() diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f247705 Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/__main__.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/__main__.cpython-310.pyc new file mode 100644 index 0000000..d90434f Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/__main__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/api.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/api.cpython-310.pyc new file mode 100644 index 0000000..3ba1b58 Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/api.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/cd.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/cd.cpython-310.pyc new file mode 100644 index 0000000..693308e Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/cd.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/constant.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/constant.cpython-310.pyc new file mode 100644 index 0000000..b9bf970 Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/constant.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/legacy.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/legacy.cpython-310.pyc new file mode 100644 index 0000000..32caa22 Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/legacy.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/md.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/md.cpython-310.pyc new file mode 100644 index 0000000..0959bfb Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/md.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/models.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/models.cpython-310.pyc new file mode 100644 index 0000000..d1ea408 Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/models.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/utils.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..649155d Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/utils.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/version.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/version.cpython-310.pyc new file mode 100644 index 0000000..202eeee Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/__pycache__/version.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/api.py b/venv/lib/python3.10/site-packages/charset_normalizer/api.py new file mode 100644 index 0000000..ebd9639 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/api.py @@ -0,0 +1,669 @@ +from __future__ import annotations + +import logging +from os import PathLike +from typing import BinaryIO + +from .cd import ( + coherence_ratio, + encoding_languages, + mb_encoding_languages, + merge_coherence_ratios, +) +from .constant import IANA_SUPPORTED, TOO_BIG_SEQUENCE, TOO_SMALL_SEQUENCE, TRACE +from .md import mess_ratio +from .models import CharsetMatch, CharsetMatches +from .utils import ( + any_specified_encoding, + cut_sequence_chunks, + iana_name, + identify_sig_or_bom, + is_cp_similar, + is_multi_byte_encoding, + should_strip_sig_or_bom, +) + +logger = logging.getLogger("charset_normalizer") +explain_handler = logging.StreamHandler() +explain_handler.setFormatter( + logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") +) + + +def from_bytes( + sequences: bytes | bytearray, + steps: int = 5, + chunk_size: int = 512, + threshold: float = 0.2, + cp_isolation: list[str] | None = None, + cp_exclusion: list[str] | None = None, + preemptive_behaviour: bool = True, + explain: bool = False, + language_threshold: float = 0.1, + enable_fallback: bool = True, +) -> CharsetMatches: + """ + Given a raw bytes sequence, return the best possibles charset usable to render str objects. + If there is no results, it is a strong indicator that the source is binary/not text. + By default, the process will extract 5 blocks of 512o each to assess the mess and coherence of a given sequence. + And will give up a particular code page after 20% of measured mess. Those criteria are customizable at will. + + The preemptive behavior DOES NOT replace the traditional detection workflow, it prioritize a particular code page + but never take it for granted. Can improve the performance. + + You may want to focus your attention to some code page or/and not others, use cp_isolation and cp_exclusion for that + purpose. + + This function will strip the SIG in the payload/sequence every time except on UTF-16, UTF-32. + By default the library does not setup any handler other than the NullHandler, if you choose to set the 'explain' + toggle to True it will alter the logger configuration to add a StreamHandler that is suitable for debugging. + Custom logging format and handler can be set manually. + """ + + if not isinstance(sequences, (bytearray, bytes)): + raise TypeError( + "Expected object of type bytes or bytearray, got: {}".format( + type(sequences) + ) + ) + + if explain: + previous_logger_level: int = logger.level + logger.addHandler(explain_handler) + logger.setLevel(TRACE) + + length: int = len(sequences) + + if length == 0: + logger.debug("Encoding detection on empty bytes, assuming utf_8 intention.") + if explain: # Defensive: ensure exit path clean handler + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level or logging.WARNING) + return CharsetMatches([CharsetMatch(sequences, "utf_8", 0.0, False, [], "")]) + + if cp_isolation is not None: + logger.log( + TRACE, + "cp_isolation is set. use this flag for debugging purpose. " + "limited list of encoding allowed : %s.", + ", ".join(cp_isolation), + ) + cp_isolation = [iana_name(cp, False) for cp in cp_isolation] + else: + cp_isolation = [] + + if cp_exclusion is not None: + logger.log( + TRACE, + "cp_exclusion is set. use this flag for debugging purpose. " + "limited list of encoding excluded : %s.", + ", ".join(cp_exclusion), + ) + cp_exclusion = [iana_name(cp, False) for cp in cp_exclusion] + else: + cp_exclusion = [] + + if length <= (chunk_size * steps): + logger.log( + TRACE, + "override steps (%i) and chunk_size (%i) as content does not fit (%i byte(s) given) parameters.", + steps, + chunk_size, + length, + ) + steps = 1 + chunk_size = length + + if steps > 1 and length / steps < chunk_size: + chunk_size = int(length / steps) + + is_too_small_sequence: bool = len(sequences) < TOO_SMALL_SEQUENCE + is_too_large_sequence: bool = len(sequences) >= TOO_BIG_SEQUENCE + + if is_too_small_sequence: + logger.log( + TRACE, + "Trying to detect encoding from a tiny portion of ({}) byte(s).".format( + length + ), + ) + elif is_too_large_sequence: + logger.log( + TRACE, + "Using lazy str decoding because the payload is quite large, ({}) byte(s).".format( + length + ), + ) + + prioritized_encodings: list[str] = [] + + specified_encoding: str | None = ( + any_specified_encoding(sequences) if preemptive_behaviour else None + ) + + if specified_encoding is not None: + prioritized_encodings.append(specified_encoding) + logger.log( + TRACE, + "Detected declarative mark in sequence. Priority +1 given for %s.", + specified_encoding, + ) + + tested: set[str] = set() + tested_but_hard_failure: list[str] = [] + tested_but_soft_failure: list[str] = [] + + fallback_ascii: CharsetMatch | None = None + fallback_u8: CharsetMatch | None = None + fallback_specified: CharsetMatch | None = None + + results: CharsetMatches = CharsetMatches() + + early_stop_results: CharsetMatches = CharsetMatches() + + sig_encoding, sig_payload = identify_sig_or_bom(sequences) + + if sig_encoding is not None: + prioritized_encodings.append(sig_encoding) + logger.log( + TRACE, + "Detected a SIG or BOM mark on first %i byte(s). Priority +1 given for %s.", + len(sig_payload), + sig_encoding, + ) + + prioritized_encodings.append("ascii") + + if "utf_8" not in prioritized_encodings: + prioritized_encodings.append("utf_8") + + for encoding_iana in prioritized_encodings + IANA_SUPPORTED: + if cp_isolation and encoding_iana not in cp_isolation: + continue + + if cp_exclusion and encoding_iana in cp_exclusion: + continue + + if encoding_iana in tested: + continue + + tested.add(encoding_iana) + + decoded_payload: str | None = None + bom_or_sig_available: bool = sig_encoding == encoding_iana + strip_sig_or_bom: bool = bom_or_sig_available and should_strip_sig_or_bom( + encoding_iana + ) + + if encoding_iana in {"utf_16", "utf_32"} and not bom_or_sig_available: + logger.log( + TRACE, + "Encoding %s won't be tested as-is because it require a BOM. Will try some sub-encoder LE/BE.", + encoding_iana, + ) + continue + if encoding_iana in {"utf_7"} and not bom_or_sig_available: + logger.log( + TRACE, + "Encoding %s won't be tested as-is because detection is unreliable without BOM/SIG.", + encoding_iana, + ) + continue + + try: + is_multi_byte_decoder: bool = is_multi_byte_encoding(encoding_iana) + except (ModuleNotFoundError, ImportError): + logger.log( + TRACE, + "Encoding %s does not provide an IncrementalDecoder", + encoding_iana, + ) + continue + + try: + if is_too_large_sequence and is_multi_byte_decoder is False: + str( + ( + sequences[: int(50e4)] + if strip_sig_or_bom is False + else sequences[len(sig_payload) : int(50e4)] + ), + encoding=encoding_iana, + ) + else: + decoded_payload = str( + ( + sequences + if strip_sig_or_bom is False + else sequences[len(sig_payload) :] + ), + encoding=encoding_iana, + ) + except (UnicodeDecodeError, LookupError) as e: + if not isinstance(e, LookupError): + logger.log( + TRACE, + "Code page %s does not fit given bytes sequence at ALL. %s", + encoding_iana, + str(e), + ) + tested_but_hard_failure.append(encoding_iana) + continue + + similar_soft_failure_test: bool = False + + for encoding_soft_failed in tested_but_soft_failure: + if is_cp_similar(encoding_iana, encoding_soft_failed): + similar_soft_failure_test = True + break + + if similar_soft_failure_test: + logger.log( + TRACE, + "%s is deemed too similar to code page %s and was consider unsuited already. Continuing!", + encoding_iana, + encoding_soft_failed, + ) + continue + + r_ = range( + 0 if not bom_or_sig_available else len(sig_payload), + length, + int(length / steps), + ) + + multi_byte_bonus: bool = ( + is_multi_byte_decoder + and decoded_payload is not None + and len(decoded_payload) < length + ) + + if multi_byte_bonus: + logger.log( + TRACE, + "Code page %s is a multi byte encoding table and it appear that at least one character " + "was encoded using n-bytes.", + encoding_iana, + ) + + max_chunk_gave_up: int = int(len(r_) / 4) + + max_chunk_gave_up = max(max_chunk_gave_up, 2) + early_stop_count: int = 0 + lazy_str_hard_failure = False + + md_chunks: list[str] = [] + md_ratios = [] + + try: + for chunk in cut_sequence_chunks( + sequences, + encoding_iana, + r_, + chunk_size, + bom_or_sig_available, + strip_sig_or_bom, + sig_payload, + is_multi_byte_decoder, + decoded_payload, + ): + md_chunks.append(chunk) + + md_ratios.append( + mess_ratio( + chunk, + threshold, + explain is True and 1 <= len(cp_isolation) <= 2, + ) + ) + + if md_ratios[-1] >= threshold: + early_stop_count += 1 + + if (early_stop_count >= max_chunk_gave_up) or ( + bom_or_sig_available and strip_sig_or_bom is False + ): + break + except ( + UnicodeDecodeError + ) as e: # Lazy str loading may have missed something there + logger.log( + TRACE, + "LazyStr Loading: After MD chunk decode, code page %s does not fit given bytes sequence at ALL. %s", + encoding_iana, + str(e), + ) + early_stop_count = max_chunk_gave_up + lazy_str_hard_failure = True + + # We might want to check the sequence again with the whole content + # Only if initial MD tests passes + if ( + not lazy_str_hard_failure + and is_too_large_sequence + and not is_multi_byte_decoder + ): + try: + sequences[int(50e3) :].decode(encoding_iana, errors="strict") + except UnicodeDecodeError as e: + logger.log( + TRACE, + "LazyStr Loading: After final lookup, code page %s does not fit given bytes sequence at ALL. %s", + encoding_iana, + str(e), + ) + tested_but_hard_failure.append(encoding_iana) + continue + + mean_mess_ratio: float = sum(md_ratios) / len(md_ratios) if md_ratios else 0.0 + if mean_mess_ratio >= threshold or early_stop_count >= max_chunk_gave_up: + tested_but_soft_failure.append(encoding_iana) + logger.log( + TRACE, + "%s was excluded because of initial chaos probing. Gave up %i time(s). " + "Computed mean chaos is %f %%.", + encoding_iana, + early_stop_count, + round(mean_mess_ratio * 100, ndigits=3), + ) + # Preparing those fallbacks in case we got nothing. + if ( + enable_fallback + and encoding_iana + in ["ascii", "utf_8", specified_encoding, "utf_16", "utf_32"] + and not lazy_str_hard_failure + ): + fallback_entry = CharsetMatch( + sequences, + encoding_iana, + threshold, + bom_or_sig_available, + [], + decoded_payload, + preemptive_declaration=specified_encoding, + ) + if encoding_iana == specified_encoding: + fallback_specified = fallback_entry + elif encoding_iana == "ascii": + fallback_ascii = fallback_entry + else: + fallback_u8 = fallback_entry + continue + + logger.log( + TRACE, + "%s passed initial chaos probing. Mean measured chaos is %f %%", + encoding_iana, + round(mean_mess_ratio * 100, ndigits=3), + ) + + if not is_multi_byte_decoder: + target_languages: list[str] = encoding_languages(encoding_iana) + else: + target_languages = mb_encoding_languages(encoding_iana) + + if target_languages: + logger.log( + TRACE, + "{} should target any language(s) of {}".format( + encoding_iana, str(target_languages) + ), + ) + + cd_ratios = [] + + # We shall skip the CD when its about ASCII + # Most of the time its not relevant to run "language-detection" on it. + if encoding_iana != "ascii": + for chunk in md_chunks: + chunk_languages = coherence_ratio( + chunk, + language_threshold, + ",".join(target_languages) if target_languages else None, + ) + + cd_ratios.append(chunk_languages) + + cd_ratios_merged = merge_coherence_ratios(cd_ratios) + + if cd_ratios_merged: + logger.log( + TRACE, + "We detected language {} using {}".format( + cd_ratios_merged, encoding_iana + ), + ) + + current_match = CharsetMatch( + sequences, + encoding_iana, + mean_mess_ratio, + bom_or_sig_available, + cd_ratios_merged, + ( + decoded_payload + if ( + is_too_large_sequence is False + or encoding_iana in [specified_encoding, "ascii", "utf_8"] + ) + else None + ), + preemptive_declaration=specified_encoding, + ) + + results.append(current_match) + + if ( + encoding_iana in [specified_encoding, "ascii", "utf_8"] + and mean_mess_ratio < 0.1 + ): + # If md says nothing to worry about, then... stop immediately! + if mean_mess_ratio == 0.0: + logger.debug( + "Encoding detection: %s is most likely the one.", + current_match.encoding, + ) + if explain: # Defensive: ensure exit path clean handler + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + return CharsetMatches([current_match]) + + early_stop_results.append(current_match) + + if ( + len(early_stop_results) + and (specified_encoding is None or specified_encoding in tested) + and "ascii" in tested + and "utf_8" in tested + ): + probable_result: CharsetMatch = early_stop_results.best() # type: ignore[assignment] + logger.debug( + "Encoding detection: %s is most likely the one.", + probable_result.encoding, + ) + if explain: # Defensive: ensure exit path clean handler + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + + return CharsetMatches([probable_result]) + + if encoding_iana == sig_encoding: + logger.debug( + "Encoding detection: %s is most likely the one as we detected a BOM or SIG within " + "the beginning of the sequence.", + encoding_iana, + ) + if explain: # Defensive: ensure exit path clean handler + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + return CharsetMatches([results[encoding_iana]]) + + if len(results) == 0: + if fallback_u8 or fallback_ascii or fallback_specified: + logger.log( + TRACE, + "Nothing got out of the detection process. Using ASCII/UTF-8/Specified fallback.", + ) + + if fallback_specified: + logger.debug( + "Encoding detection: %s will be used as a fallback match", + fallback_specified.encoding, + ) + results.append(fallback_specified) + elif ( + (fallback_u8 and fallback_ascii is None) + or ( + fallback_u8 + and fallback_ascii + and fallback_u8.fingerprint != fallback_ascii.fingerprint + ) + or (fallback_u8 is not None) + ): + logger.debug("Encoding detection: utf_8 will be used as a fallback match") + results.append(fallback_u8) + elif fallback_ascii: + logger.debug("Encoding detection: ascii will be used as a fallback match") + results.append(fallback_ascii) + + if results: + logger.debug( + "Encoding detection: Found %s as plausible (best-candidate) for content. With %i alternatives.", + results.best().encoding, # type: ignore + len(results) - 1, + ) + else: + logger.debug("Encoding detection: Unable to determine any suitable charset.") + + if explain: + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + + return results + + +def from_fp( + fp: BinaryIO, + steps: int = 5, + chunk_size: int = 512, + threshold: float = 0.20, + cp_isolation: list[str] | None = None, + cp_exclusion: list[str] | None = None, + preemptive_behaviour: bool = True, + explain: bool = False, + language_threshold: float = 0.1, + enable_fallback: bool = True, +) -> CharsetMatches: + """ + Same thing than the function from_bytes but using a file pointer that is already ready. + Will not close the file pointer. + """ + return from_bytes( + fp.read(), + steps, + chunk_size, + threshold, + cp_isolation, + cp_exclusion, + preemptive_behaviour, + explain, + language_threshold, + enable_fallback, + ) + + +def from_path( + path: str | bytes | PathLike, # type: ignore[type-arg] + steps: int = 5, + chunk_size: int = 512, + threshold: float = 0.20, + cp_isolation: list[str] | None = None, + cp_exclusion: list[str] | None = None, + preemptive_behaviour: bool = True, + explain: bool = False, + language_threshold: float = 0.1, + enable_fallback: bool = True, +) -> CharsetMatches: + """ + Same thing than the function from_bytes but with one extra step. Opening and reading given file path in binary mode. + Can raise IOError. + """ + with open(path, "rb") as fp: + return from_fp( + fp, + steps, + chunk_size, + threshold, + cp_isolation, + cp_exclusion, + preemptive_behaviour, + explain, + language_threshold, + enable_fallback, + ) + + +def is_binary( + fp_or_path_or_payload: PathLike | str | BinaryIO | bytes, # type: ignore[type-arg] + steps: int = 5, + chunk_size: int = 512, + threshold: float = 0.20, + cp_isolation: list[str] | None = None, + cp_exclusion: list[str] | None = None, + preemptive_behaviour: bool = True, + explain: bool = False, + language_threshold: float = 0.1, + enable_fallback: bool = False, +) -> bool: + """ + Detect if the given input (file, bytes, or path) points to a binary file. aka. not a string. + Based on the same main heuristic algorithms and default kwargs at the sole exception that fallbacks match + are disabled to be stricter around ASCII-compatible but unlikely to be a string. + """ + if isinstance(fp_or_path_or_payload, (str, PathLike)): + guesses = from_path( + fp_or_path_or_payload, + steps=steps, + chunk_size=chunk_size, + threshold=threshold, + cp_isolation=cp_isolation, + cp_exclusion=cp_exclusion, + preemptive_behaviour=preemptive_behaviour, + explain=explain, + language_threshold=language_threshold, + enable_fallback=enable_fallback, + ) + elif isinstance( + fp_or_path_or_payload, + ( + bytes, + bytearray, + ), + ): + guesses = from_bytes( + fp_or_path_or_payload, + steps=steps, + chunk_size=chunk_size, + threshold=threshold, + cp_isolation=cp_isolation, + cp_exclusion=cp_exclusion, + preemptive_behaviour=preemptive_behaviour, + explain=explain, + language_threshold=language_threshold, + enable_fallback=enable_fallback, + ) + else: + guesses = from_fp( + fp_or_path_or_payload, + steps=steps, + chunk_size=chunk_size, + threshold=threshold, + cp_isolation=cp_isolation, + cp_exclusion=cp_exclusion, + preemptive_behaviour=preemptive_behaviour, + explain=explain, + language_threshold=language_threshold, + enable_fallback=enable_fallback, + ) + + return not guesses diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/cd.py b/venv/lib/python3.10/site-packages/charset_normalizer/cd.py new file mode 100644 index 0000000..71a3ed5 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/cd.py @@ -0,0 +1,395 @@ +from __future__ import annotations + +import importlib +from codecs import IncrementalDecoder +from collections import Counter +from functools import lru_cache +from typing import Counter as TypeCounter + +from .constant import ( + FREQUENCIES, + KO_NAMES, + LANGUAGE_SUPPORTED_COUNT, + TOO_SMALL_SEQUENCE, + ZH_NAMES, +) +from .md import is_suspiciously_successive_range +from .models import CoherenceMatches +from .utils import ( + is_accentuated, + is_latin, + is_multi_byte_encoding, + is_unicode_range_secondary, + unicode_range, +) + + +def encoding_unicode_range(iana_name: str) -> list[str]: + """ + Return associated unicode ranges in a single byte code page. + """ + if is_multi_byte_encoding(iana_name): + raise OSError("Function not supported on multi-byte code page") + + decoder = importlib.import_module(f"encodings.{iana_name}").IncrementalDecoder + + p: IncrementalDecoder = decoder(errors="ignore") + seen_ranges: dict[str, int] = {} + character_count: int = 0 + + for i in range(0x40, 0xFF): + chunk: str = p.decode(bytes([i])) + + if chunk: + character_range: str | None = unicode_range(chunk) + + if character_range is None: + continue + + if is_unicode_range_secondary(character_range) is False: + if character_range not in seen_ranges: + seen_ranges[character_range] = 0 + seen_ranges[character_range] += 1 + character_count += 1 + + return sorted( + [ + character_range + for character_range in seen_ranges + if seen_ranges[character_range] / character_count >= 0.15 + ] + ) + + +def unicode_range_languages(primary_range: str) -> list[str]: + """ + Return inferred languages used with a unicode range. + """ + languages: list[str] = [] + + for language, characters in FREQUENCIES.items(): + for character in characters: + if unicode_range(character) == primary_range: + languages.append(language) + break + + return languages + + +@lru_cache() +def encoding_languages(iana_name: str) -> list[str]: + """ + Single-byte encoding language association. Some code page are heavily linked to particular language(s). + This function does the correspondence. + """ + unicode_ranges: list[str] = encoding_unicode_range(iana_name) + primary_range: str | None = None + + for specified_range in unicode_ranges: + if "Latin" not in specified_range: + primary_range = specified_range + break + + if primary_range is None: + return ["Latin Based"] + + return unicode_range_languages(primary_range) + + +@lru_cache() +def mb_encoding_languages(iana_name: str) -> list[str]: + """ + Multi-byte encoding language association. Some code page are heavily linked to particular language(s). + This function does the correspondence. + """ + if ( + iana_name.startswith("shift_") + or iana_name.startswith("iso2022_jp") + or iana_name.startswith("euc_j") + or iana_name == "cp932" + ): + return ["Japanese"] + if iana_name.startswith("gb") or iana_name in ZH_NAMES: + return ["Chinese"] + if iana_name.startswith("iso2022_kr") or iana_name in KO_NAMES: + return ["Korean"] + + return [] + + +@lru_cache(maxsize=LANGUAGE_SUPPORTED_COUNT) +def get_target_features(language: str) -> tuple[bool, bool]: + """ + Determine main aspects from a supported language if it contains accents and if is pure Latin. + """ + target_have_accents: bool = False + target_pure_latin: bool = True + + for character in FREQUENCIES[language]: + if not target_have_accents and is_accentuated(character): + target_have_accents = True + if target_pure_latin and is_latin(character) is False: + target_pure_latin = False + + return target_have_accents, target_pure_latin + + +def alphabet_languages( + characters: list[str], ignore_non_latin: bool = False +) -> list[str]: + """ + Return associated languages associated to given characters. + """ + languages: list[tuple[str, float]] = [] + + source_have_accents = any(is_accentuated(character) for character in characters) + + for language, language_characters in FREQUENCIES.items(): + target_have_accents, target_pure_latin = get_target_features(language) + + if ignore_non_latin and target_pure_latin is False: + continue + + if target_have_accents is False and source_have_accents: + continue + + character_count: int = len(language_characters) + + character_match_count: int = len( + [c for c in language_characters if c in characters] + ) + + ratio: float = character_match_count / character_count + + if ratio >= 0.2: + languages.append((language, ratio)) + + languages = sorted(languages, key=lambda x: x[1], reverse=True) + + return [compatible_language[0] for compatible_language in languages] + + +def characters_popularity_compare( + language: str, ordered_characters: list[str] +) -> float: + """ + Determine if a ordered characters list (by occurrence from most appearance to rarest) match a particular language. + The result is a ratio between 0. (absolutely no correspondence) and 1. (near perfect fit). + Beware that is function is not strict on the match in order to ease the detection. (Meaning close match is 1.) + """ + if language not in FREQUENCIES: + raise ValueError(f"{language} not available") + + character_approved_count: int = 0 + FREQUENCIES_language_set = set(FREQUENCIES[language]) + + ordered_characters_count: int = len(ordered_characters) + target_language_characters_count: int = len(FREQUENCIES[language]) + + large_alphabet: bool = target_language_characters_count > 26 + + for character, character_rank in zip( + ordered_characters, range(0, ordered_characters_count) + ): + if character not in FREQUENCIES_language_set: + continue + + character_rank_in_language: int = FREQUENCIES[language].index(character) + expected_projection_ratio: float = ( + target_language_characters_count / ordered_characters_count + ) + character_rank_projection: int = int(character_rank * expected_projection_ratio) + + if ( + large_alphabet is False + and abs(character_rank_projection - character_rank_in_language) > 4 + ): + continue + + if ( + large_alphabet is True + and abs(character_rank_projection - character_rank_in_language) + < target_language_characters_count / 3 + ): + character_approved_count += 1 + continue + + characters_before_source: list[str] = FREQUENCIES[language][ + 0:character_rank_in_language + ] + characters_after_source: list[str] = FREQUENCIES[language][ + character_rank_in_language: + ] + characters_before: list[str] = ordered_characters[0:character_rank] + characters_after: list[str] = ordered_characters[character_rank:] + + before_match_count: int = len( + set(characters_before) & set(characters_before_source) + ) + + after_match_count: int = len( + set(characters_after) & set(characters_after_source) + ) + + if len(characters_before_source) == 0 and before_match_count <= 4: + character_approved_count += 1 + continue + + if len(characters_after_source) == 0 and after_match_count <= 4: + character_approved_count += 1 + continue + + if ( + before_match_count / len(characters_before_source) >= 0.4 + or after_match_count / len(characters_after_source) >= 0.4 + ): + character_approved_count += 1 + continue + + return character_approved_count / len(ordered_characters) + + +def alpha_unicode_split(decoded_sequence: str) -> list[str]: + """ + Given a decoded text sequence, return a list of str. Unicode range / alphabet separation. + Ex. a text containing English/Latin with a bit a Hebrew will return two items in the resulting list; + One containing the latin letters and the other hebrew. + """ + layers: dict[str, str] = {} + + for character in decoded_sequence: + if character.isalpha() is False: + continue + + character_range: str | None = unicode_range(character) + + if character_range is None: + continue + + layer_target_range: str | None = None + + for discovered_range in layers: + if ( + is_suspiciously_successive_range(discovered_range, character_range) + is False + ): + layer_target_range = discovered_range + break + + if layer_target_range is None: + layer_target_range = character_range + + if layer_target_range not in layers: + layers[layer_target_range] = character.lower() + continue + + layers[layer_target_range] += character.lower() + + return list(layers.values()) + + +def merge_coherence_ratios(results: list[CoherenceMatches]) -> CoherenceMatches: + """ + This function merge results previously given by the function coherence_ratio. + The return type is the same as coherence_ratio. + """ + per_language_ratios: dict[str, list[float]] = {} + for result in results: + for sub_result in result: + language, ratio = sub_result + if language not in per_language_ratios: + per_language_ratios[language] = [ratio] + continue + per_language_ratios[language].append(ratio) + + merge = [ + ( + language, + round( + sum(per_language_ratios[language]) / len(per_language_ratios[language]), + 4, + ), + ) + for language in per_language_ratios + ] + + return sorted(merge, key=lambda x: x[1], reverse=True) + + +def filter_alt_coherence_matches(results: CoherenceMatches) -> CoherenceMatches: + """ + We shall NOT return "English—" in CoherenceMatches because it is an alternative + of "English". This function only keeps the best match and remove the em-dash in it. + """ + index_results: dict[str, list[float]] = dict() + + for result in results: + language, ratio = result + no_em_name: str = language.replace("—", "") + + if no_em_name not in index_results: + index_results[no_em_name] = [] + + index_results[no_em_name].append(ratio) + + if any(len(index_results[e]) > 1 for e in index_results): + filtered_results: CoherenceMatches = [] + + for language in index_results: + filtered_results.append((language, max(index_results[language]))) + + return filtered_results + + return results + + +@lru_cache(maxsize=2048) +def coherence_ratio( + decoded_sequence: str, threshold: float = 0.1, lg_inclusion: str | None = None +) -> CoherenceMatches: + """ + Detect ANY language that can be identified in given sequence. The sequence will be analysed by layers. + A layer = Character extraction by alphabets/ranges. + """ + + results: list[tuple[str, float]] = [] + ignore_non_latin: bool = False + + sufficient_match_count: int = 0 + + lg_inclusion_list = lg_inclusion.split(",") if lg_inclusion is not None else [] + if "Latin Based" in lg_inclusion_list: + ignore_non_latin = True + lg_inclusion_list.remove("Latin Based") + + for layer in alpha_unicode_split(decoded_sequence): + sequence_frequencies: TypeCounter[str] = Counter(layer) + most_common = sequence_frequencies.most_common() + + character_count: int = sum(o for c, o in most_common) + + if character_count <= TOO_SMALL_SEQUENCE: + continue + + popular_character_ordered: list[str] = [c for c, o in most_common] + + for language in lg_inclusion_list or alphabet_languages( + popular_character_ordered, ignore_non_latin + ): + ratio: float = characters_popularity_compare( + language, popular_character_ordered + ) + + if ratio < threshold: + continue + elif ratio >= 0.8: + sufficient_match_count += 1 + + results.append((language, round(ratio, 4))) + + if sufficient_match_count >= 3: + break + + return sorted( + filter_alt_coherence_matches(results), key=lambda x: x[1], reverse=True + ) diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/cli/__init__.py b/venv/lib/python3.10/site-packages/charset_normalizer/cli/__init__.py new file mode 100644 index 0000000..543a5a4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/cli/__init__.py @@ -0,0 +1,8 @@ +from __future__ import annotations + +from .__main__ import cli_detect, query_yes_no + +__all__ = ( + "cli_detect", + "query_yes_no", +) diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/cli/__main__.py b/venv/lib/python3.10/site-packages/charset_normalizer/cli/__main__.py new file mode 100644 index 0000000..cb64156 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/cli/__main__.py @@ -0,0 +1,381 @@ +from __future__ import annotations + +import argparse +import sys +import typing +from json import dumps +from os.path import abspath, basename, dirname, join, realpath +from platform import python_version +from unicodedata import unidata_version + +import charset_normalizer.md as md_module +from charset_normalizer import from_fp +from charset_normalizer.models import CliDetectionResult +from charset_normalizer.version import __version__ + + +def query_yes_no(question: str, default: str = "yes") -> bool: + """Ask a yes/no question via input() and return their answer. + + "question" is a string that is presented to the user. + "default" is the presumed answer if the user just hits . + It must be "yes" (the default), "no" or None (meaning + an answer is required of the user). + + The "answer" return value is True for "yes" or False for "no". + + Credit goes to (c) https://stackoverflow.com/questions/3041986/apt-command-line-interface-like-yes-no-input + """ + valid = {"yes": True, "y": True, "ye": True, "no": False, "n": False} + if default is None: + prompt = " [y/n] " + elif default == "yes": + prompt = " [Y/n] " + elif default == "no": + prompt = " [y/N] " + else: + raise ValueError("invalid default answer: '%s'" % default) + + while True: + sys.stdout.write(question + prompt) + choice = input().lower() + if default is not None and choice == "": + return valid[default] + elif choice in valid: + return valid[choice] + else: + sys.stdout.write("Please respond with 'yes' or 'no' (or 'y' or 'n').\n") + + +class FileType: + """Factory for creating file object types + + Instances of FileType are typically passed as type= arguments to the + ArgumentParser add_argument() method. + + Keyword Arguments: + - mode -- A string indicating how the file is to be opened. Accepts the + same values as the builtin open() function. + - bufsize -- The file's desired buffer size. Accepts the same values as + the builtin open() function. + - encoding -- The file's encoding. Accepts the same values as the + builtin open() function. + - errors -- A string indicating how encoding and decoding errors are to + be handled. Accepts the same value as the builtin open() function. + + Backported from CPython 3.12 + """ + + def __init__( + self, + mode: str = "r", + bufsize: int = -1, + encoding: str | None = None, + errors: str | None = None, + ): + self._mode = mode + self._bufsize = bufsize + self._encoding = encoding + self._errors = errors + + def __call__(self, string: str) -> typing.IO: # type: ignore[type-arg] + # the special argument "-" means sys.std{in,out} + if string == "-": + if "r" in self._mode: + return sys.stdin.buffer if "b" in self._mode else sys.stdin + elif any(c in self._mode for c in "wax"): + return sys.stdout.buffer if "b" in self._mode else sys.stdout + else: + msg = f'argument "-" with mode {self._mode}' + raise ValueError(msg) + + # all other arguments are used as file names + try: + return open(string, self._mode, self._bufsize, self._encoding, self._errors) + except OSError as e: + message = f"can't open '{string}': {e}" + raise argparse.ArgumentTypeError(message) + + def __repr__(self) -> str: + args = self._mode, self._bufsize + kwargs = [("encoding", self._encoding), ("errors", self._errors)] + args_str = ", ".join( + [repr(arg) for arg in args if arg != -1] + + [f"{kw}={arg!r}" for kw, arg in kwargs if arg is not None] + ) + return f"{type(self).__name__}({args_str})" + + +def cli_detect(argv: list[str] | None = None) -> int: + """ + CLI assistant using ARGV and ArgumentParser + :param argv: + :return: 0 if everything is fine, anything else equal trouble + """ + parser = argparse.ArgumentParser( + description="The Real First Universal Charset Detector. " + "Discover originating encoding used on text file. " + "Normalize text to unicode." + ) + + parser.add_argument( + "files", type=FileType("rb"), nargs="+", help="File(s) to be analysed" + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + default=False, + dest="verbose", + help="Display complementary information about file if any. " + "Stdout will contain logs about the detection process.", + ) + parser.add_argument( + "-a", + "--with-alternative", + action="store_true", + default=False, + dest="alternatives", + help="Output complementary possibilities if any. Top-level JSON WILL be a list.", + ) + parser.add_argument( + "-n", + "--normalize", + action="store_true", + default=False, + dest="normalize", + help="Permit to normalize input file. If not set, program does not write anything.", + ) + parser.add_argument( + "-m", + "--minimal", + action="store_true", + default=False, + dest="minimal", + help="Only output the charset detected to STDOUT. Disabling JSON output.", + ) + parser.add_argument( + "-r", + "--replace", + action="store_true", + default=False, + dest="replace", + help="Replace file when trying to normalize it instead of creating a new one.", + ) + parser.add_argument( + "-f", + "--force", + action="store_true", + default=False, + dest="force", + help="Replace file without asking if you are sure, use this flag with caution.", + ) + parser.add_argument( + "-i", + "--no-preemptive", + action="store_true", + default=False, + dest="no_preemptive", + help="Disable looking at a charset declaration to hint the detector.", + ) + parser.add_argument( + "-t", + "--threshold", + action="store", + default=0.2, + type=float, + dest="threshold", + help="Define a custom maximum amount of noise allowed in decoded content. 0. <= noise <= 1.", + ) + parser.add_argument( + "--version", + action="version", + version="Charset-Normalizer {} - Python {} - Unicode {} - SpeedUp {}".format( + __version__, + python_version(), + unidata_version, + "OFF" if md_module.__file__.lower().endswith(".py") else "ON", + ), + help="Show version information and exit.", + ) + + args = parser.parse_args(argv) + + if args.replace is True and args.normalize is False: + if args.files: + for my_file in args.files: + my_file.close() + print("Use --replace in addition of --normalize only.", file=sys.stderr) + return 1 + + if args.force is True and args.replace is False: + if args.files: + for my_file in args.files: + my_file.close() + print("Use --force in addition of --replace only.", file=sys.stderr) + return 1 + + if args.threshold < 0.0 or args.threshold > 1.0: + if args.files: + for my_file in args.files: + my_file.close() + print("--threshold VALUE should be between 0. AND 1.", file=sys.stderr) + return 1 + + x_ = [] + + for my_file in args.files: + matches = from_fp( + my_file, + threshold=args.threshold, + explain=args.verbose, + preemptive_behaviour=args.no_preemptive is False, + ) + + best_guess = matches.best() + + if best_guess is None: + print( + 'Unable to identify originating encoding for "{}". {}'.format( + my_file.name, + ( + "Maybe try increasing maximum amount of chaos." + if args.threshold < 1.0 + else "" + ), + ), + file=sys.stderr, + ) + x_.append( + CliDetectionResult( + abspath(my_file.name), + None, + [], + [], + "Unknown", + [], + False, + 1.0, + 0.0, + None, + True, + ) + ) + else: + x_.append( + CliDetectionResult( + abspath(my_file.name), + best_guess.encoding, + best_guess.encoding_aliases, + [ + cp + for cp in best_guess.could_be_from_charset + if cp != best_guess.encoding + ], + best_guess.language, + best_guess.alphabets, + best_guess.bom, + best_guess.percent_chaos, + best_guess.percent_coherence, + None, + True, + ) + ) + + if len(matches) > 1 and args.alternatives: + for el in matches: + if el != best_guess: + x_.append( + CliDetectionResult( + abspath(my_file.name), + el.encoding, + el.encoding_aliases, + [ + cp + for cp in el.could_be_from_charset + if cp != el.encoding + ], + el.language, + el.alphabets, + el.bom, + el.percent_chaos, + el.percent_coherence, + None, + False, + ) + ) + + if args.normalize is True: + if best_guess.encoding.startswith("utf") is True: + print( + '"{}" file does not need to be normalized, as it already came from unicode.'.format( + my_file.name + ), + file=sys.stderr, + ) + if my_file.closed is False: + my_file.close() + continue + + dir_path = dirname(realpath(my_file.name)) + file_name = basename(realpath(my_file.name)) + + o_: list[str] = file_name.split(".") + + if args.replace is False: + o_.insert(-1, best_guess.encoding) + if my_file.closed is False: + my_file.close() + elif ( + args.force is False + and query_yes_no( + 'Are you sure to normalize "{}" by replacing it ?'.format( + my_file.name + ), + "no", + ) + is False + ): + if my_file.closed is False: + my_file.close() + continue + + try: + x_[0].unicode_path = join(dir_path, ".".join(o_)) + + with open(x_[0].unicode_path, "wb") as fp: + fp.write(best_guess.output()) + except OSError as e: + print(str(e), file=sys.stderr) + if my_file.closed is False: + my_file.close() + return 2 + + if my_file.closed is False: + my_file.close() + + if args.minimal is False: + print( + dumps( + [el.__dict__ for el in x_] if len(x_) > 1 else x_[0].__dict__, + ensure_ascii=True, + indent=4, + ) + ) + else: + for my_file in args.files: + print( + ", ".join( + [ + el.encoding or "undefined" + for el in x_ + if el.path == abspath(my_file.name) + ] + ) + ) + + return 0 + + +if __name__ == "__main__": + cli_detect() diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/cli/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/cli/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..4230ba9 Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/cli/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/cli/__pycache__/__main__.cpython-310.pyc b/venv/lib/python3.10/site-packages/charset_normalizer/cli/__pycache__/__main__.cpython-310.pyc new file mode 100644 index 0000000..5f6e410 Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/cli/__pycache__/__main__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/constant.py b/venv/lib/python3.10/site-packages/charset_normalizer/constant.py new file mode 100644 index 0000000..cc71a01 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/constant.py @@ -0,0 +1,2015 @@ +from __future__ import annotations + +from codecs import BOM_UTF8, BOM_UTF16_BE, BOM_UTF16_LE, BOM_UTF32_BE, BOM_UTF32_LE +from encodings.aliases import aliases +from re import IGNORECASE +from re import compile as re_compile + +# Contain for each eligible encoding a list of/item bytes SIG/BOM +ENCODING_MARKS: dict[str, bytes | list[bytes]] = { + "utf_8": BOM_UTF8, + "utf_7": [ + b"\x2b\x2f\x76\x38", + b"\x2b\x2f\x76\x39", + b"\x2b\x2f\x76\x2b", + b"\x2b\x2f\x76\x2f", + b"\x2b\x2f\x76\x38\x2d", + ], + "gb18030": b"\x84\x31\x95\x33", + "utf_32": [BOM_UTF32_BE, BOM_UTF32_LE], + "utf_16": [BOM_UTF16_BE, BOM_UTF16_LE], +} + +TOO_SMALL_SEQUENCE: int = 32 +TOO_BIG_SEQUENCE: int = int(10e6) + +UTF8_MAXIMAL_ALLOCATION: int = 1_112_064 + +# Up-to-date Unicode ucd/15.0.0 +UNICODE_RANGES_COMBINED: dict[str, range] = { + "Control character": range(32), + "Basic Latin": range(32, 128), + "Latin-1 Supplement": range(128, 256), + "Latin Extended-A": range(256, 384), + "Latin Extended-B": range(384, 592), + "IPA Extensions": range(592, 688), + "Spacing Modifier Letters": range(688, 768), + "Combining Diacritical Marks": range(768, 880), + "Greek and Coptic": range(880, 1024), + "Cyrillic": range(1024, 1280), + "Cyrillic Supplement": range(1280, 1328), + "Armenian": range(1328, 1424), + "Hebrew": range(1424, 1536), + "Arabic": range(1536, 1792), + "Syriac": range(1792, 1872), + "Arabic Supplement": range(1872, 1920), + "Thaana": range(1920, 1984), + "NKo": range(1984, 2048), + "Samaritan": range(2048, 2112), + "Mandaic": range(2112, 2144), + "Syriac Supplement": range(2144, 2160), + "Arabic Extended-B": range(2160, 2208), + "Arabic Extended-A": range(2208, 2304), + "Devanagari": range(2304, 2432), + "Bengali": range(2432, 2560), + "Gurmukhi": range(2560, 2688), + "Gujarati": range(2688, 2816), + "Oriya": range(2816, 2944), + "Tamil": range(2944, 3072), + "Telugu": range(3072, 3200), + "Kannada": range(3200, 3328), + "Malayalam": range(3328, 3456), + "Sinhala": range(3456, 3584), + "Thai": range(3584, 3712), + "Lao": range(3712, 3840), + "Tibetan": range(3840, 4096), + "Myanmar": range(4096, 4256), + "Georgian": range(4256, 4352), + "Hangul Jamo": range(4352, 4608), + "Ethiopic": range(4608, 4992), + "Ethiopic Supplement": range(4992, 5024), + "Cherokee": range(5024, 5120), + "Unified Canadian Aboriginal Syllabics": range(5120, 5760), + "Ogham": range(5760, 5792), + "Runic": range(5792, 5888), + "Tagalog": range(5888, 5920), + "Hanunoo": range(5920, 5952), + "Buhid": range(5952, 5984), + "Tagbanwa": range(5984, 6016), + "Khmer": range(6016, 6144), + "Mongolian": range(6144, 6320), + "Unified Canadian Aboriginal Syllabics Extended": range(6320, 6400), + "Limbu": range(6400, 6480), + "Tai Le": range(6480, 6528), + "New Tai Lue": range(6528, 6624), + "Khmer Symbols": range(6624, 6656), + "Buginese": range(6656, 6688), + "Tai Tham": range(6688, 6832), + "Combining Diacritical Marks Extended": range(6832, 6912), + "Balinese": range(6912, 7040), + "Sundanese": range(7040, 7104), + "Batak": range(7104, 7168), + "Lepcha": range(7168, 7248), + "Ol Chiki": range(7248, 7296), + "Cyrillic Extended-C": range(7296, 7312), + "Georgian Extended": range(7312, 7360), + "Sundanese Supplement": range(7360, 7376), + "Vedic Extensions": range(7376, 7424), + "Phonetic Extensions": range(7424, 7552), + "Phonetic Extensions Supplement": range(7552, 7616), + "Combining Diacritical Marks Supplement": range(7616, 7680), + "Latin Extended Additional": range(7680, 7936), + "Greek Extended": range(7936, 8192), + "General Punctuation": range(8192, 8304), + "Superscripts and Subscripts": range(8304, 8352), + "Currency Symbols": range(8352, 8400), + "Combining Diacritical Marks for Symbols": range(8400, 8448), + "Letterlike Symbols": range(8448, 8528), + "Number Forms": range(8528, 8592), + "Arrows": range(8592, 8704), + "Mathematical Operators": range(8704, 8960), + "Miscellaneous Technical": range(8960, 9216), + "Control Pictures": range(9216, 9280), + "Optical Character Recognition": range(9280, 9312), + "Enclosed Alphanumerics": range(9312, 9472), + "Box Drawing": range(9472, 9600), + "Block Elements": range(9600, 9632), + "Geometric Shapes": range(9632, 9728), + "Miscellaneous Symbols": range(9728, 9984), + "Dingbats": range(9984, 10176), + "Miscellaneous Mathematical Symbols-A": range(10176, 10224), + "Supplemental Arrows-A": range(10224, 10240), + "Braille Patterns": range(10240, 10496), + "Supplemental Arrows-B": range(10496, 10624), + "Miscellaneous Mathematical Symbols-B": range(10624, 10752), + "Supplemental Mathematical Operators": range(10752, 11008), + "Miscellaneous Symbols and Arrows": range(11008, 11264), + "Glagolitic": range(11264, 11360), + "Latin Extended-C": range(11360, 11392), + "Coptic": range(11392, 11520), + "Georgian Supplement": range(11520, 11568), + "Tifinagh": range(11568, 11648), + "Ethiopic Extended": range(11648, 11744), + "Cyrillic Extended-A": range(11744, 11776), + "Supplemental Punctuation": range(11776, 11904), + "CJK Radicals Supplement": range(11904, 12032), + "Kangxi Radicals": range(12032, 12256), + "Ideographic Description Characters": range(12272, 12288), + "CJK Symbols and Punctuation": range(12288, 12352), + "Hiragana": range(12352, 12448), + "Katakana": range(12448, 12544), + "Bopomofo": range(12544, 12592), + "Hangul Compatibility Jamo": range(12592, 12688), + "Kanbun": range(12688, 12704), + "Bopomofo Extended": range(12704, 12736), + "CJK Strokes": range(12736, 12784), + "Katakana Phonetic Extensions": range(12784, 12800), + "Enclosed CJK Letters and Months": range(12800, 13056), + "CJK Compatibility": range(13056, 13312), + "CJK Unified Ideographs Extension A": range(13312, 19904), + "Yijing Hexagram Symbols": range(19904, 19968), + "CJK Unified Ideographs": range(19968, 40960), + "Yi Syllables": range(40960, 42128), + "Yi Radicals": range(42128, 42192), + "Lisu": range(42192, 42240), + "Vai": range(42240, 42560), + "Cyrillic Extended-B": range(42560, 42656), + "Bamum": range(42656, 42752), + "Modifier Tone Letters": range(42752, 42784), + "Latin Extended-D": range(42784, 43008), + "Syloti Nagri": range(43008, 43056), + "Common Indic Number Forms": range(43056, 43072), + "Phags-pa": range(43072, 43136), + "Saurashtra": range(43136, 43232), + "Devanagari Extended": range(43232, 43264), + "Kayah Li": range(43264, 43312), + "Rejang": range(43312, 43360), + "Hangul Jamo Extended-A": range(43360, 43392), + "Javanese": range(43392, 43488), + "Myanmar Extended-B": range(43488, 43520), + "Cham": range(43520, 43616), + "Myanmar Extended-A": range(43616, 43648), + "Tai Viet": range(43648, 43744), + "Meetei Mayek Extensions": range(43744, 43776), + "Ethiopic Extended-A": range(43776, 43824), + "Latin Extended-E": range(43824, 43888), + "Cherokee Supplement": range(43888, 43968), + "Meetei Mayek": range(43968, 44032), + "Hangul Syllables": range(44032, 55216), + "Hangul Jamo Extended-B": range(55216, 55296), + "High Surrogates": range(55296, 56192), + "High Private Use Surrogates": range(56192, 56320), + "Low Surrogates": range(56320, 57344), + "Private Use Area": range(57344, 63744), + "CJK Compatibility Ideographs": range(63744, 64256), + "Alphabetic Presentation Forms": range(64256, 64336), + "Arabic Presentation Forms-A": range(64336, 65024), + "Variation Selectors": range(65024, 65040), + "Vertical Forms": range(65040, 65056), + "Combining Half Marks": range(65056, 65072), + "CJK Compatibility Forms": range(65072, 65104), + "Small Form Variants": range(65104, 65136), + "Arabic Presentation Forms-B": range(65136, 65280), + "Halfwidth and Fullwidth Forms": range(65280, 65520), + "Specials": range(65520, 65536), + "Linear B Syllabary": range(65536, 65664), + "Linear B Ideograms": range(65664, 65792), + "Aegean Numbers": range(65792, 65856), + "Ancient Greek Numbers": range(65856, 65936), + "Ancient Symbols": range(65936, 66000), + "Phaistos Disc": range(66000, 66048), + "Lycian": range(66176, 66208), + "Carian": range(66208, 66272), + "Coptic Epact Numbers": range(66272, 66304), + "Old Italic": range(66304, 66352), + "Gothic": range(66352, 66384), + "Old Permic": range(66384, 66432), + "Ugaritic": range(66432, 66464), + "Old Persian": range(66464, 66528), + "Deseret": range(66560, 66640), + "Shavian": range(66640, 66688), + "Osmanya": range(66688, 66736), + "Osage": range(66736, 66816), + "Elbasan": range(66816, 66864), + "Caucasian Albanian": range(66864, 66928), + "Vithkuqi": range(66928, 67008), + "Linear A": range(67072, 67456), + "Latin Extended-F": range(67456, 67520), + "Cypriot Syllabary": range(67584, 67648), + "Imperial Aramaic": range(67648, 67680), + "Palmyrene": range(67680, 67712), + "Nabataean": range(67712, 67760), + "Hatran": range(67808, 67840), + "Phoenician": range(67840, 67872), + "Lydian": range(67872, 67904), + "Meroitic Hieroglyphs": range(67968, 68000), + "Meroitic Cursive": range(68000, 68096), + "Kharoshthi": range(68096, 68192), + "Old South Arabian": range(68192, 68224), + "Old North Arabian": range(68224, 68256), + "Manichaean": range(68288, 68352), + "Avestan": range(68352, 68416), + "Inscriptional Parthian": range(68416, 68448), + "Inscriptional Pahlavi": range(68448, 68480), + "Psalter Pahlavi": range(68480, 68528), + "Old Turkic": range(68608, 68688), + "Old Hungarian": range(68736, 68864), + "Hanifi Rohingya": range(68864, 68928), + "Rumi Numeral Symbols": range(69216, 69248), + "Yezidi": range(69248, 69312), + "Arabic Extended-C": range(69312, 69376), + "Old Sogdian": range(69376, 69424), + "Sogdian": range(69424, 69488), + "Old Uyghur": range(69488, 69552), + "Chorasmian": range(69552, 69600), + "Elymaic": range(69600, 69632), + "Brahmi": range(69632, 69760), + "Kaithi": range(69760, 69840), + "Sora Sompeng": range(69840, 69888), + "Chakma": range(69888, 69968), + "Mahajani": range(69968, 70016), + "Sharada": range(70016, 70112), + "Sinhala Archaic Numbers": range(70112, 70144), + "Khojki": range(70144, 70224), + "Multani": range(70272, 70320), + "Khudawadi": range(70320, 70400), + "Grantha": range(70400, 70528), + "Newa": range(70656, 70784), + "Tirhuta": range(70784, 70880), + "Siddham": range(71040, 71168), + "Modi": range(71168, 71264), + "Mongolian Supplement": range(71264, 71296), + "Takri": range(71296, 71376), + "Ahom": range(71424, 71504), + "Dogra": range(71680, 71760), + "Warang Citi": range(71840, 71936), + "Dives Akuru": range(71936, 72032), + "Nandinagari": range(72096, 72192), + "Zanabazar Square": range(72192, 72272), + "Soyombo": range(72272, 72368), + "Unified Canadian Aboriginal Syllabics Extended-A": range(72368, 72384), + "Pau Cin Hau": range(72384, 72448), + "Devanagari Extended-A": range(72448, 72544), + "Bhaiksuki": range(72704, 72816), + "Marchen": range(72816, 72896), + "Masaram Gondi": range(72960, 73056), + "Gunjala Gondi": range(73056, 73136), + "Makasar": range(73440, 73472), + "Kawi": range(73472, 73568), + "Lisu Supplement": range(73648, 73664), + "Tamil Supplement": range(73664, 73728), + "Cuneiform": range(73728, 74752), + "Cuneiform Numbers and Punctuation": range(74752, 74880), + "Early Dynastic Cuneiform": range(74880, 75088), + "Cypro-Minoan": range(77712, 77824), + "Egyptian Hieroglyphs": range(77824, 78896), + "Egyptian Hieroglyph Format Controls": range(78896, 78944), + "Anatolian Hieroglyphs": range(82944, 83584), + "Bamum Supplement": range(92160, 92736), + "Mro": range(92736, 92784), + "Tangsa": range(92784, 92880), + "Bassa Vah": range(92880, 92928), + "Pahawh Hmong": range(92928, 93072), + "Medefaidrin": range(93760, 93856), + "Miao": range(93952, 94112), + "Ideographic Symbols and Punctuation": range(94176, 94208), + "Tangut": range(94208, 100352), + "Tangut Components": range(100352, 101120), + "Khitan Small Script": range(101120, 101632), + "Tangut Supplement": range(101632, 101760), + "Kana Extended-B": range(110576, 110592), + "Kana Supplement": range(110592, 110848), + "Kana Extended-A": range(110848, 110896), + "Small Kana Extension": range(110896, 110960), + "Nushu": range(110960, 111360), + "Duployan": range(113664, 113824), + "Shorthand Format Controls": range(113824, 113840), + "Znamenny Musical Notation": range(118528, 118736), + "Byzantine Musical Symbols": range(118784, 119040), + "Musical Symbols": range(119040, 119296), + "Ancient Greek Musical Notation": range(119296, 119376), + "Kaktovik Numerals": range(119488, 119520), + "Mayan Numerals": range(119520, 119552), + "Tai Xuan Jing Symbols": range(119552, 119648), + "Counting Rod Numerals": range(119648, 119680), + "Mathematical Alphanumeric Symbols": range(119808, 120832), + "Sutton SignWriting": range(120832, 121520), + "Latin Extended-G": range(122624, 122880), + "Glagolitic Supplement": range(122880, 122928), + "Cyrillic Extended-D": range(122928, 123024), + "Nyiakeng Puachue Hmong": range(123136, 123216), + "Toto": range(123536, 123584), + "Wancho": range(123584, 123648), + "Nag Mundari": range(124112, 124160), + "Ethiopic Extended-B": range(124896, 124928), + "Mende Kikakui": range(124928, 125152), + "Adlam": range(125184, 125280), + "Indic Siyaq Numbers": range(126064, 126144), + "Ottoman Siyaq Numbers": range(126208, 126288), + "Arabic Mathematical Alphabetic Symbols": range(126464, 126720), + "Mahjong Tiles": range(126976, 127024), + "Domino Tiles": range(127024, 127136), + "Playing Cards": range(127136, 127232), + "Enclosed Alphanumeric Supplement": range(127232, 127488), + "Enclosed Ideographic Supplement": range(127488, 127744), + "Miscellaneous Symbols and Pictographs": range(127744, 128512), + "Emoticons range(Emoji)": range(128512, 128592), + "Ornamental Dingbats": range(128592, 128640), + "Transport and Map Symbols": range(128640, 128768), + "Alchemical Symbols": range(128768, 128896), + "Geometric Shapes Extended": range(128896, 129024), + "Supplemental Arrows-C": range(129024, 129280), + "Supplemental Symbols and Pictographs": range(129280, 129536), + "Chess Symbols": range(129536, 129648), + "Symbols and Pictographs Extended-A": range(129648, 129792), + "Symbols for Legacy Computing": range(129792, 130048), + "CJK Unified Ideographs Extension B": range(131072, 173792), + "CJK Unified Ideographs Extension C": range(173824, 177984), + "CJK Unified Ideographs Extension D": range(177984, 178208), + "CJK Unified Ideographs Extension E": range(178208, 183984), + "CJK Unified Ideographs Extension F": range(183984, 191472), + "CJK Compatibility Ideographs Supplement": range(194560, 195104), + "CJK Unified Ideographs Extension G": range(196608, 201552), + "CJK Unified Ideographs Extension H": range(201552, 205744), + "Tags": range(917504, 917632), + "Variation Selectors Supplement": range(917760, 918000), + "Supplementary Private Use Area-A": range(983040, 1048576), + "Supplementary Private Use Area-B": range(1048576, 1114112), +} + + +UNICODE_SECONDARY_RANGE_KEYWORD: list[str] = [ + "Supplement", + "Extended", + "Extensions", + "Modifier", + "Marks", + "Punctuation", + "Symbols", + "Forms", + "Operators", + "Miscellaneous", + "Drawing", + "Block", + "Shapes", + "Supplemental", + "Tags", +] + +RE_POSSIBLE_ENCODING_INDICATION = re_compile( + r"(?:(?:encoding)|(?:charset)|(?:coding))(?:[\:= ]{1,10})(?:[\"\']?)([a-zA-Z0-9\-_]+)(?:[\"\']?)", + IGNORECASE, +) + +IANA_NO_ALIASES = [ + "cp720", + "cp737", + "cp856", + "cp874", + "cp875", + "cp1006", + "koi8_r", + "koi8_t", + "koi8_u", +] + +IANA_SUPPORTED: list[str] = sorted( + filter( + lambda x: x.endswith("_codec") is False + and x not in {"rot_13", "tactis", "mbcs"}, + list(set(aliases.values())) + IANA_NO_ALIASES, + ) +) + +IANA_SUPPORTED_COUNT: int = len(IANA_SUPPORTED) + +# pre-computed code page that are similar using the function cp_similarity. +IANA_SUPPORTED_SIMILAR: dict[str, list[str]] = { + "cp037": ["cp1026", "cp1140", "cp273", "cp500"], + "cp1026": ["cp037", "cp1140", "cp273", "cp500"], + "cp1125": ["cp866"], + "cp1140": ["cp037", "cp1026", "cp273", "cp500"], + "cp1250": ["iso8859_2"], + "cp1251": ["kz1048", "ptcp154"], + "cp1252": ["iso8859_15", "iso8859_9", "latin_1"], + "cp1253": ["iso8859_7"], + "cp1254": ["iso8859_15", "iso8859_9", "latin_1"], + "cp1257": ["iso8859_13"], + "cp273": ["cp037", "cp1026", "cp1140", "cp500"], + "cp437": ["cp850", "cp858", "cp860", "cp861", "cp862", "cp863", "cp865"], + "cp500": ["cp037", "cp1026", "cp1140", "cp273"], + "cp850": ["cp437", "cp857", "cp858", "cp865"], + "cp857": ["cp850", "cp858", "cp865"], + "cp858": ["cp437", "cp850", "cp857", "cp865"], + "cp860": ["cp437", "cp861", "cp862", "cp863", "cp865"], + "cp861": ["cp437", "cp860", "cp862", "cp863", "cp865"], + "cp862": ["cp437", "cp860", "cp861", "cp863", "cp865"], + "cp863": ["cp437", "cp860", "cp861", "cp862", "cp865"], + "cp865": ["cp437", "cp850", "cp857", "cp858", "cp860", "cp861", "cp862", "cp863"], + "cp866": ["cp1125"], + "iso8859_10": ["iso8859_14", "iso8859_15", "iso8859_4", "iso8859_9", "latin_1"], + "iso8859_11": ["tis_620"], + "iso8859_13": ["cp1257"], + "iso8859_14": [ + "iso8859_10", + "iso8859_15", + "iso8859_16", + "iso8859_3", + "iso8859_9", + "latin_1", + ], + "iso8859_15": [ + "cp1252", + "cp1254", + "iso8859_10", + "iso8859_14", + "iso8859_16", + "iso8859_3", + "iso8859_9", + "latin_1", + ], + "iso8859_16": [ + "iso8859_14", + "iso8859_15", + "iso8859_2", + "iso8859_3", + "iso8859_9", + "latin_1", + ], + "iso8859_2": ["cp1250", "iso8859_16", "iso8859_4"], + "iso8859_3": ["iso8859_14", "iso8859_15", "iso8859_16", "iso8859_9", "latin_1"], + "iso8859_4": ["iso8859_10", "iso8859_2", "iso8859_9", "latin_1"], + "iso8859_7": ["cp1253"], + "iso8859_9": [ + "cp1252", + "cp1254", + "cp1258", + "iso8859_10", + "iso8859_14", + "iso8859_15", + "iso8859_16", + "iso8859_3", + "iso8859_4", + "latin_1", + ], + "kz1048": ["cp1251", "ptcp154"], + "latin_1": [ + "cp1252", + "cp1254", + "cp1258", + "iso8859_10", + "iso8859_14", + "iso8859_15", + "iso8859_16", + "iso8859_3", + "iso8859_4", + "iso8859_9", + ], + "mac_iceland": ["mac_roman", "mac_turkish"], + "mac_roman": ["mac_iceland", "mac_turkish"], + "mac_turkish": ["mac_iceland", "mac_roman"], + "ptcp154": ["cp1251", "kz1048"], + "tis_620": ["iso8859_11"], +} + + +CHARDET_CORRESPONDENCE: dict[str, str] = { + "iso2022_kr": "ISO-2022-KR", + "iso2022_jp": "ISO-2022-JP", + "euc_kr": "EUC-KR", + "tis_620": "TIS-620", + "utf_32": "UTF-32", + "euc_jp": "EUC-JP", + "koi8_r": "KOI8-R", + "iso8859_1": "ISO-8859-1", + "iso8859_2": "ISO-8859-2", + "iso8859_5": "ISO-8859-5", + "iso8859_6": "ISO-8859-6", + "iso8859_7": "ISO-8859-7", + "iso8859_8": "ISO-8859-8", + "utf_16": "UTF-16", + "cp855": "IBM855", + "mac_cyrillic": "MacCyrillic", + "gb2312": "GB2312", + "gb18030": "GB18030", + "cp932": "CP932", + "cp866": "IBM866", + "utf_8": "utf-8", + "utf_8_sig": "UTF-8-SIG", + "shift_jis": "SHIFT_JIS", + "big5": "Big5", + "cp1250": "windows-1250", + "cp1251": "windows-1251", + "cp1252": "Windows-1252", + "cp1253": "windows-1253", + "cp1255": "windows-1255", + "cp1256": "windows-1256", + "cp1254": "Windows-1254", + "cp949": "CP949", +} + + +COMMON_SAFE_ASCII_CHARACTERS: set[str] = { + "<", + ">", + "=", + ":", + "/", + "&", + ";", + "{", + "}", + "[", + "]", + ",", + "|", + '"', + "-", + "(", + ")", +} + +# Sample character sets — replace with full lists if needed +COMMON_CHINESE_CHARACTERS = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞" + +COMMON_JAPANESE_CHARACTERS = "日一国年大十二本中長出三時行見月分後前生五間上東四今金九入学高円子外八六下来気小七山話女北午百書先名川千水半男西電校語土木聞食車何南万毎白天母火右読友左休父雨" + +COMMON_KOREAN_CHARACTERS = "一二三四五六七八九十百千萬上下左右中人女子大小山川日月火水木金土父母天地國名年時文校學生" + +# Combine all into a set +COMMON_CJK_CHARACTERS = set( + "".join( + [ + COMMON_CHINESE_CHARACTERS, + COMMON_JAPANESE_CHARACTERS, + COMMON_KOREAN_CHARACTERS, + ] + ) +) + +KO_NAMES: set[str] = {"johab", "cp949", "euc_kr"} +ZH_NAMES: set[str] = {"big5", "cp950", "big5hkscs", "hz"} + +# Logging LEVEL below DEBUG +TRACE: int = 5 + + +# Language label that contain the em dash "—" +# character are to be considered alternative seq to origin +FREQUENCIES: dict[str, list[str]] = { + "English": [ + "e", + "a", + "t", + "i", + "o", + "n", + "s", + "r", + "h", + "l", + "d", + "c", + "u", + "m", + "f", + "p", + "g", + "w", + "y", + "b", + "v", + "k", + "x", + "j", + "z", + "q", + ], + "English—": [ + "e", + "a", + "t", + "i", + "o", + "n", + "s", + "r", + "h", + "l", + "d", + "c", + "m", + "u", + "f", + "p", + "g", + "w", + "b", + "y", + "v", + "k", + "j", + "x", + "z", + "q", + ], + "German": [ + "e", + "n", + "i", + "r", + "s", + "t", + "a", + "d", + "h", + "u", + "l", + "g", + "o", + "c", + "m", + "b", + "f", + "k", + "w", + "z", + "p", + "v", + "ü", + "ä", + "ö", + "j", + ], + "French": [ + "e", + "a", + "s", + "n", + "i", + "t", + "r", + "l", + "u", + "o", + "d", + "c", + "p", + "m", + "é", + "v", + "g", + "f", + "b", + "h", + "q", + "à", + "x", + "è", + "y", + "j", + ], + "Dutch": [ + "e", + "n", + "a", + "i", + "r", + "t", + "o", + "d", + "s", + "l", + "g", + "h", + "v", + "m", + "u", + "k", + "c", + "p", + "b", + "w", + "j", + "z", + "f", + "y", + "x", + "ë", + ], + "Italian": [ + "e", + "i", + "a", + "o", + "n", + "l", + "t", + "r", + "s", + "c", + "d", + "u", + "p", + "m", + "g", + "v", + "f", + "b", + "z", + "h", + "q", + "è", + "à", + "k", + "y", + "ò", + ], + "Polish": [ + "a", + "i", + "o", + "e", + "n", + "r", + "z", + "w", + "s", + "c", + "t", + "k", + "y", + "d", + "p", + "m", + "u", + "l", + "j", + "ł", + "g", + "b", + "h", + "ą", + "ę", + "ó", + ], + "Spanish": [ + "e", + "a", + "o", + "n", + "s", + "r", + "i", + "l", + "d", + "t", + "c", + "u", + "m", + "p", + "b", + "g", + "v", + "f", + "y", + "ó", + "h", + "q", + "í", + "j", + "z", + "á", + ], + "Russian": [ + "о", + "а", + "е", + "и", + "н", + "с", + "т", + "р", + "в", + "л", + "к", + "м", + "д", + "п", + "у", + "г", + "я", + "ы", + "з", + "б", + "й", + "ь", + "ч", + "х", + "ж", + "ц", + ], + # Jap-Kanji + "Japanese": [ + "人", + "一", + "大", + "亅", + "丁", + "丨", + "竹", + "笑", + "口", + "日", + "今", + "二", + "彳", + "行", + "十", + "土", + "丶", + "寸", + "寺", + "時", + "乙", + "丿", + "乂", + "气", + "気", + "冂", + "巾", + "亠", + "市", + "目", + "儿", + "見", + "八", + "小", + "凵", + "県", + "月", + "彐", + "門", + "間", + "木", + "東", + "山", + "出", + "本", + "中", + "刀", + "分", + "耳", + "又", + "取", + "最", + "言", + "田", + "心", + "思", + "刂", + "前", + "京", + "尹", + "事", + "生", + "厶", + "云", + "会", + "未", + "来", + "白", + "冫", + "楽", + "灬", + "馬", + "尸", + "尺", + "駅", + "明", + "耂", + "者", + "了", + "阝", + "都", + "高", + "卜", + "占", + "厂", + "广", + "店", + "子", + "申", + "奄", + "亻", + "俺", + "上", + "方", + "冖", + "学", + "衣", + "艮", + "食", + "自", + ], + # Jap-Katakana + "Japanese—": [ + "ー", + "ン", + "ス", + "・", + "ル", + "ト", + "リ", + "イ", + "ア", + "ラ", + "ッ", + "ク", + "ド", + "シ", + "レ", + "ジ", + "タ", + "フ", + "ロ", + "カ", + "テ", + "マ", + "ィ", + "グ", + "バ", + "ム", + "プ", + "オ", + "コ", + "デ", + "ニ", + "ウ", + "メ", + "サ", + "ビ", + "ナ", + "ブ", + "ャ", + "エ", + "ュ", + "チ", + "キ", + "ズ", + "ダ", + "パ", + "ミ", + "ェ", + "ョ", + "ハ", + "セ", + "ベ", + "ガ", + "モ", + "ツ", + "ネ", + "ボ", + "ソ", + "ノ", + "ァ", + "ヴ", + "ワ", + "ポ", + "ペ", + "ピ", + "ケ", + "ゴ", + "ギ", + "ザ", + "ホ", + "ゲ", + "ォ", + "ヤ", + "ヒ", + "ユ", + "ヨ", + "ヘ", + "ゼ", + "ヌ", + "ゥ", + "ゾ", + "ヶ", + "ヂ", + "ヲ", + "ヅ", + "ヵ", + "ヱ", + "ヰ", + "ヮ", + "ヽ", + "゠", + "ヾ", + "ヷ", + "ヿ", + "ヸ", + "ヹ", + "ヺ", + ], + # Jap-Hiragana + "Japanese——": [ + "の", + "に", + "る", + "た", + "と", + "は", + "し", + "い", + "を", + "で", + "て", + "が", + "な", + "れ", + "か", + "ら", + "さ", + "っ", + "り", + "す", + "あ", + "も", + "こ", + "ま", + "う", + "く", + "よ", + "き", + "ん", + "め", + "お", + "け", + "そ", + "つ", + "だ", + "や", + "え", + "ど", + "わ", + "ち", + "み", + "せ", + "じ", + "ば", + "へ", + "び", + "ず", + "ろ", + "ほ", + "げ", + "む", + "べ", + "ひ", + "ょ", + "ゆ", + "ぶ", + "ご", + "ゃ", + "ね", + "ふ", + "ぐ", + "ぎ", + "ぼ", + "ゅ", + "づ", + "ざ", + "ぞ", + "ぬ", + "ぜ", + "ぱ", + "ぽ", + "ぷ", + "ぴ", + "ぃ", + "ぁ", + "ぇ", + "ぺ", + "ゞ", + "ぢ", + "ぉ", + "ぅ", + "ゐ", + "ゝ", + "ゑ", + "゛", + "゜", + "ゎ", + "ゔ", + "゚", + "ゟ", + "゙", + "ゕ", + "ゖ", + ], + "Portuguese": [ + "a", + "e", + "o", + "s", + "i", + "r", + "d", + "n", + "t", + "m", + "u", + "c", + "l", + "p", + "g", + "v", + "b", + "f", + "h", + "ã", + "q", + "é", + "ç", + "á", + "z", + "í", + ], + "Swedish": [ + "e", + "a", + "n", + "r", + "t", + "s", + "i", + "l", + "d", + "o", + "m", + "k", + "g", + "v", + "h", + "f", + "u", + "p", + "ä", + "c", + "b", + "ö", + "å", + "y", + "j", + "x", + ], + "Chinese": [ + "的", + "一", + "是", + "不", + "了", + "在", + "人", + "有", + "我", + "他", + "这", + "个", + "们", + "中", + "来", + "上", + "大", + "为", + "和", + "国", + "地", + "到", + "以", + "说", + "时", + "要", + "就", + "出", + "会", + "可", + "也", + "你", + "对", + "生", + "能", + "而", + "子", + "那", + "得", + "于", + "着", + "下", + "自", + "之", + "年", + "过", + "发", + "后", + "作", + "里", + "用", + "道", + "行", + "所", + "然", + "家", + "种", + "事", + "成", + "方", + "多", + "经", + "么", + "去", + "法", + "学", + "如", + "都", + "同", + "现", + "当", + "没", + "动", + "面", + "起", + "看", + "定", + "天", + "分", + "还", + "进", + "好", + "小", + "部", + "其", + "些", + "主", + "样", + "理", + "心", + "她", + "本", + "前", + "开", + "但", + "因", + "只", + "从", + "想", + "实", + ], + "Ukrainian": [ + "о", + "а", + "н", + "і", + "и", + "р", + "в", + "т", + "е", + "с", + "к", + "л", + "у", + "д", + "м", + "п", + "з", + "я", + "ь", + "б", + "г", + "й", + "ч", + "х", + "ц", + "ї", + ], + "Norwegian": [ + "e", + "r", + "n", + "t", + "a", + "s", + "i", + "o", + "l", + "d", + "g", + "k", + "m", + "v", + "f", + "p", + "u", + "b", + "h", + "å", + "y", + "j", + "ø", + "c", + "æ", + "w", + ], + "Finnish": [ + "a", + "i", + "n", + "t", + "e", + "s", + "l", + "o", + "u", + "k", + "ä", + "m", + "r", + "v", + "j", + "h", + "p", + "y", + "d", + "ö", + "g", + "c", + "b", + "f", + "w", + "z", + ], + "Vietnamese": [ + "n", + "h", + "t", + "i", + "c", + "g", + "a", + "o", + "u", + "m", + "l", + "r", + "à", + "đ", + "s", + "e", + "v", + "p", + "b", + "y", + "ư", + "d", + "á", + "k", + "ộ", + "ế", + ], + "Czech": [ + "o", + "e", + "a", + "n", + "t", + "s", + "i", + "l", + "v", + "r", + "k", + "d", + "u", + "m", + "p", + "í", + "c", + "h", + "z", + "á", + "y", + "j", + "b", + "ě", + "é", + "ř", + ], + "Hungarian": [ + "e", + "a", + "t", + "l", + "s", + "n", + "k", + "r", + "i", + "o", + "z", + "á", + "é", + "g", + "m", + "b", + "y", + "v", + "d", + "h", + "u", + "p", + "j", + "ö", + "f", + "c", + ], + "Korean": [ + "이", + "다", + "에", + "의", + "는", + "로", + "하", + "을", + "가", + "고", + "지", + "서", + "한", + "은", + "기", + "으", + "년", + "대", + "사", + "시", + "를", + "리", + "도", + "인", + "스", + "일", + ], + "Indonesian": [ + "a", + "n", + "e", + "i", + "r", + "t", + "u", + "s", + "d", + "k", + "m", + "l", + "g", + "p", + "b", + "o", + "h", + "y", + "j", + "c", + "w", + "f", + "v", + "z", + "x", + "q", + ], + "Turkish": [ + "a", + "e", + "i", + "n", + "r", + "l", + "ı", + "k", + "d", + "t", + "s", + "m", + "y", + "u", + "o", + "b", + "ü", + "ş", + "v", + "g", + "z", + "h", + "c", + "p", + "ç", + "ğ", + ], + "Romanian": [ + "e", + "i", + "a", + "r", + "n", + "t", + "u", + "l", + "o", + "c", + "s", + "d", + "p", + "m", + "ă", + "f", + "v", + "î", + "g", + "b", + "ș", + "ț", + "z", + "h", + "â", + "j", + ], + "Farsi": [ + "ا", + "ی", + "ر", + "د", + "ن", + "ه", + "و", + "م", + "ت", + "ب", + "س", + "ل", + "ک", + "ش", + "ز", + "ف", + "گ", + "ع", + "خ", + "ق", + "ج", + "آ", + "پ", + "ح", + "ط", + "ص", + ], + "Arabic": [ + "ا", + "ل", + "ي", + "م", + "و", + "ن", + "ر", + "ت", + "ب", + "ة", + "ع", + "د", + "س", + "ف", + "ه", + "ك", + "ق", + "أ", + "ح", + "ج", + "ش", + "ط", + "ص", + "ى", + "خ", + "إ", + ], + "Danish": [ + "e", + "r", + "n", + "t", + "a", + "i", + "s", + "d", + "l", + "o", + "g", + "m", + "k", + "f", + "v", + "u", + "b", + "h", + "p", + "å", + "y", + "ø", + "æ", + "c", + "j", + "w", + ], + "Serbian": [ + "а", + "и", + "о", + "е", + "н", + "р", + "с", + "у", + "т", + "к", + "ј", + "в", + "д", + "м", + "п", + "л", + "г", + "з", + "б", + "a", + "i", + "e", + "o", + "n", + "ц", + "ш", + ], + "Lithuanian": [ + "i", + "a", + "s", + "o", + "r", + "e", + "t", + "n", + "u", + "k", + "m", + "l", + "p", + "v", + "d", + "j", + "g", + "ė", + "b", + "y", + "ų", + "š", + "ž", + "c", + "ą", + "į", + ], + "Slovene": [ + "e", + "a", + "i", + "o", + "n", + "r", + "s", + "l", + "t", + "j", + "v", + "k", + "d", + "p", + "m", + "u", + "z", + "b", + "g", + "h", + "č", + "c", + "š", + "ž", + "f", + "y", + ], + "Slovak": [ + "o", + "a", + "e", + "n", + "i", + "r", + "v", + "t", + "s", + "l", + "k", + "d", + "m", + "p", + "u", + "c", + "h", + "j", + "b", + "z", + "á", + "y", + "ý", + "í", + "č", + "é", + ], + "Hebrew": [ + "י", + "ו", + "ה", + "ל", + "ר", + "ב", + "ת", + "מ", + "א", + "ש", + "נ", + "ע", + "ם", + "ד", + "ק", + "ח", + "פ", + "ס", + "כ", + "ג", + "ט", + "צ", + "ן", + "ז", + "ך", + ], + "Bulgarian": [ + "а", + "и", + "о", + "е", + "н", + "т", + "р", + "с", + "в", + "л", + "к", + "д", + "п", + "м", + "з", + "г", + "я", + "ъ", + "у", + "б", + "ч", + "ц", + "й", + "ж", + "щ", + "х", + ], + "Croatian": [ + "a", + "i", + "o", + "e", + "n", + "r", + "j", + "s", + "t", + "u", + "k", + "l", + "v", + "d", + "m", + "p", + "g", + "z", + "b", + "c", + "č", + "h", + "š", + "ž", + "ć", + "f", + ], + "Hindi": [ + "क", + "र", + "स", + "न", + "त", + "म", + "ह", + "प", + "य", + "ल", + "व", + "ज", + "द", + "ग", + "ब", + "श", + "ट", + "अ", + "ए", + "थ", + "भ", + "ड", + "च", + "ध", + "ष", + "इ", + ], + "Estonian": [ + "a", + "i", + "e", + "s", + "t", + "l", + "u", + "n", + "o", + "k", + "r", + "d", + "m", + "v", + "g", + "p", + "j", + "h", + "ä", + "b", + "õ", + "ü", + "f", + "c", + "ö", + "y", + ], + "Thai": [ + "า", + "น", + "ร", + "อ", + "ก", + "เ", + "ง", + "ม", + "ย", + "ล", + "ว", + "ด", + "ท", + "ส", + "ต", + "ะ", + "ป", + "บ", + "ค", + "ห", + "แ", + "จ", + "พ", + "ช", + "ข", + "ใ", + ], + "Greek": [ + "α", + "τ", + "ο", + "ι", + "ε", + "ν", + "ρ", + "σ", + "κ", + "η", + "π", + "ς", + "υ", + "μ", + "λ", + "ί", + "ό", + "ά", + "γ", + "έ", + "δ", + "ή", + "ω", + "χ", + "θ", + "ύ", + ], + "Tamil": [ + "க", + "த", + "ப", + "ட", + "ர", + "ம", + "ல", + "ன", + "வ", + "ற", + "ய", + "ள", + "ச", + "ந", + "இ", + "ண", + "அ", + "ஆ", + "ழ", + "ங", + "எ", + "உ", + "ஒ", + "ஸ", + ], + "Kazakh": [ + "а", + "ы", + "е", + "н", + "т", + "р", + "л", + "і", + "д", + "с", + "м", + "қ", + "к", + "о", + "б", + "и", + "у", + "ғ", + "ж", + "ң", + "з", + "ш", + "й", + "п", + "г", + "ө", + ], +} + +LANGUAGE_SUPPORTED_COUNT: int = len(FREQUENCIES) diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/legacy.py b/venv/lib/python3.10/site-packages/charset_normalizer/legacy.py new file mode 100644 index 0000000..360a310 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/legacy.py @@ -0,0 +1,80 @@ +from __future__ import annotations + +from typing import TYPE_CHECKING, Any +from warnings import warn + +from .api import from_bytes +from .constant import CHARDET_CORRESPONDENCE, TOO_SMALL_SEQUENCE + +# TODO: remove this check when dropping Python 3.7 support +if TYPE_CHECKING: + from typing_extensions import TypedDict + + class ResultDict(TypedDict): + encoding: str | None + language: str + confidence: float | None + + +def detect( + byte_str: bytes, should_rename_legacy: bool = False, **kwargs: Any +) -> ResultDict: + """ + chardet legacy method + Detect the encoding of the given byte string. It should be mostly backward-compatible. + Encoding name will match Chardet own writing whenever possible. (Not on encoding name unsupported by it) + This function is deprecated and should be used to migrate your project easily, consult the documentation for + further information. Not planned for removal. + + :param byte_str: The byte sequence to examine. + :param should_rename_legacy: Should we rename legacy encodings + to their more modern equivalents? + """ + if len(kwargs): + warn( + f"charset-normalizer disregard arguments '{','.join(list(kwargs.keys()))}' in legacy function detect()" + ) + + if not isinstance(byte_str, (bytearray, bytes)): + raise TypeError( # pragma: nocover + f"Expected object of type bytes or bytearray, got: {type(byte_str)}" + ) + + if isinstance(byte_str, bytearray): + byte_str = bytes(byte_str) + + r = from_bytes(byte_str).best() + + encoding = r.encoding if r is not None else None + language = r.language if r is not None and r.language != "Unknown" else "" + confidence = 1.0 - r.chaos if r is not None else None + + # automatically lower confidence + # on small bytes samples. + # https://github.com/jawah/charset_normalizer/issues/391 + if ( + confidence is not None + and confidence >= 0.9 + and encoding + not in { + "utf_8", + "ascii", + } + and r.bom is False # type: ignore[union-attr] + and len(byte_str) < TOO_SMALL_SEQUENCE + ): + confidence -= 0.2 + + # Note: CharsetNormalizer does not return 'UTF-8-SIG' as the sig get stripped in the detection/normalization process + # but chardet does return 'utf-8-sig' and it is a valid codec name. + if r is not None and encoding == "utf_8" and r.bom: + encoding += "_sig" + + if should_rename_legacy is False and encoding in CHARDET_CORRESPONDENCE: + encoding = CHARDET_CORRESPONDENCE[encoding] + + return { + "encoding": encoding, + "language": language, + "confidence": confidence, + } diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/md.cpython-310-x86_64-linux-gnu.so b/venv/lib/python3.10/site-packages/charset_normalizer/md.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..f71a5af Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/md.cpython-310-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/md.py b/venv/lib/python3.10/site-packages/charset_normalizer/md.py new file mode 100644 index 0000000..12ce024 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/md.py @@ -0,0 +1,635 @@ +from __future__ import annotations + +from functools import lru_cache +from logging import getLogger + +from .constant import ( + COMMON_SAFE_ASCII_CHARACTERS, + TRACE, + UNICODE_SECONDARY_RANGE_KEYWORD, +) +from .utils import ( + is_accentuated, + is_arabic, + is_arabic_isolated_form, + is_case_variable, + is_cjk, + is_emoticon, + is_hangul, + is_hiragana, + is_katakana, + is_latin, + is_punctuation, + is_separator, + is_symbol, + is_thai, + is_unprintable, + remove_accent, + unicode_range, + is_cjk_uncommon, +) + + +class MessDetectorPlugin: + """ + Base abstract class used for mess detection plugins. + All detectors MUST extend and implement given methods. + """ + + def eligible(self, character: str) -> bool: + """ + Determine if given character should be fed in. + """ + raise NotImplementedError # pragma: nocover + + def feed(self, character: str) -> None: + """ + The main routine to be executed upon character. + Insert the logic in witch the text would be considered chaotic. + """ + raise NotImplementedError # pragma: nocover + + def reset(self) -> None: # pragma: no cover + """ + Permit to reset the plugin to the initial state. + """ + raise NotImplementedError + + @property + def ratio(self) -> float: + """ + Compute the chaos ratio based on what your feed() has seen. + Must NOT be lower than 0.; No restriction gt 0. + """ + raise NotImplementedError # pragma: nocover + + +class TooManySymbolOrPunctuationPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._punctuation_count: int = 0 + self._symbol_count: int = 0 + self._character_count: int = 0 + + self._last_printable_char: str | None = None + self._frenzy_symbol_in_word: bool = False + + def eligible(self, character: str) -> bool: + return character.isprintable() + + def feed(self, character: str) -> None: + self._character_count += 1 + + if ( + character != self._last_printable_char + and character not in COMMON_SAFE_ASCII_CHARACTERS + ): + if is_punctuation(character): + self._punctuation_count += 1 + elif ( + character.isdigit() is False + and is_symbol(character) + and is_emoticon(character) is False + ): + self._symbol_count += 2 + + self._last_printable_char = character + + def reset(self) -> None: # Abstract + self._punctuation_count = 0 + self._character_count = 0 + self._symbol_count = 0 + + @property + def ratio(self) -> float: + if self._character_count == 0: + return 0.0 + + ratio_of_punctuation: float = ( + self._punctuation_count + self._symbol_count + ) / self._character_count + + return ratio_of_punctuation if ratio_of_punctuation >= 0.3 else 0.0 + + +class TooManyAccentuatedPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._character_count: int = 0 + self._accentuated_count: int = 0 + + def eligible(self, character: str) -> bool: + return character.isalpha() + + def feed(self, character: str) -> None: + self._character_count += 1 + + if is_accentuated(character): + self._accentuated_count += 1 + + def reset(self) -> None: # Abstract + self._character_count = 0 + self._accentuated_count = 0 + + @property + def ratio(self) -> float: + if self._character_count < 8: + return 0.0 + + ratio_of_accentuation: float = self._accentuated_count / self._character_count + return ratio_of_accentuation if ratio_of_accentuation >= 0.35 else 0.0 + + +class UnprintablePlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._unprintable_count: int = 0 + self._character_count: int = 0 + + def eligible(self, character: str) -> bool: + return True + + def feed(self, character: str) -> None: + if is_unprintable(character): + self._unprintable_count += 1 + self._character_count += 1 + + def reset(self) -> None: # Abstract + self._unprintable_count = 0 + + @property + def ratio(self) -> float: + if self._character_count == 0: + return 0.0 + + return (self._unprintable_count * 8) / self._character_count + + +class SuspiciousDuplicateAccentPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._successive_count: int = 0 + self._character_count: int = 0 + + self._last_latin_character: str | None = None + + def eligible(self, character: str) -> bool: + return character.isalpha() and is_latin(character) + + def feed(self, character: str) -> None: + self._character_count += 1 + if ( + self._last_latin_character is not None + and is_accentuated(character) + and is_accentuated(self._last_latin_character) + ): + if character.isupper() and self._last_latin_character.isupper(): + self._successive_count += 1 + # Worse if its the same char duplicated with different accent. + if remove_accent(character) == remove_accent(self._last_latin_character): + self._successive_count += 1 + self._last_latin_character = character + + def reset(self) -> None: # Abstract + self._successive_count = 0 + self._character_count = 0 + self._last_latin_character = None + + @property + def ratio(self) -> float: + if self._character_count == 0: + return 0.0 + + return (self._successive_count * 2) / self._character_count + + +class SuspiciousRange(MessDetectorPlugin): + def __init__(self) -> None: + self._suspicious_successive_range_count: int = 0 + self._character_count: int = 0 + self._last_printable_seen: str | None = None + + def eligible(self, character: str) -> bool: + return character.isprintable() + + def feed(self, character: str) -> None: + self._character_count += 1 + + if ( + character.isspace() + or is_punctuation(character) + or character in COMMON_SAFE_ASCII_CHARACTERS + ): + self._last_printable_seen = None + return + + if self._last_printable_seen is None: + self._last_printable_seen = character + return + + unicode_range_a: str | None = unicode_range(self._last_printable_seen) + unicode_range_b: str | None = unicode_range(character) + + if is_suspiciously_successive_range(unicode_range_a, unicode_range_b): + self._suspicious_successive_range_count += 1 + + self._last_printable_seen = character + + def reset(self) -> None: # Abstract + self._character_count = 0 + self._suspicious_successive_range_count = 0 + self._last_printable_seen = None + + @property + def ratio(self) -> float: + if self._character_count <= 13: + return 0.0 + + ratio_of_suspicious_range_usage: float = ( + self._suspicious_successive_range_count * 2 + ) / self._character_count + + return ratio_of_suspicious_range_usage + + +class SuperWeirdWordPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._word_count: int = 0 + self._bad_word_count: int = 0 + self._foreign_long_count: int = 0 + + self._is_current_word_bad: bool = False + self._foreign_long_watch: bool = False + + self._character_count: int = 0 + self._bad_character_count: int = 0 + + self._buffer: str = "" + self._buffer_accent_count: int = 0 + self._buffer_glyph_count: int = 0 + + def eligible(self, character: str) -> bool: + return True + + def feed(self, character: str) -> None: + if character.isalpha(): + self._buffer += character + if is_accentuated(character): + self._buffer_accent_count += 1 + if ( + self._foreign_long_watch is False + and (is_latin(character) is False or is_accentuated(character)) + and is_cjk(character) is False + and is_hangul(character) is False + and is_katakana(character) is False + and is_hiragana(character) is False + and is_thai(character) is False + ): + self._foreign_long_watch = True + if ( + is_cjk(character) + or is_hangul(character) + or is_katakana(character) + or is_hiragana(character) + or is_thai(character) + ): + self._buffer_glyph_count += 1 + return + if not self._buffer: + return + if ( + character.isspace() or is_punctuation(character) or is_separator(character) + ) and self._buffer: + self._word_count += 1 + buffer_length: int = len(self._buffer) + + self._character_count += buffer_length + + if buffer_length >= 4: + if self._buffer_accent_count / buffer_length >= 0.5: + self._is_current_word_bad = True + # Word/Buffer ending with an upper case accentuated letter are so rare, + # that we will consider them all as suspicious. Same weight as foreign_long suspicious. + elif ( + is_accentuated(self._buffer[-1]) + and self._buffer[-1].isupper() + and all(_.isupper() for _ in self._buffer) is False + ): + self._foreign_long_count += 1 + self._is_current_word_bad = True + elif self._buffer_glyph_count == 1: + self._is_current_word_bad = True + self._foreign_long_count += 1 + if buffer_length >= 24 and self._foreign_long_watch: + camel_case_dst = [ + i + for c, i in zip(self._buffer, range(0, buffer_length)) + if c.isupper() + ] + probable_camel_cased: bool = False + + if camel_case_dst and (len(camel_case_dst) / buffer_length <= 0.3): + probable_camel_cased = True + + if not probable_camel_cased: + self._foreign_long_count += 1 + self._is_current_word_bad = True + + if self._is_current_word_bad: + self._bad_word_count += 1 + self._bad_character_count += len(self._buffer) + self._is_current_word_bad = False + + self._foreign_long_watch = False + self._buffer = "" + self._buffer_accent_count = 0 + self._buffer_glyph_count = 0 + elif ( + character not in {"<", ">", "-", "=", "~", "|", "_"} + and character.isdigit() is False + and is_symbol(character) + ): + self._is_current_word_bad = True + self._buffer += character + + def reset(self) -> None: # Abstract + self._buffer = "" + self._is_current_word_bad = False + self._foreign_long_watch = False + self._bad_word_count = 0 + self._word_count = 0 + self._character_count = 0 + self._bad_character_count = 0 + self._foreign_long_count = 0 + + @property + def ratio(self) -> float: + if self._word_count <= 10 and self._foreign_long_count == 0: + return 0.0 + + return self._bad_character_count / self._character_count + + +class CjkUncommonPlugin(MessDetectorPlugin): + """ + Detect messy CJK text that probably means nothing. + """ + + def __init__(self) -> None: + self._character_count: int = 0 + self._uncommon_count: int = 0 + + def eligible(self, character: str) -> bool: + return is_cjk(character) + + def feed(self, character: str) -> None: + self._character_count += 1 + + if is_cjk_uncommon(character): + self._uncommon_count += 1 + return + + def reset(self) -> None: # Abstract + self._character_count = 0 + self._uncommon_count = 0 + + @property + def ratio(self) -> float: + if self._character_count < 8: + return 0.0 + + uncommon_form_usage: float = self._uncommon_count / self._character_count + + # we can be pretty sure it's garbage when uncommon characters are widely + # used. otherwise it could just be traditional chinese for example. + return uncommon_form_usage / 10 if uncommon_form_usage > 0.5 else 0.0 + + +class ArchaicUpperLowerPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._buf: bool = False + + self._character_count_since_last_sep: int = 0 + + self._successive_upper_lower_count: int = 0 + self._successive_upper_lower_count_final: int = 0 + + self._character_count: int = 0 + + self._last_alpha_seen: str | None = None + self._current_ascii_only: bool = True + + def eligible(self, character: str) -> bool: + return True + + def feed(self, character: str) -> None: + is_concerned = character.isalpha() and is_case_variable(character) + chunk_sep = is_concerned is False + + if chunk_sep and self._character_count_since_last_sep > 0: + if ( + self._character_count_since_last_sep <= 64 + and character.isdigit() is False + and self._current_ascii_only is False + ): + self._successive_upper_lower_count_final += ( + self._successive_upper_lower_count + ) + + self._successive_upper_lower_count = 0 + self._character_count_since_last_sep = 0 + self._last_alpha_seen = None + self._buf = False + self._character_count += 1 + self._current_ascii_only = True + + return + + if self._current_ascii_only is True and character.isascii() is False: + self._current_ascii_only = False + + if self._last_alpha_seen is not None: + if (character.isupper() and self._last_alpha_seen.islower()) or ( + character.islower() and self._last_alpha_seen.isupper() + ): + if self._buf is True: + self._successive_upper_lower_count += 2 + self._buf = False + else: + self._buf = True + else: + self._buf = False + + self._character_count += 1 + self._character_count_since_last_sep += 1 + self._last_alpha_seen = character + + def reset(self) -> None: # Abstract + self._character_count = 0 + self._character_count_since_last_sep = 0 + self._successive_upper_lower_count = 0 + self._successive_upper_lower_count_final = 0 + self._last_alpha_seen = None + self._buf = False + self._current_ascii_only = True + + @property + def ratio(self) -> float: + if self._character_count == 0: + return 0.0 + + return self._successive_upper_lower_count_final / self._character_count + + +class ArabicIsolatedFormPlugin(MessDetectorPlugin): + def __init__(self) -> None: + self._character_count: int = 0 + self._isolated_form_count: int = 0 + + def reset(self) -> None: # Abstract + self._character_count = 0 + self._isolated_form_count = 0 + + def eligible(self, character: str) -> bool: + return is_arabic(character) + + def feed(self, character: str) -> None: + self._character_count += 1 + + if is_arabic_isolated_form(character): + self._isolated_form_count += 1 + + @property + def ratio(self) -> float: + if self._character_count < 8: + return 0.0 + + isolated_form_usage: float = self._isolated_form_count / self._character_count + + return isolated_form_usage + + +@lru_cache(maxsize=1024) +def is_suspiciously_successive_range( + unicode_range_a: str | None, unicode_range_b: str | None +) -> bool: + """ + Determine if two Unicode range seen next to each other can be considered as suspicious. + """ + if unicode_range_a is None or unicode_range_b is None: + return True + + if unicode_range_a == unicode_range_b: + return False + + if "Latin" in unicode_range_a and "Latin" in unicode_range_b: + return False + + if "Emoticons" in unicode_range_a or "Emoticons" in unicode_range_b: + return False + + # Latin characters can be accompanied with a combining diacritical mark + # eg. Vietnamese. + if ("Latin" in unicode_range_a or "Latin" in unicode_range_b) and ( + "Combining" in unicode_range_a or "Combining" in unicode_range_b + ): + return False + + keywords_range_a, keywords_range_b = ( + unicode_range_a.split(" "), + unicode_range_b.split(" "), + ) + + for el in keywords_range_a: + if el in UNICODE_SECONDARY_RANGE_KEYWORD: + continue + if el in keywords_range_b: + return False + + # Japanese Exception + range_a_jp_chars, range_b_jp_chars = ( + unicode_range_a + in ( + "Hiragana", + "Katakana", + ), + unicode_range_b in ("Hiragana", "Katakana"), + ) + if (range_a_jp_chars or range_b_jp_chars) and ( + "CJK" in unicode_range_a or "CJK" in unicode_range_b + ): + return False + if range_a_jp_chars and range_b_jp_chars: + return False + + if "Hangul" in unicode_range_a or "Hangul" in unicode_range_b: + if "CJK" in unicode_range_a or "CJK" in unicode_range_b: + return False + if unicode_range_a == "Basic Latin" or unicode_range_b == "Basic Latin": + return False + + # Chinese/Japanese use dedicated range for punctuation and/or separators. + if ("CJK" in unicode_range_a or "CJK" in unicode_range_b) or ( + unicode_range_a in ["Katakana", "Hiragana"] + and unicode_range_b in ["Katakana", "Hiragana"] + ): + if "Punctuation" in unicode_range_a or "Punctuation" in unicode_range_b: + return False + if "Forms" in unicode_range_a or "Forms" in unicode_range_b: + return False + if unicode_range_a == "Basic Latin" or unicode_range_b == "Basic Latin": + return False + + return True + + +@lru_cache(maxsize=2048) +def mess_ratio( + decoded_sequence: str, maximum_threshold: float = 0.2, debug: bool = False +) -> float: + """ + Compute a mess ratio given a decoded bytes sequence. The maximum threshold does stop the computation earlier. + """ + + detectors: list[MessDetectorPlugin] = [ + md_class() for md_class in MessDetectorPlugin.__subclasses__() + ] + + length: int = len(decoded_sequence) + 1 + + mean_mess_ratio: float = 0.0 + + if length < 512: + intermediary_mean_mess_ratio_calc: int = 32 + elif length <= 1024: + intermediary_mean_mess_ratio_calc = 64 + else: + intermediary_mean_mess_ratio_calc = 128 + + for character, index in zip(decoded_sequence + "\n", range(length)): + for detector in detectors: + if detector.eligible(character): + detector.feed(character) + + if ( + index > 0 and index % intermediary_mean_mess_ratio_calc == 0 + ) or index == length - 1: + mean_mess_ratio = sum(dt.ratio for dt in detectors) + + if mean_mess_ratio >= maximum_threshold: + break + + if debug: + logger = getLogger("charset_normalizer") + + logger.log( + TRACE, + "Mess-detector extended-analysis start. " + f"intermediary_mean_mess_ratio_calc={intermediary_mean_mess_ratio_calc} mean_mess_ratio={mean_mess_ratio} " + f"maximum_threshold={maximum_threshold}", + ) + + if len(decoded_sequence) > 16: + logger.log(TRACE, f"Starting with: {decoded_sequence[:16]}") + logger.log(TRACE, f"Ending with: {decoded_sequence[-16::]}") + + for dt in detectors: + logger.log(TRACE, f"{dt.__class__}: {dt.ratio}") + + return round(mean_mess_ratio, 3) diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/md__mypyc.cpython-310-x86_64-linux-gnu.so b/venv/lib/python3.10/site-packages/charset_normalizer/md__mypyc.cpython-310-x86_64-linux-gnu.so new file mode 100755 index 0000000..db651f8 Binary files /dev/null and b/venv/lib/python3.10/site-packages/charset_normalizer/md__mypyc.cpython-310-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/models.py b/venv/lib/python3.10/site-packages/charset_normalizer/models.py new file mode 100644 index 0000000..1042758 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/models.py @@ -0,0 +1,360 @@ +from __future__ import annotations + +from encodings.aliases import aliases +from hashlib import sha256 +from json import dumps +from re import sub +from typing import Any, Iterator, List, Tuple + +from .constant import RE_POSSIBLE_ENCODING_INDICATION, TOO_BIG_SEQUENCE +from .utils import iana_name, is_multi_byte_encoding, unicode_range + + +class CharsetMatch: + def __init__( + self, + payload: bytes, + guessed_encoding: str, + mean_mess_ratio: float, + has_sig_or_bom: bool, + languages: CoherenceMatches, + decoded_payload: str | None = None, + preemptive_declaration: str | None = None, + ): + self._payload: bytes = payload + + self._encoding: str = guessed_encoding + self._mean_mess_ratio: float = mean_mess_ratio + self._languages: CoherenceMatches = languages + self._has_sig_or_bom: bool = has_sig_or_bom + self._unicode_ranges: list[str] | None = None + + self._leaves: list[CharsetMatch] = [] + self._mean_coherence_ratio: float = 0.0 + + self._output_payload: bytes | None = None + self._output_encoding: str | None = None + + self._string: str | None = decoded_payload + + self._preemptive_declaration: str | None = preemptive_declaration + + def __eq__(self, other: object) -> bool: + if not isinstance(other, CharsetMatch): + if isinstance(other, str): + return iana_name(other) == self.encoding + return False + return self.encoding == other.encoding and self.fingerprint == other.fingerprint + + def __lt__(self, other: object) -> bool: + """ + Implemented to make sorted available upon CharsetMatches items. + """ + if not isinstance(other, CharsetMatch): + raise ValueError + + chaos_difference: float = abs(self.chaos - other.chaos) + coherence_difference: float = abs(self.coherence - other.coherence) + + # Below 1% difference --> Use Coherence + if chaos_difference < 0.01 and coherence_difference > 0.02: + return self.coherence > other.coherence + elif chaos_difference < 0.01 and coherence_difference <= 0.02: + # When having a difficult decision, use the result that decoded as many multi-byte as possible. + # preserve RAM usage! + if len(self._payload) >= TOO_BIG_SEQUENCE: + return self.chaos < other.chaos + return self.multi_byte_usage > other.multi_byte_usage + + return self.chaos < other.chaos + + @property + def multi_byte_usage(self) -> float: + return 1.0 - (len(str(self)) / len(self.raw)) + + def __str__(self) -> str: + # Lazy Str Loading + if self._string is None: + self._string = str(self._payload, self._encoding, "strict") + return self._string + + def __repr__(self) -> str: + return f"" + + def add_submatch(self, other: CharsetMatch) -> None: + if not isinstance(other, CharsetMatch) or other == self: + raise ValueError( + "Unable to add instance <{}> as a submatch of a CharsetMatch".format( + other.__class__ + ) + ) + + other._string = None # Unload RAM usage; dirty trick. + self._leaves.append(other) + + @property + def encoding(self) -> str: + return self._encoding + + @property + def encoding_aliases(self) -> list[str]: + """ + Encoding name are known by many name, using this could help when searching for IBM855 when it's listed as CP855. + """ + also_known_as: list[str] = [] + for u, p in aliases.items(): + if self.encoding == u: + also_known_as.append(p) + elif self.encoding == p: + also_known_as.append(u) + return also_known_as + + @property + def bom(self) -> bool: + return self._has_sig_or_bom + + @property + def byte_order_mark(self) -> bool: + return self._has_sig_or_bom + + @property + def languages(self) -> list[str]: + """ + Return the complete list of possible languages found in decoded sequence. + Usually not really useful. Returned list may be empty even if 'language' property return something != 'Unknown'. + """ + return [e[0] for e in self._languages] + + @property + def language(self) -> str: + """ + Most probable language found in decoded sequence. If none were detected or inferred, the property will return + "Unknown". + """ + if not self._languages: + # Trying to infer the language based on the given encoding + # Its either English or we should not pronounce ourselves in certain cases. + if "ascii" in self.could_be_from_charset: + return "English" + + # doing it there to avoid circular import + from charset_normalizer.cd import encoding_languages, mb_encoding_languages + + languages = ( + mb_encoding_languages(self.encoding) + if is_multi_byte_encoding(self.encoding) + else encoding_languages(self.encoding) + ) + + if len(languages) == 0 or "Latin Based" in languages: + return "Unknown" + + return languages[0] + + return self._languages[0][0] + + @property + def chaos(self) -> float: + return self._mean_mess_ratio + + @property + def coherence(self) -> float: + if not self._languages: + return 0.0 + return self._languages[0][1] + + @property + def percent_chaos(self) -> float: + return round(self.chaos * 100, ndigits=3) + + @property + def percent_coherence(self) -> float: + return round(self.coherence * 100, ndigits=3) + + @property + def raw(self) -> bytes: + """ + Original untouched bytes. + """ + return self._payload + + @property + def submatch(self) -> list[CharsetMatch]: + return self._leaves + + @property + def has_submatch(self) -> bool: + return len(self._leaves) > 0 + + @property + def alphabets(self) -> list[str]: + if self._unicode_ranges is not None: + return self._unicode_ranges + # list detected ranges + detected_ranges: list[str | None] = [unicode_range(char) for char in str(self)] + # filter and sort + self._unicode_ranges = sorted(list({r for r in detected_ranges if r})) + return self._unicode_ranges + + @property + def could_be_from_charset(self) -> list[str]: + """ + The complete list of encoding that output the exact SAME str result and therefore could be the originating + encoding. + This list does include the encoding available in property 'encoding'. + """ + return [self._encoding] + [m.encoding for m in self._leaves] + + def output(self, encoding: str = "utf_8") -> bytes: + """ + Method to get re-encoded bytes payload using given target encoding. Default to UTF-8. + Any errors will be simply ignored by the encoder NOT replaced. + """ + if self._output_encoding is None or self._output_encoding != encoding: + self._output_encoding = encoding + decoded_string = str(self) + if ( + self._preemptive_declaration is not None + and self._preemptive_declaration.lower() + not in ["utf-8", "utf8", "utf_8"] + ): + patched_header = sub( + RE_POSSIBLE_ENCODING_INDICATION, + lambda m: m.string[m.span()[0] : m.span()[1]].replace( + m.groups()[0], + iana_name(self._output_encoding).replace("_", "-"), # type: ignore[arg-type] + ), + decoded_string[:8192], + count=1, + ) + + decoded_string = patched_header + decoded_string[8192:] + + self._output_payload = decoded_string.encode(encoding, "replace") + + return self._output_payload # type: ignore + + @property + def fingerprint(self) -> str: + """ + Retrieve the unique SHA256 computed using the transformed (re-encoded) payload. Not the original one. + """ + return sha256(self.output()).hexdigest() + + +class CharsetMatches: + """ + Container with every CharsetMatch items ordered by default from most probable to the less one. + Act like a list(iterable) but does not implements all related methods. + """ + + def __init__(self, results: list[CharsetMatch] | None = None): + self._results: list[CharsetMatch] = sorted(results) if results else [] + + def __iter__(self) -> Iterator[CharsetMatch]: + yield from self._results + + def __getitem__(self, item: int | str) -> CharsetMatch: + """ + Retrieve a single item either by its position or encoding name (alias may be used here). + Raise KeyError upon invalid index or encoding not present in results. + """ + if isinstance(item, int): + return self._results[item] + if isinstance(item, str): + item = iana_name(item, False) + for result in self._results: + if item in result.could_be_from_charset: + return result + raise KeyError + + def __len__(self) -> int: + return len(self._results) + + def __bool__(self) -> bool: + return len(self._results) > 0 + + def append(self, item: CharsetMatch) -> None: + """ + Insert a single match. Will be inserted accordingly to preserve sort. + Can be inserted as a submatch. + """ + if not isinstance(item, CharsetMatch): + raise ValueError( + "Cannot append instance '{}' to CharsetMatches".format( + str(item.__class__) + ) + ) + # We should disable the submatch factoring when the input file is too heavy (conserve RAM usage) + if len(item.raw) < TOO_BIG_SEQUENCE: + for match in self._results: + if match.fingerprint == item.fingerprint and match.chaos == item.chaos: + match.add_submatch(item) + return + self._results.append(item) + self._results = sorted(self._results) + + def best(self) -> CharsetMatch | None: + """ + Simply return the first match. Strict equivalent to matches[0]. + """ + if not self._results: + return None + return self._results[0] + + def first(self) -> CharsetMatch | None: + """ + Redundant method, call the method best(). Kept for BC reasons. + """ + return self.best() + + +CoherenceMatch = Tuple[str, float] +CoherenceMatches = List[CoherenceMatch] + + +class CliDetectionResult: + def __init__( + self, + path: str, + encoding: str | None, + encoding_aliases: list[str], + alternative_encodings: list[str], + language: str, + alphabets: list[str], + has_sig_or_bom: bool, + chaos: float, + coherence: float, + unicode_path: str | None, + is_preferred: bool, + ): + self.path: str = path + self.unicode_path: str | None = unicode_path + self.encoding: str | None = encoding + self.encoding_aliases: list[str] = encoding_aliases + self.alternative_encodings: list[str] = alternative_encodings + self.language: str = language + self.alphabets: list[str] = alphabets + self.has_sig_or_bom: bool = has_sig_or_bom + self.chaos: float = chaos + self.coherence: float = coherence + self.is_preferred: bool = is_preferred + + @property + def __dict__(self) -> dict[str, Any]: # type: ignore + return { + "path": self.path, + "encoding": self.encoding, + "encoding_aliases": self.encoding_aliases, + "alternative_encodings": self.alternative_encodings, + "language": self.language, + "alphabets": self.alphabets, + "has_sig_or_bom": self.has_sig_or_bom, + "chaos": self.chaos, + "coherence": self.coherence, + "unicode_path": self.unicode_path, + "is_preferred": self.is_preferred, + } + + def to_json(self) -> str: + return dumps(self.__dict__, ensure_ascii=True, indent=4) diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/py.typed b/venv/lib/python3.10/site-packages/charset_normalizer/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/utils.py b/venv/lib/python3.10/site-packages/charset_normalizer/utils.py new file mode 100644 index 0000000..6bf0384 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/utils.py @@ -0,0 +1,414 @@ +from __future__ import annotations + +import importlib +import logging +import unicodedata +from codecs import IncrementalDecoder +from encodings.aliases import aliases +from functools import lru_cache +from re import findall +from typing import Generator + +from _multibytecodec import ( # type: ignore[import-not-found,import] + MultibyteIncrementalDecoder, +) + +from .constant import ( + ENCODING_MARKS, + IANA_SUPPORTED_SIMILAR, + RE_POSSIBLE_ENCODING_INDICATION, + UNICODE_RANGES_COMBINED, + UNICODE_SECONDARY_RANGE_KEYWORD, + UTF8_MAXIMAL_ALLOCATION, + COMMON_CJK_CHARACTERS, +) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_accentuated(character: str) -> bool: + try: + description: str = unicodedata.name(character) + except ValueError: # Defensive: unicode database outdated? + return False + return ( + "WITH GRAVE" in description + or "WITH ACUTE" in description + or "WITH CEDILLA" in description + or "WITH DIAERESIS" in description + or "WITH CIRCUMFLEX" in description + or "WITH TILDE" in description + or "WITH MACRON" in description + or "WITH RING ABOVE" in description + ) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def remove_accent(character: str) -> str: + decomposed: str = unicodedata.decomposition(character) + if not decomposed: + return character + + codes: list[str] = decomposed.split(" ") + + return chr(int(codes[0], 16)) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def unicode_range(character: str) -> str | None: + """ + Retrieve the Unicode range official name from a single character. + """ + character_ord: int = ord(character) + + for range_name, ord_range in UNICODE_RANGES_COMBINED.items(): + if character_ord in ord_range: + return range_name + + return None + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_latin(character: str) -> bool: + try: + description: str = unicodedata.name(character) + except ValueError: # Defensive: unicode database outdated? + return False + return "LATIN" in description + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_punctuation(character: str) -> bool: + character_category: str = unicodedata.category(character) + + if "P" in character_category: + return True + + character_range: str | None = unicode_range(character) + + if character_range is None: + return False + + return "Punctuation" in character_range + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_symbol(character: str) -> bool: + character_category: str = unicodedata.category(character) + + if "S" in character_category or "N" in character_category: + return True + + character_range: str | None = unicode_range(character) + + if character_range is None: + return False + + return "Forms" in character_range and character_category != "Lo" + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_emoticon(character: str) -> bool: + character_range: str | None = unicode_range(character) + + if character_range is None: + return False + + return "Emoticons" in character_range or "Pictographs" in character_range + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_separator(character: str) -> bool: + if character.isspace() or character in {"|", "+", "<", ">"}: + return True + + character_category: str = unicodedata.category(character) + + return "Z" in character_category or character_category in {"Po", "Pd", "Pc"} + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_case_variable(character: str) -> bool: + return character.islower() != character.isupper() + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_cjk(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: # Defensive: unicode database outdated? + return False + + return "CJK" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_hiragana(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: # Defensive: unicode database outdated? + return False + + return "HIRAGANA" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_katakana(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: # Defensive: unicode database outdated? + return False + + return "KATAKANA" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_hangul(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: # Defensive: unicode database outdated? + return False + + return "HANGUL" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_thai(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: # Defensive: unicode database outdated? + return False + + return "THAI" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_arabic(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: # Defensive: unicode database outdated? + return False + + return "ARABIC" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_arabic_isolated_form(character: str) -> bool: + try: + character_name = unicodedata.name(character) + except ValueError: # Defensive: unicode database outdated? + return False + + return "ARABIC" in character_name and "ISOLATED FORM" in character_name + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_cjk_uncommon(character: str) -> bool: + return character not in COMMON_CJK_CHARACTERS + + +@lru_cache(maxsize=len(UNICODE_RANGES_COMBINED)) +def is_unicode_range_secondary(range_name: str) -> bool: + return any(keyword in range_name for keyword in UNICODE_SECONDARY_RANGE_KEYWORD) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_unprintable(character: str) -> bool: + return ( + character.isspace() is False # includes \n \t \r \v + and character.isprintable() is False + and character != "\x1a" # Why? Its the ASCII substitute character. + and character != "\ufeff" # bug discovered in Python, + # Zero Width No-Break Space located in Arabic Presentation Forms-B, Unicode 1.1 not acknowledged as space. + ) + + +def any_specified_encoding(sequence: bytes, search_zone: int = 8192) -> str | None: + """ + Extract using ASCII-only decoder any specified encoding in the first n-bytes. + """ + if not isinstance(sequence, bytes): + raise TypeError + + seq_len: int = len(sequence) + + results: list[str] = findall( + RE_POSSIBLE_ENCODING_INDICATION, + sequence[: min(seq_len, search_zone)].decode("ascii", errors="ignore"), + ) + + if len(results) == 0: + return None + + for specified_encoding in results: + specified_encoding = specified_encoding.lower().replace("-", "_") + + encoding_alias: str + encoding_iana: str + + for encoding_alias, encoding_iana in aliases.items(): + if encoding_alias == specified_encoding: + return encoding_iana + if encoding_iana == specified_encoding: + return encoding_iana + + return None + + +@lru_cache(maxsize=128) +def is_multi_byte_encoding(name: str) -> bool: + """ + Verify is a specific encoding is a multi byte one based on it IANA name + """ + return name in { + "utf_8", + "utf_8_sig", + "utf_16", + "utf_16_be", + "utf_16_le", + "utf_32", + "utf_32_le", + "utf_32_be", + "utf_7", + } or issubclass( + importlib.import_module(f"encodings.{name}").IncrementalDecoder, + MultibyteIncrementalDecoder, + ) + + +def identify_sig_or_bom(sequence: bytes) -> tuple[str | None, bytes]: + """ + Identify and extract SIG/BOM in given sequence. + """ + + for iana_encoding in ENCODING_MARKS: + marks: bytes | list[bytes] = ENCODING_MARKS[iana_encoding] + + if isinstance(marks, bytes): + marks = [marks] + + for mark in marks: + if sequence.startswith(mark): + return iana_encoding, mark + + return None, b"" + + +def should_strip_sig_or_bom(iana_encoding: str) -> bool: + return iana_encoding not in {"utf_16", "utf_32"} + + +def iana_name(cp_name: str, strict: bool = True) -> str: + """Returns the Python normalized encoding name (Not the IANA official name).""" + cp_name = cp_name.lower().replace("-", "_") + + encoding_alias: str + encoding_iana: str + + for encoding_alias, encoding_iana in aliases.items(): + if cp_name in [encoding_alias, encoding_iana]: + return encoding_iana + + if strict: + raise ValueError(f"Unable to retrieve IANA for '{cp_name}'") + + return cp_name + + +def cp_similarity(iana_name_a: str, iana_name_b: str) -> float: + if is_multi_byte_encoding(iana_name_a) or is_multi_byte_encoding(iana_name_b): + return 0.0 + + decoder_a = importlib.import_module(f"encodings.{iana_name_a}").IncrementalDecoder + decoder_b = importlib.import_module(f"encodings.{iana_name_b}").IncrementalDecoder + + id_a: IncrementalDecoder = decoder_a(errors="ignore") + id_b: IncrementalDecoder = decoder_b(errors="ignore") + + character_match_count: int = 0 + + for i in range(255): + to_be_decoded: bytes = bytes([i]) + if id_a.decode(to_be_decoded) == id_b.decode(to_be_decoded): + character_match_count += 1 + + return character_match_count / 254 + + +def is_cp_similar(iana_name_a: str, iana_name_b: str) -> bool: + """ + Determine if two code page are at least 80% similar. IANA_SUPPORTED_SIMILAR dict was generated using + the function cp_similarity. + """ + return ( + iana_name_a in IANA_SUPPORTED_SIMILAR + and iana_name_b in IANA_SUPPORTED_SIMILAR[iana_name_a] + ) + + +def set_logging_handler( + name: str = "charset_normalizer", + level: int = logging.INFO, + format_string: str = "%(asctime)s | %(levelname)s | %(message)s", +) -> None: + logger = logging.getLogger(name) + logger.setLevel(level) + + handler = logging.StreamHandler() + handler.setFormatter(logging.Formatter(format_string)) + logger.addHandler(handler) + + +def cut_sequence_chunks( + sequences: bytes, + encoding_iana: str, + offsets: range, + chunk_size: int, + bom_or_sig_available: bool, + strip_sig_or_bom: bool, + sig_payload: bytes, + is_multi_byte_decoder: bool, + decoded_payload: str | None = None, +) -> Generator[str, None, None]: + if decoded_payload and is_multi_byte_decoder is False: + for i in offsets: + chunk = decoded_payload[i : i + chunk_size] + if not chunk: + break + yield chunk + else: + for i in offsets: + chunk_end = i + chunk_size + if chunk_end > len(sequences) + 8: + continue + + cut_sequence = sequences[i : i + chunk_size] + + if bom_or_sig_available and strip_sig_or_bom is False: + cut_sequence = sig_payload + cut_sequence + + chunk = cut_sequence.decode( + encoding_iana, + errors="ignore" if is_multi_byte_decoder else "strict", + ) + + # multi-byte bad cutting detector and adjustment + # not the cleanest way to perform that fix but clever enough for now. + if is_multi_byte_decoder and i > 0: + chunk_partial_size_chk: int = min(chunk_size, 16) + + if ( + decoded_payload + and chunk[:chunk_partial_size_chk] not in decoded_payload + ): + for j in range(i, i - 4, -1): + cut_sequence = sequences[j:chunk_end] + + if bom_or_sig_available and strip_sig_or_bom is False: + cut_sequence = sig_payload + cut_sequence + + chunk = cut_sequence.decode(encoding_iana, errors="ignore") + + if chunk[:chunk_partial_size_chk] in decoded_payload: + break + + yield chunk diff --git a/venv/lib/python3.10/site-packages/charset_normalizer/version.py b/venv/lib/python3.10/site-packages/charset_normalizer/version.py new file mode 100644 index 0000000..c843e53 --- /dev/null +++ b/venv/lib/python3.10/site-packages/charset_normalizer/version.py @@ -0,0 +1,8 @@ +""" +Expose version +""" + +from __future__ import annotations + +__version__ = "3.4.4" +VERSION = __version__.split(".") diff --git a/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/METADATA b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/METADATA new file mode 100644 index 0000000..3f433af --- /dev/null +++ b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/METADATA @@ -0,0 +1,84 @@ +Metadata-Version: 2.4 +Name: click +Version: 8.3.1 +Summary: Composable command line interface toolkit +Maintainer-email: Pallets +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-Expression: BSD-3-Clause +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +License-File: LICENSE.txt +Requires-Dist: colorama; platform_system == 'Windows' +Project-URL: Changes, https://click.palletsprojects.com/page/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://click.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/click/ + +
+ +# Click + +Click is a Python package for creating beautiful command line interfaces +in a composable way with as little code as necessary. It's the "Command +Line Interface Creation Kit". It's highly configurable but comes with +sensible defaults out of the box. + +It aims to make the process of writing command line tools quick and fun +while also preventing any frustration caused by the inability to +implement an intended CLI API. + +Click in three points: + +- Arbitrary nesting of commands +- Automatic help page generation +- Supports lazy loading of subcommands at runtime + + +## A Simple Example + +```python +import click + +@click.command() +@click.option("--count", default=1, help="Number of greetings.") +@click.option("--name", prompt="Your name", help="The person to greet.") +def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for _ in range(count): + click.echo(f"Hello, {name}!") + +if __name__ == '__main__': + hello() +``` + +``` +$ python hello.py --count=3 +Your name: Click +Hello, Click! +Hello, Click! +Hello, Click! +``` + + +## Donate + +The Pallets organization develops and supports Click and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today][]. + +[please donate today]: https://palletsprojects.com/donate + +## Contributing + +See our [detailed contributing documentation][contrib] for many ways to +contribute, including reporting issues, requesting features, asking or answering +questions, and making PRs. + +[contrib]: https://palletsprojects.com/contributing/ + diff --git a/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/RECORD b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/RECORD new file mode 100644 index 0000000..839aade --- /dev/null +++ b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/RECORD @@ -0,0 +1,40 @@ +click-8.3.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +click-8.3.1.dist-info/METADATA,sha256=XZeBrMAE0ghTE88SjfrSDuSyNCpBPplxJR1tbwD9oZg,2621 +click-8.3.1.dist-info/RECORD,, +click-8.3.1.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +click-8.3.1.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 +click/__init__.py,sha256=6YyS1aeyknZ0LYweWozNZy0A9nZ_11wmYIhv3cbQrYo,4473 +click/__pycache__/__init__.cpython-310.pyc,, +click/__pycache__/_compat.cpython-310.pyc,, +click/__pycache__/_termui_impl.cpython-310.pyc,, +click/__pycache__/_textwrap.cpython-310.pyc,, +click/__pycache__/_utils.cpython-310.pyc,, +click/__pycache__/_winconsole.cpython-310.pyc,, +click/__pycache__/core.cpython-310.pyc,, +click/__pycache__/decorators.cpython-310.pyc,, +click/__pycache__/exceptions.cpython-310.pyc,, +click/__pycache__/formatting.cpython-310.pyc,, +click/__pycache__/globals.cpython-310.pyc,, +click/__pycache__/parser.cpython-310.pyc,, +click/__pycache__/shell_completion.cpython-310.pyc,, +click/__pycache__/termui.cpython-310.pyc,, +click/__pycache__/testing.cpython-310.pyc,, +click/__pycache__/types.cpython-310.pyc,, +click/__pycache__/utils.cpython-310.pyc,, +click/_compat.py,sha256=v3xBZkFbvA1BXPRkFfBJc6-pIwPI7345m-kQEnpVAs4,18693 +click/_termui_impl.py,sha256=rgCb3On8X5A4200rA5L6i13u5iapmFer7sru57Jy6zA,27093 +click/_textwrap.py,sha256=BOae0RQ6vg3FkNgSJyOoGzG1meGMxJ_ukWVZKx_v-0o,1400 +click/_utils.py,sha256=kZwtTf5gMuCilJJceS2iTCvRvCY-0aN5rJq8gKw7p8g,943 +click/_winconsole.py,sha256=_vxUuUaxwBhoR0vUWCNuHY8VUefiMdCIyU2SXPqoF-A,8465 +click/core.py,sha256=U6Bfxt8GkjNDqyJ0HqXvluJHtyZ4sY5USAvM1Cdq7mQ,132105 +click/decorators.py,sha256=5P7abhJtAQYp_KHgjUvhMv464ERwOzrv2enNknlwHyQ,18461 +click/exceptions.py,sha256=8utf8w6V5hJXMnO_ic1FNrtbwuEn1NUu1aDwV8UqnG4,9954 +click/formatting.py,sha256=RVfwwr0rwWNpgGr8NaHodPzkIr7_tUyVh_nDdanLMNc,9730 +click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923 +click/parser.py,sha256=Q31pH0FlQZEq-UXE_ABRzlygEfvxPTuZbWNh4xfXmzw,19010 +click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +click/shell_completion.py,sha256=Cc4GQUFuWpfQBa9sF5qXeeYI7n3tI_1k6ZdSn4BZbT0,20994 +click/termui.py,sha256=hqCEjNndU-nzW08nRAkBaVgfZp_FdCA9KxfIWlKYaMc,31037 +click/testing.py,sha256=EERbzcl1br0mW0qBS9EqkknfNfXB9WQEW0ELIpkvuSs,19102 +click/types.py,sha256=ek54BNSFwPKsqtfT7jsqcc4WHui8AIFVMKM4oVZIXhc,39927 +click/utils.py,sha256=gCUoewdAhA-QLBUUHxrLh4uj6m7T1WjZZMNPvR0I7YA,20257 diff --git a/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/WHEEL b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/licenses/LICENSE.txt b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..d12a849 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click-8.3.1.dist-info/licenses/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2014 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/venv/lib/python3.10/site-packages/click/__init__.py b/venv/lib/python3.10/site-packages/click/__init__.py new file mode 100644 index 0000000..1aa547c --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/__init__.py @@ -0,0 +1,123 @@ +""" +Click is a simple Python module inspired by the stdlib optparse to make +writing command line scripts fun. Unlike other modules, it's based +around a simple API that does not come with too much magic and is +composable. +""" + +from __future__ import annotations + +from .core import Argument as Argument +from .core import Command as Command +from .core import CommandCollection as CommandCollection +from .core import Context as Context +from .core import Group as Group +from .core import Option as Option +from .core import Parameter as Parameter +from .decorators import argument as argument +from .decorators import command as command +from .decorators import confirmation_option as confirmation_option +from .decorators import group as group +from .decorators import help_option as help_option +from .decorators import make_pass_decorator as make_pass_decorator +from .decorators import option as option +from .decorators import pass_context as pass_context +from .decorators import pass_obj as pass_obj +from .decorators import password_option as password_option +from .decorators import version_option as version_option +from .exceptions import Abort as Abort +from .exceptions import BadArgumentUsage as BadArgumentUsage +from .exceptions import BadOptionUsage as BadOptionUsage +from .exceptions import BadParameter as BadParameter +from .exceptions import ClickException as ClickException +from .exceptions import FileError as FileError +from .exceptions import MissingParameter as MissingParameter +from .exceptions import NoSuchOption as NoSuchOption +from .exceptions import UsageError as UsageError +from .formatting import HelpFormatter as HelpFormatter +from .formatting import wrap_text as wrap_text +from .globals import get_current_context as get_current_context +from .termui import clear as clear +from .termui import confirm as confirm +from .termui import echo_via_pager as echo_via_pager +from .termui import edit as edit +from .termui import getchar as getchar +from .termui import launch as launch +from .termui import pause as pause +from .termui import progressbar as progressbar +from .termui import prompt as prompt +from .termui import secho as secho +from .termui import style as style +from .termui import unstyle as unstyle +from .types import BOOL as BOOL +from .types import Choice as Choice +from .types import DateTime as DateTime +from .types import File as File +from .types import FLOAT as FLOAT +from .types import FloatRange as FloatRange +from .types import INT as INT +from .types import IntRange as IntRange +from .types import ParamType as ParamType +from .types import Path as Path +from .types import STRING as STRING +from .types import Tuple as Tuple +from .types import UNPROCESSED as UNPROCESSED +from .types import UUID as UUID +from .utils import echo as echo +from .utils import format_filename as format_filename +from .utils import get_app_dir as get_app_dir +from .utils import get_binary_stream as get_binary_stream +from .utils import get_text_stream as get_text_stream +from .utils import open_file as open_file + + +def __getattr__(name: str) -> object: + import warnings + + if name == "BaseCommand": + from .core import _BaseCommand + + warnings.warn( + "'BaseCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Command' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _BaseCommand + + if name == "MultiCommand": + from .core import _MultiCommand + + warnings.warn( + "'MultiCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Group' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _MultiCommand + + if name == "OptionParser": + from .parser import _OptionParser + + warnings.warn( + "'OptionParser' is deprecated and will be removed in Click 9.0. The" + " old parser is available in 'optparse'.", + DeprecationWarning, + stacklevel=2, + ) + return _OptionParser + + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " Click 9.1. Use feature detection or" + " 'importlib.metadata.version(\"click\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("click") + + raise AttributeError(name) diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..8c0c2ae Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/_compat.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/_compat.cpython-310.pyc new file mode 100644 index 0000000..5172ce0 Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/_compat.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/_termui_impl.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/_termui_impl.cpython-310.pyc new file mode 100644 index 0000000..36cd2ae Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/_termui_impl.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/_textwrap.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/_textwrap.cpython-310.pyc new file mode 100644 index 0000000..011dd25 Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/_textwrap.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/_utils.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/_utils.cpython-310.pyc new file mode 100644 index 0000000..cabf67f Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/_utils.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/_winconsole.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/_winconsole.cpython-310.pyc new file mode 100644 index 0000000..a759ca7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/_winconsole.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/core.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/core.cpython-310.pyc new file mode 100644 index 0000000..064cd28 Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/core.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/decorators.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/decorators.cpython-310.pyc new file mode 100644 index 0000000..dbf9587 Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/decorators.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/exceptions.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/exceptions.cpython-310.pyc new file mode 100644 index 0000000..b90e73b Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/exceptions.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/formatting.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/formatting.cpython-310.pyc new file mode 100644 index 0000000..c16df4a Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/formatting.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/globals.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/globals.cpython-310.pyc new file mode 100644 index 0000000..dbd6e5d Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/globals.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/parser.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/parser.cpython-310.pyc new file mode 100644 index 0000000..be516e5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/parser.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/shell_completion.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/shell_completion.cpython-310.pyc new file mode 100644 index 0000000..4743cfd Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/shell_completion.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/termui.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/termui.cpython-310.pyc new file mode 100644 index 0000000..d4f6adb Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/termui.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/testing.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/testing.cpython-310.pyc new file mode 100644 index 0000000..106ab69 Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/testing.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/types.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/types.cpython-310.pyc new file mode 100644 index 0000000..9d677d9 Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/types.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/__pycache__/utils.cpython-310.pyc b/venv/lib/python3.10/site-packages/click/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..097a81f Binary files /dev/null and b/venv/lib/python3.10/site-packages/click/__pycache__/utils.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/click/_compat.py b/venv/lib/python3.10/site-packages/click/_compat.py new file mode 100644 index 0000000..f2726b9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/_compat.py @@ -0,0 +1,622 @@ +from __future__ import annotations + +import codecs +import collections.abc as cabc +import io +import os +import re +import sys +import typing as t +from types import TracebackType +from weakref import WeakKeyDictionary + +CYGWIN = sys.platform.startswith("cygwin") +WIN = sys.platform.startswith("win") +auto_wrap_for_ansi: t.Callable[[t.TextIO], t.TextIO] | None = None +_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") + + +def _make_text_stream( + stream: t.BinaryIO, + encoding: str | None, + errors: str | None, + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if encoding is None: + encoding = get_best_encoding(stream) + if errors is None: + errors = "replace" + return _NonClosingTextIOWrapper( + stream, + encoding, + errors, + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def is_ascii_encoding(encoding: str) -> bool: + """Checks if a given encoding is ascii.""" + try: + return codecs.lookup(encoding).name == "ascii" + except LookupError: + return False + + +def get_best_encoding(stream: t.IO[t.Any]) -> str: + """Returns the default stream encoding if not found.""" + rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() + if is_ascii_encoding(rv): + return "utf-8" + return rv + + +class _NonClosingTextIOWrapper(io.TextIOWrapper): + def __init__( + self, + stream: t.BinaryIO, + encoding: str | None, + errors: str | None, + force_readable: bool = False, + force_writable: bool = False, + **extra: t.Any, + ) -> None: + self._stream = stream = t.cast( + t.BinaryIO, _FixupStream(stream, force_readable, force_writable) + ) + super().__init__(stream, encoding, errors, **extra) + + def __del__(self) -> None: + try: + self.detach() + except Exception: + pass + + def isatty(self) -> bool: + # https://bitbucket.org/pypy/pypy/issue/1803 + return self._stream.isatty() + + +class _FixupStream: + """The new io interface needs more from streams than streams + traditionally implement. As such, this fix-up code is necessary in + some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). + """ + + def __init__( + self, + stream: t.BinaryIO, + force_readable: bool = False, + force_writable: bool = False, + ): + self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._stream, name) + + def read1(self, size: int) -> bytes: + f = getattr(self._stream, "read1", None) + + if f is not None: + return t.cast(bytes, f(size)) + + return self._stream.read(size) + + def readable(self) -> bool: + if self._force_readable: + return True + x = getattr(self._stream, "readable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.read(0) + except Exception: + return False + return True + + def writable(self) -> bool: + if self._force_writable: + return True + x = getattr(self._stream, "writable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.write(b"") + except Exception: + try: + self._stream.write(b"") + except Exception: + return False + return True + + def seekable(self) -> bool: + x = getattr(self._stream, "seekable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.seek(self._stream.tell()) + except Exception: + return False + return True + + +def _is_binary_reader(stream: t.IO[t.Any], default: bool = False) -> bool: + try: + return isinstance(stream.read(0), bytes) + except Exception: + return default + # This happens in some cases where the stream was already + # closed. In this case, we assume the default. + + +def _is_binary_writer(stream: t.IO[t.Any], default: bool = False) -> bool: + try: + stream.write(b"") + except Exception: + try: + stream.write("") + return False + except Exception: + pass + return default + return True + + +def _find_binary_reader(stream: t.IO[t.Any]) -> t.BinaryIO | None: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_reader(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_reader(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _find_binary_writer(stream: t.IO[t.Any]) -> t.BinaryIO | None: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_writer(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_writer(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _stream_is_misconfigured(stream: t.TextIO) -> bool: + """A stream is misconfigured if its encoding is ASCII.""" + # If the stream does not have an encoding set, we assume it's set + # to ASCII. This appears to happen in certain unittest + # environments. It's not quite clear what the correct behavior is + # but this at least will force Click to recover somehow. + return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") + + +def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: str | None) -> bool: + """A stream attribute is compatible if it is equal to the + desired value or the desired value is unset and the attribute + has a value. + """ + stream_value = getattr(stream, attr, None) + return stream_value == value or (value is None and stream_value is not None) + + +def _is_compatible_text_stream( + stream: t.TextIO, encoding: str | None, errors: str | None +) -> bool: + """Check if a stream's encoding and errors attributes are + compatible with the desired values. + """ + return _is_compat_stream_attr( + stream, "encoding", encoding + ) and _is_compat_stream_attr(stream, "errors", errors) + + +def _force_correct_text_stream( + text_stream: t.IO[t.Any], + encoding: str | None, + errors: str | None, + is_binary: t.Callable[[t.IO[t.Any], bool], bool], + find_binary: t.Callable[[t.IO[t.Any]], t.BinaryIO | None], + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if is_binary(text_stream, False): + binary_reader = t.cast(t.BinaryIO, text_stream) + else: + text_stream = t.cast(t.TextIO, text_stream) + # If the stream looks compatible, and won't default to a + # misconfigured ascii encoding, return it as-is. + if _is_compatible_text_stream(text_stream, encoding, errors) and not ( + encoding is None and _stream_is_misconfigured(text_stream) + ): + return text_stream + + # Otherwise, get the underlying binary reader. + possible_binary_reader = find_binary(text_stream) + + # If that's not possible, silently use the original reader + # and get mojibake instead of exceptions. + if possible_binary_reader is None: + return text_stream + + binary_reader = possible_binary_reader + + # Default errors to replace instead of strict in order to get + # something that works. + if errors is None: + errors = "replace" + + # Wrap the binary stream in a text stream with the correct + # encoding parameters. + return _make_text_stream( + binary_reader, + encoding, + errors, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def _force_correct_text_reader( + text_reader: t.IO[t.Any], + encoding: str | None, + errors: str | None, + force_readable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_reader, + encoding, + errors, + _is_binary_reader, + _find_binary_reader, + force_readable=force_readable, + ) + + +def _force_correct_text_writer( + text_writer: t.IO[t.Any], + encoding: str | None, + errors: str | None, + force_writable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_writer, + encoding, + errors, + _is_binary_writer, + _find_binary_writer, + force_writable=force_writable, + ) + + +def get_binary_stdin() -> t.BinaryIO: + reader = _find_binary_reader(sys.stdin) + if reader is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdin.") + return reader + + +def get_binary_stdout() -> t.BinaryIO: + writer = _find_binary_writer(sys.stdout) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdout.") + return writer + + +def get_binary_stderr() -> t.BinaryIO: + writer = _find_binary_writer(sys.stderr) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stderr.") + return writer + + +def get_text_stdin(encoding: str | None = None, errors: str | None = None) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True) + + +def get_text_stdout(encoding: str | None = None, errors: str | None = None) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True) + + +def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> t.TextIO: + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True) + + +def _wrap_io_open( + file: str | os.PathLike[str] | int, + mode: str, + encoding: str | None, + errors: str | None, +) -> t.IO[t.Any]: + """Handles not passing ``encoding`` and ``errors`` in binary mode.""" + if "b" in mode: + return open(file, mode) + + return open(file, mode, encoding=encoding, errors=errors) + + +def open_stream( + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, +) -> tuple[t.IO[t.Any], bool]: + binary = "b" in mode + filename = os.fspath(filename) + + # Standard streams first. These are simple because they ignore the + # atomic flag. Use fsdecode to handle Path("-"). + if os.fsdecode(filename) == "-": + if any(m in mode for m in ["w", "a", "x"]): + if binary: + return get_binary_stdout(), False + return get_text_stdout(encoding=encoding, errors=errors), False + if binary: + return get_binary_stdin(), False + return get_text_stdin(encoding=encoding, errors=errors), False + + # Non-atomic writes directly go out through the regular open functions. + if not atomic: + return _wrap_io_open(filename, mode, encoding, errors), True + + # Some usability stuff for atomic writes + if "a" in mode: + raise ValueError( + "Appending to an existing file is not supported, because that" + " would involve an expensive `copy`-operation to a temporary" + " file. Open the file in normal `w`-mode and copy explicitly" + " if that's what you're after." + ) + if "x" in mode: + raise ValueError("Use the `overwrite`-parameter instead.") + if "w" not in mode: + raise ValueError("Atomic writes only make sense with `w`-mode.") + + # Atomic writes are more complicated. They work by opening a file + # as a proxy in the same folder and then using the fdopen + # functionality to wrap it in a Python file. Then we wrap it in an + # atomic file that moves the file over on close. + import errno + import random + + try: + perm: int | None = os.stat(filename).st_mode + except OSError: + perm = None + + flags = os.O_RDWR | os.O_CREAT | os.O_EXCL + + if binary: + flags |= getattr(os, "O_BINARY", 0) + + while True: + tmp_filename = os.path.join( + os.path.dirname(filename), + f".__atomic-write{random.randrange(1 << 32):08x}", + ) + try: + fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) + break + except OSError as e: + if e.errno == errno.EEXIST or ( + os.name == "nt" + and e.errno == errno.EACCES + and os.path.isdir(e.filename) + and os.access(e.filename, os.W_OK) + ): + continue + raise + + if perm is not None: + os.chmod(tmp_filename, perm) # in case perm includes bits in umask + + f = _wrap_io_open(fd, mode, encoding, errors) + af = _AtomicFile(f, tmp_filename, os.path.realpath(filename)) + return t.cast(t.IO[t.Any], af), True + + +class _AtomicFile: + def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None: + self._f = f + self._tmp_filename = tmp_filename + self._real_filename = real_filename + self.closed = False + + @property + def name(self) -> str: + return self._real_filename + + def close(self, delete: bool = False) -> None: + if self.closed: + return + self._f.close() + os.replace(self._tmp_filename, self._real_filename) + self.closed = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._f, name) + + def __enter__(self) -> _AtomicFile: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.close(delete=exc_type is not None) + + def __repr__(self) -> str: + return repr(self._f) + + +def strip_ansi(value: str) -> str: + return _ansi_re.sub("", value) + + +def _is_jupyter_kernel_output(stream: t.IO[t.Any]) -> bool: + while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): + stream = stream._stream + + return stream.__class__.__module__.startswith("ipykernel.") + + +def should_strip_ansi( + stream: t.IO[t.Any] | None = None, color: bool | None = None +) -> bool: + if color is None: + if stream is None: + stream = sys.stdin + return not isatty(stream) and not _is_jupyter_kernel_output(stream) + return not color + + +# On Windows, wrap the output streams with colorama to support ANSI +# color codes. +# NOTE: double check is needed so mypy does not analyze this on Linux +if sys.platform.startswith("win") and WIN: + from ._winconsole import _get_windows_console_stream + + def _get_argv_encoding() -> str: + import locale + + return locale.getpreferredencoding() + + _ansi_stream_wrappers: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def auto_wrap_for_ansi(stream: t.TextIO, color: bool | None = None) -> t.TextIO: + """Support ANSI color and style codes on Windows by wrapping a + stream with colorama. + """ + try: + cached = _ansi_stream_wrappers.get(stream) + except Exception: + cached = None + + if cached is not None: + return cached + + import colorama + + strip = should_strip_ansi(stream, color) + ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) + rv = t.cast(t.TextIO, ansi_wrapper.stream) + _write = rv.write + + def _safe_write(s: str) -> int: + try: + return _write(s) + except BaseException: + ansi_wrapper.reset_all() + raise + + rv.write = _safe_write # type: ignore[method-assign] + + try: + _ansi_stream_wrappers[stream] = rv + except Exception: + pass + + return rv + +else: + + def _get_argv_encoding() -> str: + return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding() + + def _get_windows_console_stream( + f: t.TextIO, encoding: str | None, errors: str | None + ) -> t.TextIO | None: + return None + + +def term_len(x: str) -> int: + return len(strip_ansi(x)) + + +def isatty(stream: t.IO[t.Any]) -> bool: + try: + return stream.isatty() + except Exception: + return False + + +def _make_cached_stream_func( + src_func: t.Callable[[], t.TextIO | None], + wrapper_func: t.Callable[[], t.TextIO], +) -> t.Callable[[], t.TextIO | None]: + cache: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def func() -> t.TextIO | None: + stream = src_func() + + if stream is None: + return None + + try: + rv = cache.get(stream) + except Exception: + rv = None + if rv is not None: + return rv + rv = wrapper_func() + try: + cache[stream] = rv + except Exception: + pass + return rv + + return func + + +_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin) +_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout) +_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) + + +binary_streams: cabc.Mapping[str, t.Callable[[], t.BinaryIO]] = { + "stdin": get_binary_stdin, + "stdout": get_binary_stdout, + "stderr": get_binary_stderr, +} + +text_streams: cabc.Mapping[str, t.Callable[[str | None, str | None], t.TextIO]] = { + "stdin": get_text_stdin, + "stdout": get_text_stdout, + "stderr": get_text_stderr, +} diff --git a/venv/lib/python3.10/site-packages/click/_termui_impl.py b/venv/lib/python3.10/site-packages/click/_termui_impl.py new file mode 100644 index 0000000..ee8225c --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/_termui_impl.py @@ -0,0 +1,852 @@ +""" +This module contains implementations for the termui module. To keep the +import time of Click down, some infrequently used functionality is +placed in this module and only imported as needed. +""" + +from __future__ import annotations + +import collections.abc as cabc +import contextlib +import math +import os +import shlex +import sys +import time +import typing as t +from gettext import gettext as _ +from io import StringIO +from pathlib import Path +from types import TracebackType + +from ._compat import _default_text_stdout +from ._compat import CYGWIN +from ._compat import get_best_encoding +from ._compat import isatty +from ._compat import open_stream +from ._compat import strip_ansi +from ._compat import term_len +from ._compat import WIN +from .exceptions import ClickException +from .utils import echo + +V = t.TypeVar("V") + +if os.name == "nt": + BEFORE_BAR = "\r" + AFTER_BAR = "\n" +else: + BEFORE_BAR = "\r\033[?25l" + AFTER_BAR = "\033[?25h\n" + + +class ProgressBar(t.Generic[V]): + def __init__( + self, + iterable: cabc.Iterable[V] | None, + length: int | None = None, + fill_char: str = "#", + empty_char: str = " ", + bar_template: str = "%(bar)s", + info_sep: str = " ", + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + item_show_func: t.Callable[[V | None], str | None] | None = None, + label: str | None = None, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, + width: int = 30, + ) -> None: + self.fill_char = fill_char + self.empty_char = empty_char + self.bar_template = bar_template + self.info_sep = info_sep + self.hidden = hidden + self.show_eta = show_eta + self.show_percent = show_percent + self.show_pos = show_pos + self.item_show_func = item_show_func + self.label: str = label or "" + + if file is None: + file = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + file = StringIO() + + self.file = file + self.color = color + self.update_min_steps = update_min_steps + self._completed_intervals = 0 + self.width: int = width + self.autowidth: bool = width == 0 + + if length is None: + from operator import length_hint + + length = length_hint(iterable, -1) + + if length == -1: + length = None + if iterable is None: + if length is None: + raise TypeError("iterable or length is required") + iterable = t.cast("cabc.Iterable[V]", range(length)) + self.iter: cabc.Iterable[V] = iter(iterable) + self.length = length + self.pos: int = 0 + self.avg: list[float] = [] + self.last_eta: float + self.start: float + self.start = self.last_eta = time.time() + self.eta_known: bool = False + self.finished: bool = False + self.max_width: int | None = None + self.entered: bool = False + self.current_item: V | None = None + self._is_atty = isatty(self.file) + self._last_line: str | None = None + + def __enter__(self) -> ProgressBar[V]: + self.entered = True + self.render_progress() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.render_finish() + + def __iter__(self) -> cabc.Iterator[V]: + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + self.render_progress() + return self.generator() + + def __next__(self) -> V: + # Iteration is defined in terms of a generator function, + # returned by iter(self); use that to define next(). This works + # because `self.iter` is an iterable consumed by that generator, + # so it is re-entry safe. Calling `next(self.generator())` + # twice works and does "what you want". + return next(iter(self)) + + def render_finish(self) -> None: + if self.hidden or not self._is_atty: + return + self.file.write(AFTER_BAR) + self.file.flush() + + @property + def pct(self) -> float: + if self.finished: + return 1.0 + return min(self.pos / (float(self.length or 1) or 1), 1.0) + + @property + def time_per_iteration(self) -> float: + if not self.avg: + return 0.0 + return sum(self.avg) / float(len(self.avg)) + + @property + def eta(self) -> float: + if self.length is not None and not self.finished: + return self.time_per_iteration * (self.length - self.pos) + return 0.0 + + def format_eta(self) -> str: + if self.eta_known: + t = int(self.eta) + seconds = t % 60 + t //= 60 + minutes = t % 60 + t //= 60 + hours = t % 24 + t //= 24 + if t > 0: + return f"{t}d {hours:02}:{minutes:02}:{seconds:02}" + else: + return f"{hours:02}:{minutes:02}:{seconds:02}" + return "" + + def format_pos(self) -> str: + pos = str(self.pos) + if self.length is not None: + pos += f"/{self.length}" + return pos + + def format_pct(self) -> str: + return f"{int(self.pct * 100): 4}%"[1:] + + def format_bar(self) -> str: + if self.length is not None: + bar_length = int(self.pct * self.width) + bar = self.fill_char * bar_length + bar += self.empty_char * (self.width - bar_length) + elif self.finished: + bar = self.fill_char * self.width + else: + chars = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + chars[ + int( + (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5) + * self.width + ) + ] = self.fill_char + bar = "".join(chars) + return bar + + def format_progress_line(self) -> str: + show_percent = self.show_percent + + info_bits = [] + if self.length is not None and show_percent is None: + show_percent = not self.show_pos + + if self.show_pos: + info_bits.append(self.format_pos()) + if show_percent: + info_bits.append(self.format_pct()) + if self.show_eta and self.eta_known and not self.finished: + info_bits.append(self.format_eta()) + if self.item_show_func is not None: + item_info = self.item_show_func(self.current_item) + if item_info is not None: + info_bits.append(item_info) + + return ( + self.bar_template + % { + "label": self.label, + "bar": self.format_bar(), + "info": self.info_sep.join(info_bits), + } + ).rstrip() + + def render_progress(self) -> None: + if self.hidden: + return + + if not self._is_atty: + # Only output the label once if the output is not a TTY. + if self._last_line != self.label: + self._last_line = self.label + echo(self.label, file=self.file, color=self.color) + return + + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + import shutil + + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, shutil.get_terminal_size().columns - clutter_length) + if new_width < old_width and self.max_width is not None: + buf.append(BEFORE_BAR) + buf.append(" " * self.max_width) + self.max_width = new_width + self.width = new_width + + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width + + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(" " * (clear_width - line_len)) + line = "".join(buf) + # Render the line only if it changed. + + if line != self._last_line: + self._last_line = line + echo(line, file=self.file, color=self.color, nl=False) + self.file.flush() + + def make_step(self, n_steps: int) -> None: + self.pos += n_steps + if self.length is not None and self.pos >= self.length: + self.finished = True + + if (time.time() - self.last_eta) < 1.0: + return + + self.last_eta = time.time() + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] + + self.eta_known = self.length is not None + + def update(self, n_steps: int, current_item: V | None = None) -> None: + """Update the progress bar by advancing a specified number of + steps, and optionally set the ``current_item`` for this new + position. + + :param n_steps: Number of steps to advance. + :param current_item: Optional item to set as ``current_item`` + for the updated position. + + .. versionchanged:: 8.0 + Added the ``current_item`` optional parameter. + + .. versionchanged:: 8.0 + Only render when the number of steps meets the + ``update_min_steps`` threshold. + """ + if current_item is not None: + self.current_item = current_item + + self._completed_intervals += n_steps + + if self._completed_intervals >= self.update_min_steps: + self.make_step(self._completed_intervals) + self.render_progress() + self._completed_intervals = 0 + + def finish(self) -> None: + self.eta_known = False + self.current_item = None + self.finished = True + + def generator(self) -> cabc.Iterator[V]: + """Return a generator which yields the items added to the bar + during construction, and updates the progress bar *after* the + yielded block returns. + """ + # WARNING: the iterator interface for `ProgressBar` relies on + # this and only works because this is a simple generator which + # doesn't create or manage additional state. If this function + # changes, the impact should be evaluated both against + # `iter(bar)` and `next(bar)`. `next()` in particular may call + # `self.generator()` repeatedly, and this must remain safe in + # order for that interface to work. + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + + if not self._is_atty: + yield from self.iter + else: + for rv in self.iter: + self.current_item = rv + + # This allows show_item_func to be updated before the + # item is processed. Only trigger at the beginning of + # the update interval. + if self._completed_intervals == 0: + self.render_progress() + + yield rv + self.update(1) + + self.finish() + self.render_progress() + + +def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None: + """Decide what method to use for paging through text.""" + stdout = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if stdout is None: + stdout = StringIO() + + if not isatty(sys.stdin) or not isatty(stdout): + return _nullpager(stdout, generator, color) + + # Split and normalize the pager command into parts. + pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""), posix=False) + if pager_cmd_parts: + if WIN: + if _tempfilepager(generator, pager_cmd_parts, color): + return + elif _pipepager(generator, pager_cmd_parts, color): + return + + if os.environ.get("TERM") in ("dumb", "emacs"): + return _nullpager(stdout, generator, color) + if (WIN or sys.platform.startswith("os2")) and _tempfilepager( + generator, ["more"], color + ): + return + if _pipepager(generator, ["less"], color): + return + + import tempfile + + fd, filename = tempfile.mkstemp() + os.close(fd) + try: + if _pipepager(generator, ["more"], color): + return + return _nullpager(stdout, generator, color) + finally: + os.unlink(filename) + + +def _pipepager( + generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None +) -> bool: + """Page through text by feeding it to another program. Invoking a + pager through this might support colors. + + Returns `True` if the command was found, `False` otherwise and thus another + pager should be attempted. + """ + # Split the command into the invoked CLI and its parameters. + if not cmd_parts: + return False + + import shutil + + cmd = cmd_parts[0] + cmd_params = cmd_parts[1:] + + cmd_filepath = shutil.which(cmd) + if not cmd_filepath: + return False + + # Produces a normalized absolute path string. + # multi-call binaries such as busybox derive their identity from the symlink + # less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox) + cmd_path = Path(cmd_filepath).absolute() + cmd_name = cmd_path.name + + import subprocess + + # Make a local copy of the environment to not affect the global one. + env = dict(os.environ) + + # If we're piping to less and the user hasn't decided on colors, we enable + # them by default we find the -R flag in the command line arguments. + if color is None and cmd_name == "less": + less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_params)}" + if not less_flags: + env["LESS"] = "-R" + color = True + elif "r" in less_flags or "R" in less_flags: + color = True + + c = subprocess.Popen( + [str(cmd_path)] + cmd_params, + shell=False, + stdin=subprocess.PIPE, + env=env, + errors="replace", + text=True, + ) + assert c.stdin is not None + try: + for text in generator: + if not color: + text = strip_ansi(text) + + c.stdin.write(text) + except BrokenPipeError: + # In case the pager exited unexpectedly, ignore the broken pipe error. + pass + except Exception as e: + # In case there is an exception we want to close the pager immediately + # and let the caller handle it. + # Otherwise the pager will keep running, and the user may not notice + # the error message, or worse yet it may leave the terminal in a broken state. + c.terminate() + raise e + finally: + # We must close stdin and wait for the pager to exit before we continue + try: + c.stdin.close() + # Close implies flush, so it might throw a BrokenPipeError if the pager + # process exited already. + except BrokenPipeError: + pass + + # Less doesn't respect ^C, but catches it for its own UI purposes (aborting + # search or other commands inside less). + # + # That means when the user hits ^C, the parent process (click) terminates, + # but less is still alive, paging the output and messing up the terminal. + # + # If the user wants to make the pager exit on ^C, they should set + # `LESS='-K'`. It's not our decision to make. + while True: + try: + c.wait() + except KeyboardInterrupt: + pass + else: + break + + return True + + +def _tempfilepager( + generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None +) -> bool: + """Page through text by invoking a program on a temporary file. + + Returns `True` if the command was found, `False` otherwise and thus another + pager should be attempted. + """ + # Split the command into the invoked CLI and its parameters. + if not cmd_parts: + return False + + import shutil + + cmd = cmd_parts[0] + + cmd_filepath = shutil.which(cmd) + if not cmd_filepath: + return False + # Produces a normalized absolute path string. + # multi-call binaries such as busybox derive their identity from the symlink + # less -> busybox. resolve() causes them to misbehave. (eg. less becomes busybox) + cmd_path = Path(cmd_filepath).absolute() + + import subprocess + import tempfile + + fd, filename = tempfile.mkstemp() + # TODO: This never terminates if the passed generator never terminates. + text = "".join(generator) + if not color: + text = strip_ansi(text) + encoding = get_best_encoding(sys.stdout) + with open_stream(filename, "wb")[0] as f: + f.write(text.encode(encoding)) + try: + subprocess.call([str(cmd_path), filename]) + except OSError: + # Command not found + pass + finally: + os.close(fd) + os.unlink(filename) + + return True + + +def _nullpager( + stream: t.TextIO, generator: cabc.Iterable[str], color: bool | None +) -> None: + """Simply print unformatted text. This is the ultimate fallback.""" + for text in generator: + if not color: + text = strip_ansi(text) + stream.write(text) + + +class Editor: + def __init__( + self, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", + ) -> None: + self.editor = editor + self.env = env + self.require_save = require_save + self.extension = extension + + def get_editor(self) -> str: + if self.editor is not None: + return self.editor + for key in "VISUAL", "EDITOR": + rv = os.environ.get(key) + if rv: + return rv + if WIN: + return "notepad" + + from shutil import which + + for editor in "sensible-editor", "vim", "nano": + if which(editor) is not None: + return editor + return "vi" + + def edit_files(self, filenames: cabc.Iterable[str]) -> None: + import subprocess + + editor = self.get_editor() + environ: dict[str, str] | None = None + + if self.env: + environ = os.environ.copy() + environ.update(self.env) + + exc_filename = " ".join(f'"{filename}"' for filename in filenames) + + try: + c = subprocess.Popen( + args=f"{editor} {exc_filename}", env=environ, shell=True + ) + exit_code = c.wait() + if exit_code != 0: + raise ClickException( + _("{editor}: Editing failed").format(editor=editor) + ) + except OSError as e: + raise ClickException( + _("{editor}: Editing failed: {e}").format(editor=editor, e=e) + ) from e + + @t.overload + def edit(self, text: bytes | bytearray) -> bytes | None: ... + + # We cannot know whether or not the type expected is str or bytes when None + # is passed, so str is returned as that was what was done before. + @t.overload + def edit(self, text: str | None) -> str | None: ... + + def edit(self, text: str | bytes | bytearray | None) -> str | bytes | None: + import tempfile + + if text is None: + data: bytes | bytearray = b"" + elif isinstance(text, (bytes, bytearray)): + data = text + else: + if text and not text.endswith("\n"): + text += "\n" + + if WIN: + data = text.replace("\n", "\r\n").encode("utf-8-sig") + else: + data = text.encode("utf-8") + + fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension) + f: t.BinaryIO + + try: + with os.fdopen(fd, "wb") as f: + f.write(data) + + # If the filesystem resolution is 1 second, like Mac OS + # 10.12 Extended, or 2 seconds, like FAT32, and the editor + # closes very fast, require_save can fail. Set the modified + # time to be 2 seconds in the past to work around this. + os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2)) + # Depending on the resolution, the exact value might not be + # recorded, so get the new recorded value. + timestamp = os.path.getmtime(name) + + self.edit_files((name,)) + + if self.require_save and os.path.getmtime(name) == timestamp: + return None + + with open(name, "rb") as f: + rv = f.read() + + if isinstance(text, (bytes, bytearray)): + return rv + + return rv.decode("utf-8-sig").replace("\r\n", "\n") + finally: + os.unlink(name) + + +def open_url(url: str, wait: bool = False, locate: bool = False) -> int: + import subprocess + + def _unquote_file(url: str) -> str: + from urllib.parse import unquote + + if url.startswith("file://"): + url = unquote(url[7:]) + + return url + + if sys.platform == "darwin": + args = ["open"] + if wait: + args.append("-W") + if locate: + args.append("-R") + args.append(_unquote_file(url)) + null = open("/dev/null", "w") + try: + return subprocess.Popen(args, stderr=null).wait() + finally: + null.close() + elif WIN: + if locate: + url = _unquote_file(url) + args = ["explorer", f"/select,{url}"] + else: + args = ["start"] + if wait: + args.append("/WAIT") + args.append("") + args.append(url) + try: + return subprocess.call(args) + except OSError: + # Command not found + return 127 + elif CYGWIN: + if locate: + url = _unquote_file(url) + args = ["cygstart", os.path.dirname(url)] + else: + args = ["cygstart"] + if wait: + args.append("-w") + args.append(url) + try: + return subprocess.call(args) + except OSError: + # Command not found + return 127 + + try: + if locate: + url = os.path.dirname(_unquote_file(url)) or "." + else: + url = _unquote_file(url) + c = subprocess.Popen(["xdg-open", url]) + if wait: + return c.wait() + return 0 + except OSError: + if url.startswith(("http://", "https://")) and not locate and not wait: + import webbrowser + + webbrowser.open(url) + return 0 + return 1 + + +def _translate_ch_to_exc(ch: str) -> None: + if ch == "\x03": + raise KeyboardInterrupt() + + if ch == "\x04" and not WIN: # Unix-like, Ctrl+D + raise EOFError() + + if ch == "\x1a" and WIN: # Windows, Ctrl+Z + raise EOFError() + + return None + + +if sys.platform == "win32": + import msvcrt + + @contextlib.contextmanager + def raw_terminal() -> cabc.Iterator[int]: + yield -1 + + def getchar(echo: bool) -> str: + # The function `getch` will return a bytes object corresponding to + # the pressed character. Since Windows 10 build 1803, it will also + # return \x00 when called a second time after pressing a regular key. + # + # `getwch` does not share this probably-bugged behavior. Moreover, it + # returns a Unicode object by default, which is what we want. + # + # Either of these functions will return \x00 or \xe0 to indicate + # a special key, and you need to call the same function again to get + # the "rest" of the code. The fun part is that \u00e0 is + # "latin small letter a with grave", so if you type that on a French + # keyboard, you _also_ get a \xe0. + # E.g., consider the Up arrow. This returns \xe0 and then \x48. The + # resulting Unicode string reads as "a with grave" + "capital H". + # This is indistinguishable from when the user actually types + # "a with grave" and then "capital H". + # + # When \xe0 is returned, we assume it's part of a special-key sequence + # and call `getwch` again, but that means that when the user types + # the \u00e0 character, `getchar` doesn't return until a second + # character is typed. + # The alternative is returning immediately, but that would mess up + # cross-platform handling of arrow keys and others that start with + # \xe0. Another option is using `getch`, but then we can't reliably + # read non-ASCII characters, because return values of `getch` are + # limited to the current 8-bit codepage. + # + # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` + # is doing the right thing in more situations than with `getch`. + + if echo: + func = t.cast(t.Callable[[], str], msvcrt.getwche) + else: + func = t.cast(t.Callable[[], str], msvcrt.getwch) + + rv = func() + + if rv in ("\x00", "\xe0"): + # \x00 and \xe0 are control characters that indicate special key, + # see above. + rv += func() + + _translate_ch_to_exc(rv) + return rv + +else: + import termios + import tty + + @contextlib.contextmanager + def raw_terminal() -> cabc.Iterator[int]: + f: t.TextIO | None + fd: int + + if not isatty(sys.stdin): + f = open("/dev/tty") + fd = f.fileno() + else: + fd = sys.stdin.fileno() + f = None + + try: + old_settings = termios.tcgetattr(fd) + + try: + tty.setraw(fd) + yield fd + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + sys.stdout.flush() + + if f is not None: + f.close() + except termios.error: + pass + + def getchar(echo: bool) -> str: + with raw_terminal() as fd: + ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace") + + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + + _translate_ch_to_exc(ch) + return ch diff --git a/venv/lib/python3.10/site-packages/click/_textwrap.py b/venv/lib/python3.10/site-packages/click/_textwrap.py new file mode 100644 index 0000000..97fbee3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/_textwrap.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import collections.abc as cabc +import textwrap +from contextlib import contextmanager + + +class TextWrapper(textwrap.TextWrapper): + def _handle_long_word( + self, + reversed_chunks: list[str], + cur_line: list[str], + cur_len: int, + width: int, + ) -> None: + space_left = max(width - cur_len, 1) + + if self.break_long_words: + last = reversed_chunks[-1] + cut = last[:space_left] + res = last[space_left:] + cur_line.append(cut) + reversed_chunks[-1] = res + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + @contextmanager + def extra_indent(self, indent: str) -> cabc.Iterator[None]: + old_initial_indent = self.initial_indent + old_subsequent_indent = self.subsequent_indent + self.initial_indent += indent + self.subsequent_indent += indent + + try: + yield + finally: + self.initial_indent = old_initial_indent + self.subsequent_indent = old_subsequent_indent + + def indent_only(self, text: str) -> str: + rv = [] + + for idx, line in enumerate(text.splitlines()): + indent = self.initial_indent + + if idx > 0: + indent = self.subsequent_indent + + rv.append(f"{indent}{line}") + + return "\n".join(rv) diff --git a/venv/lib/python3.10/site-packages/click/_utils.py b/venv/lib/python3.10/site-packages/click/_utils.py new file mode 100644 index 0000000..09fb008 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/_utils.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import enum +import typing as t + + +class Sentinel(enum.Enum): + """Enum used to define sentinel values. + + .. seealso:: + + `PEP 661 - Sentinel Values `_. + """ + + UNSET = object() + FLAG_NEEDS_VALUE = object() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + +UNSET = Sentinel.UNSET +"""Sentinel used to indicate that a value is not set.""" + +FLAG_NEEDS_VALUE = Sentinel.FLAG_NEEDS_VALUE +"""Sentinel used to indicate an option was passed as a flag without a +value but is not a flag option. + +``Option.consume_value`` uses this to prompt or use the ``flag_value``. +""" + +T_UNSET = t.Literal[UNSET] # type: ignore[valid-type] +"""Type hint for the :data:`UNSET` sentinel value.""" + +T_FLAG_NEEDS_VALUE = t.Literal[FLAG_NEEDS_VALUE] # type: ignore[valid-type] +"""Type hint for the :data:`FLAG_NEEDS_VALUE` sentinel value.""" diff --git a/venv/lib/python3.10/site-packages/click/_winconsole.py b/venv/lib/python3.10/site-packages/click/_winconsole.py new file mode 100644 index 0000000..e56c7c6 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/_winconsole.py @@ -0,0 +1,296 @@ +# This module is based on the excellent work by Adam Bartoš who +# provided a lot of what went into the implementation here in +# the discussion to issue1602 in the Python bug tracker. +# +# There are some general differences in regards to how this works +# compared to the original patches as we do not need to patch +# the entire interpreter but just work in our little world of +# echo and prompt. +from __future__ import annotations + +import collections.abc as cabc +import io +import sys +import time +import typing as t +from ctypes import Array +from ctypes import byref +from ctypes import c_char +from ctypes import c_char_p +from ctypes import c_int +from ctypes import c_ssize_t +from ctypes import c_ulong +from ctypes import c_void_p +from ctypes import POINTER +from ctypes import py_object +from ctypes import Structure +from ctypes.wintypes import DWORD +from ctypes.wintypes import HANDLE +from ctypes.wintypes import LPCWSTR +from ctypes.wintypes import LPWSTR + +from ._compat import _NonClosingTextIOWrapper + +assert sys.platform == "win32" +import msvcrt # noqa: E402 +from ctypes import windll # noqa: E402 +from ctypes import WINFUNCTYPE # noqa: E402 + +c_ssize_p = POINTER(c_ssize_t) + +kernel32 = windll.kernel32 +GetStdHandle = kernel32.GetStdHandle +ReadConsoleW = kernel32.ReadConsoleW +WriteConsoleW = kernel32.WriteConsoleW +GetConsoleMode = kernel32.GetConsoleMode +GetLastError = kernel32.GetLastError +GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) +CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( + ("CommandLineToArgvW", windll.shell32) +) +LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32)) + +STDIN_HANDLE = GetStdHandle(-10) +STDOUT_HANDLE = GetStdHandle(-11) +STDERR_HANDLE = GetStdHandle(-12) + +PyBUF_SIMPLE = 0 +PyBUF_WRITABLE = 1 + +ERROR_SUCCESS = 0 +ERROR_NOT_ENOUGH_MEMORY = 8 +ERROR_OPERATION_ABORTED = 995 + +STDIN_FILENO = 0 +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + +EOF = b"\x1a" +MAX_BYTES_WRITTEN = 32767 + +if t.TYPE_CHECKING: + try: + # Using `typing_extensions.Buffer` instead of `collections.abc` + # on Windows for some reason does not have `Sized` implemented. + from collections.abc import Buffer # type: ignore + except ImportError: + from typing_extensions import Buffer + +try: + from ctypes import pythonapi +except ImportError: + # On PyPy we cannot get buffers so our ability to operate here is + # severely limited. + get_buffer = None +else: + + class Py_buffer(Structure): + _fields_ = [ # noqa: RUF012 + ("buf", c_void_p), + ("obj", py_object), + ("len", c_ssize_t), + ("itemsize", c_ssize_t), + ("readonly", c_int), + ("ndim", c_int), + ("format", c_char_p), + ("shape", c_ssize_p), + ("strides", c_ssize_p), + ("suboffsets", c_ssize_p), + ("internal", c_void_p), + ] + + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release + + def get_buffer(obj: Buffer, writable: bool = False) -> Array[c_char]: + buf = Py_buffer() + flags: int = PyBUF_WRITABLE if writable else PyBUF_SIMPLE + PyObject_GetBuffer(py_object(obj), byref(buf), flags) + + try: + buffer_type = c_char * buf.len + out: Array[c_char] = buffer_type.from_address(buf.buf) + return out + finally: + PyBuffer_Release(byref(buf)) + + +class _WindowsConsoleRawIOBase(io.RawIOBase): + def __init__(self, handle: int | None) -> None: + self.handle = handle + + def isatty(self) -> t.Literal[True]: + super().isatty() + return True + + +class _WindowsConsoleReader(_WindowsConsoleRawIOBase): + def readable(self) -> t.Literal[True]: + return True + + def readinto(self, b: Buffer) -> int: + bytes_to_be_read = len(b) + if not bytes_to_be_read: + return 0 + elif bytes_to_be_read % 2: + raise ValueError( + "cannot read odd number of bytes from UTF-16-LE encoded console" + ) + + buffer = get_buffer(b, writable=True) + code_units_to_be_read = bytes_to_be_read // 2 + code_units_read = c_ulong() + + rv = ReadConsoleW( + HANDLE(self.handle), + buffer, + code_units_to_be_read, + byref(code_units_read), + None, + ) + if GetLastError() == ERROR_OPERATION_ABORTED: + # wait for KeyboardInterrupt + time.sleep(0.1) + if not rv: + raise OSError(f"Windows error: {GetLastError()}") + + if buffer[0] == EOF: + return 0 + return 2 * code_units_read.value + + +class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): + def writable(self) -> t.Literal[True]: + return True + + @staticmethod + def _get_error_message(errno: int) -> str: + if errno == ERROR_SUCCESS: + return "ERROR_SUCCESS" + elif errno == ERROR_NOT_ENOUGH_MEMORY: + return "ERROR_NOT_ENOUGH_MEMORY" + return f"Windows error {errno}" + + def write(self, b: Buffer) -> int: + bytes_to_be_written = len(b) + buf = get_buffer(b) + code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 + code_units_written = c_ulong() + + WriteConsoleW( + HANDLE(self.handle), + buf, + code_units_to_be_written, + byref(code_units_written), + None, + ) + bytes_written = 2 * code_units_written.value + + if bytes_written == 0 and bytes_to_be_written > 0: + raise OSError(self._get_error_message(GetLastError())) + return bytes_written + + +class ConsoleStream: + def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None: + self._text_stream = text_stream + self.buffer = byte_stream + + @property + def name(self) -> str: + return self.buffer.name + + def write(self, x: t.AnyStr) -> int: + if isinstance(x, str): + return self._text_stream.write(x) + try: + self.flush() + except Exception: + pass + return self.buffer.write(x) + + def writelines(self, lines: cabc.Iterable[t.AnyStr]) -> None: + for line in lines: + self.write(line) + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._text_stream, name) + + def isatty(self) -> bool: + return self.buffer.isatty() + + def __repr__(self) -> str: + return f"" + + +def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +_stream_factories: cabc.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = { + 0: _get_text_stdin, + 1: _get_text_stdout, + 2: _get_text_stderr, +} + + +def _is_console(f: t.TextIO) -> bool: + if not hasattr(f, "fileno"): + return False + + try: + fileno = f.fileno() + except (OSError, io.UnsupportedOperation): + return False + + handle = msvcrt.get_osfhandle(fileno) + return bool(GetConsoleMode(handle, byref(DWORD()))) + + +def _get_windows_console_stream( + f: t.TextIO, encoding: str | None, errors: str | None +) -> t.TextIO | None: + if ( + get_buffer is None + or encoding not in {"utf-16-le", None} + or errors not in {"strict", None} + or not _is_console(f) + ): + return None + + func = _stream_factories.get(f.fileno()) + if func is None: + return None + + b = getattr(f, "buffer", None) + + if b is None: + return None + + return func(b) diff --git a/venv/lib/python3.10/site-packages/click/core.py b/venv/lib/python3.10/site-packages/click/core.py new file mode 100644 index 0000000..57f549c --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/core.py @@ -0,0 +1,3415 @@ +from __future__ import annotations + +import collections.abc as cabc +import enum +import errno +import inspect +import os +import sys +import typing as t +from collections import abc +from collections import Counter +from contextlib import AbstractContextManager +from contextlib import contextmanager +from contextlib import ExitStack +from functools import update_wrapper +from gettext import gettext as _ +from gettext import ngettext +from itertools import repeat +from types import TracebackType + +from . import types +from ._utils import FLAG_NEEDS_VALUE +from ._utils import UNSET +from .exceptions import Abort +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import Exit +from .exceptions import MissingParameter +from .exceptions import NoArgsIsHelpError +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import join_options +from .globals import pop_context +from .globals import push_context +from .parser import _OptionParser +from .parser import _split_opt +from .termui import confirm +from .termui import prompt +from .termui import style +from .utils import _detect_program_name +from .utils import _expand_args +from .utils import echo +from .utils import make_default_short_help +from .utils import make_str +from .utils import PacifyFlushWrapper + +if t.TYPE_CHECKING: + from .shell_completion import CompletionItem + +F = t.TypeVar("F", bound="t.Callable[..., t.Any]") +V = t.TypeVar("V") + + +def _complete_visible_commands( + ctx: Context, incomplete: str +) -> cabc.Iterator[tuple[str, Command]]: + """List all the subcommands of a group that start with the + incomplete value and aren't hidden. + + :param ctx: Invocation context for the group. + :param incomplete: Value being completed. May be empty. + """ + multi = t.cast(Group, ctx.command) + + for name in multi.list_commands(ctx): + if name.startswith(incomplete): + command = multi.get_command(ctx, name) + + if command is not None and not command.hidden: + yield name, command + + +def _check_nested_chain( + base_command: Group, cmd_name: str, cmd: Command, register: bool = False +) -> None: + if not base_command.chain or not isinstance(cmd, Group): + return + + if register: + message = ( + f"It is not possible to add the group {cmd_name!r} to another" + f" group {base_command.name!r} that is in chain mode." + ) + else: + message = ( + f"Found the group {cmd_name!r} as subcommand to another group " + f" {base_command.name!r} that is in chain mode. This is not supported." + ) + + raise RuntimeError(message) + + +def batch(iterable: cabc.Iterable[V], batch_size: int) -> list[tuple[V, ...]]: + return list(zip(*repeat(iter(iterable), batch_size), strict=False)) + + +@contextmanager +def augment_usage_errors( + ctx: Context, param: Parameter | None = None +) -> cabc.Iterator[None]: + """Context manager that attaches extra information to exceptions.""" + try: + yield + except BadParameter as e: + if e.ctx is None: + e.ctx = ctx + if param is not None and e.param is None: + e.param = param + raise + except UsageError as e: + if e.ctx is None: + e.ctx = ctx + raise + + +def iter_params_for_processing( + invocation_order: cabc.Sequence[Parameter], + declaration_order: cabc.Sequence[Parameter], +) -> list[Parameter]: + """Returns all declared parameters in the order they should be processed. + + The declared parameters are re-shuffled depending on the order in which + they were invoked, as well as the eagerness of each parameters. + + The invocation order takes precedence over the declaration order. I.e. the + order in which the user provided them to the CLI is respected. + + This behavior and its effect on callback evaluation is detailed at: + https://click.palletsprojects.com/en/stable/advanced/#callback-evaluation-order + """ + + def sort_key(item: Parameter) -> tuple[bool, float]: + try: + idx: float = invocation_order.index(item) + except ValueError: + idx = float("inf") + + return not item.is_eager, idx + + return sorted(declaration_order, key=sort_key) + + +class ParameterSource(enum.Enum): + """This is an :class:`~enum.Enum` that indicates the source of a + parameter's value. + + Use :meth:`click.Context.get_parameter_source` to get the + source for a parameter by name. + + .. versionchanged:: 8.0 + Use :class:`~enum.Enum` and drop the ``validate`` method. + + .. versionchanged:: 8.0 + Added the ``PROMPT`` value. + """ + + COMMANDLINE = enum.auto() + """The value was provided by the command line args.""" + ENVIRONMENT = enum.auto() + """The value was provided with an environment variable.""" + DEFAULT = enum.auto() + """Used the default specified by the parameter.""" + DEFAULT_MAP = enum.auto() + """Used a default provided by :attr:`Context.default_map`.""" + PROMPT = enum.auto() + """Used a prompt to confirm a default or provide a value.""" + + +class Context: + """The context is a special internal object that holds state relevant + for the script execution at every single level. It's normally invisible + to commands unless they opt-in to getting access to it. + + The context is useful as it can pass internal objects around and can + control special execution features such as reading data from + environment variables. + + A context can be used as context manager in which case it will call + :meth:`close` on teardown. + + :param command: the command class for this context. + :param parent: the parent context. + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it is usually + the name of the script, for commands below it it's + the name of the script. + :param obj: an arbitrary object of user data. + :param auto_envvar_prefix: the prefix to use for automatic environment + variables. If this is `None` then reading + from environment variables is disabled. This + does not affect manually set environment + variables which are always read. + :param default_map: a dictionary (like object) with default values + for parameters. + :param terminal_width: the width of the terminal. The default is + inherit from parent context. If no context + defines the terminal width then auto + detection will be applied. + :param max_content_width: the maximum width for content rendered by + Click (this currently only affects help + pages). This defaults to 80 characters if + not overridden. In other words: even if the + terminal is larger than that, Click will not + format things wider than 80 characters by + default. In addition to that, formatters might + add some safety mapping on the right. + :param resilient_parsing: if this flag is enabled then Click will + parse without any interactivity or callback + invocation. Default values will also be + ignored. This is useful for implementing + things such as completion support. + :param allow_extra_args: if this is set to `True` then extra arguments + at the end will not raise an error and will be + kept on the context. The default is to inherit + from the command. + :param allow_interspersed_args: if this is set to `False` then options + and arguments cannot be mixed. The + default is to inherit from the command. + :param ignore_unknown_options: instructs click to ignore options it does + not know and keeps them for later + processing. + :param help_option_names: optionally a list of strings that define how + the default help parameter is named. The + default is ``['--help']``. + :param token_normalize_func: an optional function that is used to + normalize tokens (options, choices, + etc.). This for instance can be used to + implement case insensitive behavior. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are used in texts that Click prints which is by + default not the case. This for instance would affect + help output. + :param show_default: Show the default value for commands. If this + value is not set, it defaults to the value from the parent + context. ``Command.show_default`` overrides this default for the + specific command. + + .. versionchanged:: 8.2 + The ``protected_args`` attribute is deprecated and will be removed in + Click 9.0. ``args`` will contain remaining unparsed tokens. + + .. versionchanged:: 8.1 + The ``show_default`` parameter is overridden by + ``Command.show_default``, instead of the other way around. + + .. versionchanged:: 8.0 + The ``show_default`` parameter defaults to the value from the + parent context. + + .. versionchanged:: 7.1 + Added the ``show_default`` parameter. + + .. versionchanged:: 4.0 + Added the ``color``, ``ignore_unknown_options``, and + ``max_content_width`` parameters. + + .. versionchanged:: 3.0 + Added the ``allow_extra_args`` and ``allow_interspersed_args`` + parameters. + + .. versionchanged:: 2.0 + Added the ``resilient_parsing``, ``help_option_names``, and + ``token_normalize_func`` parameters. + """ + + #: The formatter class to create with :meth:`make_formatter`. + #: + #: .. versionadded:: 8.0 + formatter_class: type[HelpFormatter] = HelpFormatter + + def __init__( + self, + command: Command, + parent: Context | None = None, + info_name: str | None = None, + obj: t.Any | None = None, + auto_envvar_prefix: str | None = None, + default_map: cabc.MutableMapping[str, t.Any] | None = None, + terminal_width: int | None = None, + max_content_width: int | None = None, + resilient_parsing: bool = False, + allow_extra_args: bool | None = None, + allow_interspersed_args: bool | None = None, + ignore_unknown_options: bool | None = None, + help_option_names: list[str] | None = None, + token_normalize_func: t.Callable[[str], str] | None = None, + color: bool | None = None, + show_default: bool | None = None, + ) -> None: + #: the parent context or `None` if none exists. + self.parent = parent + #: the :class:`Command` for this context. + self.command = command + #: the descriptive information name + self.info_name = info_name + #: Map of parameter names to their parsed values. Parameters + #: with ``expose_value=False`` are not stored. + self.params: dict[str, t.Any] = {} + #: the leftover arguments. + self.args: list[str] = [] + #: protected arguments. These are arguments that are prepended + #: to `args` when certain parsing scenarios are encountered but + #: must be never propagated to another arguments. This is used + #: to implement nested parsing. + self._protected_args: list[str] = [] + #: the collected prefixes of the command's options. + self._opt_prefixes: set[str] = set(parent._opt_prefixes) if parent else set() + + if obj is None and parent is not None: + obj = parent.obj + + #: the user object stored. + self.obj: t.Any = obj + self._meta: dict[str, t.Any] = getattr(parent, "meta", {}) + + #: A dictionary (-like object) with defaults for parameters. + if ( + default_map is None + and info_name is not None + and parent is not None + and parent.default_map is not None + ): + default_map = parent.default_map.get(info_name) + + self.default_map: cabc.MutableMapping[str, t.Any] | None = default_map + + #: This flag indicates if a subcommand is going to be executed. A + #: group callback can use this information to figure out if it's + #: being executed directly or because the execution flow passes + #: onwards to a subcommand. By default it's None, but it can be + #: the name of the subcommand to execute. + #: + #: If chaining is enabled this will be set to ``'*'`` in case + #: any commands are executed. It is however not possible to + #: figure out which ones. If you require this knowledge you + #: should use a :func:`result_callback`. + self.invoked_subcommand: str | None = None + + if terminal_width is None and parent is not None: + terminal_width = parent.terminal_width + + #: The width of the terminal (None is autodetection). + self.terminal_width: int | None = terminal_width + + if max_content_width is None and parent is not None: + max_content_width = parent.max_content_width + + #: The maximum width of formatted content (None implies a sensible + #: default which is 80 for most things). + self.max_content_width: int | None = max_content_width + + if allow_extra_args is None: + allow_extra_args = command.allow_extra_args + + #: Indicates if the context allows extra args or if it should + #: fail on parsing. + #: + #: .. versionadded:: 3.0 + self.allow_extra_args = allow_extra_args + + if allow_interspersed_args is None: + allow_interspersed_args = command.allow_interspersed_args + + #: Indicates if the context allows mixing of arguments and + #: options or not. + #: + #: .. versionadded:: 3.0 + self.allow_interspersed_args: bool = allow_interspersed_args + + if ignore_unknown_options is None: + ignore_unknown_options = command.ignore_unknown_options + + #: Instructs click to ignore options that a command does not + #: understand and will store it on the context for later + #: processing. This is primarily useful for situations where you + #: want to call into external programs. Generally this pattern is + #: strongly discouraged because it's not possibly to losslessly + #: forward all arguments. + #: + #: .. versionadded:: 4.0 + self.ignore_unknown_options: bool = ignore_unknown_options + + if help_option_names is None: + if parent is not None: + help_option_names = parent.help_option_names + else: + help_option_names = ["--help"] + + #: The names for the help options. + self.help_option_names: list[str] = help_option_names + + if token_normalize_func is None and parent is not None: + token_normalize_func = parent.token_normalize_func + + #: An optional normalization function for tokens. This is + #: options, choices, commands etc. + self.token_normalize_func: t.Callable[[str], str] | None = token_normalize_func + + #: Indicates if resilient parsing is enabled. In that case Click + #: will do its best to not cause any failures and default values + #: will be ignored. Useful for completion. + self.resilient_parsing: bool = resilient_parsing + + # If there is no envvar prefix yet, but the parent has one and + # the command on this level has a name, we can expand the envvar + # prefix automatically. + if auto_envvar_prefix is None: + if ( + parent is not None + and parent.auto_envvar_prefix is not None + and self.info_name is not None + ): + auto_envvar_prefix = ( + f"{parent.auto_envvar_prefix}_{self.info_name.upper()}" + ) + else: + auto_envvar_prefix = auto_envvar_prefix.upper() + + if auto_envvar_prefix is not None: + auto_envvar_prefix = auto_envvar_prefix.replace("-", "_") + + self.auto_envvar_prefix: str | None = auto_envvar_prefix + + if color is None and parent is not None: + color = parent.color + + #: Controls if styling output is wanted or not. + self.color: bool | None = color + + if show_default is None and parent is not None: + show_default = parent.show_default + + #: Show option default values when formatting help text. + self.show_default: bool | None = show_default + + self._close_callbacks: list[t.Callable[[], t.Any]] = [] + self._depth = 0 + self._parameter_source: dict[str, ParameterSource] = {} + self._exit_stack = ExitStack() + + @property + def protected_args(self) -> list[str]: + import warnings + + warnings.warn( + "'protected_args' is deprecated and will be removed in Click 9.0." + " 'args' will contain remaining unparsed tokens.", + DeprecationWarning, + stacklevel=2, + ) + return self._protected_args + + def to_info_dict(self) -> dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. This traverses the entire CLI + structure. + + .. code-block:: python + + with Context(cli) as ctx: + info = ctx.to_info_dict() + + .. versionadded:: 8.0 + """ + return { + "command": self.command.to_info_dict(self), + "info_name": self.info_name, + "allow_extra_args": self.allow_extra_args, + "allow_interspersed_args": self.allow_interspersed_args, + "ignore_unknown_options": self.ignore_unknown_options, + "auto_envvar_prefix": self.auto_envvar_prefix, + } + + def __enter__(self) -> Context: + self._depth += 1 + push_context(self) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> bool | None: + self._depth -= 1 + exit_result: bool | None = None + if self._depth == 0: + exit_result = self._close_with_exception_info(exc_type, exc_value, tb) + pop_context() + + return exit_result + + @contextmanager + def scope(self, cleanup: bool = True) -> cabc.Iterator[Context]: + """This helper method can be used with the context object to promote + it to the current thread local (see :func:`get_current_context`). + The default behavior of this is to invoke the cleanup functions which + can be disabled by setting `cleanup` to `False`. The cleanup + functions are typically used for things such as closing file handles. + + If the cleanup is intended the context object can also be directly + used as a context manager. + + Example usage:: + + with ctx.scope(): + assert get_current_context() is ctx + + This is equivalent:: + + with ctx: + assert get_current_context() is ctx + + .. versionadded:: 5.0 + + :param cleanup: controls if the cleanup functions should be run or + not. The default is to run these functions. In + some situations the context only wants to be + temporarily pushed in which case this can be disabled. + Nested pushes automatically defer the cleanup. + """ + if not cleanup: + self._depth += 1 + try: + with self as rv: + yield rv + finally: + if not cleanup: + self._depth -= 1 + + @property + def meta(self) -> dict[str, t.Any]: + """This is a dictionary which is shared with all the contexts + that are nested. It exists so that click utilities can store some + state here if they need to. It is however the responsibility of + that code to manage this dictionary well. + + The keys are supposed to be unique dotted strings. For instance + module paths are a good choice for it. What is stored in there is + irrelevant for the operation of click. However what is important is + that code that places data here adheres to the general semantics of + the system. + + Example usage:: + + LANG_KEY = f'{__name__}.lang' + + def set_language(value): + ctx = get_current_context() + ctx.meta[LANG_KEY] = value + + def get_language(): + return get_current_context().meta.get(LANG_KEY, 'en_US') + + .. versionadded:: 5.0 + """ + return self._meta + + def make_formatter(self) -> HelpFormatter: + """Creates the :class:`~click.HelpFormatter` for the help and + usage output. + + To quickly customize the formatter class used without overriding + this method, set the :attr:`formatter_class` attribute. + + .. versionchanged:: 8.0 + Added the :attr:`formatter_class` attribute. + """ + return self.formatter_class( + width=self.terminal_width, max_width=self.max_content_width + ) + + def with_resource(self, context_manager: AbstractContextManager[V]) -> V: + """Register a resource as if it were used in a ``with`` + statement. The resource will be cleaned up when the context is + popped. + + Uses :meth:`contextlib.ExitStack.enter_context`. It calls the + resource's ``__enter__()`` method and returns the result. When + the context is popped, it closes the stack, which calls the + resource's ``__exit__()`` method. + + To register a cleanup function for something that isn't a + context manager, use :meth:`call_on_close`. Or use something + from :mod:`contextlib` to turn it into a context manager first. + + .. code-block:: python + + @click.group() + @click.option("--name") + @click.pass_context + def cli(ctx): + ctx.obj = ctx.with_resource(connect_db(name)) + + :param context_manager: The context manager to enter. + :return: Whatever ``context_manager.__enter__()`` returns. + + .. versionadded:: 8.0 + """ + return self._exit_stack.enter_context(context_manager) + + def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + """Register a function to be called when the context tears down. + + This can be used to close resources opened during the script + execution. Resources that support Python's context manager + protocol which would be used in a ``with`` statement should be + registered with :meth:`with_resource` instead. + + :param f: The function to execute on teardown. + """ + return self._exit_stack.callback(f) + + def close(self) -> None: + """Invoke all close callbacks registered with + :meth:`call_on_close`, and exit all context managers entered + with :meth:`with_resource`. + """ + self._close_with_exception_info(None, None, None) + + def _close_with_exception_info( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> bool | None: + """Unwind the exit stack by calling its :meth:`__exit__` providing the exception + information to allow for exception handling by the various resources registered + using :meth;`with_resource` + + :return: Whatever ``exit_stack.__exit__()`` returns. + """ + exit_result = self._exit_stack.__exit__(exc_type, exc_value, tb) + # In case the context is reused, create a new exit stack. + self._exit_stack = ExitStack() + + return exit_result + + @property + def command_path(self) -> str: + """The computed command path. This is used for the ``usage`` + information on the help page. It's automatically created by + combining the info names of the chain of contexts to the root. + """ + rv = "" + if self.info_name is not None: + rv = self.info_name + if self.parent is not None: + parent_command_path = [self.parent.command_path] + + if isinstance(self.parent.command, Command): + for param in self.parent.command.get_params(self): + parent_command_path.extend(param.get_usage_pieces(self)) + + rv = f"{' '.join(parent_command_path)} {rv}" + return rv.lstrip() + + def find_root(self) -> Context: + """Finds the outermost context.""" + node = self + while node.parent is not None: + node = node.parent + return node + + def find_object(self, object_type: type[V]) -> V | None: + """Finds the closest object of a given type.""" + node: Context | None = self + + while node is not None: + if isinstance(node.obj, object_type): + return node.obj + + node = node.parent + + return None + + def ensure_object(self, object_type: type[V]) -> V: + """Like :meth:`find_object` but sets the innermost object to a + new instance of `object_type` if it does not exist. + """ + rv = self.find_object(object_type) + if rv is None: + self.obj = rv = object_type() + return rv + + @t.overload + def lookup_default( + self, name: str, call: t.Literal[True] = True + ) -> t.Any | None: ... + + @t.overload + def lookup_default( + self, name: str, call: t.Literal[False] = ... + ) -> t.Any | t.Callable[[], t.Any] | None: ... + + def lookup_default(self, name: str, call: bool = True) -> t.Any | None: + """Get the default for a parameter from :attr:`default_map`. + + :param name: Name of the parameter. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + if self.default_map is not None: + value = self.default_map.get(name, UNSET) + + if call and callable(value): + return value() + + return value + + return UNSET + + def fail(self, message: str) -> t.NoReturn: + """Aborts the execution of the program with a specific error + message. + + :param message: the error message to fail with. + """ + raise UsageError(message, self) + + def abort(self) -> t.NoReturn: + """Aborts the script.""" + raise Abort() + + def exit(self, code: int = 0) -> t.NoReturn: + """Exits the application with a given exit code. + + .. versionchanged:: 8.2 + Callbacks and context managers registered with :meth:`call_on_close` + and :meth:`with_resource` are closed before exiting. + """ + self.close() + raise Exit(code) + + def get_usage(self) -> str: + """Helper method to get formatted usage string for the current + context and command. + """ + return self.command.get_usage(self) + + def get_help(self) -> str: + """Helper method to get formatted help page for the current + context and command. + """ + return self.command.get_help(self) + + def _make_sub_context(self, command: Command) -> Context: + """Create a new context of the same type as this context, but + for a new command. + + :meta private: + """ + return type(self)(command, info_name=command.name, parent=self) + + @t.overload + def invoke( + self, callback: t.Callable[..., V], /, *args: t.Any, **kwargs: t.Any + ) -> V: ... + + @t.overload + def invoke(self, callback: Command, /, *args: t.Any, **kwargs: t.Any) -> t.Any: ... + + def invoke( + self, callback: Command | t.Callable[..., V], /, *args: t.Any, **kwargs: t.Any + ) -> t.Any | V: + """Invokes a command callback in exactly the way it expects. There + are two ways to invoke this method: + + 1. the first argument can be a callback and all other arguments and + keyword arguments are forwarded directly to the function. + 2. the first argument is a click command object. In that case all + arguments are forwarded as well but proper click parameters + (options and click arguments) must be keyword arguments and Click + will fill in defaults. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if :meth:`forward` is called at multiple levels. + + .. versionchanged:: 3.2 + A new context is created, and missing arguments use default values. + """ + if isinstance(callback, Command): + other_cmd = callback + + if other_cmd.callback is None: + raise TypeError( + "The given command does not have a callback that can be invoked." + ) + else: + callback = t.cast("t.Callable[..., V]", other_cmd.callback) + + ctx = self._make_sub_context(other_cmd) + + for param in other_cmd.params: + if param.name not in kwargs and param.expose_value: + default_value = param.get_default(ctx) + # We explicitly hide the :attr:`UNSET` value to the user, as we + # choose to make it an implementation detail. And because ``invoke`` + # has been designed as part of Click public API, we return ``None`` + # instead. Refs: + # https://github.com/pallets/click/issues/3066 + # https://github.com/pallets/click/issues/3065 + # https://github.com/pallets/click/pull/3068 + if default_value is UNSET: + default_value = None + kwargs[param.name] = param.type_cast_value( # type: ignore + ctx, default_value + ) + + # Track all kwargs as params, so that forward() will pass + # them on in subsequent calls. + ctx.params.update(kwargs) + else: + ctx = self + + with augment_usage_errors(self): + with ctx: + return callback(*args, **kwargs) + + def forward(self, cmd: Command, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Similar to :meth:`invoke` but fills in default keyword + arguments from the current context if the other command expects + it. This cannot invoke callbacks directly, only other commands. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if ``forward`` is called at multiple levels. + """ + # Can only forward to other commands, not direct callbacks. + if not isinstance(cmd, Command): + raise TypeError("Callback is not a command.") + + for param in self.params: + if param not in kwargs: + kwargs[param] = self.params[param] + + return self.invoke(cmd, *args, **kwargs) + + def set_parameter_source(self, name: str, source: ParameterSource) -> None: + """Set the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + :param name: The name of the parameter. + :param source: A member of :class:`~click.core.ParameterSource`. + """ + self._parameter_source[name] = source + + def get_parameter_source(self, name: str) -> ParameterSource | None: + """Get the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + This can be useful for determining when a user specified a value + on the command line that is the same as the default value. It + will be :attr:`~click.core.ParameterSource.DEFAULT` only if the + value was actually taken from the default. + + :param name: The name of the parameter. + :rtype: ParameterSource + + .. versionchanged:: 8.0 + Returns ``None`` if the parameter was not provided from any + source. + """ + return self._parameter_source.get(name) + + +class Command: + """Commands are the basic building block of command line interfaces in + Click. A basic command handles command line parsing and might dispatch + more parsing to commands nested below it. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + :param callback: the callback to invoke. This is optional. + :param params: the parameters to register with this command. This can + be either :class:`Option` or :class:`Argument` objects. + :param help: the help string to use for this command. + :param epilog: like the help string but it's printed at the end of the + help page after everything else. + :param short_help: the short help to use for this command. This is + shown on the command listing of the parent command. + :param add_help_option: by default each command registers a ``--help`` + option. This can be disabled by this parameter. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is disabled by default. + If enabled this will add ``--help`` as argument + if no arguments are passed + :param hidden: hide this command from help outputs. + :param deprecated: If ``True`` or non-empty string, issues a message + indicating that the command is deprecated and highlights + its deprecation in --help. The message can be customized + by using a string as the value. + + .. versionchanged:: 8.2 + This is the base class for all commands, not ``BaseCommand``. + ``deprecated`` can be set to a string as well to customize the + deprecation message. + + .. versionchanged:: 8.1 + ``help``, ``epilog``, and ``short_help`` are stored unprocessed, + all formatting is done when outputting help text, not at init, + and is done even if not using the ``@command`` decorator. + + .. versionchanged:: 8.0 + Added a ``repr`` showing the command name. + + .. versionchanged:: 7.1 + Added the ``no_args_is_help`` parameter. + + .. versionchanged:: 2.0 + Added the ``context_settings`` parameter. + """ + + #: The context class to create with :meth:`make_context`. + #: + #: .. versionadded:: 8.0 + context_class: type[Context] = Context + + #: the default for the :attr:`Context.allow_extra_args` flag. + allow_extra_args = False + + #: the default for the :attr:`Context.allow_interspersed_args` flag. + allow_interspersed_args = True + + #: the default for the :attr:`Context.ignore_unknown_options` flag. + ignore_unknown_options = False + + def __init__( + self, + name: str | None, + context_settings: cabc.MutableMapping[str, t.Any] | None = None, + callback: t.Callable[..., t.Any] | None = None, + params: list[Parameter] | None = None, + help: str | None = None, + epilog: str | None = None, + short_help: str | None = None, + options_metavar: str | None = "[OPTIONS]", + add_help_option: bool = True, + no_args_is_help: bool = False, + hidden: bool = False, + deprecated: bool | str = False, + ) -> None: + #: the name the command thinks it has. Upon registering a command + #: on a :class:`Group` the group will default the command name + #: with this information. You should instead use the + #: :class:`Context`\'s :attr:`~Context.info_name` attribute. + self.name = name + + if context_settings is None: + context_settings = {} + + #: an optional dictionary with defaults passed to the context. + self.context_settings: cabc.MutableMapping[str, t.Any] = context_settings + + #: the callback to execute when the command fires. This might be + #: `None` in which case nothing happens. + self.callback = callback + #: the list of parameters for this command in the order they + #: should show up in the help page and execute. Eager parameters + #: will automatically be handled before non eager ones. + self.params: list[Parameter] = params or [] + self.help = help + self.epilog = epilog + self.options_metavar = options_metavar + self.short_help = short_help + self.add_help_option = add_help_option + self._help_option = None + self.no_args_is_help = no_args_is_help + self.hidden = hidden + self.deprecated = deprecated + + def to_info_dict(self, ctx: Context) -> dict[str, t.Any]: + return { + "name": self.name, + "params": [param.to_info_dict() for param in self.get_params(ctx)], + "help": self.help, + "epilog": self.epilog, + "short_help": self.short_help, + "hidden": self.hidden, + "deprecated": self.deprecated, + } + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def get_usage(self, ctx: Context) -> str: + """Formats the usage line into a string and returns it. + + Calls :meth:`format_usage` internally. + """ + formatter = ctx.make_formatter() + self.format_usage(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_params(self, ctx: Context) -> list[Parameter]: + params = self.params + help_option = self.get_help_option(ctx) + + if help_option is not None: + params = [*params, help_option] + + if __debug__: + import warnings + + opts = [opt for param in params for opt in param.opts] + opts_counter = Counter(opts) + duplicate_opts = (opt for opt, count in opts_counter.items() if count > 1) + + for duplicate_opt in duplicate_opts: + warnings.warn( + ( + f"The parameter {duplicate_opt} is used more than once. " + "Remove its duplicate as parameters should be unique." + ), + stacklevel=3, + ) + + return params + + def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the usage line into the formatter. + + This is a low-level method called by :meth:`get_usage`. + """ + pieces = self.collect_usage_pieces(ctx) + formatter.write_usage(ctx.command_path, " ".join(pieces)) + + def collect_usage_pieces(self, ctx: Context) -> list[str]: + """Returns all the pieces that go into the usage line and returns + it as a list of strings. + """ + rv = [self.options_metavar] if self.options_metavar else [] + + for param in self.get_params(ctx): + rv.extend(param.get_usage_pieces(ctx)) + + return rv + + def get_help_option_names(self, ctx: Context) -> list[str]: + """Returns the names for the help option.""" + all_names = set(ctx.help_option_names) + for param in self.params: + all_names.difference_update(param.opts) + all_names.difference_update(param.secondary_opts) + return list(all_names) + + def get_help_option(self, ctx: Context) -> Option | None: + """Returns the help option object. + + Skipped if :attr:`add_help_option` is ``False``. + + .. versionchanged:: 8.1.8 + The help option is now cached to avoid creating it multiple times. + """ + help_option_names = self.get_help_option_names(ctx) + + if not help_option_names or not self.add_help_option: + return None + + # Cache the help option object in private _help_option attribute to + # avoid creating it multiple times. Not doing this will break the + # callback odering by iter_params_for_processing(), which relies on + # object comparison. + if self._help_option is None: + # Avoid circular import. + from .decorators import help_option + + # Apply help_option decorator and pop resulting option + help_option(*help_option_names)(self) + self._help_option = self.params.pop() # type: ignore[assignment] + + return self._help_option + + def make_parser(self, ctx: Context) -> _OptionParser: + """Creates the underlying option parser for this command.""" + parser = _OptionParser(ctx) + for param in self.get_params(ctx): + param.add_to_parser(parser, ctx) + return parser + + def get_help(self, ctx: Context) -> str: + """Formats the help into a string and returns it. + + Calls :meth:`format_help` internally. + """ + formatter = ctx.make_formatter() + self.format_help(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_short_help_str(self, limit: int = 45) -> str: + """Gets short help for the command or makes it by shortening the + long help string. + """ + if self.short_help: + text = inspect.cleandoc(self.short_help) + elif self.help: + text = make_default_short_help(self.help, limit) + else: + text = "" + + if self.deprecated: + deprecated_message = ( + f"(DEPRECATED: {self.deprecated})" + if isinstance(self.deprecated, str) + else "(DEPRECATED)" + ) + text = _("{text} {deprecated_message}").format( + text=text, deprecated_message=deprecated_message + ) + + return text.strip() + + def format_help(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help into the formatter if it exists. + + This is a low-level method called by :meth:`get_help`. + + This calls the following methods: + + - :meth:`format_usage` + - :meth:`format_help_text` + - :meth:`format_options` + - :meth:`format_epilog` + """ + self.format_usage(ctx, formatter) + self.format_help_text(ctx, formatter) + self.format_options(ctx, formatter) + self.format_epilog(ctx, formatter) + + def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help text to the formatter if it exists.""" + if self.help is not None: + # truncate the help text to the first form feed + text = inspect.cleandoc(self.help).partition("\f")[0] + else: + text = "" + + if self.deprecated: + deprecated_message = ( + f"(DEPRECATED: {self.deprecated})" + if isinstance(self.deprecated, str) + else "(DEPRECATED)" + ) + text = _("{text} {deprecated_message}").format( + text=text, deprecated_message=deprecated_message + ) + + if text: + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(text) + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes all the options into the formatter if they exist.""" + opts = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None: + opts.append(rv) + + if opts: + with formatter.section(_("Options")): + formatter.write_dl(opts) + + def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the epilog into the formatter if it exists.""" + if self.epilog: + epilog = inspect.cleandoc(self.epilog) + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(epilog) + + def make_context( + self, + info_name: str | None, + args: list[str], + parent: Context | None = None, + **extra: t.Any, + ) -> Context: + """This function when given an info name and arguments will kick + off the parsing and create a new :class:`Context`. It does not + invoke the actual command callback though. + + To quickly customize the context class used without overriding + this method, set the :attr:`context_class` attribute. + + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it's usually + the name of the script, for commands below it's + the name of the command. + :param args: the arguments to parse as list of strings. + :param parent: the parent context if available. + :param extra: extra keyword arguments forwarded to the context + constructor. + + .. versionchanged:: 8.0 + Added the :attr:`context_class` attribute. + """ + for key, value in self.context_settings.items(): + if key not in extra: + extra[key] = value + + ctx = self.context_class(self, info_name=info_name, parent=parent, **extra) + + with ctx.scope(cleanup=False): + self.parse_args(ctx, args) + return ctx + + def parse_args(self, ctx: Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + raise NoArgsIsHelpError(ctx) + + parser = self.make_parser(ctx) + opts, args, param_order = parser.parse_args(args=args) + + for param in iter_params_for_processing(param_order, self.get_params(ctx)): + _, args = param.handle_parse_result(ctx, opts, args) + + # We now have all parameters' values into `ctx.params`, but the data may contain + # the `UNSET` sentinel. + # Convert `UNSET` to `None` to ensure that the user doesn't see `UNSET`. + # + # Waiting until after the initial parse to convert allows us to treat `UNSET` + # more like a missing value when multiple params use the same name. + # Refs: + # https://github.com/pallets/click/issues/3071 + # https://github.com/pallets/click/pull/3079 + for name, value in ctx.params.items(): + if value is UNSET: + ctx.params[name] = None + + if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + ctx.fail( + ngettext( + "Got unexpected extra argument ({args})", + "Got unexpected extra arguments ({args})", + len(args), + ).format(args=" ".join(map(str, args))) + ) + + ctx.args = args + ctx._opt_prefixes.update(parser._opt_prefixes) + return args + + def invoke(self, ctx: Context) -> t.Any: + """Given a context, this invokes the attached callback (if it exists) + in the right way. + """ + if self.deprecated: + extra_message = ( + f" {self.deprecated}" if isinstance(self.deprecated, str) else "" + ) + message = _( + "DeprecationWarning: The command {name!r} is deprecated.{extra_message}" + ).format(name=self.name, extra_message=extra_message) + echo(style(message, fg="red"), err=True) + + if self.callback is not None: + return ctx.invoke(self.callback, **ctx.params) + + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: + """Return a list of completions for the incomplete value. Looks + at the names of options and chained multi-commands. + + Any command could be part of a chained multi-command, so sibling + commands are valid at any point during command completion. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results: list[CompletionItem] = [] + + if incomplete and not incomplete[0].isalnum(): + for param in self.get_params(ctx): + if ( + not isinstance(param, Option) + or param.hidden + or ( + not param.multiple + and ctx.get_parameter_source(param.name) # type: ignore + is ParameterSource.COMMANDLINE + ) + ): + continue + + results.extend( + CompletionItem(name, help=param.help) + for name in [*param.opts, *param.secondary_opts] + if name.startswith(incomplete) + ) + + while ctx.parent is not None: + ctx = ctx.parent + + if isinstance(ctx.command, Group) and ctx.command.chain: + results.extend( + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + if name not in ctx._protected_args + ) + + return results + + @t.overload + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: t.Literal[True] = True, + **extra: t.Any, + ) -> t.NoReturn: ... + + @t.overload + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: bool = ..., + **extra: t.Any, + ) -> t.Any: ... + + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: bool = True, + windows_expand_args: bool = True, + **extra: t.Any, + ) -> t.Any: + """This is the way to invoke a script with all the bells and + whistles as a command line application. This will always terminate + the application after a call. If this is not wanted, ``SystemExit`` + needs to be caught. + + This method is also available by directly calling the instance of + a :class:`Command`. + + :param args: the arguments that should be used for parsing. If not + provided, ``sys.argv[1:]`` is used. + :param prog_name: the program name that should be used. By default + the program name is constructed by taking the file + name from ``sys.argv[0]``. + :param complete_var: the environment variable that controls the + bash completion support. The default is + ``"__COMPLETE"`` with prog_name in + uppercase. + :param standalone_mode: the default behavior is to invoke the script + in standalone mode. Click will then + handle exceptions and convert them into + error messages and the function will never + return but shut down the interpreter. If + this is set to `False` they will be + propagated to the caller and the return + value of this function is the return value + of :meth:`invoke`. + :param windows_expand_args: Expand glob patterns, user dir, and + env vars in command line args on Windows. + :param extra: extra keyword arguments are forwarded to the context + constructor. See :class:`Context` for more information. + + .. versionchanged:: 8.0.1 + Added the ``windows_expand_args`` parameter to allow + disabling command line arg expansion on Windows. + + .. versionchanged:: 8.0 + When taking arguments from ``sys.argv`` on Windows, glob + patterns, user dir, and env vars are expanded. + + .. versionchanged:: 3.0 + Added the ``standalone_mode`` parameter. + """ + if args is None: + args = sys.argv[1:] + + if os.name == "nt" and windows_expand_args: + args = _expand_args(args) + else: + args = list(args) + + if prog_name is None: + prog_name = _detect_program_name() + + # Process shell completion requests and exit early. + self._main_shell_completion(extra, prog_name, complete_var) + + try: + try: + with self.make_context(prog_name, args, **extra) as ctx: + rv = self.invoke(ctx) + if not standalone_mode: + return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness + ctx.exit() + except (EOFError, KeyboardInterrupt) as e: + echo(file=sys.stderr) + raise Abort() from e + except ClickException as e: + if not standalone_mode: + raise + e.show() + sys.exit(e.exit_code) + except OSError as e: + if e.errno == errno.EPIPE: + sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout)) + sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr)) + sys.exit(1) + else: + raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code + except Abort: + if not standalone_mode: + raise + echo(_("Aborted!"), file=sys.stderr) + sys.exit(1) + + def _main_shell_completion( + self, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str | None = None, + ) -> None: + """Check if the shell is asking for tab completion, process + that, then exit early. Called from :meth:`main` before the + program is invoked. + + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. Defaults to + ``_{PROG_NAME}_COMPLETE``. + + .. versionchanged:: 8.2.0 + Dots (``.``) in ``prog_name`` are replaced with underscores (``_``). + """ + if complete_var is None: + complete_name = prog_name.replace("-", "_").replace(".", "_") + complete_var = f"_{complete_name}_COMPLETE".upper() + + instruction = os.environ.get(complete_var) + + if not instruction: + return + + from .shell_completion import shell_complete + + rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction) + sys.exit(rv) + + def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Alias for :meth:`main`.""" + return self.main(*args, **kwargs) + + +class _FakeSubclassCheck(type): + def __subclasscheck__(cls, subclass: type) -> bool: + return issubclass(subclass, cls.__bases__[0]) + + def __instancecheck__(cls, instance: t.Any) -> bool: + return isinstance(instance, cls.__bases__[0]) + + +class _BaseCommand(Command, metaclass=_FakeSubclassCheck): + """ + .. deprecated:: 8.2 + Will be removed in Click 9.0. Use ``Command`` instead. + """ + + +class Group(Command): + """A group is a command that nests other commands (or more groups). + + :param name: The name of the group command. + :param commands: Map names to :class:`Command` objects. Can be a list, which + will use :attr:`Command.name` as the keys. + :param invoke_without_command: Invoke the group's callback even if a + subcommand is not given. + :param no_args_is_help: If no arguments are given, show the group's help and + exit. Defaults to the opposite of ``invoke_without_command``. + :param subcommand_metavar: How to represent the subcommand argument in help. + The default will represent whether ``chain`` is set or not. + :param chain: Allow passing more than one subcommand argument. After parsing + a command's arguments, if any arguments remain another command will be + matched, and so on. + :param result_callback: A function to call after the group's and + subcommand's callbacks. The value returned by the subcommand is passed. + If ``chain`` is enabled, the value will be a list of values returned by + all the commands. If ``invoke_without_command`` is enabled, the value + will be the value returned by the group's callback, or an empty list if + ``chain`` is enabled. + :param kwargs: Other arguments passed to :class:`Command`. + + .. versionchanged:: 8.0 + The ``commands`` argument can be a list of command objects. + + .. versionchanged:: 8.2 + Merged with and replaces the ``MultiCommand`` base class. + """ + + allow_extra_args = True + allow_interspersed_args = False + + #: If set, this is used by the group's :meth:`command` decorator + #: as the default :class:`Command` class. This is useful to make all + #: subcommands use a custom command class. + #: + #: .. versionadded:: 8.0 + command_class: type[Command] | None = None + + #: If set, this is used by the group's :meth:`group` decorator + #: as the default :class:`Group` class. This is useful to make all + #: subgroups use a custom group class. + #: + #: If set to the special value :class:`type` (literally + #: ``group_class = type``), this group's class will be used as the + #: default class. This makes a custom group class continue to make + #: custom groups. + #: + #: .. versionadded:: 8.0 + group_class: type[Group] | type[type] | None = None + # Literal[type] isn't valid, so use Type[type] + + def __init__( + self, + name: str | None = None, + commands: cabc.MutableMapping[str, Command] + | cabc.Sequence[Command] + | None = None, + invoke_without_command: bool = False, + no_args_is_help: bool | None = None, + subcommand_metavar: str | None = None, + chain: bool = False, + result_callback: t.Callable[..., t.Any] | None = None, + **kwargs: t.Any, + ) -> None: + super().__init__(name, **kwargs) + + if commands is None: + commands = {} + elif isinstance(commands, abc.Sequence): + commands = {c.name: c for c in commands if c.name is not None} + + #: The registered subcommands by their exported names. + self.commands: cabc.MutableMapping[str, Command] = commands + + if no_args_is_help is None: + no_args_is_help = not invoke_without_command + + self.no_args_is_help = no_args_is_help + self.invoke_without_command = invoke_without_command + + if subcommand_metavar is None: + if chain: + subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." + else: + subcommand_metavar = "COMMAND [ARGS]..." + + self.subcommand_metavar = subcommand_metavar + self.chain = chain + # The result callback that is stored. This can be set or + # overridden with the :func:`result_callback` decorator. + self._result_callback = result_callback + + if self.chain: + for param in self.params: + if isinstance(param, Argument) and not param.required: + raise RuntimeError( + "A group in chain mode cannot have optional arguments." + ) + + def to_info_dict(self, ctx: Context) -> dict[str, t.Any]: + info_dict = super().to_info_dict(ctx) + commands = {} + + for name in self.list_commands(ctx): + command = self.get_command(ctx, name) + + if command is None: + continue + + sub_ctx = ctx._make_sub_context(command) + + with sub_ctx.scope(cleanup=False): + commands[name] = command.to_info_dict(sub_ctx) + + info_dict.update(commands=commands, chain=self.chain) + return info_dict + + def add_command(self, cmd: Command, name: str | None = None) -> None: + """Registers another :class:`Command` with this group. If the name + is not provided, the name of the command is used. + """ + name = name or cmd.name + if name is None: + raise TypeError("Command has no name.") + _check_nested_chain(self, name, cmd, register=True) + self.commands[name] = cmd + + @t.overload + def command(self, __func: t.Callable[..., t.Any]) -> Command: ... + + @t.overload + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Command]: ... + + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Command] | Command: + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` and + immediately registers the created command with this group by + calling :meth:`add_command`. + + To customize the command class used, set the + :attr:`command_class` attribute. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.0 + Added the :attr:`command_class` attribute. + """ + from .decorators import command + + func: t.Callable[..., t.Any] | None = None + + if args and callable(args[0]): + assert len(args) == 1 and not kwargs, ( + "Use 'command(**kwargs)(callable)' to provide arguments." + ) + (func,) = args + args = () + + if self.command_class and kwargs.get("cls") is None: + kwargs["cls"] = self.command_class + + def decorator(f: t.Callable[..., t.Any]) -> Command: + cmd: Command = command(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + if func is not None: + return decorator(func) + + return decorator + + @t.overload + def group(self, __func: t.Callable[..., t.Any]) -> Group: ... + + @t.overload + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Group]: ... + + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Group] | Group: + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` and + immediately registers the created group with this group by + calling :meth:`add_command`. + + To customize the group class used, set the :attr:`group_class` + attribute. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.0 + Added the :attr:`group_class` attribute. + """ + from .decorators import group + + func: t.Callable[..., t.Any] | None = None + + if args and callable(args[0]): + assert len(args) == 1 and not kwargs, ( + "Use 'group(**kwargs)(callable)' to provide arguments." + ) + (func,) = args + args = () + + if self.group_class is not None and kwargs.get("cls") is None: + if self.group_class is type: + kwargs["cls"] = type(self) + else: + kwargs["cls"] = self.group_class + + def decorator(f: t.Callable[..., t.Any]) -> Group: + cmd: Group = group(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + if func is not None: + return decorator(func) + + return decorator + + def result_callback(self, replace: bool = False) -> t.Callable[[F], F]: + """Adds a result callback to the command. By default if a + result callback is already registered this will chain them but + this can be disabled with the `replace` parameter. The result + callback is invoked with the return value of the subcommand + (or the list of return values from all subcommands if chaining + is enabled) as well as the parameters as they would be passed + to the main callback. + + Example:: + + @click.group() + @click.option('-i', '--input', default=23) + def cli(input): + return 42 + + @cli.result_callback() + def process_result(result, input): + return result + input + + :param replace: if set to `True` an already existing result + callback will be removed. + + .. versionchanged:: 8.0 + Renamed from ``resultcallback``. + + .. versionadded:: 3.0 + """ + + def decorator(f: F) -> F: + old_callback = self._result_callback + + if old_callback is None or replace: + self._result_callback = f + return f + + def function(value: t.Any, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + inner = old_callback(value, *args, **kwargs) + return f(inner, *args, **kwargs) + + self._result_callback = rv = update_wrapper(t.cast(F, function), f) + return rv # type: ignore[return-value] + + return decorator + + def get_command(self, ctx: Context, cmd_name: str) -> Command | None: + """Given a context and a command name, this returns a :class:`Command` + object if it exists or returns ``None``. + """ + return self.commands.get(cmd_name) + + def list_commands(self, ctx: Context) -> list[str]: + """Returns a list of subcommand names in the order they should appear.""" + return sorted(self.commands) + + def collect_usage_pieces(self, ctx: Context) -> list[str]: + rv = super().collect_usage_pieces(ctx) + rv.append(self.subcommand_metavar) + return rv + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + super().format_options(ctx, formatter) + self.format_commands(ctx, formatter) + + def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None: + """Extra format methods for multi methods that adds all the commands + after the options. + """ + commands = [] + for subcommand in self.list_commands(ctx): + cmd = self.get_command(ctx, subcommand) + # What is this, the tool lied about a command. Ignore it + if cmd is None: + continue + if cmd.hidden: + continue + + commands.append((subcommand, cmd)) + + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section(_("Commands")): + formatter.write_dl(rows) + + def parse_args(self, ctx: Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + raise NoArgsIsHelpError(ctx) + + rest = super().parse_args(ctx, args) + + if self.chain: + ctx._protected_args = rest + ctx.args = [] + elif rest: + ctx._protected_args, ctx.args = rest[:1], rest[1:] + + return ctx.args + + def invoke(self, ctx: Context) -> t.Any: + def _process_result(value: t.Any) -> t.Any: + if self._result_callback is not None: + value = ctx.invoke(self._result_callback, value, **ctx.params) + return value + + if not ctx._protected_args: + if self.invoke_without_command: + # No subcommand was invoked, so the result callback is + # invoked with the group return value for regular + # groups, or an empty list for chained groups. + with ctx: + rv = super().invoke(ctx) + return _process_result([] if self.chain else rv) + ctx.fail(_("Missing command.")) + + # Fetch args back out + args = [*ctx._protected_args, *ctx.args] + ctx.args = [] + ctx._protected_args = [] + + # If we're not in chain mode, we only allow the invocation of a + # single command but we also inform the current context about the + # name of the command to invoke. + if not self.chain: + # Make sure the context is entered so we do not clean up + # resources until the result processor has worked. + with ctx: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + ctx.invoked_subcommand = cmd_name + super().invoke(ctx) + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) + with sub_ctx: + return _process_result(sub_ctx.command.invoke(sub_ctx)) + + # In chain mode we create the contexts step by step, but after the + # base command has been invoked. Because at that point we do not + # know the subcommands yet, the invoked subcommand attribute is + # set to ``*`` to inform the command that subcommands are executed + # but nothing else. + with ctx: + ctx.invoked_subcommand = "*" if args else None + super().invoke(ctx) + + # Otherwise we make every single context and invoke them in a + # chain. In that case the return value to the result processor + # is the list of all invoked subcommand's results. + contexts = [] + while args: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + ) + contexts.append(sub_ctx) + args, sub_ctx.args = sub_ctx.args, [] + + rv = [] + for sub_ctx in contexts: + with sub_ctx: + rv.append(sub_ctx.command.invoke(sub_ctx)) + return _process_result(rv) + + def resolve_command( + self, ctx: Context, args: list[str] + ) -> tuple[str | None, Command | None, list[str]]: + cmd_name = make_str(args[0]) + original_cmd_name = cmd_name + + # Get the command + cmd = self.get_command(ctx, cmd_name) + + # If we can't find the command but there is a normalization + # function available, we try with that one. + if cmd is None and ctx.token_normalize_func is not None: + cmd_name = ctx.token_normalize_func(cmd_name) + cmd = self.get_command(ctx, cmd_name) + + # If we don't find the command we want to show an error message + # to the user that it was not provided. However, there is + # something else we should do: if the first argument looks like + # an option we want to kick off parsing again for arguments to + # resolve things like --help which now should go to the main + # place. + if cmd is None and not ctx.resilient_parsing: + if _split_opt(cmd_name)[0]: + self.parse_args(ctx, args) + ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name)) + return cmd_name if cmd else None, cmd, args[1:] + + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: + """Return a list of completions for the incomplete value. Looks + at the names of options, subcommands, and chained + multi-commands. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results = [ + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + ] + results.extend(super().shell_complete(ctx, incomplete)) + return results + + +class _MultiCommand(Group, metaclass=_FakeSubclassCheck): + """ + .. deprecated:: 8.2 + Will be removed in Click 9.0. Use ``Group`` instead. + """ + + +class CommandCollection(Group): + """A :class:`Group` that looks up subcommands on other groups. If a command + is not found on this group, each registered source is checked in order. + Parameters on a source are not added to this group, and a source's callback + is not invoked when invoking its commands. In other words, this "flattens" + commands in many groups into this one group. + + :param name: The name of the group command. + :param sources: A list of :class:`Group` objects to look up commands from. + :param kwargs: Other arguments passed to :class:`Group`. + + .. versionchanged:: 8.2 + This is a subclass of ``Group``. Commands are looked up first on this + group, then each of its sources. + """ + + def __init__( + self, + name: str | None = None, + sources: list[Group] | None = None, + **kwargs: t.Any, + ) -> None: + super().__init__(name, **kwargs) + #: The list of registered groups. + self.sources: list[Group] = sources or [] + + def add_source(self, group: Group) -> None: + """Add a group as a source of commands.""" + self.sources.append(group) + + def get_command(self, ctx: Context, cmd_name: str) -> Command | None: + rv = super().get_command(ctx, cmd_name) + + if rv is not None: + return rv + + for source in self.sources: + rv = source.get_command(ctx, cmd_name) + + if rv is not None: + if self.chain: + _check_nested_chain(self, cmd_name, rv) + + return rv + + return None + + def list_commands(self, ctx: Context) -> list[str]: + rv: set[str] = set(super().list_commands(ctx)) + + for source in self.sources: + rv.update(source.list_commands(ctx)) + + return sorted(rv) + + +def _check_iter(value: t.Any) -> cabc.Iterator[t.Any]: + """Check if the value is iterable but not a string. Raises a type + error, or return an iterator over the value. + """ + if isinstance(value, str): + raise TypeError + + return iter(value) + + +class Parameter: + r"""A parameter to a command comes in two versions: they are either + :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently + not supported by design as some of the internals for parsing are + intentionally not finalized. + + Some settings are supported by both options and arguments. + + :param param_decls: the parameter declarations for this option or + argument. This is a list of flags or argument + names. + :param type: the type that should be used. Either a :class:`ParamType` + or a Python type. The latter is converted into the former + automatically if supported. + :param required: controls if this is optional or not. + :param default: the default value if omitted. This can also be a callable, + in which case it's invoked when the default is needed + without any arguments. + :param callback: A function to further process or validate the value + after type conversion. It is called as ``f(ctx, param, value)`` + and must return the value. It is called for all sources, + including prompts. + :param nargs: the number of arguments to match. If not ``1`` the return + value is a tuple instead of single value. The default for + nargs is ``1`` (except if the type is a tuple, then it's + the arity of the tuple). If ``nargs=-1``, all remaining + parameters are collected. + :param metavar: how the value is represented in the help page. + :param expose_value: if this is `True` then the value is passed onwards + to the command callback and stored on the context, + otherwise it's skipped. + :param is_eager: eager values are processed before non eager ones. This + should not be set for arguments or it will inverse the + order of processing. + :param envvar: environment variable(s) that are used to provide a default value for + this parameter. This can be a string or a sequence of strings. If a sequence is + given, only the first non-empty environment variable is used for the parameter. + :param shell_complete: A function that returns custom shell + completions. Used instead of the param's type completion if + given. Takes ``ctx, param, incomplete`` and must return a list + of :class:`~click.shell_completion.CompletionItem` or a list of + strings. + :param deprecated: If ``True`` or non-empty string, issues a message + indicating that the argument is deprecated and highlights + its deprecation in --help. The message can be customized + by using a string as the value. A deprecated parameter + cannot be required, a ValueError will be raised otherwise. + + .. versionchanged:: 8.2.0 + Introduction of ``deprecated``. + + .. versionchanged:: 8.2 + Adding duplicate parameter names to a :class:`~click.core.Command` will + result in a ``UserWarning`` being shown. + + .. versionchanged:: 8.2 + Adding duplicate parameter names to a :class:`~click.core.Command` will + result in a ``UserWarning`` being shown. + + .. versionchanged:: 8.0 + ``process_value`` validates required parameters and bounded + ``nargs``, and invokes the parameter callback before returning + the value. This allows the callback to validate prompts. + ``full_process_value`` is removed. + + .. versionchanged:: 8.0 + ``autocompletion`` is renamed to ``shell_complete`` and has new + semantics described above. The old name is deprecated and will + be removed in 8.1, until then it will be wrapped to match the + new requirements. + + .. versionchanged:: 8.0 + For ``multiple=True, nargs>1``, the default must be a list of + tuples. + + .. versionchanged:: 8.0 + Setting a default is no longer required for ``nargs>1``, it will + default to ``None``. ``multiple=True`` or ``nargs=-1`` will + default to ``()``. + + .. versionchanged:: 7.1 + Empty environment variables are ignored rather than taking the + empty string value. This makes it possible for scripts to clear + variables if they can't unset them. + + .. versionchanged:: 2.0 + Changed signature for parameter callback to also be passed the + parameter. The old callback format will still work, but it will + raise a warning to give you a chance to migrate the code easier. + """ + + param_type_name = "parameter" + + def __init__( + self, + param_decls: cabc.Sequence[str] | None = None, + type: types.ParamType | t.Any | None = None, + required: bool = False, + # XXX The default historically embed two concepts: + # - the declaration of a Parameter object carrying the default (handy to + # arbitrage the default value of coupled Parameters sharing the same + # self.name, like flag options), + # - and the actual value of the default. + # It is confusing and is the source of many issues discussed in: + # https://github.com/pallets/click/pull/3030 + # In the future, we might think of splitting it in two, not unlike + # Option.is_flag and Option.flag_value: we could have something like + # Parameter.is_default and Parameter.default_value. + default: t.Any | t.Callable[[], t.Any] | None = UNSET, + callback: t.Callable[[Context, Parameter, t.Any], t.Any] | None = None, + nargs: int | None = None, + multiple: bool = False, + metavar: str | None = None, + expose_value: bool = True, + is_eager: bool = False, + envvar: str | cabc.Sequence[str] | None = None, + shell_complete: t.Callable[ + [Context, Parameter, str], list[CompletionItem] | list[str] + ] + | None = None, + deprecated: bool | str = False, + ) -> None: + self.name: str | None + self.opts: list[str] + self.secondary_opts: list[str] + self.name, self.opts, self.secondary_opts = self._parse_decls( + param_decls or (), expose_value + ) + self.type: types.ParamType = types.convert_type(type, default) + + # Default nargs to what the type tells us if we have that + # information available. + if nargs is None: + if self.type.is_composite: + nargs = self.type.arity + else: + nargs = 1 + + self.required = required + self.callback = callback + self.nargs = nargs + self.multiple = multiple + self.expose_value = expose_value + self.default: t.Any | t.Callable[[], t.Any] | None = default + self.is_eager = is_eager + self.metavar = metavar + self.envvar = envvar + self._custom_shell_complete = shell_complete + self.deprecated = deprecated + + if __debug__: + if self.type.is_composite and nargs != self.type.arity: + raise ValueError( + f"'nargs' must be {self.type.arity} (or None) for" + f" type {self.type!r}, but it was {nargs}." + ) + + if required and deprecated: + raise ValueError( + f"The {self.param_type_name} '{self.human_readable_name}' " + "is deprecated and still required. A deprecated " + f"{self.param_type_name} cannot be required." + ) + + def to_info_dict(self) -> dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionchanged:: 8.3.0 + Returns ``None`` for the :attr:`default` if it was not set. + + .. versionadded:: 8.0 + """ + return { + "name": self.name, + "param_type_name": self.param_type_name, + "opts": self.opts, + "secondary_opts": self.secondary_opts, + "type": self.type.to_info_dict(), + "required": self.required, + "nargs": self.nargs, + "multiple": self.multiple, + # We explicitly hide the :attr:`UNSET` value to the user, as we choose to + # make it an implementation detail. And because ``to_info_dict`` has been + # designed for documentation purposes, we return ``None`` instead. + "default": self.default if self.default is not UNSET else None, + "envvar": self.envvar, + } + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str | None, list[str], list[str]]: + raise NotImplementedError() + + @property + def human_readable_name(self) -> str: + """Returns the human readable name of this parameter. This is the + same as the name for options, but the metavar for arguments. + """ + return self.name # type: ignore + + def make_metavar(self, ctx: Context) -> str: + if self.metavar is not None: + return self.metavar + + metavar = self.type.get_metavar(param=self, ctx=ctx) + + if metavar is None: + metavar = self.type.name.upper() + + if self.nargs != 1: + metavar += "..." + + return metavar + + @t.overload + def get_default( + self, ctx: Context, call: t.Literal[True] = True + ) -> t.Any | None: ... + + @t.overload + def get_default( + self, ctx: Context, call: bool = ... + ) -> t.Any | t.Callable[[], t.Any] | None: ... + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Any | t.Callable[[], t.Any] | None: + """Get the default for the parameter. Tries + :meth:`Context.lookup_default` first, then the local default. + + :param ctx: Current context. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0.2 + Type casting is no longer performed when getting a default. + + .. versionchanged:: 8.0.1 + Type casting can fail in resilient parsing mode. Invalid + defaults will not prevent showing help text. + + .. versionchanged:: 8.0 + Looks at ``ctx.default_map`` first. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + value = ctx.lookup_default(self.name, call=False) # type: ignore + + if value is UNSET: + value = self.default + + if call and callable(value): + value = value() + + return value + + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: + raise NotImplementedError() + + def consume_value( + self, ctx: Context, opts: cabc.Mapping[str, t.Any] + ) -> tuple[t.Any, ParameterSource]: + """Returns the parameter value produced by the parser. + + If the parser did not produce a value from user input, the value is either + sourced from the environment variable, the default map, or the parameter's + default value. In that order of precedence. + + If no value is found, an internal sentinel value is returned. + + :meta private: + """ + # Collect from the parse the value passed by the user to the CLI. + value = opts.get(self.name, UNSET) # type: ignore + # If the value is set, it means it was sourced from the command line by the + # parser, otherwise it left unset by default. + source = ( + ParameterSource.COMMANDLINE + if value is not UNSET + else ParameterSource.DEFAULT + ) + + if value is UNSET: + envvar_value = self.value_from_envvar(ctx) + if envvar_value is not None: + value = envvar_value + source = ParameterSource.ENVIRONMENT + + if value is UNSET: + default_map_value = ctx.lookup_default(self.name) # type: ignore + if default_map_value is not UNSET: + value = default_map_value + source = ParameterSource.DEFAULT_MAP + + if value is UNSET: + default_value = self.get_default(ctx) + if default_value is not UNSET: + value = default_value + source = ParameterSource.DEFAULT + + return value, source + + def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any: + """Convert and validate a value against the parameter's + :attr:`type`, :attr:`multiple`, and :attr:`nargs`. + """ + if value is None: + if self.multiple or self.nargs == -1: + return () + else: + return value + + def check_iter(value: t.Any) -> cabc.Iterator[t.Any]: + try: + return _check_iter(value) + except TypeError: + # This should only happen when passing in args manually, + # the parser should construct an iterable when parsing + # the command line. + raise BadParameter( + _("Value must be an iterable."), ctx=ctx, param=self + ) from None + + # Define the conversion function based on nargs and type. + + if self.nargs == 1 or self.type.is_composite: + + def convert(value: t.Any) -> t.Any: + return self.type(value, param=self, ctx=ctx) + + elif self.nargs == -1: + + def convert(value: t.Any) -> t.Any: # tuple[t.Any, ...] + return tuple(self.type(x, self, ctx) for x in check_iter(value)) + + else: # nargs > 1 + + def convert(value: t.Any) -> t.Any: # tuple[t.Any, ...] + value = tuple(check_iter(value)) + + if len(value) != self.nargs: + raise BadParameter( + ngettext( + "Takes {nargs} values but 1 was given.", + "Takes {nargs} values but {len} were given.", + len(value), + ).format(nargs=self.nargs, len=len(value)), + ctx=ctx, + param=self, + ) + + return tuple(self.type(x, self, ctx) for x in value) + + if self.multiple: + return tuple(convert(x) for x in check_iter(value)) + + return convert(value) + + def value_is_missing(self, value: t.Any) -> bool: + """A value is considered missing if: + + - it is :attr:`UNSET`, + - or if it is an empty sequence while the parameter is suppose to have + non-single value (i.e. :attr:`nargs` is not ``1`` or :attr:`multiple` is + set). + + :meta private: + """ + if value is UNSET: + return True + + if (self.nargs != 1 or self.multiple) and value == (): + return True + + return False + + def process_value(self, ctx: Context, value: t.Any) -> t.Any: + """Process the value of this parameter: + + 1. Type cast the value using :meth:`type_cast_value`. + 2. Check if the value is missing (see: :meth:`value_is_missing`), and raise + :exc:`MissingParameter` if it is required. + 3. If a :attr:`callback` is set, call it to have the value replaced by the + result of the callback. If the value was not set, the callback receive + ``None``. This keep the legacy behavior as it was before the introduction of + the :attr:`UNSET` sentinel. + + :meta private: + """ + # shelter `type_cast_value` from ever seeing an `UNSET` value by handling the + # cases in which `UNSET` gets special treatment explicitly at this layer + # + # Refs: + # https://github.com/pallets/click/issues/3069 + if value is UNSET: + if self.multiple or self.nargs == -1: + value = () + else: + value = self.type_cast_value(ctx, value) + + if self.required and self.value_is_missing(value): + raise MissingParameter(ctx=ctx, param=self) + + if self.callback is not None: + # Legacy case: UNSET is not exposed directly to the callback, but converted + # to None. + if value is UNSET: + value = None + + # Search for parameters with UNSET values in the context. + unset_keys = {k: None for k, v in ctx.params.items() if v is UNSET} + # No UNSET values, call the callback as usual. + if not unset_keys: + value = self.callback(ctx, self, value) + + # Legacy case: provide a temporarily manipulated context to the callback + # to hide UNSET values as None. + # + # Refs: + # https://github.com/pallets/click/issues/3136 + # https://github.com/pallets/click/pull/3137 + else: + # Add another layer to the context stack to clearly hint that the + # context is temporarily modified. + with ctx: + # Update the context parameters to replace UNSET with None. + ctx.params.update(unset_keys) + # Feed these fake context parameters to the callback. + value = self.callback(ctx, self, value) + # Restore the UNSET values in the context parameters. + ctx.params.update( + { + k: UNSET + for k in unset_keys + # Only restore keys that are present and still None, in case + # the callback modified other parameters. + if k in ctx.params and ctx.params[k] is None + } + ) + + return value + + def resolve_envvar_value(self, ctx: Context) -> str | None: + """Returns the value found in the environment variable(s) attached to this + parameter. + + Environment variables values are `always returned as strings + `_. + + This method returns ``None`` if: + + - the :attr:`envvar` property is not set on the :class:`Parameter`, + - the environment variable is not found in the environment, + - the variable is found in the environment but its value is empty (i.e. the + environment variable is present but has an empty string). + + If :attr:`envvar` is setup with multiple environment variables, + then only the first non-empty value is returned. + + .. caution:: + + The raw value extracted from the environment is not normalized and is + returned as-is. Any normalization or reconciliation is performed later by + the :class:`Parameter`'s :attr:`type`. + + :meta private: + """ + if not self.envvar: + return None + + if isinstance(self.envvar, str): + rv = os.environ.get(self.envvar) + + if rv: + return rv + else: + for envvar in self.envvar: + rv = os.environ.get(envvar) + + # Return the first non-empty value of the list of environment variables. + if rv: + return rv + # Else, absence of value is interpreted as an environment variable that + # is not set, so proceed to the next one. + + return None + + def value_from_envvar(self, ctx: Context) -> str | cabc.Sequence[str] | None: + """Process the raw environment variable string for this parameter. + + Returns the string as-is or splits it into a sequence of strings if the + parameter is expecting multiple values (i.e. its :attr:`nargs` property is set + to a value other than ``1``). + + :meta private: + """ + rv = self.resolve_envvar_value(ctx) + + if rv is not None and self.nargs != 1: + return self.type.split_envvar_value(rv) + + return rv + + def handle_parse_result( + self, ctx: Context, opts: cabc.Mapping[str, t.Any], args: list[str] + ) -> tuple[t.Any, list[str]]: + """Process the value produced by the parser from user input. + + Always process the value through the Parameter's :attr:`type`, wherever it + comes from. + + If the parameter is deprecated, this method warn the user about it. But only if + the value has been explicitly set by the user (and as such, is not coming from + a default). + + :meta private: + """ + with augment_usage_errors(ctx, param=self): + value, source = self.consume_value(ctx, opts) + + ctx.set_parameter_source(self.name, source) # type: ignore + + # Display a deprecation warning if necessary. + if ( + self.deprecated + and value is not UNSET + and source not in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + ): + extra_message = ( + f" {self.deprecated}" if isinstance(self.deprecated, str) else "" + ) + message = _( + "DeprecationWarning: The {param_type} {name!r} is deprecated." + "{extra_message}" + ).format( + param_type=self.param_type_name, + name=self.human_readable_name, + extra_message=extra_message, + ) + echo(style(message, fg="red"), err=True) + + # Process the value through the parameter's type. + try: + value = self.process_value(ctx, value) + except Exception: + if not ctx.resilient_parsing: + raise + # In resilient parsing mode, we do not want to fail the command if the + # value is incompatible with the parameter type, so we reset the value + # to UNSET, which will be interpreted as a missing value. + value = UNSET + + # Add parameter's value to the context. + if ( + self.expose_value + # We skip adding the value if it was previously set by another parameter + # targeting the same variable name. This prevents parameters competing for + # the same name to override each other. + and (self.name not in ctx.params or ctx.params[self.name] is UNSET) + ): + # Click is logically enforcing that the name is None if the parameter is + # not to be exposed. We still assert it here to please the type checker. + assert self.name is not None, ( + f"{self!r} parameter's name should not be None when exposing value." + ) + ctx.params[self.name] = value + + return value, args + + def get_help_record(self, ctx: Context) -> tuple[str, str] | None: + pass + + def get_usage_pieces(self, ctx: Context) -> list[str]: + return [] + + def get_error_hint(self, ctx: Context) -> str: + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + """ + hint_list = self.opts or [self.human_readable_name] + return " / ".join(f"'{x}'" for x in hint_list) + + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: + """Return a list of completions for the incomplete value. If a + ``shell_complete`` function was given during init, it is used. + Otherwise, the :attr:`type` + :meth:`~click.types.ParamType.shell_complete` function is used. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + if self._custom_shell_complete is not None: + results = self._custom_shell_complete(ctx, self, incomplete) + + if results and isinstance(results[0], str): + from click.shell_completion import CompletionItem + + results = [CompletionItem(c) for c in results] + + return t.cast("list[CompletionItem]", results) + + return self.type.shell_complete(ctx, self, incomplete) + + +class Option(Parameter): + """Options are usually optional values on the command line and + have some extra features that arguments don't have. + + All other parameters are passed onwards to the parameter constructor. + + :param show_default: Show the default value for this option in its + help text. Values are not shown by default, unless + :attr:`Context.show_default` is ``True``. If this value is a + string, it shows that string in parentheses instead of the + actual value. This is particularly useful for dynamic options. + For single option boolean flags, the default remains hidden if + its value is ``False``. + :param show_envvar: Controls if an environment variable should be + shown on the help page and error messages. + Normally, environment variables are not shown. + :param prompt: If set to ``True`` or a non empty string then the + user will be prompted for input. If set to ``True`` the prompt + will be the option name capitalized. A deprecated option cannot be + prompted. + :param confirmation_prompt: Prompt a second time to confirm the + value if it was prompted for. Can be set to a string instead of + ``True`` to customize the message. + :param prompt_required: If set to ``False``, the user will be + prompted for input only when the option was specified as a flag + without a value. + :param hide_input: If this is ``True`` then the input on the prompt + will be hidden from the user. This is useful for password input. + :param is_flag: forces this option to act as a flag. The default is + auto detection. + :param flag_value: which value should be used for this flag if it's + enabled. This is set to a boolean automatically if + the option string contains a slash to mark two options. + :param multiple: if this is set to `True` then the argument is accepted + multiple times and recorded. This is similar to ``nargs`` + in how it works but supports arbitrary number of + arguments. + :param count: this flag makes an option increment an integer. + :param allow_from_autoenv: if this is enabled then the value of this + parameter will be pulled from an environment + variable in case a prefix is defined on the + context. + :param help: the help string. + :param hidden: hide this option from help outputs. + :param attrs: Other command arguments described in :class:`Parameter`. + + .. versionchanged:: 8.2 + ``envvar`` used with ``flag_value`` will always use the ``flag_value``, + previously it would use the value of the environment variable. + + .. versionchanged:: 8.1 + Help text indentation is cleaned here instead of only in the + ``@option`` decorator. + + .. versionchanged:: 8.1 + The ``show_default`` parameter overrides + ``Context.show_default``. + + .. versionchanged:: 8.1 + The default of a single option boolean flag is not shown if the + default value is ``False``. + + .. versionchanged:: 8.0.1 + ``type`` is detected from ``flag_value`` if given. + """ + + param_type_name = "option" + + def __init__( + self, + param_decls: cabc.Sequence[str] | None = None, + show_default: bool | str | None = None, + prompt: bool | str = False, + confirmation_prompt: bool | str = False, + prompt_required: bool = True, + hide_input: bool = False, + is_flag: bool | None = None, + flag_value: t.Any = UNSET, + multiple: bool = False, + count: bool = False, + allow_from_autoenv: bool = True, + type: types.ParamType | t.Any | None = None, + help: str | None = None, + hidden: bool = False, + show_choices: bool = True, + show_envvar: bool = False, + deprecated: bool | str = False, + **attrs: t.Any, + ) -> None: + if help: + help = inspect.cleandoc(help) + + super().__init__( + param_decls, type=type, multiple=multiple, deprecated=deprecated, **attrs + ) + + if prompt is True: + if self.name is None: + raise TypeError("'name' is required with 'prompt=True'.") + + prompt_text: str | None = self.name.replace("_", " ").capitalize() + elif prompt is False: + prompt_text = None + else: + prompt_text = prompt + + if deprecated: + deprecated_message = ( + f"(DEPRECATED: {deprecated})" + if isinstance(deprecated, str) + else "(DEPRECATED)" + ) + help = help + deprecated_message if help is not None else deprecated_message + + self.prompt = prompt_text + self.confirmation_prompt = confirmation_prompt + self.prompt_required = prompt_required + self.hide_input = hide_input + self.hidden = hidden + + # The _flag_needs_value property tells the parser that this option is a flag + # that cannot be used standalone and needs a value. With this information, the + # parser can determine whether to consider the next user-provided argument in + # the CLI as a value for this flag or as a new option. + # If prompt is enabled but not required, then it opens the possibility for the + # option to gets its value from the user. + self._flag_needs_value = self.prompt is not None and not self.prompt_required + + # Auto-detect if this is a flag or not. + if is_flag is None: + # Implicitly a flag because flag_value was set. + if flag_value is not UNSET: + is_flag = True + # Not a flag, but when used as a flag it shows a prompt. + elif self._flag_needs_value: + is_flag = False + # Implicitly a flag because secondary options names were given. + elif self.secondary_opts: + is_flag = True + # The option is explicitly not a flag. But we do not know yet if it needs a + # value or not. So we look at the default value to determine it. + elif is_flag is False and not self._flag_needs_value: + self._flag_needs_value = self.default is UNSET + + if is_flag: + # Set missing default for flags if not explicitly required or prompted. + if self.default is UNSET and not self.required and not self.prompt: + if multiple: + self.default = () + + # Auto-detect the type of the flag based on the flag_value. + if type is None: + # A flag without a flag_value is a boolean flag. + if flag_value is UNSET: + self.type: types.ParamType = types.BoolParamType() + # If the flag value is a boolean, use BoolParamType. + elif isinstance(flag_value, bool): + self.type = types.BoolParamType() + # Otherwise, guess the type from the flag value. + else: + self.type = types.convert_type(None, flag_value) + + self.is_flag: bool = bool(is_flag) + self.is_bool_flag: bool = bool( + is_flag and isinstance(self.type, types.BoolParamType) + ) + self.flag_value: t.Any = flag_value + + # Set boolean flag default to False if unset and not required. + if self.is_bool_flag: + if self.default is UNSET and not self.required: + self.default = False + + # Support the special case of aligning the default value with the flag_value + # for flags whose default is explicitly set to True. Note that as long as we + # have this condition, there is no way a flag can have a default set to True, + # and a flag_value set to something else. Refs: + # https://github.com/pallets/click/issues/3024#issuecomment-3146199461 + # https://github.com/pallets/click/pull/3030/commits/06847da + if self.default is True and self.flag_value is not UNSET: + self.default = self.flag_value + + # Set the default flag_value if it is not set. + if self.flag_value is UNSET: + if self.is_flag: + self.flag_value = True + else: + self.flag_value = None + + # Counting. + self.count = count + if count: + if type is None: + self.type = types.IntRange(min=0) + if self.default is UNSET: + self.default = 0 + + self.allow_from_autoenv = allow_from_autoenv + self.help = help + self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar + + if __debug__: + if deprecated and prompt: + raise ValueError("`deprecated` options cannot use `prompt`.") + + if self.nargs == -1: + raise TypeError("nargs=-1 is not supported for options.") + + if not self.is_bool_flag and self.secondary_opts: + raise TypeError("Secondary flag is not valid for non-boolean flag.") + + if self.is_bool_flag and self.hide_input and self.prompt is not None: + raise TypeError( + "'prompt' with 'hide_input' is not valid for boolean flag." + ) + + if self.count: + if self.multiple: + raise TypeError("'count' is not valid with 'multiple'.") + + if self.is_flag: + raise TypeError("'count' is not valid with 'is_flag'.") + + def to_info_dict(self) -> dict[str, t.Any]: + """ + .. versionchanged:: 8.3.0 + Returns ``None`` for the :attr:`flag_value` if it was not set. + """ + info_dict = super().to_info_dict() + info_dict.update( + help=self.help, + prompt=self.prompt, + is_flag=self.is_flag, + # We explicitly hide the :attr:`UNSET` value to the user, as we choose to + # make it an implementation detail. And because ``to_info_dict`` has been + # designed for documentation purposes, we return ``None`` instead. + flag_value=self.flag_value if self.flag_value is not UNSET else None, + count=self.count, + hidden=self.hidden, + ) + return info_dict + + def get_error_hint(self, ctx: Context) -> str: + result = super().get_error_hint(ctx) + if self.show_envvar and self.envvar is not None: + result += f" (env var: '{self.envvar}')" + return result + + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str | None, list[str], list[str]]: + opts = [] + secondary_opts = [] + name = None + possible_names = [] + + for decl in decls: + if decl.isidentifier(): + if name is not None: + raise TypeError(f"Name '{name}' defined twice") + name = decl + else: + split_char = ";" if decl[:1] == "/" else "/" + if split_char in decl: + first, second = decl.split(split_char, 1) + first = first.rstrip() + if first: + possible_names.append(_split_opt(first)) + opts.append(first) + second = second.lstrip() + if second: + secondary_opts.append(second.lstrip()) + if first == second: + raise ValueError( + f"Boolean option {decl!r} cannot use the" + " same flag for true/false." + ) + else: + possible_names.append(_split_opt(decl)) + opts.append(decl) + + if name is None and possible_names: + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace("-", "_").lower() + if not name.isidentifier(): + name = None + + if name is None: + if not expose_value: + return None, opts, secondary_opts + raise TypeError( + f"Could not determine name for option with declarations {decls!r}" + ) + + if not opts and not secondary_opts: + raise TypeError( + f"No options defined but a name was passed ({name})." + " Did you mean to declare an argument instead? Did" + f" you mean to pass '--{name}'?" + ) + + return name, opts, secondary_opts + + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: + if self.multiple: + action = "append" + elif self.count: + action = "count" + else: + action = "store" + + if self.is_flag: + action = f"{action}_const" + + if self.is_bool_flag and self.secondary_opts: + parser.add_option( + obj=self, opts=self.opts, dest=self.name, action=action, const=True + ) + parser.add_option( + obj=self, + opts=self.secondary_opts, + dest=self.name, + action=action, + const=False, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + const=self.flag_value, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + nargs=self.nargs, + ) + + def get_help_record(self, ctx: Context) -> tuple[str, str] | None: + if self.hidden: + return None + + any_prefix_is_slash = False + + def _write_opts(opts: cabc.Sequence[str]) -> str: + nonlocal any_prefix_is_slash + + rv, any_slashes = join_options(opts) + + if any_slashes: + any_prefix_is_slash = True + + if not self.is_flag and not self.count: + rv += f" {self.make_metavar(ctx=ctx)}" + + return rv + + rv = [_write_opts(self.opts)] + + if self.secondary_opts: + rv.append(_write_opts(self.secondary_opts)) + + help = self.help or "" + + extra = self.get_help_extra(ctx) + extra_items = [] + if "envvars" in extra: + extra_items.append( + _("env var: {var}").format(var=", ".join(extra["envvars"])) + ) + if "default" in extra: + extra_items.append(_("default: {default}").format(default=extra["default"])) + if "range" in extra: + extra_items.append(extra["range"]) + if "required" in extra: + extra_items.append(_(extra["required"])) + + if extra_items: + extra_str = "; ".join(extra_items) + help = f"{help} [{extra_str}]" if help else f"[{extra_str}]" + + return ("; " if any_prefix_is_slash else " / ").join(rv), help + + def get_help_extra(self, ctx: Context) -> types.OptionHelpExtra: + extra: types.OptionHelpExtra = {} + + if self.show_envvar: + envvar = self.envvar + + if envvar is None: + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + + if envvar is not None: + if isinstance(envvar, str): + extra["envvars"] = (envvar,) + else: + extra["envvars"] = tuple(str(d) for d in envvar) + + # Temporarily enable resilient parsing to avoid type casting + # failing for the default. Might be possible to extend this to + # help formatting in general. + resilient = ctx.resilient_parsing + ctx.resilient_parsing = True + + try: + default_value = self.get_default(ctx, call=False) + finally: + ctx.resilient_parsing = resilient + + show_default = False + show_default_is_str = False + + if self.show_default is not None: + if isinstance(self.show_default, str): + show_default_is_str = show_default = True + else: + show_default = self.show_default + elif ctx.show_default is not None: + show_default = ctx.show_default + + if show_default_is_str or ( + show_default and (default_value not in (None, UNSET)) + ): + if show_default_is_str: + default_string = f"({self.show_default})" + elif isinstance(default_value, (list, tuple)): + default_string = ", ".join(str(d) for d in default_value) + elif isinstance(default_value, enum.Enum): + default_string = default_value.name + elif inspect.isfunction(default_value): + default_string = _("(dynamic)") + elif self.is_bool_flag and self.secondary_opts: + # For boolean flags that have distinct True/False opts, + # use the opt without prefix instead of the value. + default_string = _split_opt( + (self.opts if default_value else self.secondary_opts)[0] + )[1] + elif self.is_bool_flag and not self.secondary_opts and not default_value: + default_string = "" + elif default_value == "": + default_string = '""' + else: + default_string = str(default_value) + + if default_string: + extra["default"] = default_string + + if ( + isinstance(self.type, types._NumberRangeBase) + # skip count with default range type + and not (self.count and self.type.min == 0 and self.type.max is None) + ): + range_str = self.type._describe_range() + + if range_str: + extra["range"] = range_str + + if self.required: + extra["required"] = "required" + + return extra + + def prompt_for_value(self, ctx: Context) -> t.Any: + """This is an alternative flow that can be activated in the full + value processing if a value does not exist. It will prompt the + user until a valid value exists and then returns the processed + value as result. + """ + assert self.prompt is not None + + # Calculate the default before prompting anything to lock in the value before + # attempting any user interaction. + default = self.get_default(ctx) + + # A boolean flag can use a simplified [y/n] confirmation prompt. + if self.is_bool_flag: + # If we have no boolean default, we force the user to explicitly provide + # one. + if default in (UNSET, None): + default = None + # Nothing prevent you to declare an option that is simultaneously: + # 1) auto-detected as a boolean flag, + # 2) allowed to prompt, and + # 3) still declare a non-boolean default. + # This forced casting into a boolean is necessary to align any non-boolean + # default to the prompt, which is going to be a [y/n]-style confirmation + # because the option is still a boolean flag. That way, instead of [y/n], + # we get [Y/n] or [y/N] depending on the truthy value of the default. + # Refs: https://github.com/pallets/click/pull/3030#discussion_r2289180249 + else: + default = bool(default) + return confirm(self.prompt, default) + + # If show_default is set to True/False, provide this to `prompt` as well. For + # non-bool values of `show_default`, we use `prompt`'s default behavior + prompt_kwargs: t.Any = {} + if isinstance(self.show_default, bool): + prompt_kwargs["show_default"] = self.show_default + + return prompt( + self.prompt, + # Use ``None`` to inform the prompt() function to reiterate until a valid + # value is provided by the user if we have no default. + default=None if default is UNSET else default, + type=self.type, + hide_input=self.hide_input, + show_choices=self.show_choices, + confirmation_prompt=self.confirmation_prompt, + value_proc=lambda x: self.process_value(ctx, x), + **prompt_kwargs, + ) + + def resolve_envvar_value(self, ctx: Context) -> str | None: + """:class:`Option` resolves its environment variable the same way as + :func:`Parameter.resolve_envvar_value`, but it also supports + :attr:`Context.auto_envvar_prefix`. If we could not find an environment from + the :attr:`envvar` property, we fallback on :attr:`Context.auto_envvar_prefix` + to build dynamiccaly the environment variable name using the + :python:`{ctx.auto_envvar_prefix}_{self.name.upper()}` template. + + :meta private: + """ + rv = super().resolve_envvar_value(ctx) + + if rv is not None: + return rv + + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + rv = os.environ.get(envvar) + + if rv: + return rv + + return None + + def value_from_envvar(self, ctx: Context) -> t.Any: + """For :class:`Option`, this method processes the raw environment variable + string the same way as :func:`Parameter.value_from_envvar` does. + + But in the case of non-boolean flags, the value is analyzed to determine if the + flag is activated or not, and returns a boolean of its activation, or the + :attr:`flag_value` if the latter is set. + + This method also takes care of repeated options (i.e. options with + :attr:`multiple` set to ``True``). + + :meta private: + """ + rv = self.resolve_envvar_value(ctx) + + # Absent environment variable or an empty string is interpreted as unset. + if rv is None: + return None + + # Non-boolean flags are more liberal in what they accept. But a flag being a + # flag, its envvar value still needs to be analyzed to determine if the flag is + # activated or not. + if self.is_flag and not self.is_bool_flag: + # If the flag_value is set and match the envvar value, return it + # directly. + if self.flag_value is not UNSET and rv == self.flag_value: + return self.flag_value + # Analyze the envvar value as a boolean to know if the flag is + # activated or not. + return types.BoolParamType.str_to_bool(rv) + + # Split the envvar value if it is allowed to be repeated. + value_depth = (self.nargs != 1) + bool(self.multiple) + if value_depth > 0: + multi_rv = self.type.split_envvar_value(rv) + if self.multiple and self.nargs != 1: + multi_rv = batch(multi_rv, self.nargs) # type: ignore[assignment] + + return multi_rv + + return rv + + def consume_value( + self, ctx: Context, opts: cabc.Mapping[str, Parameter] + ) -> tuple[t.Any, ParameterSource]: + """For :class:`Option`, the value can be collected from an interactive prompt + if the option is a flag that needs a value (and the :attr:`prompt` property is + set). + + Additionally, this method handles flag option that are activated without a + value, in which case the :attr:`flag_value` is returned. + + :meta private: + """ + value, source = super().consume_value(ctx, opts) + + # The parser will emit a sentinel value if the option is allowed to as a flag + # without a value. + if value is FLAG_NEEDS_VALUE: + # If the option allows for a prompt, we start an interaction with the user. + if self.prompt is not None and not ctx.resilient_parsing: + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + # Else the flag takes its flag_value as value. + else: + value = self.flag_value + source = ParameterSource.COMMANDLINE + + # A flag which is activated always returns the flag value, unless the value + # comes from the explicitly sets default. + elif ( + self.is_flag + and value is True + and not self.is_bool_flag + and source not in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + ): + value = self.flag_value + + # Re-interpret a multiple option which has been sent as-is by the parser. + # Here we replace each occurrence of value-less flags (marked by the + # FLAG_NEEDS_VALUE sentinel) with the flag_value. + elif ( + self.multiple + and value is not UNSET + and source not in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + and any(v is FLAG_NEEDS_VALUE for v in value) + ): + value = [self.flag_value if v is FLAG_NEEDS_VALUE else v for v in value] + source = ParameterSource.COMMANDLINE + + # The value wasn't set, or used the param's default, prompt for one to the user + # if prompting is enabled. + elif ( + ( + value is UNSET + or source in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + ) + and self.prompt is not None + and (self.required or self.prompt_required) + and not ctx.resilient_parsing + ): + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + + return value, source + + def process_value(self, ctx: Context, value: t.Any) -> t.Any: + # process_value has to be overridden on Options in order to capture + # `value == UNSET` cases before `type_cast_value()` gets called. + # + # Refs: + # https://github.com/pallets/click/issues/3069 + if self.is_flag and not self.required and self.is_bool_flag and value is UNSET: + value = False + + if self.callback is not None: + value = self.callback(ctx, self, value) + + return value + + # in the normal case, rely on Parameter.process_value + return super().process_value(ctx, value) + + +class Argument(Parameter): + """Arguments are positional parameters to a command. They generally + provide fewer features than options but can have infinite ``nargs`` + and are required by default. + + All parameters are passed onwards to the constructor of :class:`Parameter`. + """ + + param_type_name = "argument" + + def __init__( + self, + param_decls: cabc.Sequence[str], + required: bool | None = None, + **attrs: t.Any, + ) -> None: + # Auto-detect the requirement status of the argument if not explicitly set. + if required is None: + # The argument gets automatically required if it has no explicit default + # value set and is setup to match at least one value. + if attrs.get("default", UNSET) is UNSET: + required = attrs.get("nargs", 1) > 0 + # If the argument has a default value, it is not required. + else: + required = False + + if "multiple" in attrs: + raise TypeError("__init__() got an unexpected keyword argument 'multiple'.") + + super().__init__(param_decls, required=required, **attrs) + + @property + def human_readable_name(self) -> str: + if self.metavar is not None: + return self.metavar + return self.name.upper() # type: ignore + + def make_metavar(self, ctx: Context) -> str: + if self.metavar is not None: + return self.metavar + var = self.type.get_metavar(param=self, ctx=ctx) + if not var: + var = self.name.upper() # type: ignore + if self.deprecated: + var += "!" + if not self.required: + var = f"[{var}]" + if self.nargs != 1: + var += "..." + return var + + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str | None, list[str], list[str]]: + if not decls: + if not expose_value: + return None, [], [] + raise TypeError("Argument is marked as exposed, but does not have a name.") + if len(decls) == 1: + name = arg = decls[0] + name = name.replace("-", "_").lower() + else: + raise TypeError( + "Arguments take exactly one parameter declaration, got" + f" {len(decls)}: {decls}." + ) + return name, [arg], [] + + def get_usage_pieces(self, ctx: Context) -> list[str]: + return [self.make_metavar(ctx)] + + def get_error_hint(self, ctx: Context) -> str: + return f"'{self.make_metavar(ctx)}'" + + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: + parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) + + +def __getattr__(name: str) -> object: + import warnings + + if name == "BaseCommand": + warnings.warn( + "'BaseCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Command' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _BaseCommand + + if name == "MultiCommand": + warnings.warn( + "'MultiCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Group' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _MultiCommand + + raise AttributeError(name) diff --git a/venv/lib/python3.10/site-packages/click/decorators.py b/venv/lib/python3.10/site-packages/click/decorators.py new file mode 100644 index 0000000..21f4c34 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/decorators.py @@ -0,0 +1,551 @@ +from __future__ import annotations + +import inspect +import typing as t +from functools import update_wrapper +from gettext import gettext as _ + +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter +from .globals import get_current_context +from .utils import echo + +if t.TYPE_CHECKING: + import typing_extensions as te + + P = te.ParamSpec("P") + +R = t.TypeVar("R") +T = t.TypeVar("T") +_AnyCallable = t.Callable[..., t.Any] +FC = t.TypeVar("FC", bound="_AnyCallable | Command") + + +def pass_context(f: t.Callable[te.Concatenate[Context, P], R]) -> t.Callable[P, R]: + """Marks a callback as wanting to receive the current context + object as first argument. + """ + + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + return f(get_current_context(), *args, **kwargs) + + return update_wrapper(new_func, f) + + +def pass_obj(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]: + """Similar to :func:`pass_context`, but only pass the object on the + context onwards (:attr:`Context.obj`). This is useful if that object + represents the state of a nested system. + """ + + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + return f(get_current_context().obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + +def make_pass_decorator( + object_type: type[T], ensure: bool = False +) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]: + """Given an object type this creates a decorator that will work + similar to :func:`pass_obj` but instead of passing the object of the + current context, it will find the innermost context of type + :func:`object_type`. + + This generates a decorator that works roughly like this:: + + from functools import update_wrapper + + def decorator(f): + @pass_context + def new_func(ctx, *args, **kwargs): + obj = ctx.find_object(object_type) + return ctx.invoke(f, obj, *args, **kwargs) + return update_wrapper(new_func, f) + return decorator + + :param object_type: the type of the object to pass. + :param ensure: if set to `True`, a new object will be created and + remembered on the context if it's not there yet. + """ + + def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]: + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + ctx = get_current_context() + + obj: T | None + if ensure: + obj = ctx.ensure_object(object_type) + else: + obj = ctx.find_object(object_type) + + if obj is None: + raise RuntimeError( + "Managed to invoke callback without a context" + f" object of type {object_type.__name__!r}" + " existing." + ) + + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + return decorator + + +def pass_meta_key( + key: str, *, doc_description: str | None = None +) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]: + """Create a decorator that passes a key from + :attr:`click.Context.meta` as the first argument to the decorated + function. + + :param key: Key in ``Context.meta`` to pass. + :param doc_description: Description of the object being passed, + inserted into the decorator's docstring. Defaults to "the 'key' + key from Context.meta". + + .. versionadded:: 8.0 + """ + + def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]: + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + ctx = get_current_context() + obj = ctx.meta[key] + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + if doc_description is None: + doc_description = f"the {key!r} key from :attr:`click.Context.meta`" + + decorator.__doc__ = ( + f"Decorator that passes {doc_description} as the first argument" + " to the decorated function." + ) + return decorator + + +CmdType = t.TypeVar("CmdType", bound=Command) + + +# variant: no call, directly as decorator for a function. +@t.overload +def command(name: _AnyCallable) -> Command: ... + + +# variant: with positional name and with positional or keyword cls argument: +# @command(namearg, CommandCls, ...) or @command(namearg, cls=CommandCls, ...) +@t.overload +def command( + name: str | None, + cls: type[CmdType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], CmdType]: ... + + +# variant: name omitted, cls _must_ be a keyword argument, @command(cls=CommandCls, ...) +@t.overload +def command( + name: None = None, + *, + cls: type[CmdType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], CmdType]: ... + + +# variant: with optional string name, no cls argument provided. +@t.overload +def command( + name: str | None = ..., cls: None = None, **attrs: t.Any +) -> t.Callable[[_AnyCallable], Command]: ... + + +def command( + name: str | _AnyCallable | None = None, + cls: type[CmdType] | None = None, + **attrs: t.Any, +) -> Command | t.Callable[[_AnyCallable], Command | CmdType]: + r"""Creates a new :class:`Command` and uses the decorated function as + callback. This will also automatically attach all decorated + :func:`option`\s and :func:`argument`\s as parameters to the command. + + The name of the command defaults to the name of the function, converted to + lowercase, with underscores ``_`` replaced by dashes ``-``, and the suffixes + ``_command``, ``_cmd``, ``_group``, and ``_grp`` are removed. For example, + ``init_data_command`` becomes ``init-data``. + + All keyword arguments are forwarded to the underlying command class. + For the ``params`` argument, any decorated params are appended to + the end of the list. + + Once decorated the function turns into a :class:`Command` instance + that can be invoked as a command line utility or be attached to a + command :class:`Group`. + + :param name: The name of the command. Defaults to modifying the function's + name as described above. + :param cls: The command class to create. Defaults to :class:`Command`. + + .. versionchanged:: 8.2 + The suffixes ``_command``, ``_cmd``, ``_group``, and ``_grp`` are + removed when generating the name. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.1 + The ``params`` argument can be used. Decorated params are + appended to the end of the list. + """ + + func: t.Callable[[_AnyCallable], t.Any] | None = None + + if callable(name): + func = name + name = None + assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class." + assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments." + + if cls is None: + cls = t.cast("type[CmdType]", Command) + + def decorator(f: _AnyCallable) -> CmdType: + if isinstance(f, Command): + raise TypeError("Attempted to convert a callback into a command twice.") + + attr_params = attrs.pop("params", None) + params = attr_params if attr_params is not None else [] + + try: + decorator_params = f.__click_params__ # type: ignore + except AttributeError: + pass + else: + del f.__click_params__ # type: ignore + params.extend(reversed(decorator_params)) + + if attrs.get("help") is None: + attrs["help"] = f.__doc__ + + if t.TYPE_CHECKING: + assert cls is not None + assert not callable(name) + + if name is not None: + cmd_name = name + else: + cmd_name = f.__name__.lower().replace("_", "-") + cmd_left, sep, suffix = cmd_name.rpartition("-") + + if sep and suffix in {"command", "cmd", "group", "grp"}: + cmd_name = cmd_left + + cmd = cls(name=cmd_name, callback=f, params=params, **attrs) + cmd.__doc__ = f.__doc__ + return cmd + + if func is not None: + return decorator(func) + + return decorator + + +GrpType = t.TypeVar("GrpType", bound=Group) + + +# variant: no call, directly as decorator for a function. +@t.overload +def group(name: _AnyCallable) -> Group: ... + + +# variant: with positional name and with positional or keyword cls argument: +# @group(namearg, GroupCls, ...) or @group(namearg, cls=GroupCls, ...) +@t.overload +def group( + name: str | None, + cls: type[GrpType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], GrpType]: ... + + +# variant: name omitted, cls _must_ be a keyword argument, @group(cmd=GroupCls, ...) +@t.overload +def group( + name: None = None, + *, + cls: type[GrpType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], GrpType]: ... + + +# variant: with optional string name, no cls argument provided. +@t.overload +def group( + name: str | None = ..., cls: None = None, **attrs: t.Any +) -> t.Callable[[_AnyCallable], Group]: ... + + +def group( + name: str | _AnyCallable | None = None, + cls: type[GrpType] | None = None, + **attrs: t.Any, +) -> Group | t.Callable[[_AnyCallable], Group | GrpType]: + """Creates a new :class:`Group` with a function as callback. This + works otherwise the same as :func:`command` just that the `cls` + parameter is set to :class:`Group`. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + """ + if cls is None: + cls = t.cast("type[GrpType]", Group) + + if callable(name): + return command(cls=cls, **attrs)(name) + + return command(name, cls, **attrs) + + +def _param_memo(f: t.Callable[..., t.Any], param: Parameter) -> None: + if isinstance(f, Command): + f.params.append(param) + else: + if not hasattr(f, "__click_params__"): + f.__click_params__ = [] # type: ignore + + f.__click_params__.append(param) # type: ignore + + +def argument( + *param_decls: str, cls: type[Argument] | None = None, **attrs: t.Any +) -> t.Callable[[FC], FC]: + """Attaches an argument to the command. All positional arguments are + passed as parameter declarations to :class:`Argument`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Argument` instance manually + and attaching it to the :attr:`Command.params` list. + + For the default argument class, refer to :class:`Argument` and + :class:`Parameter` for descriptions of parameters. + + :param cls: the argument class to instantiate. This defaults to + :class:`Argument`. + :param param_decls: Passed as positional arguments to the constructor of + ``cls``. + :param attrs: Passed as keyword arguments to the constructor of ``cls``. + """ + if cls is None: + cls = Argument + + def decorator(f: FC) -> FC: + _param_memo(f, cls(param_decls, **attrs)) + return f + + return decorator + + +def option( + *param_decls: str, cls: type[Option] | None = None, **attrs: t.Any +) -> t.Callable[[FC], FC]: + """Attaches an option to the command. All positional arguments are + passed as parameter declarations to :class:`Option`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Option` instance manually + and attaching it to the :attr:`Command.params` list. + + For the default option class, refer to :class:`Option` and + :class:`Parameter` for descriptions of parameters. + + :param cls: the option class to instantiate. This defaults to + :class:`Option`. + :param param_decls: Passed as positional arguments to the constructor of + ``cls``. + :param attrs: Passed as keyword arguments to the constructor of ``cls``. + """ + if cls is None: + cls = Option + + def decorator(f: FC) -> FC: + _param_memo(f, cls(param_decls, **attrs)) + return f + + return decorator + + +def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--yes`` option which shows a prompt before continuing if + not passed. If the prompt is declined, the program will exit. + + :param param_decls: One or more option names. Defaults to the single + value ``"--yes"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value: + ctx.abort() + + if not param_decls: + param_decls = ("--yes",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("callback", callback) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("prompt", "Do you want to continue?") + kwargs.setdefault("help", "Confirm the action without prompting.") + return option(*param_decls, **kwargs) + + +def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--password`` option which prompts for a password, hiding + input and asking to enter the value again for confirmation. + + :param param_decls: One or more option names. Defaults to the single + value ``"--password"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + if not param_decls: + param_decls = ("--password",) + + kwargs.setdefault("prompt", True) + kwargs.setdefault("confirmation_prompt", True) + kwargs.setdefault("hide_input", True) + return option(*param_decls, **kwargs) + + +def version_option( + version: str | None = None, + *param_decls: str, + package_name: str | None = None, + prog_name: str | None = None, + message: str | None = None, + **kwargs: t.Any, +) -> t.Callable[[FC], FC]: + """Add a ``--version`` option which immediately prints the version + number and exits the program. + + If ``version`` is not provided, Click will try to detect it using + :func:`importlib.metadata.version` to get the version for the + ``package_name``. + + If ``package_name`` is not provided, Click will try to detect it by + inspecting the stack frames. This will be used to detect the + version, so it must match the name of the installed package. + + :param version: The version number to show. If not provided, Click + will try to detect it. + :param param_decls: One or more option names. Defaults to the single + value ``"--version"``. + :param package_name: The package name to detect the version from. If + not provided, Click will try to detect it. + :param prog_name: The name of the CLI to show in the message. If not + provided, it will be detected from the command. + :param message: The message to show. The values ``%(prog)s``, + ``%(package)s``, and ``%(version)s`` are available. Defaults to + ``"%(prog)s, version %(version)s"``. + :param kwargs: Extra arguments are passed to :func:`option`. + :raise RuntimeError: ``version`` could not be detected. + + .. versionchanged:: 8.0 + Add the ``package_name`` parameter, and the ``%(package)s`` + value for messages. + + .. versionchanged:: 8.0 + Use :mod:`importlib.metadata` instead of ``pkg_resources``. The + version is detected based on the package name, not the entry + point name. The Python package name must match the installed + package name, or be passed with ``package_name=``. + """ + if message is None: + message = _("%(prog)s, version %(version)s") + + if version is None and package_name is None: + frame = inspect.currentframe() + f_back = frame.f_back if frame is not None else None + f_globals = f_back.f_globals if f_back is not None else None + # break reference cycle + # https://docs.python.org/3/library/inspect.html#the-interpreter-stack + del frame + + if f_globals is not None: + package_name = f_globals.get("__name__") + + if package_name == "__main__": + package_name = f_globals.get("__package__") + + if package_name: + package_name = package_name.partition(".")[0] + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + nonlocal prog_name + nonlocal version + + if prog_name is None: + prog_name = ctx.find_root().info_name + + if version is None and package_name is not None: + import importlib.metadata + + try: + version = importlib.metadata.version(package_name) + except importlib.metadata.PackageNotFoundError: + raise RuntimeError( + f"{package_name!r} is not installed. Try passing" + " 'package_name' instead." + ) from None + + if version is None: + raise RuntimeError( + f"Could not determine the version for {package_name!r} automatically." + ) + + echo( + message % {"prog": prog_name, "package": package_name, "version": version}, + color=ctx.color, + ) + ctx.exit() + + if not param_decls: + param_decls = ("--version",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show the version and exit.")) + kwargs["callback"] = callback + return option(*param_decls, **kwargs) + + +def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Pre-configured ``--help`` option which immediately prints the help page + and exits the program. + + :param param_decls: One or more option names. Defaults to the single + value ``"--help"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def show_help(ctx: Context, param: Parameter, value: bool) -> None: + """Callback that print the help page on ```` and exits.""" + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + if not param_decls: + param_decls = ("--help",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show this message and exit.")) + kwargs.setdefault("callback", show_help) + + return option(*param_decls, **kwargs) diff --git a/venv/lib/python3.10/site-packages/click/exceptions.py b/venv/lib/python3.10/site-packages/click/exceptions.py new file mode 100644 index 0000000..4d782ee --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/exceptions.py @@ -0,0 +1,308 @@ +from __future__ import annotations + +import collections.abc as cabc +import typing as t +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import get_text_stderr +from .globals import resolve_color_default +from .utils import echo +from .utils import format_filename + +if t.TYPE_CHECKING: + from .core import Command + from .core import Context + from .core import Parameter + + +def _join_param_hints(param_hint: cabc.Sequence[str] | str | None) -> str | None: + if param_hint is not None and not isinstance(param_hint, str): + return " / ".join(repr(x) for x in param_hint) + + return param_hint + + +class ClickException(Exception): + """An exception that Click can handle and show to the user.""" + + #: The exit code for this exception. + exit_code = 1 + + def __init__(self, message: str) -> None: + super().__init__(message) + # The context will be removed by the time we print the message, so cache + # the color settings here to be used later on (in `show`) + self.show_color: bool | None = resolve_color_default() + self.message = message + + def format_message(self) -> str: + return self.message + + def __str__(self) -> str: + return self.message + + def show(self, file: t.IO[t.Any] | None = None) -> None: + if file is None: + file = get_text_stderr() + + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=self.show_color, + ) + + +class UsageError(ClickException): + """An internal exception that signals a usage error. This typically + aborts any further handling. + + :param message: the error message to display. + :param ctx: optionally the context that caused this error. Click will + fill in the context automatically in some situations. + """ + + exit_code = 2 + + def __init__(self, message: str, ctx: Context | None = None) -> None: + super().__init__(message) + self.ctx = ctx + self.cmd: Command | None = self.ctx.command if self.ctx else None + + def show(self, file: t.IO[t.Any] | None = None) -> None: + if file is None: + file = get_text_stderr() + color = None + hint = "" + if ( + self.ctx is not None + and self.ctx.command.get_help_option(self.ctx) is not None + ): + hint = _("Try '{command} {option}' for help.").format( + command=self.ctx.command_path, option=self.ctx.help_option_names[0] + ) + hint = f"{hint}\n" + if self.ctx is not None: + color = self.ctx.color + echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color) + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=color, + ) + + +class BadParameter(UsageError): + """An exception that formats out a standardized error message for a + bad parameter. This is useful when thrown from a callback or type as + Click will attach contextual information to it (for instance, which + parameter it is). + + .. versionadded:: 2.0 + + :param param: the parameter object that caused this error. This can + be left out, and Click will attach this info itself + if possible. + :param param_hint: a string that shows up as parameter name. This + can be used as alternative to `param` in cases + where custom validation should happen. If it is + a string it's used as such, if it's a list then + each item is quoted and separated. + """ + + def __init__( + self, + message: str, + ctx: Context | None = None, + param: Parameter | None = None, + param_hint: cabc.Sequence[str] | str | None = None, + ) -> None: + super().__init__(message, ctx) + self.param = param + self.param_hint = param_hint + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) # type: ignore + else: + return _("Invalid value: {message}").format(message=self.message) + + return _("Invalid value for {param_hint}: {message}").format( + param_hint=_join_param_hints(param_hint), message=self.message + ) + + +class MissingParameter(BadParameter): + """Raised if click required an option or argument but it was not + provided when invoking the script. + + .. versionadded:: 4.0 + + :param param_type: a string that indicates the type of the parameter. + The default is to inherit the parameter type from + the given `param`. Valid values are ``'parameter'``, + ``'option'`` or ``'argument'``. + """ + + def __init__( + self, + message: str | None = None, + ctx: Context | None = None, + param: Parameter | None = None, + param_hint: cabc.Sequence[str] | str | None = None, + param_type: str | None = None, + ) -> None: + super().__init__(message or "", ctx, param, param_hint) + self.param_type = param_type + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint: cabc.Sequence[str] | str | None = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) # type: ignore + else: + param_hint = None + + param_hint = _join_param_hints(param_hint) + param_hint = f" {param_hint}" if param_hint else "" + + param_type = self.param_type + if param_type is None and self.param is not None: + param_type = self.param.param_type_name + + msg = self.message + if self.param is not None: + msg_extra = self.param.type.get_missing_message( + param=self.param, ctx=self.ctx + ) + if msg_extra: + if msg: + msg += f". {msg_extra}" + else: + msg = msg_extra + + msg = f" {msg}" if msg else "" + + # Translate param_type for known types. + if param_type == "argument": + missing = _("Missing argument") + elif param_type == "option": + missing = _("Missing option") + elif param_type == "parameter": + missing = _("Missing parameter") + else: + missing = _("Missing {param_type}").format(param_type=param_type) + + return f"{missing}{param_hint}.{msg}" + + def __str__(self) -> str: + if not self.message: + param_name = self.param.name if self.param else None + return _("Missing parameter: {param_name}").format(param_name=param_name) + else: + return self.message + + +class NoSuchOption(UsageError): + """Raised if click attempted to handle an option that does not + exist. + + .. versionadded:: 4.0 + """ + + def __init__( + self, + option_name: str, + message: str | None = None, + possibilities: cabc.Sequence[str] | None = None, + ctx: Context | None = None, + ) -> None: + if message is None: + message = _("No such option: {name}").format(name=option_name) + + super().__init__(message, ctx) + self.option_name = option_name + self.possibilities = possibilities + + def format_message(self) -> str: + if not self.possibilities: + return self.message + + possibility_str = ", ".join(sorted(self.possibilities)) + suggest = ngettext( + "Did you mean {possibility}?", + "(Possible options: {possibilities})", + len(self.possibilities), + ).format(possibility=possibility_str, possibilities=possibility_str) + return f"{self.message} {suggest}" + + +class BadOptionUsage(UsageError): + """Raised if an option is generally supplied but the use of the option + was incorrect. This is for instance raised if the number of arguments + for an option is not correct. + + .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. + """ + + def __init__( + self, option_name: str, message: str, ctx: Context | None = None + ) -> None: + super().__init__(message, ctx) + self.option_name = option_name + + +class BadArgumentUsage(UsageError): + """Raised if an argument is generally supplied but the use of the argument + was incorrect. This is for instance raised if the number of values + for an argument is not correct. + + .. versionadded:: 6.0 + """ + + +class NoArgsIsHelpError(UsageError): + def __init__(self, ctx: Context) -> None: + self.ctx: Context + super().__init__(ctx.get_help(), ctx=ctx) + + def show(self, file: t.IO[t.Any] | None = None) -> None: + echo(self.format_message(), file=file, err=True, color=self.ctx.color) + + +class FileError(ClickException): + """Raised if a file cannot be opened.""" + + def __init__(self, filename: str, hint: str | None = None) -> None: + if hint is None: + hint = _("unknown error") + + super().__init__(hint) + self.ui_filename: str = format_filename(filename) + self.filename = filename + + def format_message(self) -> str: + return _("Could not open file {filename!r}: {message}").format( + filename=self.ui_filename, message=self.message + ) + + +class Abort(RuntimeError): + """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + + __slots__ = ("exit_code",) + + def __init__(self, code: int = 0) -> None: + self.exit_code: int = code diff --git a/venv/lib/python3.10/site-packages/click/formatting.py b/venv/lib/python3.10/site-packages/click/formatting.py new file mode 100644 index 0000000..0b64f83 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/formatting.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +import collections.abc as cabc +from contextlib import contextmanager +from gettext import gettext as _ + +from ._compat import term_len +from .parser import _split_opt + +# Can force a width. This is used by the test system +FORCED_WIDTH: int | None = None + + +def measure_table(rows: cabc.Iterable[tuple[str, str]]) -> tuple[int, ...]: + widths: dict[int, int] = {} + + for row in rows: + for idx, col in enumerate(row): + widths[idx] = max(widths.get(idx, 0), term_len(col)) + + return tuple(y for x, y in sorted(widths.items())) + + +def iter_rows( + rows: cabc.Iterable[tuple[str, str]], col_count: int +) -> cabc.Iterator[tuple[str, ...]]: + for row in rows: + yield row + ("",) * (col_count - len(row)) + + +def wrap_text( + text: str, + width: int = 78, + initial_indent: str = "", + subsequent_indent: str = "", + preserve_paragraphs: bool = False, +) -> str: + """A helper function that intelligently wraps text. By default, it + assumes that it operates on a single paragraph of text but if the + `preserve_paragraphs` parameter is provided it will intelligently + handle paragraphs (defined by two empty lines). + + If paragraphs are handled, a paragraph can be prefixed with an empty + line containing the ``\\b`` character (``\\x08``) to indicate that + no rewrapping should happen in that block. + + :param text: the text that should be rewrapped. + :param width: the maximum width for the text. + :param initial_indent: the initial indent that should be placed on the + first line as a string. + :param subsequent_indent: the indent string that should be placed on + each consecutive line. + :param preserve_paragraphs: if this flag is set then the wrapping will + intelligently handle paragraphs. + """ + from ._textwrap import TextWrapper + + text = text.expandtabs() + wrapper = TextWrapper( + width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + replace_whitespace=False, + ) + if not preserve_paragraphs: + return wrapper.fill(text) + + p: list[tuple[int, bool, str]] = [] + buf: list[str] = [] + indent = None + + def _flush_par() -> None: + if not buf: + return + if buf[0].strip() == "\b": + p.append((indent or 0, True, "\n".join(buf[1:]))) + else: + p.append((indent or 0, False, " ".join(buf))) + del buf[:] + + for line in text.splitlines(): + if not line: + _flush_par() + indent = None + else: + if indent is None: + orig_len = term_len(line) + line = line.lstrip() + indent = orig_len - term_len(line) + buf.append(line) + _flush_par() + + rv = [] + for indent, raw, text in p: + with wrapper.extra_indent(" " * indent): + if raw: + rv.append(wrapper.indent_only(text)) + else: + rv.append(wrapper.fill(text)) + + return "\n\n".join(rv) + + +class HelpFormatter: + """This class helps with formatting text-based help pages. It's + usually just needed for very special internal cases, but it's also + exposed so that developers can write their own fancy outputs. + + At present, it always writes into memory. + + :param indent_increment: the additional increment for each level. + :param width: the width for the text. This defaults to the terminal + width clamped to a maximum of 78. + """ + + def __init__( + self, + indent_increment: int = 2, + width: int | None = None, + max_width: int | None = None, + ) -> None: + self.indent_increment = indent_increment + if max_width is None: + max_width = 80 + if width is None: + import shutil + + width = FORCED_WIDTH + if width is None: + width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50) + self.width = width + self.current_indent: int = 0 + self.buffer: list[str] = [] + + def write(self, string: str) -> None: + """Writes a unicode string into the internal buffer.""" + self.buffer.append(string) + + def indent(self) -> None: + """Increases the indentation.""" + self.current_indent += self.indent_increment + + def dedent(self) -> None: + """Decreases the indentation.""" + self.current_indent -= self.indent_increment + + def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> None: + """Writes a usage line into the buffer. + + :param prog: the program name. + :param args: whitespace separated list of arguments. + :param prefix: The prefix for the first line. Defaults to + ``"Usage: "``. + """ + if prefix is None: + prefix = f"{_('Usage:')} " + + usage_prefix = f"{prefix:>{self.current_indent}}{prog} " + text_width = self.width - self.current_indent + + if text_width >= (term_len(usage_prefix) + 20): + # The arguments will fit to the right of the prefix. + indent = " " * term_len(usage_prefix) + self.write( + wrap_text( + args, + text_width, + initial_indent=usage_prefix, + subsequent_indent=indent, + ) + ) + else: + # The prefix is too long, put the arguments on the next line. + self.write(usage_prefix) + self.write("\n") + indent = " " * (max(self.current_indent, term_len(prefix)) + 4) + self.write( + wrap_text( + args, text_width, initial_indent=indent, subsequent_indent=indent + ) + ) + + self.write("\n") + + def write_heading(self, heading: str) -> None: + """Writes a heading into the buffer.""" + self.write(f"{'':>{self.current_indent}}{heading}:\n") + + def write_paragraph(self) -> None: + """Writes a paragraph into the buffer.""" + if self.buffer: + self.write("\n") + + def write_text(self, text: str) -> None: + """Writes re-indented text into the buffer. This rewraps and + preserves paragraphs. + """ + indent = " " * self.current_indent + self.write( + wrap_text( + text, + self.width, + initial_indent=indent, + subsequent_indent=indent, + preserve_paragraphs=True, + ) + ) + self.write("\n") + + def write_dl( + self, + rows: cabc.Sequence[tuple[str, str]], + col_max: int = 30, + col_spacing: int = 2, + ) -> None: + """Writes a definition list into the buffer. This is how options + and commands are usually formatted. + + :param rows: a list of two item tuples for the terms and values. + :param col_max: the maximum width of the first column. + :param col_spacing: the number of spaces between the first and + second column. + """ + rows = list(rows) + widths = measure_table(rows) + if len(widths) != 2: + raise TypeError("Expected two columns for definition list") + + first_col = min(widths[0], col_max) + col_spacing + + for first, second in iter_rows(rows, len(widths)): + self.write(f"{'':>{self.current_indent}}{first}") + if not second: + self.write("\n") + continue + if term_len(first) <= first_col - col_spacing: + self.write(" " * (first_col - term_len(first))) + else: + self.write("\n") + self.write(" " * (first_col + self.current_indent)) + + text_width = max(self.width - first_col - 2, 10) + wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) + lines = wrapped_text.splitlines() + + if lines: + self.write(f"{lines[0]}\n") + + for line in lines[1:]: + self.write(f"{'':>{first_col + self.current_indent}}{line}\n") + else: + self.write("\n") + + @contextmanager + def section(self, name: str) -> cabc.Iterator[None]: + """Helpful context manager that writes a paragraph, a heading, + and the indents. + + :param name: the section name that is written as heading. + """ + self.write_paragraph() + self.write_heading(name) + self.indent() + try: + yield + finally: + self.dedent() + + @contextmanager + def indentation(self) -> cabc.Iterator[None]: + """A context manager that increases the indentation.""" + self.indent() + try: + yield + finally: + self.dedent() + + def getvalue(self) -> str: + """Returns the buffer contents.""" + return "".join(self.buffer) + + +def join_options(options: cabc.Sequence[str]) -> tuple[str, bool]: + """Given a list of option strings this joins them in the most appropriate + way and returns them in the form ``(formatted_string, + any_prefix_is_slash)`` where the second item in the tuple is a flag that + indicates if any of the option prefixes was a slash. + """ + rv = [] + any_prefix_is_slash = False + + for opt in options: + prefix = _split_opt(opt)[0] + + if prefix == "/": + any_prefix_is_slash = True + + rv.append((len(prefix), opt)) + + rv.sort(key=lambda x: x[0]) + return ", ".join(x[1] for x in rv), any_prefix_is_slash diff --git a/venv/lib/python3.10/site-packages/click/globals.py b/venv/lib/python3.10/site-packages/click/globals.py new file mode 100644 index 0000000..a2f9172 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/globals.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import typing as t +from threading import local + +if t.TYPE_CHECKING: + from .core import Context + +_local = local() + + +@t.overload +def get_current_context(silent: t.Literal[False] = False) -> Context: ... + + +@t.overload +def get_current_context(silent: bool = ...) -> Context | None: ... + + +def get_current_context(silent: bool = False) -> Context | None: + """Returns the current click context. This can be used as a way to + access the current context object from anywhere. This is a more implicit + alternative to the :func:`pass_context` decorator. This function is + primarily useful for helpers such as :func:`echo` which might be + interested in changing its behavior based on the current context. + + To push the current context, :meth:`Context.scope` can be used. + + .. versionadded:: 5.0 + + :param silent: if set to `True` the return value is `None` if no context + is available. The default behavior is to raise a + :exc:`RuntimeError`. + """ + try: + return t.cast("Context", _local.stack[-1]) + except (AttributeError, IndexError) as e: + if not silent: + raise RuntimeError("There is no active click context.") from e + + return None + + +def push_context(ctx: Context) -> None: + """Pushes a new context to the current stack.""" + _local.__dict__.setdefault("stack", []).append(ctx) + + +def pop_context() -> None: + """Removes the top level from the stack.""" + _local.stack.pop() + + +def resolve_color_default(color: bool | None = None) -> bool | None: + """Internal helper to get the default value of the color flag. If a + value is passed it's returned unchanged, otherwise it's looked up from + the current context. + """ + if color is not None: + return color + + ctx = get_current_context(silent=True) + + if ctx is not None: + return ctx.color + + return None diff --git a/venv/lib/python3.10/site-packages/click/parser.py b/venv/lib/python3.10/site-packages/click/parser.py new file mode 100644 index 0000000..1ea1f71 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/parser.py @@ -0,0 +1,532 @@ +""" +This module started out as largely a copy paste from the stdlib's +optparse module with the features removed that we do not need from +optparse because we implement them in Click on a higher level (for +instance type handling, help formatting and a lot more). + +The plan is to remove more and more from here over time. + +The reason this is a different module and not optparse from the stdlib +is that there are differences in 2.x and 3.x about the error messages +generated and optparse in the stdlib uses gettext for no good reason +and might cause us issues. + +Click uses parts of optparse written by Gregory P. Ward and maintained +by the Python Software Foundation. This is limited to code in parser.py. + +Copyright 2001-2006 Gregory P. Ward. All rights reserved. +Copyright 2002-2006 Python Software Foundation. All rights reserved. +""" + +# This code uses parts of optparse written by Gregory P. Ward and +# maintained by the Python Software Foundation. +# Copyright 2001-2006 Gregory P. Ward +# Copyright 2002-2006 Python Software Foundation +from __future__ import annotations + +import collections.abc as cabc +import typing as t +from collections import deque +from gettext import gettext as _ +from gettext import ngettext + +from ._utils import FLAG_NEEDS_VALUE +from ._utils import UNSET +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import NoSuchOption +from .exceptions import UsageError + +if t.TYPE_CHECKING: + from ._utils import T_FLAG_NEEDS_VALUE + from ._utils import T_UNSET + from .core import Argument as CoreArgument + from .core import Context + from .core import Option as CoreOption + from .core import Parameter as CoreParameter + +V = t.TypeVar("V") + + +def _unpack_args( + args: cabc.Sequence[str], nargs_spec: cabc.Sequence[int] +) -> tuple[cabc.Sequence[str | cabc.Sequence[str | None] | None], list[str]]: + """Given an iterable of arguments and an iterable of nargs specifications, + it returns a tuple with all the unpacked arguments at the first index + and all remaining arguments as the second. + + The nargs specification is the number of arguments that should be consumed + or `-1` to indicate that this position should eat up all the remainders. + + Missing items are filled with ``UNSET``. + """ + args = deque(args) + nargs_spec = deque(nargs_spec) + rv: list[str | tuple[str | T_UNSET, ...] | T_UNSET] = [] + spos: int | None = None + + def _fetch(c: deque[V]) -> V | T_UNSET: + try: + if spos is None: + return c.popleft() + else: + return c.pop() + except IndexError: + return UNSET + + while nargs_spec: + nargs = _fetch(nargs_spec) + + if nargs is None: + continue + + if nargs == 1: + rv.append(_fetch(args)) # type: ignore[arg-type] + elif nargs > 1: + x = [_fetch(args) for _ in range(nargs)] + + # If we're reversed, we're pulling in the arguments in reverse, + # so we need to turn them around. + if spos is not None: + x.reverse() + + rv.append(tuple(x)) + elif nargs < 0: + if spos is not None: + raise TypeError("Cannot have two nargs < 0") + + spos = len(rv) + rv.append(UNSET) + + # spos is the position of the wildcard (star). If it's not `None`, + # we fill it with the remainder. + if spos is not None: + rv[spos] = tuple(args) + args = [] + rv[spos + 1 :] = reversed(rv[spos + 1 :]) + + return tuple(rv), list(args) + + +def _split_opt(opt: str) -> tuple[str, str]: + first = opt[:1] + if first.isalnum(): + return "", opt + if opt[1:2] == first: + return opt[:2], opt[2:] + return first, opt[1:] + + +def _normalize_opt(opt: str, ctx: Context | None) -> str: + if ctx is None or ctx.token_normalize_func is None: + return opt + prefix, opt = _split_opt(opt) + return f"{prefix}{ctx.token_normalize_func(opt)}" + + +class _Option: + def __init__( + self, + obj: CoreOption, + opts: cabc.Sequence[str], + dest: str | None, + action: str | None = None, + nargs: int = 1, + const: t.Any | None = None, + ): + self._short_opts = [] + self._long_opts = [] + self.prefixes: set[str] = set() + + for opt in opts: + prefix, value = _split_opt(opt) + if not prefix: + raise ValueError(f"Invalid start character for option ({opt})") + self.prefixes.add(prefix[0]) + if len(prefix) == 1 and len(value) == 1: + self._short_opts.append(opt) + else: + self._long_opts.append(opt) + self.prefixes.add(prefix) + + if action is None: + action = "store" + + self.dest = dest + self.action = action + self.nargs = nargs + self.const = const + self.obj = obj + + @property + def takes_value(self) -> bool: + return self.action in ("store", "append") + + def process(self, value: t.Any, state: _ParsingState) -> None: + if self.action == "store": + state.opts[self.dest] = value # type: ignore + elif self.action == "store_const": + state.opts[self.dest] = self.const # type: ignore + elif self.action == "append": + state.opts.setdefault(self.dest, []).append(value) # type: ignore + elif self.action == "append_const": + state.opts.setdefault(self.dest, []).append(self.const) # type: ignore + elif self.action == "count": + state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore + else: + raise ValueError(f"unknown action '{self.action}'") + state.order.append(self.obj) + + +class _Argument: + def __init__(self, obj: CoreArgument, dest: str | None, nargs: int = 1): + self.dest = dest + self.nargs = nargs + self.obj = obj + + def process( + self, + value: str | cabc.Sequence[str | None] | None | T_UNSET, + state: _ParsingState, + ) -> None: + if self.nargs > 1: + assert isinstance(value, cabc.Sequence) + holes = sum(1 for x in value if x is UNSET) + if holes == len(value): + value = UNSET + elif holes != 0: + raise BadArgumentUsage( + _("Argument {name!r} takes {nargs} values.").format( + name=self.dest, nargs=self.nargs + ) + ) + + # We failed to collect any argument value so we consider the argument as unset. + if value == (): + value = UNSET + + state.opts[self.dest] = value # type: ignore + state.order.append(self.obj) + + +class _ParsingState: + def __init__(self, rargs: list[str]) -> None: + self.opts: dict[str, t.Any] = {} + self.largs: list[str] = [] + self.rargs = rargs + self.order: list[CoreParameter] = [] + + +class _OptionParser: + """The option parser is an internal class that is ultimately used to + parse options and arguments. It's modelled after optparse and brings + a similar but vastly simplified API. It should generally not be used + directly as the high level Click classes wrap it for you. + + It's not nearly as extensible as optparse or argparse as it does not + implement features that are implemented on a higher level (such as + types or defaults). + + :param ctx: optionally the :class:`~click.Context` where this parser + should go with. + + .. deprecated:: 8.2 + Will be removed in Click 9.0. + """ + + def __init__(self, ctx: Context | None = None) -> None: + #: The :class:`~click.Context` for this parser. This might be + #: `None` for some advanced use cases. + self.ctx = ctx + #: This controls how the parser deals with interspersed arguments. + #: If this is set to `False`, the parser will stop on the first + #: non-option. Click uses this to implement nested subcommands + #: safely. + self.allow_interspersed_args: bool = True + #: This tells the parser how to deal with unknown options. By + #: default it will error out (which is sensible), but there is a + #: second mode where it will ignore it and continue processing + #: after shifting all the unknown options into the resulting args. + self.ignore_unknown_options: bool = False + + if ctx is not None: + self.allow_interspersed_args = ctx.allow_interspersed_args + self.ignore_unknown_options = ctx.ignore_unknown_options + + self._short_opt: dict[str, _Option] = {} + self._long_opt: dict[str, _Option] = {} + self._opt_prefixes = {"-", "--"} + self._args: list[_Argument] = [] + + def add_option( + self, + obj: CoreOption, + opts: cabc.Sequence[str], + dest: str | None, + action: str | None = None, + nargs: int = 1, + const: t.Any | None = None, + ) -> None: + """Adds a new option named `dest` to the parser. The destination + is not inferred (unlike with optparse) and needs to be explicitly + provided. Action can be any of ``store``, ``store_const``, + ``append``, ``append_const`` or ``count``. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + opts = [_normalize_opt(opt, self.ctx) for opt in opts] + option = _Option(obj, opts, dest, action=action, nargs=nargs, const=const) + self._opt_prefixes.update(option.prefixes) + for opt in option._short_opts: + self._short_opt[opt] = option + for opt in option._long_opts: + self._long_opt[opt] = option + + def add_argument(self, obj: CoreArgument, dest: str | None, nargs: int = 1) -> None: + """Adds a positional argument named `dest` to the parser. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + self._args.append(_Argument(obj, dest=dest, nargs=nargs)) + + def parse_args( + self, args: list[str] + ) -> tuple[dict[str, t.Any], list[str], list[CoreParameter]]: + """Parses positional arguments and returns ``(values, args, order)`` + for the parsed options and arguments as well as the leftover + arguments if there are any. The order is a list of objects as they + appear on the command line. If arguments appear multiple times they + will be memorized multiple times as well. + """ + state = _ParsingState(args) + try: + self._process_args_for_options(state) + self._process_args_for_args(state) + except UsageError: + if self.ctx is None or not self.ctx.resilient_parsing: + raise + return state.opts, state.largs, state.order + + def _process_args_for_args(self, state: _ParsingState) -> None: + pargs, args = _unpack_args( + state.largs + state.rargs, [x.nargs for x in self._args] + ) + + for idx, arg in enumerate(self._args): + arg.process(pargs[idx], state) + + state.largs = args + state.rargs = [] + + def _process_args_for_options(self, state: _ParsingState) -> None: + while state.rargs: + arg = state.rargs.pop(0) + arglen = len(arg) + # Double dashes always handled explicitly regardless of what + # prefixes are valid. + if arg == "--": + return + elif arg[:1] in self._opt_prefixes and arglen > 1: + self._process_opts(arg, state) + elif self.allow_interspersed_args: + state.largs.append(arg) + else: + state.rargs.insert(0, arg) + return + + # Say this is the original argument list: + # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)] + # ^ + # (we are about to process arg(i)). + # + # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of + # [arg0, ..., arg(i-1)] (any options and their arguments will have + # been removed from largs). + # + # The while loop will usually consume 1 or more arguments per pass. + # If it consumes 1 (eg. arg is an option that takes no arguments), + # then after _process_arg() is done the situation is: + # + # largs = subset of [arg0, ..., arg(i)] + # rargs = [arg(i+1), ..., arg(N-1)] + # + # If allow_interspersed_args is false, largs will always be + # *empty* -- still a subset of [arg0, ..., arg(i-1)], but + # not a very interesting subset! + + def _match_long_opt( + self, opt: str, explicit_value: str | None, state: _ParsingState + ) -> None: + if opt not in self._long_opt: + from difflib import get_close_matches + + possibilities = get_close_matches(opt, self._long_opt) + raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) + + option = self._long_opt[opt] + if option.takes_value: + # At this point it's safe to modify rargs by injecting the + # explicit value, because no exception is raised in this + # branch. This means that the inserted value will be fully + # consumed. + if explicit_value is not None: + state.rargs.insert(0, explicit_value) + + value = self._get_value_from_state(opt, option, state) + + elif explicit_value is not None: + raise BadOptionUsage( + opt, _("Option {name!r} does not take a value.").format(name=opt) + ) + + else: + value = UNSET + + option.process(value, state) + + def _match_short_opt(self, arg: str, state: _ParsingState) -> None: + stop = False + i = 1 + prefix = arg[0] + unknown_options = [] + + for ch in arg[1:]: + opt = _normalize_opt(f"{prefix}{ch}", self.ctx) + option = self._short_opt.get(opt) + i += 1 + + if not option: + if self.ignore_unknown_options: + unknown_options.append(ch) + continue + raise NoSuchOption(opt, ctx=self.ctx) + if option.takes_value: + # Any characters left in arg? Pretend they're the + # next arg, and stop consuming characters of arg. + if i < len(arg): + state.rargs.insert(0, arg[i:]) + stop = True + + value = self._get_value_from_state(opt, option, state) + + else: + value = UNSET + + option.process(value, state) + + if stop: + break + + # If we got any unknown options we recombine the string of the + # remaining options and re-attach the prefix, then report that + # to the state as new larg. This way there is basic combinatorics + # that can be achieved while still ignoring unknown arguments. + if self.ignore_unknown_options and unknown_options: + state.largs.append(f"{prefix}{''.join(unknown_options)}") + + def _get_value_from_state( + self, option_name: str, option: _Option, state: _ParsingState + ) -> str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE: + nargs = option.nargs + + value: str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE + + if len(state.rargs) < nargs: + if option.obj._flag_needs_value: + # Option allows omitting the value. + value = FLAG_NEEDS_VALUE + else: + raise BadOptionUsage( + option_name, + ngettext( + "Option {name!r} requires an argument.", + "Option {name!r} requires {nargs} arguments.", + nargs, + ).format(name=option_name, nargs=nargs), + ) + elif nargs == 1: + next_rarg = state.rargs[0] + + if ( + option.obj._flag_needs_value + and isinstance(next_rarg, str) + and next_rarg[:1] in self._opt_prefixes + and len(next_rarg) > 1 + ): + # The next arg looks like the start of an option, don't + # use it as the value if omitting the value is allowed. + value = FLAG_NEEDS_VALUE + else: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + return value + + def _process_opts(self, arg: str, state: _ParsingState) -> None: + explicit_value = None + # Long option handling happens in two parts. The first part is + # supporting explicitly attached values. In any case, we will try + # to long match the option first. + if "=" in arg: + long_opt, explicit_value = arg.split("=", 1) + else: + long_opt = arg + norm_long_opt = _normalize_opt(long_opt, self.ctx) + + # At this point we will match the (assumed) long option through + # the long option matching code. Note that this allows options + # like "-foo" to be matched as long options. + try: + self._match_long_opt(norm_long_opt, explicit_value, state) + except NoSuchOption: + # At this point the long option matching failed, and we need + # to try with short options. However there is a special rule + # which says, that if we have a two character options prefix + # (applies to "--foo" for instance), we do not dispatch to the + # short option code and will instead raise the no option + # error. + if arg[:2] not in self._opt_prefixes: + self._match_short_opt(arg, state) + return + + if not self.ignore_unknown_options: + raise + + state.largs.append(arg) + + +def __getattr__(name: str) -> object: + import warnings + + if name in { + "OptionParser", + "Argument", + "Option", + "split_opt", + "normalize_opt", + "ParsingState", + }: + warnings.warn( + f"'parser.{name}' is deprecated and will be removed in Click 9.0." + " The old parser is available in 'optparse'.", + DeprecationWarning, + stacklevel=2, + ) + return globals()[f"_{name}"] + + if name == "split_arg_string": + from .shell_completion import split_arg_string + + warnings.warn( + "Importing 'parser.split_arg_string' is deprecated, it will only be" + " available in 'shell_completion' in Click 9.0.", + DeprecationWarning, + stacklevel=2, + ) + return split_arg_string + + raise AttributeError(name) diff --git a/venv/lib/python3.10/site-packages/click/py.typed b/venv/lib/python3.10/site-packages/click/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/click/shell_completion.py b/venv/lib/python3.10/site-packages/click/shell_completion.py new file mode 100644 index 0000000..8f1564c --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/shell_completion.py @@ -0,0 +1,667 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import re +import typing as t +from gettext import gettext as _ + +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter +from .core import ParameterSource +from .utils import echo + + +def shell_complete( + cli: Command, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str, + instruction: str, +) -> int: + """Perform shell completion for the given CLI program. + + :param cli: Command being called. + :param ctx_args: Extra arguments to pass to + ``cli.make_context``. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + :param instruction: Value of ``complete_var`` with the completion + instruction and shell, in the form ``instruction_shell``. + :return: Status code to exit with. + """ + shell, _, instruction = instruction.partition("_") + comp_cls = get_completion_class(shell) + + if comp_cls is None: + return 1 + + comp = comp_cls(cli, ctx_args, prog_name, complete_var) + + if instruction == "source": + echo(comp.source()) + return 0 + + if instruction == "complete": + echo(comp.complete()) + return 0 + + return 1 + + +class CompletionItem: + """Represents a completion value and metadata about the value. The + default metadata is ``type`` to indicate special shell handling, + and ``help`` if a shell supports showing a help string next to the + value. + + Arbitrary parameters can be passed when creating the object, and + accessed using ``item.attr``. If an attribute wasn't passed, + accessing it returns ``None``. + + :param value: The completion suggestion. + :param type: Tells the shell script to provide special completion + support for the type. Click uses ``"dir"`` and ``"file"``. + :param help: String shown next to the value if supported. + :param kwargs: Arbitrary metadata. The built-in implementations + don't use this, but custom type completions paired with custom + shell support could use it. + """ + + __slots__ = ("value", "type", "help", "_info") + + def __init__( + self, + value: t.Any, + type: str = "plain", + help: str | None = None, + **kwargs: t.Any, + ) -> None: + self.value: t.Any = value + self.type: str = type + self.help: str | None = help + self._info = kwargs + + def __getattr__(self, name: str) -> t.Any: + return self._info.get(name) + + +# Only Bash >= 4.4 has the nosort option. +_SOURCE_BASH = """\ +%(complete_func)s() { + local IFS=$'\\n' + local response + + response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \ +%(complete_var)s=bash_complete $1) + + for completion in $response; do + IFS=',' read type value <<< "$completion" + + if [[ $type == 'dir' ]]; then + COMPREPLY=() + compopt -o dirnames + elif [[ $type == 'file' ]]; then + COMPREPLY=() + compopt -o default + elif [[ $type == 'plain' ]]; then + COMPREPLY+=($value) + fi + done + + return 0 +} + +%(complete_func)s_setup() { + complete -o nosort -F %(complete_func)s %(prog_name)s +} + +%(complete_func)s_setup; +""" + +# See ZshComplete.format_completion below, and issue #2703, before +# changing this script. +# +# (TL;DR: _describe is picky about the format, but this Zsh script snippet +# is already widely deployed. So freeze this script, and use clever-ish +# handling of colons in ZshComplet.format_completion.) +_SOURCE_ZSH = """\ +#compdef %(prog_name)s + +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + (( ! $+commands[%(prog_name)s] )) && return 1 + + response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \ +%(complete_var)s=zsh_complete %(prog_name)s)}") + + for type key descr in ${response}; do + if [[ "$type" == "plain" ]]; then + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + elif [[ "$type" == "dir" ]]; then + _path_files -/ + elif [[ "$type" == "file" ]]; then + _path_files -f + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -a completions + fi +} + +if [[ $zsh_eval_context[-1] == loadautofunc ]]; then + # autoload from fpath, call function directly + %(complete_func)s "$@" +else + # eval/source/. command, register function for later + compdef %(complete_func)s %(prog_name)s +fi +""" + +_SOURCE_FISH = """\ +function %(complete_func)s; + set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \ +COMP_CWORD=(commandline -t) %(prog_name)s); + + for completion in $response; + set -l metadata (string split "," $completion); + + if test $metadata[1] = "dir"; + __fish_complete_directories $metadata[2]; + else if test $metadata[1] = "file"; + __fish_complete_path $metadata[2]; + else if test $metadata[1] = "plain"; + echo $metadata[2]; + end; + end; +end; + +complete --no-files --command %(prog_name)s --arguments \ +"(%(complete_func)s)"; +""" + + +class ShellComplete: + """Base class for providing shell completion support. A subclass for + a given shell will override attributes and methods to implement the + completion instructions (``source`` and ``complete``). + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + + .. versionadded:: 8.0 + """ + + name: t.ClassVar[str] + """Name to register the shell as with :func:`add_completion_class`. + This is used in completion instructions (``{name}_source`` and + ``{name}_complete``). + """ + + source_template: t.ClassVar[str] + """Completion script template formatted by :meth:`source`. This must + be provided by subclasses. + """ + + def __init__( + self, + cli: Command, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str, + ) -> None: + self.cli = cli + self.ctx_args = ctx_args + self.prog_name = prog_name + self.complete_var = complete_var + + @property + def func_name(self) -> str: + """The name of the shell function defined by the completion + script. + """ + safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII) + return f"_{safe_name}_completion" + + def source_vars(self) -> dict[str, t.Any]: + """Vars for formatting :attr:`source_template`. + + By default this provides ``complete_func``, ``complete_var``, + and ``prog_name``. + """ + return { + "complete_func": self.func_name, + "complete_var": self.complete_var, + "prog_name": self.prog_name, + } + + def source(self) -> str: + """Produce the shell script that defines the completion + function. By default this ``%``-style formats + :attr:`source_template` with the dict returned by + :meth:`source_vars`. + """ + return self.source_template % self.source_vars() + + def get_completion_args(self) -> tuple[list[str], str]: + """Use the env vars defined by the shell script to return a + tuple of ``args, incomplete``. This must be implemented by + subclasses. + """ + raise NotImplementedError + + def get_completions(self, args: list[str], incomplete: str) -> list[CompletionItem]: + """Determine the context and last complete command or parameter + from the complete args. Call that object's ``shell_complete`` + method to get the completions for the incomplete value. + + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args) + obj, incomplete = _resolve_incomplete(ctx, args, incomplete) + return obj.shell_complete(ctx, incomplete) + + def format_completion(self, item: CompletionItem) -> str: + """Format a completion item into the form recognized by the + shell script. This must be implemented by subclasses. + + :param item: Completion item to format. + """ + raise NotImplementedError + + def complete(self) -> str: + """Produce the completion data to send back to the shell. + + By default this calls :meth:`get_completion_args`, gets the + completions, then calls :meth:`format_completion` for each + completion. + """ + args, incomplete = self.get_completion_args() + completions = self.get_completions(args, incomplete) + out = [self.format_completion(item) for item in completions] + return "\n".join(out) + + +class BashComplete(ShellComplete): + """Shell completion for Bash.""" + + name = "bash" + source_template = _SOURCE_BASH + + @staticmethod + def _check_version() -> None: + import shutil + import subprocess + + bash_exe = shutil.which("bash") + + if bash_exe is None: + match = None + else: + output = subprocess.run( + [bash_exe, "--norc", "-c", 'echo "${BASH_VERSION}"'], + stdout=subprocess.PIPE, + ) + match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) + + if match is not None: + major, minor = match.groups() + + if major < "4" or major == "4" and minor < "4": + echo( + _( + "Shell completion is not supported for Bash" + " versions older than 4.4." + ), + err=True, + ) + else: + echo( + _("Couldn't detect Bash version, shell completion is not supported."), + err=True, + ) + + def source(self) -> str: + self._check_version() + return super().source() + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + return f"{item.type},{item.value}" + + +class ZshComplete(ShellComplete): + """Shell completion for Zsh.""" + + name = "zsh" + source_template = _SOURCE_ZSH + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + help_ = item.help or "_" + # The zsh completion script uses `_describe` on items with help + # texts (which splits the item help from the item value at the + # first unescaped colon) and `compadd` on items without help + # text (which uses the item value as-is and does not support + # colon escaping). So escape colons in the item value if and + # only if the item help is not the sentinel "_" value, as used + # by the completion script. + # + # (The zsh completion script is potentially widely deployed, and + # thus harder to fix than this method.) + # + # See issue #1812 and issue #2703 for further context. + value = item.value.replace(":", r"\:") if help_ != "_" else item.value + return f"{item.type}\n{value}\n{help_}" + + +class FishComplete(ShellComplete): + """Shell completion for Fish.""" + + name = "fish" + source_template = _SOURCE_FISH + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + incomplete = os.environ["COMP_CWORD"] + if incomplete: + incomplete = split_arg_string(incomplete)[0] + args = cwords[1:] + + # Fish stores the partial word in both COMP_WORDS and + # COMP_CWORD, remove it from complete args. + if incomplete and args and args[-1] == incomplete: + args.pop() + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + if item.help: + return f"{item.type},{item.value}\t{item.help}" + + return f"{item.type},{item.value}" + + +ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]") + + +_available_shells: dict[str, type[ShellComplete]] = { + "bash": BashComplete, + "fish": FishComplete, + "zsh": ZshComplete, +} + + +def add_completion_class( + cls: ShellCompleteType, name: str | None = None +) -> ShellCompleteType: + """Register a :class:`ShellComplete` subclass under the given name. + The name will be provided by the completion instruction environment + variable during completion. + + :param cls: The completion class that will handle completion for the + shell. + :param name: Name to register the class under. Defaults to the + class's ``name`` attribute. + """ + if name is None: + name = cls.name + + _available_shells[name] = cls + + return cls + + +def get_completion_class(shell: str) -> type[ShellComplete] | None: + """Look up a registered :class:`ShellComplete` subclass by the name + provided by the completion instruction environment variable. If the + name isn't registered, returns ``None``. + + :param shell: Name the class is registered under. + """ + return _available_shells.get(shell) + + +def split_arg_string(string: str) -> list[str]: + """Split an argument string as with :func:`shlex.split`, but don't + fail if the string is incomplete. Ignores a missing closing quote or + incomplete escape sequence and uses the partial token as-is. + + .. code-block:: python + + split_arg_string("example 'my file") + ["example", "my file"] + + split_arg_string("example my\\") + ["example", "my"] + + :param string: String to split. + + .. versionchanged:: 8.2 + Moved to ``shell_completion`` from ``parser``. + """ + import shlex + + lex = shlex.shlex(string, posix=True) + lex.whitespace_split = True + lex.commenters = "" + out = [] + + try: + for token in lex: + out.append(token) + except ValueError: + # Raised when end-of-string is reached in an invalid state. Use + # the partial token as-is. The quote or escape character is in + # lex.state, not lex.token. + out.append(lex.token) + + return out + + +def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: + """Determine if the given parameter is an argument that can still + accept values. + + :param ctx: Invocation context for the command represented by the + parsed complete args. + :param param: Argument object being checked. + """ + if not isinstance(param, Argument): + return False + + assert param.name is not None + # Will be None if expose_value is False. + value = ctx.params.get(param.name) + return ( + param.nargs == -1 + or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE + or ( + param.nargs > 1 + and isinstance(value, (tuple, list)) + and len(value) < param.nargs + ) + ) + + +def _start_of_option(ctx: Context, value: str) -> bool: + """Check if the value looks like the start of an option.""" + if not value: + return False + + c = value[0] + return c in ctx._opt_prefixes + + +def _is_incomplete_option(ctx: Context, args: list[str], param: Parameter) -> bool: + """Determine if the given parameter is an option that needs a value. + + :param args: List of complete args before the incomplete value. + :param param: Option object being checked. + """ + if not isinstance(param, Option): + return False + + if param.is_flag or param.count: + return False + + last_option = None + + for index, arg in enumerate(reversed(args)): + if index + 1 > param.nargs: + break + + if _start_of_option(ctx, arg): + last_option = arg + break + + return last_option is not None and last_option in param.opts + + +def _resolve_context( + cli: Command, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + args: list[str], +) -> Context: + """Produce the context hierarchy starting with the command and + traversing the complete arguments. This only follows the commands, + it doesn't trigger input prompts or callbacks. + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param args: List of complete args before the incomplete value. + """ + ctx_args["resilient_parsing"] = True + with cli.make_context(prog_name, args.copy(), **ctx_args) as ctx: + args = ctx._protected_args + ctx.args + + while args: + command = ctx.command + + if isinstance(command, Group): + if not command.chain: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + with cmd.make_context( + name, args, parent=ctx, resilient_parsing=True + ) as sub_ctx: + ctx = sub_ctx + args = ctx._protected_args + ctx.args + else: + sub_ctx = ctx + + while args: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + with cmd.make_context( + name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True, + ) as sub_sub_ctx: + sub_ctx = sub_sub_ctx + args = sub_ctx.args + + ctx = sub_ctx + args = [*sub_ctx._protected_args, *sub_ctx.args] + else: + break + + return ctx + + +def _resolve_incomplete( + ctx: Context, args: list[str], incomplete: str +) -> tuple[Command | Parameter, str]: + """Find the Click object that will handle the completion of the + incomplete value. Return the object and the incomplete value. + + :param ctx: Invocation context for the command represented by + the parsed complete args. + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + # Different shells treat an "=" between a long option name and + # value differently. Might keep the value joined, return the "=" + # as a separate item, or return the split name and value. Always + # split and discard the "=" to make completion easier. + if incomplete == "=": + incomplete = "" + elif "=" in incomplete and _start_of_option(ctx, incomplete): + name, _, incomplete = incomplete.partition("=") + args.append(name) + + # The "--" marker tells Click to stop treating values as options + # even if they start with the option character. If it hasn't been + # given and the incomplete arg looks like an option, the current + # command will provide option name completions. + if "--" not in args and _start_of_option(ctx, incomplete): + return ctx.command, incomplete + + params = ctx.command.get_params(ctx) + + # If the last complete arg is an option name with an incomplete + # value, the option will provide value completions. + for param in params: + if _is_incomplete_option(ctx, args, param): + return param, incomplete + + # It's not an option name or value. The first argument without a + # parsed value will provide value completions. + for param in params: + if _is_incomplete_argument(ctx, param): + return param, incomplete + + # There were no unparsed arguments, the command may be a group that + # will provide command name completions. + return ctx.command, incomplete diff --git a/venv/lib/python3.10/site-packages/click/termui.py b/venv/lib/python3.10/site-packages/click/termui.py new file mode 100644 index 0000000..2e98a07 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/termui.py @@ -0,0 +1,883 @@ +from __future__ import annotations + +import collections.abc as cabc +import inspect +import io +import itertools +import sys +import typing as t +from contextlib import AbstractContextManager +from gettext import gettext as _ + +from ._compat import isatty +from ._compat import strip_ansi +from .exceptions import Abort +from .exceptions import UsageError +from .globals import resolve_color_default +from .types import Choice +from .types import convert_type +from .types import ParamType +from .utils import echo +from .utils import LazyFile + +if t.TYPE_CHECKING: + from ._termui_impl import ProgressBar + +V = t.TypeVar("V") + +# The prompt functions to use. The doc tools currently override these +# functions to customize how they work. +visible_prompt_func: t.Callable[[str], str] = input + +_ansi_colors = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "reset": 39, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, +} +_ansi_reset_all = "\033[0m" + + +def hidden_prompt_func(prompt: str) -> str: + import getpass + + return getpass.getpass(prompt) + + +def _build_prompt( + text: str, + suffix: str, + show_default: bool = False, + default: t.Any | None = None, + show_choices: bool = True, + type: ParamType | None = None, +) -> str: + prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += f" ({', '.join(map(str, type.choices))})" + if default is not None and show_default: + prompt = f"{prompt} [{_format_default(default)}]" + return f"{prompt}{suffix}" + + +def _format_default(default: t.Any) -> t.Any: + if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"): + return default.name + + return default + + +def prompt( + text: str, + default: t.Any | None = None, + hide_input: bool = False, + confirmation_prompt: bool | str = False, + type: ParamType | t.Any | None = None, + value_proc: t.Callable[[str], t.Any] | None = None, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, + show_choices: bool = True, +) -> t.Any: + """Prompts a user for input. This is a convenience function that can + be used to prompt a user for input later. + + If the user aborts the input by sending an interrupt signal, this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the text to show for the prompt. + :param default: the default value to use if no input happens. If this + is not given it will prompt until it's aborted. + :param hide_input: if this is set to true then the input value will + be hidden. + :param confirmation_prompt: Prompt a second time to confirm the + value. Can be set to a string instead of ``True`` to customize + the message. + :param type: the type to use to check the value against. + :param value_proc: if this parameter is provided it's a function that + is invoked instead of the type conversion to + convert a value. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". + + .. versionchanged:: 8.3.1 + A space is no longer appended to the prompt. + + .. versionadded:: 8.0 + ``confirmation_prompt`` can be a custom string. + + .. versionadded:: 7.0 + Added the ``show_choices`` parameter. + + .. versionadded:: 6.0 + Added unicode support for cmd.exe on Windows. + + .. versionadded:: 4.0 + Added the `err` parameter. + + """ + + def prompt_func(text: str) -> str: + f = hidden_prompt_func if hide_input else visible_prompt_func + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(text[:-1], nl=False, err=err) + # Echo the last character to stdout to work around an issue where + # readline causes backspace to clear the whole line. + return f(text[-1:]) + except (KeyboardInterrupt, EOFError): + # getpass doesn't print a newline if the user aborts input with ^C. + # Allegedly this behavior is inherited from getpass(3). + # A doc bug has been filed at https://bugs.python.org/issue24711 + if hide_input: + echo(None, err=err) + raise Abort() from None + + if value_proc is None: + value_proc = convert_type(type, default) + + prompt = _build_prompt( + text, prompt_suffix, show_default, default, show_choices, type + ) + + if confirmation_prompt: + if confirmation_prompt is True: + confirmation_prompt = _("Repeat for confirmation") + + confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix) + + while True: + while True: + value = prompt_func(prompt) + if value: + break + elif default is not None: + value = default + break + try: + result = value_proc(value) + except UsageError as e: + if hide_input: + echo(_("Error: The value you entered was invalid."), err=err) + else: + echo(_("Error: {e.message}").format(e=e), err=err) + continue + if not confirmation_prompt: + return result + while True: + value2 = prompt_func(confirmation_prompt) + is_empty = not value and not value2 + if value2 or is_empty: + break + if value == value2: + return result + echo(_("Error: The two entered values do not match."), err=err) + + +def confirm( + text: str, + default: bool | None = False, + abort: bool = False, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, +) -> bool: + """Prompts for confirmation (yes/no question). + + If the user aborts the input by sending a interrupt signal this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the question to ask. + :param default: The default value to use when no input is given. If + ``None``, repeat until input is given. + :param abort: if this is set to `True` a negative answer aborts the + exception by raising :exc:`Abort`. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + + .. versionchanged:: 8.3.1 + A space is no longer appended to the prompt. + + .. versionchanged:: 8.0 + Repeat until input is given if ``default`` is ``None``. + + .. versionadded:: 4.0 + Added the ``err`` parameter. + """ + prompt = _build_prompt( + text, + prompt_suffix, + show_default, + "y/n" if default is None else ("Y/n" if default else "y/N"), + ) + + while True: + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(prompt[:-1], nl=False, err=err) + # Echo the last character to stdout to work around an issue where + # readline causes backspace to clear the whole line. + value = visible_prompt_func(prompt[-1:]).lower().strip() + except (KeyboardInterrupt, EOFError): + raise Abort() from None + if value in ("y", "yes"): + rv = True + elif value in ("n", "no"): + rv = False + elif default is not None and value == "": + rv = default + else: + echo(_("Error: invalid input"), err=err) + continue + break + if abort and not rv: + raise Abort() + return rv + + +def echo_via_pager( + text_or_generator: cabc.Iterable[str] | t.Callable[[], cabc.Iterable[str]] | str, + color: bool | None = None, +) -> None: + """This function takes a text and shows it via an environment specific + pager on stdout. + + .. versionchanged:: 3.0 + Added the `color` flag. + + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + color = resolve_color_default(color) + + if inspect.isgeneratorfunction(text_or_generator): + i = t.cast("t.Callable[[], cabc.Iterable[str]]", text_or_generator)() + elif isinstance(text_or_generator, str): + i = [text_or_generator] + else: + i = iter(t.cast("cabc.Iterable[str]", text_or_generator)) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, str) else str(el) for el in i) + + from ._termui_impl import pager + + return pager(itertools.chain(text_generator, "\n"), color) + + +@t.overload +def progressbar( + *, + length: int, + label: str | None = None, + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, +) -> ProgressBar[int]: ... + + +@t.overload +def progressbar( + iterable: cabc.Iterable[V] | None = None, + length: int | None = None, + label: str | None = None, + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + item_show_func: t.Callable[[V | None], str | None] | None = None, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, +) -> ProgressBar[V]: ... + + +def progressbar( + iterable: cabc.Iterable[V] | None = None, + length: int | None = None, + label: str | None = None, + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + item_show_func: t.Callable[[V | None], str | None] | None = None, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, +) -> ProgressBar[V]: + """This function creates an iterable context manager that can be used + to iterate over something while showing a progress bar. It will + either iterate over the `iterable` or `length` items (that are counted + up). While iteration happens, this function will print a rendered + progress bar to the given `file` (defaults to stdout) and will attempt + to calculate remaining time and more. By default, this progress bar + will not be rendered if the file is not a terminal. + + The context manager creates the progress bar. When the context + manager is entered the progress bar is already created. With every + iteration over the progress bar, the iterable passed to the bar is + advanced and the bar is updated. When the context manager exits, + a newline is printed and the progress bar is finalized on screen. + + Note: The progress bar is currently designed for use cases where the + total progress can be expected to take at least several seconds. + Because of this, the ProgressBar class object won't display + progress that is considered too fast, and progress where the time + between steps is less than a second. + + No printing must happen or the progress bar will be unintentionally + destroyed. + + Example usage:: + + with progressbar(items) as bar: + for item in bar: + do_something_with(item) + + Alternatively, if no iterable is specified, one can manually update the + progress bar through the `update()` method instead of directly + iterating over the progress bar. The update method accepts the number + of steps to increment the bar with:: + + with progressbar(length=chunks.total_bytes) as bar: + for chunk in chunks: + process_chunk(chunk) + bar.update(chunks.bytes) + + The ``update()`` method also takes an optional value specifying the + ``current_item`` at the new position. This is useful when used + together with ``item_show_func`` to customize the output for each + manual step:: + + with click.progressbar( + length=total_size, + label='Unzipping archive', + item_show_func=lambda a: a.filename + ) as bar: + for archive in zip_file: + archive.extract() + bar.update(archive.size, archive) + + :param iterable: an iterable to iterate over. If not provided the length + is required. + :param length: the number of items to iterate over. By default the + progressbar will attempt to ask the iterator about its + length, which might or might not work. If an iterable is + also provided this parameter can be used to override the + length. If an iterable is not provided the progress bar + will iterate over a range of that length. + :param label: the label to show next to the progress bar. + :param hidden: hide the progressbar. Defaults to ``False``. When no tty is + detected, it will only print the progressbar label. Setting this to + ``False`` also disables that. + :param show_eta: enables or disables the estimated time display. This is + automatically disabled if the length cannot be + determined. + :param show_percent: enables or disables the percentage display. The + default is `True` if the iterable has a length or + `False` if not. + :param show_pos: enables or disables the absolute position display. The + default is `False`. + :param item_show_func: A function called with the current item which + can return a string to show next to the progress bar. If the + function returns ``None`` nothing is shown. The current item can + be ``None``, such as when entering and exiting the bar. + :param fill_char: the character to use to show the filled part of the + progress bar. + :param empty_char: the character to use to show the non-filled part of + the progress bar. + :param bar_template: the format string to use as template for the bar. + The parameters in it are ``label`` for the label, + ``bar`` for the progress bar and ``info`` for the + info section. + :param info_sep: the separator between multiple info items (eta etc.) + :param width: the width of the progress bar in characters, 0 means full + terminal width + :param file: The file to write to. If this is not a terminal then + only the label is printed. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are included anywhere in the progress bar output + which is not the case by default. + :param update_min_steps: Render only when this many updates have + completed. This allows tuning for very fast iterators. + + .. versionadded:: 8.2 + The ``hidden`` argument. + + .. versionchanged:: 8.0 + Output is shown even if execution time is less than 0.5 seconds. + + .. versionchanged:: 8.0 + ``item_show_func`` shows the current item, not the previous one. + + .. versionchanged:: 8.0 + Labels are echoed if the output is not a TTY. Reverts a change + in 7.0 that removed all output. + + .. versionadded:: 8.0 + The ``update_min_steps`` parameter. + + .. versionadded:: 4.0 + The ``color`` parameter and ``update`` method. + + .. versionadded:: 2.0 + """ + from ._termui_impl import ProgressBar + + color = resolve_color_default(color) + return ProgressBar( + iterable=iterable, + length=length, + hidden=hidden, + show_eta=show_eta, + show_percent=show_percent, + show_pos=show_pos, + item_show_func=item_show_func, + fill_char=fill_char, + empty_char=empty_char, + bar_template=bar_template, + info_sep=info_sep, + file=file, + label=label, + width=width, + color=color, + update_min_steps=update_min_steps, + ) + + +def clear() -> None: + """Clears the terminal screen. This will have the effect of clearing + the whole visible space of the terminal and moving the cursor to the + top left. This does not do anything if not connected to a terminal. + + .. versionadded:: 2.0 + """ + if not isatty(sys.stdout): + return + + # ANSI escape \033[2J clears the screen, \033[1;1H moves the cursor + echo("\033[2J\033[1;1H", nl=False) + + +def _interpret_color(color: int | tuple[int, int, int] | str, offset: int = 0) -> str: + if isinstance(color, int): + return f"{38 + offset};5;{color:d}" + + if isinstance(color, (tuple, list)): + r, g, b = color + return f"{38 + offset};2;{r:d};{g:d};{b:d}" + + return str(_ansi_colors[color] + offset) + + +def style( + text: t.Any, + fg: int | tuple[int, int, int] | str | None = None, + bg: int | tuple[int, int, int] | str | None = None, + bold: bool | None = None, + dim: bool | None = None, + underline: bool | None = None, + overline: bool | None = None, + italic: bool | None = None, + blink: bool | None = None, + reverse: bool | None = None, + strikethrough: bool | None = None, + reset: bool = True, +) -> str: + """Styles a text with ANSI styles and returns the new string. By + default the styling is self contained which means that at the end + of the string a reset code is issued. This can be prevented by + passing ``reset=False``. + + Examples:: + + click.echo(click.style('Hello World!', fg='green')) + click.echo(click.style('ATTENTION!', blink=True)) + click.echo(click.style('Some things', reverse=True, fg='cyan')) + click.echo(click.style('More colors', fg=(255, 12, 128), bg=117)) + + Supported color names: + + * ``black`` (might be a gray) + * ``red`` + * ``green`` + * ``yellow`` (might be an orange) + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` + * ``reset`` (reset the color code only) + + If the terminal supports it, color may also be specified as: + + - An integer in the interval [0, 255]. The terminal must support + 8-bit/256-color mode. + - An RGB tuple of three integers in [0, 255]. The terminal must + support 24-bit/true-color mode. + + See https://en.wikipedia.org/wiki/ANSI_color and + https://gist.github.com/XVilka/8346728 for more information. + + :param text: the string to style with ansi codes. + :param fg: if provided this will become the foreground color. + :param bg: if provided this will become the background color. + :param bold: if provided this will enable or disable bold mode. + :param dim: if provided this will enable or disable dim mode. This is + badly supported. + :param underline: if provided this will enable or disable underline. + :param overline: if provided this will enable or disable overline. + :param italic: if provided this will enable or disable italic. + :param blink: if provided this will enable or disable blinking. + :param reverse: if provided this will enable or disable inverse + rendering (foreground becomes background and the + other way round). + :param strikethrough: if provided this will enable or disable + striking through text. + :param reset: by default a reset-all code is added at the end of the + string which means that styles do not carry over. This + can be disabled to compose styles. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. + + .. versionchanged:: 8.0 + Added support for 256 and RGB color codes. + + .. versionchanged:: 8.0 + Added the ``strikethrough``, ``italic``, and ``overline`` + parameters. + + .. versionchanged:: 7.0 + Added support for bright colors. + + .. versionadded:: 2.0 + """ + if not isinstance(text, str): + text = str(text) + + bits = [] + + if fg: + try: + bits.append(f"\033[{_interpret_color(fg)}m") + except KeyError: + raise TypeError(f"Unknown color {fg!r}") from None + + if bg: + try: + bits.append(f"\033[{_interpret_color(bg, 10)}m") + except KeyError: + raise TypeError(f"Unknown color {bg!r}") from None + + if bold is not None: + bits.append(f"\033[{1 if bold else 22}m") + if dim is not None: + bits.append(f"\033[{2 if dim else 22}m") + if underline is not None: + bits.append(f"\033[{4 if underline else 24}m") + if overline is not None: + bits.append(f"\033[{53 if overline else 55}m") + if italic is not None: + bits.append(f"\033[{3 if italic else 23}m") + if blink is not None: + bits.append(f"\033[{5 if blink else 25}m") + if reverse is not None: + bits.append(f"\033[{7 if reverse else 27}m") + if strikethrough is not None: + bits.append(f"\033[{9 if strikethrough else 29}m") + bits.append(text) + if reset: + bits.append(_ansi_reset_all) + return "".join(bits) + + +def unstyle(text: str) -> str: + """Removes ANSI styling information from a string. Usually it's not + necessary to use this function as Click's echo function will + automatically remove styling if necessary. + + .. versionadded:: 2.0 + + :param text: the text to remove style information from. + """ + return strip_ansi(text) + + +def secho( + message: t.Any | None = None, + file: t.IO[t.AnyStr] | None = None, + nl: bool = True, + err: bool = False, + color: bool | None = None, + **styles: t.Any, +) -> None: + """This function combines :func:`echo` and :func:`style` into one + call. As such the following two calls are the same:: + + click.secho('Hello World!', fg='green') + click.echo(click.style('Hello World!', fg='green')) + + All keyword arguments are forwarded to the underlying functions + depending on which one they go with. + + Non-string types will be converted to :class:`str`. However, + :class:`bytes` are passed directly to :meth:`echo` without applying + style. If you want to style bytes that represent text, call + :meth:`bytes.decode` first. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. Bytes are + passed through without style applied. + + .. versionadded:: 2.0 + """ + if message is not None and not isinstance(message, (bytes, bytearray)): + message = style(message, **styles) + + return echo(message, file=file, nl=nl, err=err, color=color) + + +@t.overload +def edit( + text: bytes | bytearray, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = False, + extension: str = ".txt", +) -> bytes | None: ... + + +@t.overload +def edit( + text: str, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", +) -> str | None: ... + + +@t.overload +def edit( + text: None = None, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", + filename: str | cabc.Iterable[str] | None = None, +) -> None: ... + + +def edit( + text: str | bytes | bytearray | None = None, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", + filename: str | cabc.Iterable[str] | None = None, +) -> str | bytes | bytearray | None: + r"""Edits the given text in the defined editor. If an editor is given + (should be the full path to the executable but the regular operating + system search path is used for finding the executable) it overrides + the detected editor. Optionally, some environment variables can be + used. If the editor is closed without changes, `None` is returned. In + case a file is edited directly the return value is always `None` and + `require_save` and `extension` are ignored. + + If the editor cannot be opened a :exc:`UsageError` is raised. + + Note for Windows: to simplify cross-platform usage, the newlines are + automatically converted from POSIX to Windows and vice versa. As such, + the message here will have ``\n`` as newline markers. + + :param text: the text to edit. + :param editor: optionally the editor to use. Defaults to automatic + detection. + :param env: environment variables to forward to the editor. + :param require_save: if this is true, then not saving in the editor + will make the return value become `None`. + :param extension: the extension to tell the editor about. This defaults + to `.txt` but changing this might change syntax + highlighting. + :param filename: if provided it will edit this file instead of the + provided text contents. It will not use a temporary + file as an indirection in that case. If the editor supports + editing multiple files at once, a sequence of files may be + passed as well. Invoke `click.file` once per file instead + if multiple files cannot be managed at once or editing the + files serially is desired. + + .. versionchanged:: 8.2.0 + ``filename`` now accepts any ``Iterable[str]`` in addition to a ``str`` + if the ``editor`` supports editing multiple files at once. + + """ + from ._termui_impl import Editor + + ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension) + + if filename is None: + return ed.edit(text) + + if isinstance(filename, str): + filename = (filename,) + + ed.edit_files(filenames=filename) + return None + + +def launch(url: str, wait: bool = False, locate: bool = False) -> int: + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + Examples:: + + click.launch('https://click.palletsprojects.com/') + click.launch('/my/downloaded/file', locate=True) + + .. versionadded:: 2.0 + + :param url: URL or filename of the thing to launch. + :param wait: Wait for the program to exit before returning. This + only works if the launched program blocks. In particular, + ``xdg-open`` on Linux does not block. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + from ._termui_impl import open_url + + return open_url(url, wait=wait, locate=locate) + + +# If this is provided, getchar() calls into this instead. This is used +# for unittesting purposes. +_getchar: t.Callable[[bool], str] | None = None + + +def getchar(echo: bool = False) -> str: + """Fetches a single character from the terminal and returns it. This + will always return a unicode character and under certain rare + circumstances this might return more than one character. The + situations which more than one character is returned is when for + whatever reason multiple characters end up in the terminal buffer or + standard input was not actually a terminal. + + Note that this will always read from the terminal, even if something + is piped into the standard input. + + Note for Windows: in rare cases when typing non-ASCII characters, this + function might wait for a second character and then return both at once. + This is because certain Unicode characters look like special-key markers. + + .. versionadded:: 2.0 + + :param echo: if set to `True`, the character read will also show up on + the terminal. The default is to not show it. + """ + global _getchar + + if _getchar is None: + from ._termui_impl import getchar as f + + _getchar = f + + return _getchar(echo) + + +def raw_terminal() -> AbstractContextManager[int]: + from ._termui_impl import raw_terminal as f + + return f() + + +def pause(info: str | None = None, err: bool = False) -> None: + """This command stops execution and waits for the user to press any + key to continue. This is similar to the Windows batch "pause" + command. If the program is not run through a terminal, this command + will instead do nothing. + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param info: The message to print before pausing. Defaults to + ``"Press any key to continue..."``. + :param err: if set to message goes to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + if not isatty(sys.stdin) or not isatty(sys.stdout): + return + + if info is None: + info = _("Press any key to continue...") + + try: + if info: + echo(info, nl=False, err=err) + try: + getchar() + except (KeyboardInterrupt, EOFError): + pass + finally: + if info: + echo(err=err) diff --git a/venv/lib/python3.10/site-packages/click/testing.py b/venv/lib/python3.10/site-packages/click/testing.py new file mode 100644 index 0000000..f6f60b8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/testing.py @@ -0,0 +1,577 @@ +from __future__ import annotations + +import collections.abc as cabc +import contextlib +import io +import os +import shlex +import sys +import tempfile +import typing as t +from types import TracebackType + +from . import _compat +from . import formatting +from . import termui +from . import utils +from ._compat import _find_binary_reader + +if t.TYPE_CHECKING: + from _typeshed import ReadableBuffer + + from .core import Command + + +class EchoingStdin: + def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None: + self._input = input + self._output = output + self._paused = False + + def __getattr__(self, x: str) -> t.Any: + return getattr(self._input, x) + + def _echo(self, rv: bytes) -> bytes: + if not self._paused: + self._output.write(rv) + + return rv + + def read(self, n: int = -1) -> bytes: + return self._echo(self._input.read(n)) + + def read1(self, n: int = -1) -> bytes: + return self._echo(self._input.read1(n)) # type: ignore + + def readline(self, n: int = -1) -> bytes: + return self._echo(self._input.readline(n)) + + def readlines(self) -> list[bytes]: + return [self._echo(x) for x in self._input.readlines()] + + def __iter__(self) -> cabc.Iterator[bytes]: + return iter(self._echo(x) for x in self._input) + + def __repr__(self) -> str: + return repr(self._input) + + +@contextlib.contextmanager +def _pause_echo(stream: EchoingStdin | None) -> cabc.Iterator[None]: + if stream is None: + yield + else: + stream._paused = True + yield + stream._paused = False + + +class BytesIOCopy(io.BytesIO): + """Patch ``io.BytesIO`` to let the written stream be copied to another. + + .. versionadded:: 8.2 + """ + + def __init__(self, copy_to: io.BytesIO) -> None: + super().__init__() + self.copy_to = copy_to + + def flush(self) -> None: + super().flush() + self.copy_to.flush() + + def write(self, b: ReadableBuffer) -> int: + self.copy_to.write(b) + return super().write(b) + + +class StreamMixer: + """Mixes `` and `` streams. + + The result is available in the ``output`` attribute. + + .. versionadded:: 8.2 + """ + + def __init__(self) -> None: + self.output: io.BytesIO = io.BytesIO() + self.stdout: io.BytesIO = BytesIOCopy(copy_to=self.output) + self.stderr: io.BytesIO = BytesIOCopy(copy_to=self.output) + + def __del__(self) -> None: + """ + Guarantee that embedded file-like objects are closed in a + predictable order, protecting against races between + self.output being closed and other streams being flushed on close + + .. versionadded:: 8.2.2 + """ + self.stderr.close() + self.stdout.close() + self.output.close() + + +class _NamedTextIOWrapper(io.TextIOWrapper): + def __init__( + self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any + ) -> None: + super().__init__(buffer, **kwargs) + self._name = name + self._mode = mode + + @property + def name(self) -> str: + return self._name + + @property + def mode(self) -> str: + return self._mode + + +def make_input_stream( + input: str | bytes | t.IO[t.Any] | None, charset: str +) -> t.BinaryIO: + # Is already an input stream. + if hasattr(input, "read"): + rv = _find_binary_reader(t.cast("t.IO[t.Any]", input)) + + if rv is not None: + return rv + + raise TypeError("Could not find binary reader for input stream.") + + if input is None: + input = b"" + elif isinstance(input, str): + input = input.encode(charset) + + return io.BytesIO(input) + + +class Result: + """Holds the captured result of an invoked CLI script. + + :param runner: The runner that created the result + :param stdout_bytes: The standard output as bytes. + :param stderr_bytes: The standard error as bytes. + :param output_bytes: A mix of ``stdout_bytes`` and ``stderr_bytes``, as the + user would see it in its terminal. + :param return_value: The value returned from the invoked command. + :param exit_code: The exit code as integer. + :param exception: The exception that happened if one did. + :param exc_info: Exception information (exception type, exception instance, + traceback type). + + .. versionchanged:: 8.2 + ``stderr_bytes`` no longer optional, ``output_bytes`` introduced and + ``mix_stderr`` has been removed. + + .. versionadded:: 8.0 + Added ``return_value``. + """ + + def __init__( + self, + runner: CliRunner, + stdout_bytes: bytes, + stderr_bytes: bytes, + output_bytes: bytes, + return_value: t.Any, + exit_code: int, + exception: BaseException | None, + exc_info: tuple[type[BaseException], BaseException, TracebackType] + | None = None, + ): + self.runner = runner + self.stdout_bytes = stdout_bytes + self.stderr_bytes = stderr_bytes + self.output_bytes = output_bytes + self.return_value = return_value + self.exit_code = exit_code + self.exception = exception + self.exc_info = exc_info + + @property + def output(self) -> str: + """The terminal output as unicode string, as the user would see it. + + .. versionchanged:: 8.2 + No longer a proxy for ``self.stdout``. Now has its own independent stream + that is mixing `` and ``, in the order they were written. + """ + return self.output_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stdout(self) -> str: + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stderr(self) -> str: + """The standard error as unicode string. + + .. versionchanged:: 8.2 + No longer raise an exception, always returns the `` string. + """ + return self.stderr_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + def __repr__(self) -> str: + exc_str = repr(self.exception) if self.exception else "okay" + return f"<{type(self).__name__} {exc_str}>" + + +class CliRunner: + """The CLI runner provides functionality to invoke a Click command line + script for unittesting purposes in a isolated environment. This only + works in single-threaded systems without any concurrency as it changes the + global interpreter state. + + :param charset: the character set for the input and output data. + :param env: a dictionary with environment variables for overriding. + :param echo_stdin: if this is set to `True`, then reading from `` writes + to ``. This is useful for showing examples in + some circumstances. Note that regular prompts + will automatically echo the input. + :param catch_exceptions: Whether to catch any exceptions other than + ``SystemExit`` when running :meth:`~CliRunner.invoke`. + + .. versionchanged:: 8.2 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 8.2 + ``mix_stderr`` parameter has been removed. + """ + + def __init__( + self, + charset: str = "utf-8", + env: cabc.Mapping[str, str | None] | None = None, + echo_stdin: bool = False, + catch_exceptions: bool = True, + ) -> None: + self.charset = charset + self.env: cabc.Mapping[str, str | None] = env or {} + self.echo_stdin = echo_stdin + self.catch_exceptions = catch_exceptions + + def get_default_prog_name(self, cli: Command) -> str: + """Given a command object it will return the default program name + for it. The default is the `name` attribute or ``"root"`` if not + set. + """ + return cli.name or "root" + + def make_env( + self, overrides: cabc.Mapping[str, str | None] | None = None + ) -> cabc.Mapping[str, str | None]: + """Returns the environment overrides for invoking a script.""" + rv = dict(self.env) + if overrides: + rv.update(overrides) + return rv + + @contextlib.contextmanager + def isolation( + self, + input: str | bytes | t.IO[t.Any] | None = None, + env: cabc.Mapping[str, str | None] | None = None, + color: bool = False, + ) -> cabc.Iterator[tuple[io.BytesIO, io.BytesIO, io.BytesIO]]: + """A context manager that sets up the isolation for invoking of a + command line tool. This sets up `` with the given input data + and `os.environ` with the overrides from the given dictionary. + This also rebinds some internals in Click to be mocked (like the + prompt functionality). + + This is automatically done in the :meth:`invoke` method. + + :param input: the input stream to put into `sys.stdin`. + :param env: the environment overrides as dictionary. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionadded:: 8.2 + An additional output stream is returned, which is a mix of + `` and `` streams. + + .. versionchanged:: 8.2 + Always returns the `` stream. + + .. versionchanged:: 8.0 + `` is opened with ``errors="backslashreplace"`` + instead of the default ``"strict"``. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + """ + bytes_input = make_input_stream(input, self.charset) + echo_input = None + + old_stdin = sys.stdin + old_stdout = sys.stdout + old_stderr = sys.stderr + old_forced_width = formatting.FORCED_WIDTH + formatting.FORCED_WIDTH = 80 + + env = self.make_env(env) + + stream_mixer = StreamMixer() + + if self.echo_stdin: + bytes_input = echo_input = t.cast( + t.BinaryIO, EchoingStdin(bytes_input, stream_mixer.stdout) + ) + + sys.stdin = text_input = _NamedTextIOWrapper( + bytes_input, encoding=self.charset, name="", mode="r" + ) + + if self.echo_stdin: + # Force unbuffered reads, otherwise TextIOWrapper reads a + # large chunk which is echoed early. + text_input._CHUNK_SIZE = 1 # type: ignore + + sys.stdout = _NamedTextIOWrapper( + stream_mixer.stdout, encoding=self.charset, name="", mode="w" + ) + + sys.stderr = _NamedTextIOWrapper( + stream_mixer.stderr, + encoding=self.charset, + name="", + mode="w", + errors="backslashreplace", + ) + + @_pause_echo(echo_input) # type: ignore + def visible_input(prompt: str | None = None) -> str: + sys.stdout.write(prompt or "") + try: + val = next(text_input).rstrip("\r\n") + except StopIteration as e: + raise EOFError() from e + sys.stdout.write(f"{val}\n") + sys.stdout.flush() + return val + + @_pause_echo(echo_input) # type: ignore + def hidden_input(prompt: str | None = None) -> str: + sys.stdout.write(f"{prompt or ''}\n") + sys.stdout.flush() + try: + return next(text_input).rstrip("\r\n") + except StopIteration as e: + raise EOFError() from e + + @_pause_echo(echo_input) # type: ignore + def _getchar(echo: bool) -> str: + char = sys.stdin.read(1) + + if echo: + sys.stdout.write(char) + + sys.stdout.flush() + return char + + default_color = color + + def should_strip_ansi( + stream: t.IO[t.Any] | None = None, color: bool | None = None + ) -> bool: + if color is None: + return not default_color + return not color + + old_visible_prompt_func = termui.visible_prompt_func + old_hidden_prompt_func = termui.hidden_prompt_func + old__getchar_func = termui._getchar + old_should_strip_ansi = utils.should_strip_ansi # type: ignore + old__compat_should_strip_ansi = _compat.should_strip_ansi + termui.visible_prompt_func = visible_input + termui.hidden_prompt_func = hidden_input + termui._getchar = _getchar + utils.should_strip_ansi = should_strip_ansi # type: ignore + _compat.should_strip_ansi = should_strip_ansi + + old_env = {} + try: + for key, value in env.items(): + old_env[key] = os.environ.get(key) + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + yield (stream_mixer.stdout, stream_mixer.stderr, stream_mixer.output) + finally: + for key, value in old_env.items(): + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + sys.stdout = old_stdout + sys.stderr = old_stderr + sys.stdin = old_stdin + termui.visible_prompt_func = old_visible_prompt_func + termui.hidden_prompt_func = old_hidden_prompt_func + termui._getchar = old__getchar_func + utils.should_strip_ansi = old_should_strip_ansi # type: ignore + _compat.should_strip_ansi = old__compat_should_strip_ansi + formatting.FORCED_WIDTH = old_forced_width + + def invoke( + self, + cli: Command, + args: str | cabc.Sequence[str] | None = None, + input: str | bytes | t.IO[t.Any] | None = None, + env: cabc.Mapping[str, str | None] | None = None, + catch_exceptions: bool | None = None, + color: bool = False, + **extra: t.Any, + ) -> Result: + """Invokes a command in an isolated environment. The arguments are + forwarded directly to the command line script, the `extra` keyword + arguments are passed to the :meth:`~clickpkg.Command.main` function of + the command. + + This returns a :class:`Result` object. + + :param cli: the command to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. + :param input: the input data for `sys.stdin`. + :param env: the environment overrides. + :param catch_exceptions: Whether to catch any other exceptions than + ``SystemExit``. If :data:`None`, the value + from :class:`CliRunner` is used. + :param extra: the keyword arguments to pass to :meth:`main`. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionadded:: 8.2 + The result object has the ``output_bytes`` attribute with + the mix of ``stdout_bytes`` and ``stderr_bytes``, as the user would + see it in its terminal. + + .. versionchanged:: 8.2 + The result object always returns the ``stderr_bytes`` stream. + + .. versionchanged:: 8.0 + The result object has the ``return_value`` attribute with + the value returned from the invoked command. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionchanged:: 3.0 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 3.0 + The result object has the ``exc_info`` attribute with the + traceback if available. + """ + exc_info = None + if catch_exceptions is None: + catch_exceptions = self.catch_exceptions + + with self.isolation(input=input, env=env, color=color) as outstreams: + return_value = None + exception: BaseException | None = None + exit_code = 0 + + if isinstance(args, str): + args = shlex.split(args) + + try: + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + return_value = cli.main(args=args or (), prog_name=prog_name, **extra) + except SystemExit as e: + exc_info = sys.exc_info() + e_code = t.cast("int | t.Any | None", e.code) + + if e_code is None: + e_code = 0 + + if e_code != 0: + exception = e + + if not isinstance(e_code, int): + sys.stdout.write(str(e_code)) + sys.stdout.write("\n") + e_code = 1 + + exit_code = e_code + + except Exception as e: + if not catch_exceptions: + raise + exception = e + exit_code = 1 + exc_info = sys.exc_info() + finally: + sys.stdout.flush() + sys.stderr.flush() + stdout = outstreams[0].getvalue() + stderr = outstreams[1].getvalue() + output = outstreams[2].getvalue() + + return Result( + runner=self, + stdout_bytes=stdout, + stderr_bytes=stderr, + output_bytes=output, + return_value=return_value, + exit_code=exit_code, + exception=exception, + exc_info=exc_info, # type: ignore + ) + + @contextlib.contextmanager + def isolated_filesystem( + self, temp_dir: str | os.PathLike[str] | None = None + ) -> cabc.Iterator[str]: + """A context manager that creates a temporary directory and + changes the current working directory to it. This isolates tests + that affect the contents of the CWD to prevent them from + interfering with each other. + + :param temp_dir: Create the temporary directory under this + directory. If given, the created directory is not removed + when exiting. + + .. versionchanged:: 8.0 + Added the ``temp_dir`` parameter. + """ + cwd = os.getcwd() + dt = tempfile.mkdtemp(dir=temp_dir) + os.chdir(dt) + + try: + yield dt + finally: + os.chdir(cwd) + + if temp_dir is None: + import shutil + + try: + shutil.rmtree(dt) + except OSError: + pass diff --git a/venv/lib/python3.10/site-packages/click/types.py b/venv/lib/python3.10/site-packages/click/types.py new file mode 100644 index 0000000..e71c1c2 --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/types.py @@ -0,0 +1,1209 @@ +from __future__ import annotations + +import collections.abc as cabc +import enum +import os +import stat +import sys +import typing as t +from datetime import datetime +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import _get_argv_encoding +from ._compat import open_stream +from .exceptions import BadParameter +from .utils import format_filename +from .utils import LazyFile +from .utils import safecall + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .core import Context + from .core import Parameter + from .shell_completion import CompletionItem + +ParamTypeValue = t.TypeVar("ParamTypeValue") + + +class ParamType: + """Represents the type of a parameter. Validates and converts values + from the command line or Python into the correct type. + + To implement a custom type, subclass and implement at least the + following: + + - The :attr:`name` class attribute must be set. + - Calling an instance of the type with ``None`` must return + ``None``. This is already implemented by default. + - :meth:`convert` must convert string values to the correct type. + - :meth:`convert` must accept values that are already the correct + type. + - It must be able to convert a value if the ``ctx`` and ``param`` + arguments are ``None``. This can occur when converting prompt + input. + """ + + is_composite: t.ClassVar[bool] = False + arity: t.ClassVar[int] = 1 + + #: the descriptive name of this type + name: str + + #: if a list of this type is expected and the value is pulled from a + #: string environment variable, this is what splits it up. `None` + #: means any whitespace. For all parameters the general rule is that + #: whitespace splits them up. The exception are paths and files which + #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on + #: Windows). + envvar_list_splitter: t.ClassVar[str | None] = None + + def to_info_dict(self) -> dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionadded:: 8.0 + """ + # The class name without the "ParamType" suffix. + param_type = type(self).__name__.partition("ParamType")[0] + param_type = param_type.partition("ParameterType")[0] + + # Custom subclasses might not remember to set a name. + if hasattr(self, "name"): + name = self.name + else: + name = param_type + + return {"param_type": param_type, "name": name} + + def __call__( + self, + value: t.Any, + param: Parameter | None = None, + ctx: Context | None = None, + ) -> t.Any: + if value is not None: + return self.convert(value, param, ctx) + + def get_metavar(self, param: Parameter, ctx: Context) -> str | None: + """Returns the metavar default for this param if it provides one.""" + + def get_missing_message(self, param: Parameter, ctx: Context | None) -> str | None: + """Optionally might return extra information about a missing + parameter. + + .. versionadded:: 2.0 + """ + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + """Convert the value to the correct type. This is not called if + the value is ``None`` (the missing value). + + This must accept string values from the command line, as well as + values that are already the correct type. It may also convert + other compatible types. + + The ``param`` and ``ctx`` arguments may be ``None`` in certain + situations, such as when converting prompt input. + + If the value cannot be converted, call :meth:`fail` with a + descriptive message. + + :param value: The value to convert. + :param param: The parameter that is using this type to convert + its value. May be ``None``. + :param ctx: The current context that arrived at this value. May + be ``None``. + """ + return value + + def split_envvar_value(self, rv: str) -> cabc.Sequence[str]: + """Given a value from an environment variable this splits it up + into small chunks depending on the defined envvar list splitter. + + If the splitter is set to `None`, which means that whitespace splits, + then leading and trailing whitespace is ignored. Otherwise, leading + and trailing splitters usually lead to empty items being included. + """ + return (rv or "").split(self.envvar_list_splitter) + + def fail( + self, + message: str, + param: Parameter | None = None, + ctx: Context | None = None, + ) -> t.NoReturn: + """Helper method to fail with an invalid value message.""" + raise BadParameter(message, ctx=ctx, param=param) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Return a list of + :class:`~click.shell_completion.CompletionItem` objects for the + incomplete value. Most types do not provide completions, but + some do, and this allows custom types to provide custom + completions as well. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + return [] + + +class CompositeParamType(ParamType): + is_composite = True + + @property + def arity(self) -> int: # type: ignore + raise NotImplementedError() + + +class FuncParamType(ParamType): + def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None: + self.name: str = func.__name__ + self.func = func + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["func"] = self.func + return info_dict + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + try: + return self.func(value) + except ValueError: + try: + value = str(value) + except UnicodeError: + value = value.decode("utf-8", "replace") + + self.fail(value, param, ctx) + + +class UnprocessedParamType(ParamType): + name = "text" + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + return value + + def __repr__(self) -> str: + return "UNPROCESSED" + + +class StringParamType(ParamType): + name = "text" + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + if isinstance(value, bytes): + enc = _get_argv_encoding() + try: + value = value.decode(enc) + except UnicodeError: + fs_enc = sys.getfilesystemencoding() + if fs_enc != enc: + try: + value = value.decode(fs_enc) + except UnicodeError: + value = value.decode("utf-8", "replace") + else: + value = value.decode("utf-8", "replace") + return value + return str(value) + + def __repr__(self) -> str: + return "STRING" + + +class Choice(ParamType, t.Generic[ParamTypeValue]): + """The choice type allows a value to be checked against a fixed set + of supported values. + + You may pass any iterable value which will be converted to a tuple + and thus will only be iterated once. + + The resulting value will always be one of the originally passed choices. + See :meth:`normalize_choice` for more info on the mapping of strings + to choices. See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. + + .. versionchanged:: 8.2.0 + Non-``str`` ``choices`` are now supported. It can additionally be any + iterable. Before you were not recommended to pass anything but a list or + tuple. + + .. versionadded:: 8.2.0 + Choice normalization can be overridden via :meth:`normalize_choice`. + """ + + name = "choice" + + def __init__( + self, choices: cabc.Iterable[ParamTypeValue], case_sensitive: bool = True + ) -> None: + self.choices: cabc.Sequence[ParamTypeValue] = tuple(choices) + self.case_sensitive = case_sensitive + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["choices"] = self.choices + info_dict["case_sensitive"] = self.case_sensitive + return info_dict + + def _normalized_mapping( + self, ctx: Context | None = None + ) -> cabc.Mapping[ParamTypeValue, str]: + """ + Returns mapping where keys are the original choices and the values are + the normalized values that are accepted via the command line. + + This is a simple wrapper around :meth:`normalize_choice`, use that + instead which is supported. + """ + return { + choice: self.normalize_choice( + choice=choice, + ctx=ctx, + ) + for choice in self.choices + } + + def normalize_choice(self, choice: ParamTypeValue, ctx: Context | None) -> str: + """ + Normalize a choice value, used to map a passed string to a choice. + Each choice must have a unique normalized value. + + By default uses :meth:`Context.token_normalize_func` and if not case + sensitive, convert it to a casefolded value. + + .. versionadded:: 8.2.0 + """ + normed_value = choice.name if isinstance(choice, enum.Enum) else str(choice) + + if ctx is not None and ctx.token_normalize_func is not None: + normed_value = ctx.token_normalize_func(normed_value) + + if not self.case_sensitive: + normed_value = normed_value.casefold() + + return normed_value + + def get_metavar(self, param: Parameter, ctx: Context) -> str | None: + if param.param_type_name == "option" and not param.show_choices: # type: ignore + choice_metavars = [ + convert_type(type(choice)).name.upper() for choice in self.choices + ] + choices_str = "|".join([*dict.fromkeys(choice_metavars)]) + else: + choices_str = "|".join( + [str(i) for i in self._normalized_mapping(ctx=ctx).values()] + ) + + # Use curly braces to indicate a required argument. + if param.required and param.param_type_name == "argument": + return f"{{{choices_str}}}" + + # Use square braces to indicate an option or optional argument. + return f"[{choices_str}]" + + def get_missing_message(self, param: Parameter, ctx: Context | None) -> str: + """ + Message shown when no choice is passed. + + .. versionchanged:: 8.2.0 Added ``ctx`` argument. + """ + return _("Choose from:\n\t{choices}").format( + choices=",\n\t".join(self._normalized_mapping(ctx=ctx).values()) + ) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> ParamTypeValue: + """ + For a given value from the parser, normalize it and find its + matching normalized value in the list of choices. Then return the + matched "original" choice. + """ + normed_value = self.normalize_choice(choice=value, ctx=ctx) + normalized_mapping = self._normalized_mapping(ctx=ctx) + + try: + return next( + original + for original, normalized in normalized_mapping.items() + if normalized == normed_value + ) + except StopIteration: + self.fail( + self.get_invalid_choice_message(value=value, ctx=ctx), + param=param, + ctx=ctx, + ) + + def get_invalid_choice_message(self, value: t.Any, ctx: Context | None) -> str: + """Get the error message when the given choice is invalid. + + :param value: The invalid value. + + .. versionadded:: 8.2 + """ + choices_str = ", ".join(map(repr, self._normalized_mapping(ctx=ctx).values())) + return ngettext( + "{value!r} is not {choice}.", + "{value!r} is not one of {choices}.", + len(self.choices), + ).format(value=value, choice=choices_str, choices=choices_str) + + def __repr__(self) -> str: + return f"Choice({list(self.choices)})" + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Complete choices that start with the incomplete value. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + str_choices = map(str, self.choices) + + if self.case_sensitive: + matched = (c for c in str_choices if c.startswith(incomplete)) + else: + incomplete = incomplete.lower() + matched = (c for c in str_choices if c.lower().startswith(incomplete)) + + return [CompletionItem(c) for c in matched] + + +class DateTime(ParamType): + """The DateTime type converts date strings into `datetime` objects. + + The format strings which are checked are configurable, but default to some + common (non-timezone aware) ISO 8601 formats. + + When specifying *DateTime* formats, you should only pass a list or a tuple. + Other iterables, like generators, may lead to surprising results. + + The format strings are processed using ``datetime.strptime``, and this + consequently defines the format strings which are allowed. + + Parsing is tried using each format, in order, and the first format which + parses successfully is used. + + :param formats: A list or tuple of date format strings, in the order in + which they should be tried. Defaults to + ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, + ``'%Y-%m-%d %H:%M:%S'``. + """ + + name = "datetime" + + def __init__(self, formats: cabc.Sequence[str] | None = None): + self.formats: cabc.Sequence[str] = formats or [ + "%Y-%m-%d", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", + ] + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["formats"] = self.formats + return info_dict + + def get_metavar(self, param: Parameter, ctx: Context) -> str | None: + return f"[{'|'.join(self.formats)}]" + + def _try_to_convert_date(self, value: t.Any, format: str) -> datetime | None: + try: + return datetime.strptime(value, format) + except ValueError: + return None + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + if isinstance(value, datetime): + return value + + for format in self.formats: + converted = self._try_to_convert_date(value, format) + + if converted is not None: + return converted + + formats_str = ", ".join(map(repr, self.formats)) + self.fail( + ngettext( + "{value!r} does not match the format {format}.", + "{value!r} does not match the formats {formats}.", + len(self.formats), + ).format(value=value, format=formats_str, formats=formats_str), + param, + ctx, + ) + + def __repr__(self) -> str: + return "DateTime" + + +class _NumberParamTypeBase(ParamType): + _number_class: t.ClassVar[type[t.Any]] + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + try: + return self._number_class(value) + except ValueError: + self.fail( + _("{value!r} is not a valid {number_type}.").format( + value=value, number_type=self.name + ), + param, + ctx, + ) + + +class _NumberRangeBase(_NumberParamTypeBase): + def __init__( + self, + min: float | None = None, + max: float | None = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + self.min = min + self.max = max + self.min_open = min_open + self.max_open = max_open + self.clamp = clamp + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + min=self.min, + max=self.max, + min_open=self.min_open, + max_open=self.max_open, + clamp=self.clamp, + ) + return info_dict + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + import operator + + rv = super().convert(value, param, ctx) + lt_min: bool = self.min is not None and ( + operator.le if self.min_open else operator.lt + )(rv, self.min) + gt_max: bool = self.max is not None and ( + operator.ge if self.max_open else operator.gt + )(rv, self.max) + + if self.clamp: + if lt_min: + return self._clamp(self.min, 1, self.min_open) # type: ignore + + if gt_max: + return self._clamp(self.max, -1, self.max_open) # type: ignore + + if lt_min or gt_max: + self.fail( + _("{value} is not in the range {range}.").format( + value=rv, range=self._describe_range() + ), + param, + ctx, + ) + + return rv + + def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float: + """Find the valid value to clamp to bound in the given + direction. + + :param bound: The boundary value. + :param dir: 1 or -1 indicating the direction to move. + :param open: If true, the range does not include the bound. + """ + raise NotImplementedError + + def _describe_range(self) -> str: + """Describe the range for use in help text.""" + if self.min is None: + op = "<" if self.max_open else "<=" + return f"x{op}{self.max}" + + if self.max is None: + op = ">" if self.min_open else ">=" + return f"x{op}{self.min}" + + lop = "<" if self.min_open else "<=" + rop = "<" if self.max_open else "<=" + return f"{self.min}{lop}x{rop}{self.max}" + + def __repr__(self) -> str: + clamp = " clamped" if self.clamp else "" + return f"<{type(self).__name__} {self._describe_range()}{clamp}>" + + +class IntParamType(_NumberParamTypeBase): + name = "integer" + _number_class = int + + def __repr__(self) -> str: + return "INT" + + +class IntRange(_NumberRangeBase, IntParamType): + """Restrict an :data:`click.INT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "integer range" + + def _clamp( # type: ignore + self, bound: int, dir: t.Literal[1, -1], open: bool + ) -> int: + if not open: + return bound + + return bound + dir + + +class FloatParamType(_NumberParamTypeBase): + name = "float" + _number_class = float + + def __repr__(self) -> str: + return "FLOAT" + + +class FloatRange(_NumberRangeBase, FloatParamType): + """Restrict a :data:`click.FLOAT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. This is not supported if either + boundary is marked ``open``. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "float range" + + def __init__( + self, + min: float | None = None, + max: float | None = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + super().__init__( + min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp + ) + + if (min_open or max_open) and clamp: + raise TypeError("Clamping is not supported for open bounds.") + + def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float: + if not open: + return bound + + # Could use math.nextafter here, but clamping an + # open float range doesn't seem to be particularly useful. It's + # left up to the user to write a callback to do it if needed. + raise RuntimeError("Clamping is not supported for open bounds.") + + +class BoolParamType(ParamType): + name = "boolean" + + bool_states: dict[str, bool] = { + "1": True, + "0": False, + "yes": True, + "no": False, + "true": True, + "false": False, + "on": True, + "off": False, + "t": True, + "f": False, + "y": True, + "n": False, + # Absence of value is considered False. + "": False, + } + """A mapping of string values to boolean states. + + Mapping is inspired by :py:attr:`configparser.ConfigParser.BOOLEAN_STATES` + and extends it. + + .. caution:: + String values are lower-cased, as the ``str_to_bool`` comparison function + below is case-insensitive. + + .. warning:: + The mapping is not exhaustive, and does not cover all possible boolean strings + representations. It will remains as it is to avoid endless bikeshedding. + + Future work my be considered to make this mapping user-configurable from public + API. + """ + + @staticmethod + def str_to_bool(value: str | bool) -> bool | None: + """Convert a string to a boolean value. + + If the value is already a boolean, it is returned as-is. If the value is a + string, it is stripped of whitespaces and lower-cased, then checked against + the known boolean states pre-defined in the `BoolParamType.bool_states` mapping + above. + + Returns `None` if the value does not match any known boolean state. + """ + if isinstance(value, bool): + return value + return BoolParamType.bool_states.get(value.strip().lower()) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> bool: + normalized = self.str_to_bool(value) + if normalized is None: + self.fail( + _( + "{value!r} is not a valid boolean. Recognized values: {states}" + ).format(value=value, states=", ".join(sorted(self.bool_states))), + param, + ctx, + ) + return normalized + + def __repr__(self) -> str: + return "BOOL" + + +class UUIDParameterType(ParamType): + name = "uuid" + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + import uuid + + if isinstance(value, uuid.UUID): + return value + + value = value.strip() + + try: + return uuid.UUID(value) + except ValueError: + self.fail( + _("{value!r} is not a valid UUID.").format(value=value), param, ctx + ) + + def __repr__(self) -> str: + return "UUID" + + +class File(ParamType): + """Declares a parameter to be a file for reading or writing. The file + is automatically closed once the context tears down (after the command + finished working). + + Files can be opened for reading or writing. The special value ``-`` + indicates stdin or stdout depending on the mode. + + By default, the file is opened for reading text data, but it can also be + opened in binary mode or for writing. The encoding parameter can be used + to force a specific encoding. + + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. + + Files can also be opened atomically in which case all writes go into a + separate file in the same folder and upon completion the file will + be moved over to the original location. This is useful if a file + regularly read by other users is modified. + + See :ref:`file-args` for more information. + + .. versionchanged:: 2.0 + Added the ``atomic`` parameter. + """ + + name = "filename" + envvar_list_splitter: t.ClassVar[str] = os.path.pathsep + + def __init__( + self, + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool | None = None, + atomic: bool = False, + ) -> None: + self.mode = mode + self.encoding = encoding + self.errors = errors + self.lazy = lazy + self.atomic = atomic + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update(mode=self.mode, encoding=self.encoding) + return info_dict + + def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool: + if self.lazy is not None: + return self.lazy + if os.fspath(value) == "-": + return False + elif "w" in self.mode: + return True + return False + + def convert( + self, + value: str | os.PathLike[str] | t.IO[t.Any], + param: Parameter | None, + ctx: Context | None, + ) -> t.IO[t.Any]: + if _is_file_like(value): + return value + + value = t.cast("str | os.PathLike[str]", value) + + try: + lazy = self.resolve_lazy_flag(value) + + if lazy: + lf = LazyFile( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + + if ctx is not None: + ctx.call_on_close(lf.close_intelligently) + + return t.cast("t.IO[t.Any]", lf) + + f, should_close = open_stream( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + + # If a context is provided, we automatically close the file + # at the end of the context execution (or flush out). If a + # context does not exist, it's the caller's responsibility to + # properly close the file. This for instance happens when the + # type is used with prompts. + if ctx is not None: + if should_close: + ctx.call_on_close(safecall(f.close)) + else: + ctx.call_on_close(safecall(f.flush)) + + return f + except OSError as e: + self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Return a special completion marker that tells the completion + system to use the shell to provide file path completions. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + return [CompletionItem(incomplete, type="file")] + + +def _is_file_like(value: t.Any) -> te.TypeGuard[t.IO[t.Any]]: + return hasattr(value, "read") or hasattr(value, "write") + + +class Path(ParamType): + """The ``Path`` type is similar to the :class:`File` type, but + returns the filename instead of an open file. Various checks can be + enabled to validate the type of file and permissions. + + :param exists: The file or directory needs to exist for the value to + be valid. If this is not set to ``True``, and the file does not + exist, then all further checks are silently skipped. + :param file_okay: Allow a file as a value. + :param dir_okay: Allow a directory as a value. + :param readable: if true, a readable check is performed. + :param writable: if true, a writable check is performed. + :param executable: if true, an executable check is performed. + :param resolve_path: Make the value absolute and resolve any + symlinks. A ``~`` is not expanded, as this is supposed to be + done by the shell only. + :param allow_dash: Allow a single dash as a value, which indicates + a standard stream (but does not open it). Use + :func:`~click.open_file` to handle opening this value. + :param path_type: Convert the incoming path value to this type. If + ``None``, keep Python's default, which is ``str``. Useful to + convert to :class:`pathlib.Path`. + + .. versionchanged:: 8.1 + Added the ``executable`` parameter. + + .. versionchanged:: 8.0 + Allow passing ``path_type=pathlib.Path``. + + .. versionchanged:: 6.0 + Added the ``allow_dash`` parameter. + """ + + envvar_list_splitter: t.ClassVar[str] = os.path.pathsep + + def __init__( + self, + exists: bool = False, + file_okay: bool = True, + dir_okay: bool = True, + writable: bool = False, + readable: bool = True, + resolve_path: bool = False, + allow_dash: bool = False, + path_type: type[t.Any] | None = None, + executable: bool = False, + ): + self.exists = exists + self.file_okay = file_okay + self.dir_okay = dir_okay + self.readable = readable + self.writable = writable + self.executable = executable + self.resolve_path = resolve_path + self.allow_dash = allow_dash + self.type = path_type + + if self.file_okay and not self.dir_okay: + self.name: str = _("file") + elif self.dir_okay and not self.file_okay: + self.name = _("directory") + else: + self.name = _("path") + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + exists=self.exists, + file_okay=self.file_okay, + dir_okay=self.dir_okay, + writable=self.writable, + readable=self.readable, + allow_dash=self.allow_dash, + ) + return info_dict + + def coerce_path_result( + self, value: str | os.PathLike[str] + ) -> str | bytes | os.PathLike[str]: + if self.type is not None and not isinstance(value, self.type): + if self.type is str: + return os.fsdecode(value) + elif self.type is bytes: + return os.fsencode(value) + else: + return t.cast("os.PathLike[str]", self.type(value)) + + return value + + def convert( + self, + value: str | os.PathLike[str], + param: Parameter | None, + ctx: Context | None, + ) -> str | bytes | os.PathLike[str]: + rv = value + + is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-") + + if not is_dash: + if self.resolve_path: + rv = os.path.realpath(rv) + + try: + st = os.stat(rv) + except OSError: + if not self.exists: + return self.coerce_path_result(rv) + self.fail( + _("{name} {filename!r} does not exist.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if not self.file_okay and stat.S_ISREG(st.st_mode): + self.fail( + _("{name} {filename!r} is a file.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + if not self.dir_okay and stat.S_ISDIR(st.st_mode): + self.fail( + _("{name} {filename!r} is a directory.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.readable and not os.access(rv, os.R_OK): + self.fail( + _("{name} {filename!r} is not readable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.writable and not os.access(rv, os.W_OK): + self.fail( + _("{name} {filename!r} is not writable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.executable and not os.access(value, os.X_OK): + self.fail( + _("{name} {filename!r} is not executable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + return self.coerce_path_result(rv) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Return a special completion marker that tells the completion + system to use the shell to provide path completions for only + directories or any paths. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + type = "dir" if self.dir_okay and not self.file_okay else "file" + return [CompletionItem(incomplete, type=type)] + + +class Tuple(CompositeParamType): + """The default behavior of Click is to apply a type on a value directly. + This works well in most cases, except for when `nargs` is set to a fixed + count and different types should be used for different items. In this + case the :class:`Tuple` type can be used. This type can only be used + if `nargs` is set to a fixed number. + + For more information see :ref:`tuple-type`. + + This can be selected by using a Python tuple literal as a type. + + :param types: a list of types that should be used for the tuple items. + """ + + def __init__(self, types: cabc.Sequence[type[t.Any] | ParamType]) -> None: + self.types: cabc.Sequence[ParamType] = [convert_type(ty) for ty in types] + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["types"] = [t.to_info_dict() for t in self.types] + return info_dict + + @property + def name(self) -> str: # type: ignore + return f"<{' '.join(ty.name for ty in self.types)}>" + + @property + def arity(self) -> int: # type: ignore + return len(self.types) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + len_type = len(self.types) + len_value = len(value) + + if len_value != len_type: + self.fail( + ngettext( + "{len_type} values are required, but {len_value} was given.", + "{len_type} values are required, but {len_value} were given.", + len_value, + ).format(len_type=len_type, len_value=len_value), + param=param, + ctx=ctx, + ) + + return tuple( + ty(x, param, ctx) for ty, x in zip(self.types, value, strict=False) + ) + + +def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType: + """Find the most appropriate :class:`ParamType` for the given Python + type. If the type isn't provided, it can be inferred from a default + value. + """ + guessed_type = False + + if ty is None and default is not None: + if isinstance(default, (tuple, list)): + # If the default is empty, ty will remain None and will + # return STRING. + if default: + item = default[0] + + # A tuple of tuples needs to detect the inner types. + # Can't call convert recursively because that would + # incorrectly unwind the tuple to a single type. + if isinstance(item, (tuple, list)): + ty = tuple(map(type, item)) + else: + ty = type(item) + else: + ty = type(default) + + guessed_type = True + + if isinstance(ty, tuple): + return Tuple(ty) + + if isinstance(ty, ParamType): + return ty + + if ty is str or ty is None: + return STRING + + if ty is int: + return INT + + if ty is float: + return FLOAT + + if ty is bool: + return BOOL + + if guessed_type: + return STRING + + if __debug__: + try: + if issubclass(ty, ParamType): + raise AssertionError( + f"Attempted to use an uninstantiated parameter type ({ty})." + ) + except TypeError: + # ty is an instance (correct), so issubclass fails. + pass + + return FuncParamType(ty) + + +#: A dummy parameter type that just does nothing. From a user's +#: perspective this appears to just be the same as `STRING` but +#: internally no string conversion takes place if the input was bytes. +#: This is usually useful when working with file paths as they can +#: appear in bytes and unicode. +#: +#: For path related uses the :class:`Path` type is a better choice but +#: there are situations where an unprocessed type is useful which is why +#: it is is provided. +#: +#: .. versionadded:: 4.0 +UNPROCESSED = UnprocessedParamType() + +#: A unicode string parameter type which is the implicit default. This +#: can also be selected by using ``str`` as type. +STRING = StringParamType() + +#: An integer parameter. This can also be selected by using ``int`` as +#: type. +INT = IntParamType() + +#: A floating point value parameter. This can also be selected by using +#: ``float`` as type. +FLOAT = FloatParamType() + +#: A boolean parameter. This is the default for boolean flags. This can +#: also be selected by using ``bool`` as a type. +BOOL = BoolParamType() + +#: A UUID parameter. +UUID = UUIDParameterType() + + +class OptionHelpExtra(t.TypedDict, total=False): + envvars: tuple[str, ...] + default: str + range: str + required: str diff --git a/venv/lib/python3.10/site-packages/click/utils.py b/venv/lib/python3.10/site-packages/click/utils.py new file mode 100644 index 0000000..beae26f --- /dev/null +++ b/venv/lib/python3.10/site-packages/click/utils.py @@ -0,0 +1,627 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import re +import sys +import typing as t +from functools import update_wrapper +from types import ModuleType +from types import TracebackType + +from ._compat import _default_text_stderr +from ._compat import _default_text_stdout +from ._compat import _find_binary_writer +from ._compat import auto_wrap_for_ansi +from ._compat import binary_streams +from ._compat import open_stream +from ._compat import should_strip_ansi +from ._compat import strip_ansi +from ._compat import text_streams +from ._compat import WIN +from .globals import resolve_color_default + +if t.TYPE_CHECKING: + import typing_extensions as te + + P = te.ParamSpec("P") + +R = t.TypeVar("R") + + +def _posixify(name: str) -> str: + return "-".join(name.split()).lower() + + +def safecall(func: t.Callable[P, R]) -> t.Callable[P, R | None]: + """Wraps a function so that it swallows exceptions.""" + + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None: + try: + return func(*args, **kwargs) + except Exception: + pass + return None + + return update_wrapper(wrapper, func) + + +def make_str(value: t.Any) -> str: + """Converts a value into a valid string.""" + if isinstance(value, bytes): + try: + return value.decode(sys.getfilesystemencoding()) + except UnicodeError: + return value.decode("utf-8", "replace") + return str(value) + + +def make_default_short_help(help: str, max_length: int = 45) -> str: + """Returns a condensed version of help string.""" + # Consider only the first paragraph. + paragraph_end = help.find("\n\n") + + if paragraph_end != -1: + help = help[:paragraph_end] + + # Collapse newlines, tabs, and spaces. + words = help.split() + + if not words: + return "" + + # The first paragraph started with a "no rewrap" marker, ignore it. + if words[0] == "\b": + words = words[1:] + + total_length = 0 + last_index = len(words) - 1 + + for i, word in enumerate(words): + total_length += len(word) + (i > 0) + + if total_length > max_length: # too long, truncate + break + + if word[-1] == ".": # sentence end, truncate without "..." + return " ".join(words[: i + 1]) + + if total_length == max_length and i != last_index: + break # not at sentence end, truncate with "..." + else: + return " ".join(words) # no truncation needed + + # Account for the length of the suffix. + total_length += len("...") + + # remove words until the length is short enough + while i > 0: + total_length -= len(words[i]) + (i > 0) + + if total_length <= max_length: + break + + i -= 1 + + return " ".join(words[:i]) + "..." + + +class LazyFile: + """A lazy file works like a regular file but it does not fully open + the file but it does perform some basic checks early to see if the + filename parameter does make sense. This is useful for safely opening + files for writing. + """ + + def __init__( + self, + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, + ): + self.name: str = os.fspath(filename) + self.mode = mode + self.encoding = encoding + self.errors = errors + self.atomic = atomic + self._f: t.IO[t.Any] | None + self.should_close: bool + + if self.name == "-": + self._f, self.should_close = open_stream(filename, mode, encoding, errors) + else: + if "r" in mode: + # Open and close the file in case we're opening it for + # reading so that we can catch at least some errors in + # some cases early. + open(filename, mode).close() + self._f = None + self.should_close = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self.open(), name) + + def __repr__(self) -> str: + if self._f is not None: + return repr(self._f) + return f"" + + def open(self) -> t.IO[t.Any]: + """Opens the file if it's not yet open. This call might fail with + a :exc:`FileError`. Not handling this error will produce an error + that Click shows. + """ + if self._f is not None: + return self._f + try: + rv, self.should_close = open_stream( + self.name, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + except OSError as e: + from .exceptions import FileError + + raise FileError(self.name, hint=e.strerror) from e + self._f = rv + return rv + + def close(self) -> None: + """Closes the underlying file, no matter what.""" + if self._f is not None: + self._f.close() + + def close_intelligently(self) -> None: + """This function only closes the file if it was opened by the lazy + file wrapper. For instance this will never close stdin. + """ + if self.should_close: + self.close() + + def __enter__(self) -> LazyFile: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.close_intelligently() + + def __iter__(self) -> cabc.Iterator[t.AnyStr]: + self.open() + return iter(self._f) # type: ignore + + +class KeepOpenFile: + def __init__(self, file: t.IO[t.Any]) -> None: + self._file: t.IO[t.Any] = file + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._file, name) + + def __enter__(self) -> KeepOpenFile: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + pass + + def __repr__(self) -> str: + return repr(self._file) + + def __iter__(self) -> cabc.Iterator[t.AnyStr]: + return iter(self._file) + + +def echo( + message: t.Any | None = None, + file: t.IO[t.Any] | None = None, + nl: bool = True, + err: bool = False, + color: bool | None = None, +) -> None: + """Print a message and newline to stdout or a file. This should be + used instead of :func:`print` because it provides better support + for different data, files, and environments. + + Compared to :func:`print`, this does the following: + + - Ensures that the output encoding is not misconfigured on Linux. + - Supports Unicode in the Windows console. + - Supports writing to binary outputs, and supports writing bytes + to text outputs. + - Supports colors and styles on Windows. + - Removes ANSI color and style codes if the output does not look + like an interactive terminal. + - Always flushes the output. + + :param message: The string or bytes to output. Other objects are + converted to strings. + :param file: The file to write to. Defaults to ``stdout``. + :param err: Write to ``stderr`` instead of ``stdout``. + :param nl: Print a newline after the message. Enabled by default. + :param color: Force showing or hiding colors and other styles. By + default Click will remove color if the output does not look like + an interactive terminal. + + .. versionchanged:: 6.0 + Support Unicode output on the Windows console. Click does not + modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()`` + will still not support Unicode. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionadded:: 3.0 + Added the ``err`` parameter. + + .. versionchanged:: 2.0 + Support colors on Windows if colorama is installed. + """ + if file is None: + if err: + file = _default_text_stderr() + else: + file = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + return + + # Convert non bytes/text into the native string type. + if message is not None and not isinstance(message, (str, bytes, bytearray)): + out: str | bytes | bytearray | None = str(message) + else: + out = message + + if nl: + out = out or "" + if isinstance(out, str): + out += "\n" + else: + out += b"\n" + + if not out: + file.flush() + return + + # If there is a message and the value looks like bytes, we manually + # need to find the binary stream and write the message in there. + # This is done separately so that most stream types will work as you + # would expect. Eg: you can write to StringIO for other cases. + if isinstance(out, (bytes, bytearray)): + binary_file = _find_binary_writer(file) + + if binary_file is not None: + file.flush() + binary_file.write(out) + binary_file.flush() + return + + # ANSI style code support. For no message or bytes, nothing happens. + # When outputting to a file instead of a terminal, strip codes. + else: + color = resolve_color_default(color) + + if should_strip_ansi(file, color): + out = strip_ansi(out) + elif WIN: + if auto_wrap_for_ansi is not None: + file = auto_wrap_for_ansi(file, color) # type: ignore + elif not color: + out = strip_ansi(out) + + file.write(out) # type: ignore + file.flush() + + +def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryIO: + """Returns a system stream for byte processing. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + """ + opener = binary_streams.get(name) + if opener is None: + raise TypeError(f"Unknown standard stream '{name}'") + return opener() + + +def get_text_stream( + name: t.Literal["stdin", "stdout", "stderr"], + encoding: str | None = None, + errors: str | None = "strict", +) -> t.TextIO: + """Returns a system stream for text processing. This usually returns + a wrapped stream around a binary stream returned from + :func:`get_binary_stream` but it also can take shortcuts for already + correctly configured streams. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + :param encoding: overrides the detected default encoding. + :param errors: overrides the default error mode. + """ + opener = text_streams.get(name) + if opener is None: + raise TypeError(f"Unknown standard stream '{name}'") + return opener(encoding, errors) + + +def open_file( + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool = False, + atomic: bool = False, +) -> t.IO[t.Any]: + """Open a file, with extra behavior to handle ``'-'`` to indicate + a standard stream, lazy open on write, and atomic write. Similar to + the behavior of the :class:`~click.File` param type. + + If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is + wrapped so that using it in a context manager will not close it. + This makes it possible to use the function without accidentally + closing a standard stream: + + .. code-block:: python + + with open_file(filename) as f: + ... + + :param filename: The name or Path of the file to open, or ``'-'`` for + ``stdin``/``stdout``. + :param mode: The mode in which to open the file. + :param encoding: The encoding to decode or encode a file opened in + text mode. + :param errors: The error handling mode. + :param lazy: Wait to open the file until it is accessed. For read + mode, the file is temporarily opened to raise access errors + early, then closed until it is read again. + :param atomic: Write to a temporary file and replace the given file + on close. + + .. versionadded:: 3.0 + """ + if lazy: + return t.cast( + "t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic=atomic) + ) + + f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) + + if not should_close: + f = t.cast("t.IO[t.Any]", KeepOpenFile(f)) + + return f + + +def format_filename( + filename: str | bytes | os.PathLike[str] | os.PathLike[bytes], + shorten: bool = False, +) -> str: + """Format a filename as a string for display. Ensures the filename can be + displayed by replacing any invalid bytes or surrogate escapes in the name + with the replacement character ``�``. + + Invalid bytes or surrogate escapes will raise an error when written to a + stream with ``errors="strict"``. This will typically happen with ``stdout`` + when the locale is something like ``en_GB.UTF-8``. + + Many scenarios *are* safe to write surrogates though, due to PEP 538 and + PEP 540, including: + + - Writing to ``stderr``, which uses ``errors="backslashreplace"``. + - The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens + stdout and stderr with ``errors="surrogateescape"``. + - None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``. + - Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``. + Python opens stdout and stderr with ``errors="surrogateescape"``. + + :param filename: formats a filename for UI display. This will also convert + the filename into unicode without failing. + :param shorten: this optionally shortens the filename to strip of the + path that leads up to it. + """ + if shorten: + filename = os.path.basename(filename) + else: + filename = os.fspath(filename) + + if isinstance(filename, bytes): + filename = filename.decode(sys.getfilesystemencoding(), "replace") + else: + filename = filename.encode("utf-8", "surrogateescape").decode( + "utf-8", "replace" + ) + + return filename + + +def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str: + r"""Returns the config folder for the application. The default behavior + is to return whatever is most appropriate for the operating system. + + To give you an idea, for an app called ``"Foo Bar"``, something like + the following folders could be returned: + + Mac OS X: + ``~/Library/Application Support/Foo Bar`` + Mac OS X (POSIX): + ``~/.foo-bar`` + Unix: + ``~/.config/foo-bar`` + Unix (POSIX): + ``~/.foo-bar`` + Windows (roaming): + ``C:\Users\\AppData\Roaming\Foo Bar`` + Windows (not roaming): + ``C:\Users\\AppData\Local\Foo Bar`` + + .. versionadded:: 2.0 + + :param app_name: the application name. This should be properly capitalized + and can contain whitespace. + :param roaming: controls if the folder should be roaming or not on Windows. + Has no effect otherwise. + :param force_posix: if this is set to `True` then on any POSIX system the + folder will be stored in the home folder with a leading + dot instead of the XDG config home or darwin's + application support folder. + """ + if WIN: + key = "APPDATA" if roaming else "LOCALAPPDATA" + folder = os.environ.get(key) + if folder is None: + folder = os.path.expanduser("~") + return os.path.join(folder, app_name) + if force_posix: + return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}")) + if sys.platform == "darwin": + return os.path.join( + os.path.expanduser("~/Library/Application Support"), app_name + ) + return os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + _posixify(app_name), + ) + + +class PacifyFlushWrapper: + """This wrapper is used to catch and suppress BrokenPipeErrors resulting + from ``.flush()`` being called on broken pipe during the shutdown/final-GC + of the Python interpreter. Notably ``.flush()`` is always called on + ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any + other cleanup code, and the case where the underlying file is not a broken + pipe, all calls and attributes are proxied. + """ + + def __init__(self, wrapped: t.IO[t.Any]) -> None: + self.wrapped = wrapped + + def flush(self) -> None: + try: + self.wrapped.flush() + except OSError as e: + import errno + + if e.errno != errno.EPIPE: + raise + + def __getattr__(self, attr: str) -> t.Any: + return getattr(self.wrapped, attr) + + +def _detect_program_name( + path: str | None = None, _main: ModuleType | None = None +) -> str: + """Determine the command used to run the program, for use in help + text. If a file or entry point was executed, the file name is + returned. If ``python -m`` was used to execute a module or package, + ``python -m name`` is returned. + + This doesn't try to be too precise, the goal is to give a concise + name for help text. Files are only shown as their name without the + path. ``python`` is only shown for modules, and the full path to + ``sys.executable`` is not shown. + + :param path: The Python file being executed. Python puts this in + ``sys.argv[0]``, which is used by default. + :param _main: The ``__main__`` module. This should only be passed + during internal testing. + + .. versionadded:: 8.0 + Based on command args detection in the Werkzeug reloader. + + :meta private: + """ + if _main is None: + _main = sys.modules["__main__"] + + if not path: + path = sys.argv[0] + + # The value of __package__ indicates how Python was called. It may + # not exist if a setuptools script is installed as an egg. It may be + # set incorrectly for entry points created with pip on Windows. + # It is set to "" inside a Shiv or PEX zipapp. + if getattr(_main, "__package__", None) in {None, ""} or ( + os.name == "nt" + and _main.__package__ == "" + and not os.path.exists(path) + and os.path.exists(f"{path}.exe") + ): + # Executed a file, like "python app.py". + return os.path.basename(path) + + # Executed a module, like "python -m example". + # Rewritten by Python from "-m script" to "/path/to/script.py". + # Need to look at main module to determine how it was executed. + py_module = t.cast(str, _main.__package__) + name = os.path.splitext(os.path.basename(path))[0] + + # A submodule like "example.cli". + if name != "__main__": + py_module = f"{py_module}.{name}" + + return f"python -m {py_module.lstrip('.')}" + + +def _expand_args( + args: cabc.Iterable[str], + *, + user: bool = True, + env: bool = True, + glob_recursive: bool = True, +) -> list[str]: + """Simulate Unix shell expansion with Python functions. + + See :func:`glob.glob`, :func:`os.path.expanduser`, and + :func:`os.path.expandvars`. + + This is intended for use on Windows, where the shell does not do any + expansion. It may not exactly match what a Unix shell would do. + + :param args: List of command line arguments to expand. + :param user: Expand user home directory. + :param env: Expand environment variables. + :param glob_recursive: ``**`` matches directories recursively. + + .. versionchanged:: 8.1 + Invalid glob patterns are treated as empty expansions rather + than raising an error. + + .. versionadded:: 8.0 + + :meta private: + """ + from glob import glob + + out = [] + + for arg in args: + if user: + arg = os.path.expanduser(arg) + + if env: + arg = os.path.expandvars(arg) + + try: + matches = glob(arg, recursive=glob_recursive) + except re.error: + matches = [] + + if not matches: + out.append(arg) + else: + out.extend(matches) + + return out diff --git a/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/METADATA b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/METADATA new file mode 100644 index 0000000..a1b5c57 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/METADATA @@ -0,0 +1,441 @@ +Metadata-Version: 2.1 +Name: colorama +Version: 0.4.6 +Summary: Cross-platform colored terminal text. +Project-URL: Homepage, https://github.com/tartley/colorama +Author-email: Jonathan Hartley +License-File: LICENSE.txt +Keywords: ansi,color,colour,crossplatform,terminal,text,windows,xplatform +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Console +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Terminals +Requires-Python: !=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7 +Description-Content-Type: text/x-rst + +.. image:: https://img.shields.io/pypi/v/colorama.svg + :target: https://pypi.org/project/colorama/ + :alt: Latest Version + +.. image:: https://img.shields.io/pypi/pyversions/colorama.svg + :target: https://pypi.org/project/colorama/ + :alt: Supported Python versions + +.. image:: https://github.com/tartley/colorama/actions/workflows/test.yml/badge.svg + :target: https://github.com/tartley/colorama/actions/workflows/test.yml + :alt: Build Status + +Colorama +======== + +Makes ANSI escape character sequences (for producing colored terminal text and +cursor positioning) work under MS Windows. + +.. |donate| image:: https://www.paypalobjects.com/en_US/i/btn/btn_donate_SM.gif + :target: https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=2MZ9D2GMLYCUJ&item_name=Colorama¤cy_code=USD + :alt: Donate with Paypal + +`PyPI for releases `_ | +`Github for source `_ | +`Colorama for enterprise on Tidelift `_ + +If you find Colorama useful, please |donate| to the authors. Thank you! + +Installation +------------ + +Tested on CPython 2.7, 3.7, 3.8, 3.9 and 3.10 and Pypy 2.7 and 3.8. + +No requirements other than the standard library. + +.. code-block:: bash + + pip install colorama + # or + conda install -c anaconda colorama + +Description +----------- + +ANSI escape character sequences have long been used to produce colored terminal +text and cursor positioning on Unix and Macs. Colorama makes this work on +Windows, too, by wrapping ``stdout``, stripping ANSI sequences it finds (which +would appear as gobbledygook in the output), and converting them into the +appropriate win32 calls to modify the state of the terminal. On other platforms, +Colorama does nothing. + +This has the upshot of providing a simple cross-platform API for printing +colored terminal text from Python, and has the happy side-effect that existing +applications or libraries which use ANSI sequences to produce colored output on +Linux or Macs can now also work on Windows, simply by calling +``colorama.just_fix_windows_console()`` (since v0.4.6) or ``colorama.init()`` +(all versions, but may have other side-effects – see below). + +An alternative approach is to install ``ansi.sys`` on Windows machines, which +provides the same behaviour for all applications running in terminals. Colorama +is intended for situations where that isn't easy (e.g., maybe your app doesn't +have an installer.) + +Demo scripts in the source code repository print some colored text using +ANSI sequences. Compare their output under Gnome-terminal's built in ANSI +handling, versus on Windows Command-Prompt using Colorama: + +.. image:: https://github.com/tartley/colorama/raw/master/screenshots/ubuntu-demo.png + :width: 661 + :height: 357 + :alt: ANSI sequences on Ubuntu under gnome-terminal. + +.. image:: https://github.com/tartley/colorama/raw/master/screenshots/windows-demo.png + :width: 668 + :height: 325 + :alt: Same ANSI sequences on Windows, using Colorama. + +These screenshots show that, on Windows, Colorama does not support ANSI 'dim +text'; it looks the same as 'normal text'. + +Usage +----- + +Initialisation +.............. + +If the only thing you want from Colorama is to get ANSI escapes to work on +Windows, then run: + +.. code-block:: python + + from colorama import just_fix_windows_console + just_fix_windows_console() + +If you're on a recent version of Windows 10 or better, and your stdout/stderr +are pointing to a Windows console, then this will flip the magic configuration +switch to enable Windows' built-in ANSI support. + +If you're on an older version of Windows, and your stdout/stderr are pointing to +a Windows console, then this will wrap ``sys.stdout`` and/or ``sys.stderr`` in a +magic file object that intercepts ANSI escape sequences and issues the +appropriate Win32 calls to emulate them. + +In all other circumstances, it does nothing whatsoever. Basically the idea is +that this makes Windows act like Unix with respect to ANSI escape handling. + +It's safe to call this function multiple times. It's safe to call this function +on non-Windows platforms, but it won't do anything. It's safe to call this +function when one or both of your stdout/stderr are redirected to a file – it +won't do anything to those streams. + +Alternatively, you can use the older interface with more features (but also more +potential footguns): + +.. code-block:: python + + from colorama import init + init() + +This does the same thing as ``just_fix_windows_console``, except for the +following differences: + +- It's not safe to call ``init`` multiple times; you can end up with multiple + layers of wrapping and broken ANSI support. + +- Colorama will apply a heuristic to guess whether stdout/stderr support ANSI, + and if it thinks they don't, then it will wrap ``sys.stdout`` and + ``sys.stderr`` in a magic file object that strips out ANSI escape sequences + before printing them. This happens on all platforms, and can be convenient if + you want to write your code to emit ANSI escape sequences unconditionally, and + let Colorama decide whether they should actually be output. But note that + Colorama's heuristic is not particularly clever. + +- ``init`` also accepts explicit keyword args to enable/disable various + functionality – see below. + +To stop using Colorama before your program exits, simply call ``deinit()``. +This will restore ``stdout`` and ``stderr`` to their original values, so that +Colorama is disabled. To resume using Colorama again, call ``reinit()``; it is +cheaper than calling ``init()`` again (but does the same thing). + +Most users should depend on ``colorama >= 0.4.6``, and use +``just_fix_windows_console``. The old ``init`` interface will be supported +indefinitely for backwards compatibility, but we don't plan to fix any issues +with it, also for backwards compatibility. + +Colored Output +.............. + +Cross-platform printing of colored text can then be done using Colorama's +constant shorthand for ANSI escape sequences. These are deliberately +rudimentary, see below. + +.. code-block:: python + + from colorama import Fore, Back, Style + print(Fore.RED + 'some red text') + print(Back.GREEN + 'and with a green background') + print(Style.DIM + 'and in dim text') + print(Style.RESET_ALL) + print('back to normal now') + +...or simply by manually printing ANSI sequences from your own code: + +.. code-block:: python + + print('\033[31m' + 'some red text') + print('\033[39m') # and reset to default color + +...or, Colorama can be used in conjunction with existing ANSI libraries +such as the venerable `Termcolor `_ +the fabulous `Blessings `_, +or the incredible `_Rich `_. + +If you wish Colorama's Fore, Back and Style constants were more capable, +then consider using one of the above highly capable libraries to generate +colors, etc, and use Colorama just for its primary purpose: to convert +those ANSI sequences to also work on Windows: + +SIMILARLY, do not send PRs adding the generation of new ANSI types to Colorama. +We are only interested in converting ANSI codes to win32 API calls, not +shortcuts like the above to generate ANSI characters. + +.. code-block:: python + + from colorama import just_fix_windows_console + from termcolor import colored + + # use Colorama to make Termcolor work on Windows too + just_fix_windows_console() + + # then use Termcolor for all colored text output + print(colored('Hello, World!', 'green', 'on_red')) + +Available formatting constants are:: + + Fore: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET. + Back: BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE, RESET. + Style: DIM, NORMAL, BRIGHT, RESET_ALL + +``Style.RESET_ALL`` resets foreground, background, and brightness. Colorama will +perform this reset automatically on program exit. + +These are fairly well supported, but not part of the standard:: + + Fore: LIGHTBLACK_EX, LIGHTRED_EX, LIGHTGREEN_EX, LIGHTYELLOW_EX, LIGHTBLUE_EX, LIGHTMAGENTA_EX, LIGHTCYAN_EX, LIGHTWHITE_EX + Back: LIGHTBLACK_EX, LIGHTRED_EX, LIGHTGREEN_EX, LIGHTYELLOW_EX, LIGHTBLUE_EX, LIGHTMAGENTA_EX, LIGHTCYAN_EX, LIGHTWHITE_EX + +Cursor Positioning +.................. + +ANSI codes to reposition the cursor are supported. See ``demos/demo06.py`` for +an example of how to generate them. + +Init Keyword Args +................. + +``init()`` accepts some ``**kwargs`` to override default behaviour. + +init(autoreset=False): + If you find yourself repeatedly sending reset sequences to turn off color + changes at the end of every print, then ``init(autoreset=True)`` will + automate that: + + .. code-block:: python + + from colorama import init + init(autoreset=True) + print(Fore.RED + 'some red text') + print('automatically back to default color again') + +init(strip=None): + Pass ``True`` or ``False`` to override whether ANSI codes should be + stripped from the output. The default behaviour is to strip if on Windows + or if output is redirected (not a tty). + +init(convert=None): + Pass ``True`` or ``False`` to override whether to convert ANSI codes in the + output into win32 calls. The default behaviour is to convert if on Windows + and output is to a tty (terminal). + +init(wrap=True): + On Windows, Colorama works by replacing ``sys.stdout`` and ``sys.stderr`` + with proxy objects, which override the ``.write()`` method to do their work. + If this wrapping causes you problems, then this can be disabled by passing + ``init(wrap=False)``. The default behaviour is to wrap if ``autoreset`` or + ``strip`` or ``convert`` are True. + + When wrapping is disabled, colored printing on non-Windows platforms will + continue to work as normal. To do cross-platform colored output, you can + use Colorama's ``AnsiToWin32`` proxy directly: + + .. code-block:: python + + import sys + from colorama import init, AnsiToWin32 + init(wrap=False) + stream = AnsiToWin32(sys.stderr).stream + + # Python 2 + print >>stream, Fore.BLUE + 'blue text on stderr' + + # Python 3 + print(Fore.BLUE + 'blue text on stderr', file=stream) + +Recognised ANSI Sequences +......................... + +ANSI sequences generally take the form:: + + ESC [ ; ... + +Where ```` is an integer, and ```` is a single letter. Zero or +more params are passed to a ````. If no params are passed, it is +generally synonymous with passing a single zero. No spaces exist in the +sequence; they have been inserted here simply to read more easily. + +The only ANSI sequences that Colorama converts into win32 calls are:: + + ESC [ 0 m # reset all (colors and brightness) + ESC [ 1 m # bright + ESC [ 2 m # dim (looks same as normal brightness) + ESC [ 22 m # normal brightness + + # FOREGROUND: + ESC [ 30 m # black + ESC [ 31 m # red + ESC [ 32 m # green + ESC [ 33 m # yellow + ESC [ 34 m # blue + ESC [ 35 m # magenta + ESC [ 36 m # cyan + ESC [ 37 m # white + ESC [ 39 m # reset + + # BACKGROUND + ESC [ 40 m # black + ESC [ 41 m # red + ESC [ 42 m # green + ESC [ 43 m # yellow + ESC [ 44 m # blue + ESC [ 45 m # magenta + ESC [ 46 m # cyan + ESC [ 47 m # white + ESC [ 49 m # reset + + # cursor positioning + ESC [ y;x H # position cursor at x across, y down + ESC [ y;x f # position cursor at x across, y down + ESC [ n A # move cursor n lines up + ESC [ n B # move cursor n lines down + ESC [ n C # move cursor n characters forward + ESC [ n D # move cursor n characters backward + + # clear the screen + ESC [ mode J # clear the screen + + # clear the line + ESC [ mode K # clear the line + +Multiple numeric params to the ``'m'`` command can be combined into a single +sequence:: + + ESC [ 36 ; 45 ; 1 m # bright cyan text on magenta background + +All other ANSI sequences of the form ``ESC [ ; ... `` +are silently stripped from the output on Windows. + +Any other form of ANSI sequence, such as single-character codes or alternative +initial characters, are not recognised or stripped. It would be cool to add +them though. Let me know if it would be useful for you, via the Issues on +GitHub. + +Status & Known Problems +----------------------- + +I've personally only tested it on Windows XP (CMD, Console2), Ubuntu +(gnome-terminal, xterm), and OS X. + +Some valid ANSI sequences aren't recognised. + +If you're hacking on the code, see `README-hacking.md`_. ESPECIALLY, see the +explanation there of why we do not want PRs that allow Colorama to generate new +types of ANSI codes. + +See outstanding issues and wish-list: +https://github.com/tartley/colorama/issues + +If anything doesn't work for you, or doesn't do what you expected or hoped for, +I'd love to hear about it on that issues list, would be delighted by patches, +and would be happy to grant commit access to anyone who submits a working patch +or two. + +.. _README-hacking.md: README-hacking.md + +License +------- + +Copyright Jonathan Hartley & Arnon Yaari, 2013-2020. BSD 3-Clause license; see +LICENSE file. + +Professional support +-------------------- + +.. |tideliftlogo| image:: https://cdn2.hubspot.net/hubfs/4008838/website/logos/logos_for_download/Tidelift_primary-shorthand-logo.png + :alt: Tidelift + :target: https://tidelift.com/subscription/pkg/pypi-colorama?utm_source=pypi-colorama&utm_medium=referral&utm_campaign=readme + +.. list-table:: + :widths: 10 100 + + * - |tideliftlogo| + - Professional support for colorama is available as part of the + `Tidelift Subscription`_. + Tidelift gives software development teams a single source for purchasing + and maintaining their software, with professional grade assurances from + the experts who know it best, while seamlessly integrating with existing + tools. + +.. _Tidelift Subscription: https://tidelift.com/subscription/pkg/pypi-colorama?utm_source=pypi-colorama&utm_medium=referral&utm_campaign=readme + +Thanks +------ + +See the CHANGELOG for more thanks! + +* Marc Schlaich (schlamar) for a ``setup.py`` fix for Python2.5. +* Marc Abramowitz, reported & fixed a crash on exit with closed ``stdout``, + providing a solution to issue #7's setuptools/distutils debate, + and other fixes. +* User 'eryksun', for guidance on correctly instantiating ``ctypes.windll``. +* Matthew McCormick for politely pointing out a longstanding crash on non-Win. +* Ben Hoyt, for a magnificent fix under 64-bit Windows. +* Jesse at Empty Square for submitting a fix for examples in the README. +* User 'jamessp', an observant documentation fix for cursor positioning. +* User 'vaal1239', Dave Mckee & Lackner Kristof for a tiny but much-needed Win7 + fix. +* Julien Stuyck, for wisely suggesting Python3 compatible updates to README. +* Daniel Griffith for multiple fabulous patches. +* Oscar Lesta for a valuable fix to stop ANSI chars being sent to non-tty + output. +* Roger Binns, for many suggestions, valuable feedback, & bug reports. +* Tim Golden for thought and much appreciated feedback on the initial idea. +* User 'Zearin' for updates to the README file. +* John Szakmeister for adding support for light colors +* Charles Merriam for adding documentation to demos +* Jurko for a fix on 64-bit Windows CPython2.5 w/o ctypes +* Florian Bruhin for a fix when stdout or stderr are None +* Thomas Weininger for fixing ValueError on Windows +* Remi Rampin for better Github integration and fixes to the README file +* Simeon Visser for closing a file handle using 'with' and updating classifiers + to include Python 3.3 and 3.4 +* Andy Neff for fixing RESET of LIGHT_EX colors. +* Jonathan Hartley for the initial idea and implementation. diff --git a/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/RECORD b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/RECORD new file mode 100644 index 0000000..687d244 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/RECORD @@ -0,0 +1,31 @@ +colorama-0.4.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +colorama-0.4.6.dist-info/METADATA,sha256=e67SnrUMOym9sz_4TjF3vxvAV4T3aF7NyqRHHH3YEMw,17158 +colorama-0.4.6.dist-info/RECORD,, +colorama-0.4.6.dist-info/WHEEL,sha256=cdcF4Fbd0FPtw2EMIOwH-3rSOTUdTCeOSXRMD1iLUb8,105 +colorama-0.4.6.dist-info/licenses/LICENSE.txt,sha256=ysNcAmhuXQSlpxQL-zs25zrtSWZW6JEQLkKIhteTAxg,1491 +colorama/__init__.py,sha256=wePQA4U20tKgYARySLEC047ucNX-g8pRLpYBuiHlLb8,266 +colorama/__pycache__/__init__.cpython-310.pyc,, +colorama/__pycache__/ansi.cpython-310.pyc,, +colorama/__pycache__/ansitowin32.cpython-310.pyc,, +colorama/__pycache__/initialise.cpython-310.pyc,, +colorama/__pycache__/win32.cpython-310.pyc,, +colorama/__pycache__/winterm.cpython-310.pyc,, +colorama/ansi.py,sha256=Top4EeEuaQdBWdteKMEcGOTeKeF19Q-Wo_6_Cj5kOzQ,2522 +colorama/ansitowin32.py,sha256=vPNYa3OZbxjbuFyaVo0Tmhmy1FZ1lKMWCnT7odXpItk,11128 +colorama/initialise.py,sha256=-hIny86ClXo39ixh5iSCfUIa2f_h_bgKRDW7gqs-KLU,3325 +colorama/tests/__init__.py,sha256=MkgPAEzGQd-Rq0w0PZXSX2LadRWhUECcisJY8lSrm4Q,75 +colorama/tests/__pycache__/__init__.cpython-310.pyc,, +colorama/tests/__pycache__/ansi_test.cpython-310.pyc,, +colorama/tests/__pycache__/ansitowin32_test.cpython-310.pyc,, +colorama/tests/__pycache__/initialise_test.cpython-310.pyc,, +colorama/tests/__pycache__/isatty_test.cpython-310.pyc,, +colorama/tests/__pycache__/utils.cpython-310.pyc,, +colorama/tests/__pycache__/winterm_test.cpython-310.pyc,, +colorama/tests/ansi_test.py,sha256=FeViDrUINIZcr505PAxvU4AjXz1asEiALs9GXMhwRaE,2839 +colorama/tests/ansitowin32_test.py,sha256=RN7AIhMJ5EqDsYaCjVo-o4u8JzDD4ukJbmevWKS70rY,10678 +colorama/tests/initialise_test.py,sha256=BbPy-XfyHwJ6zKozuQOvNvQZzsx9vdb_0bYXn7hsBTc,6741 +colorama/tests/isatty_test.py,sha256=Pg26LRpv0yQDB5Ac-sxgVXG7hsA1NYvapFgApZfYzZg,1866 +colorama/tests/utils.py,sha256=1IIRylG39z5-dzq09R_ngufxyPZxgldNbrxKxUGwGKE,1079 +colorama/tests/winterm_test.py,sha256=qoWFPEjym5gm2RuMwpf3pOis3a5r_PJZFCzK254JL8A,3709 +colorama/win32.py,sha256=YQOKwMTwtGBbsY4dL5HYTvwTeP9wIQra5MvPNddpxZs,6181 +colorama/winterm.py,sha256=XCQFDHjPi6AHYNdZwy0tA02H-Jh48Jp-HvCjeLeLp3U,7134 diff --git a/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/WHEEL b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/WHEEL new file mode 100644 index 0000000..d79189f --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.11.1 +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any diff --git a/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/licenses/LICENSE.txt b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..3105888 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama-0.4.6.dist-info/licenses/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2010 Jonathan Hartley +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +* Neither the name of the copyright holders, nor those of its contributors + may be used to endorse or promote products derived from this software without + specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/venv/lib/python3.10/site-packages/colorama/__init__.py b/venv/lib/python3.10/site-packages/colorama/__init__.py new file mode 100644 index 0000000..383101c --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/__init__.py @@ -0,0 +1,7 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +from .initialise import init, deinit, reinit, colorama_text, just_fix_windows_console +from .ansi import Fore, Back, Style, Cursor +from .ansitowin32 import AnsiToWin32 + +__version__ = '0.4.6' + diff --git a/venv/lib/python3.10/site-packages/colorama/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..6b62858 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/__pycache__/ansi.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/__pycache__/ansi.cpython-310.pyc new file mode 100644 index 0000000..1d95de5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/__pycache__/ansi.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/__pycache__/ansitowin32.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/__pycache__/ansitowin32.cpython-310.pyc new file mode 100644 index 0000000..2c7a9bb Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/__pycache__/ansitowin32.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/__pycache__/initialise.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/__pycache__/initialise.cpython-310.pyc new file mode 100644 index 0000000..35b7f08 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/__pycache__/initialise.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/__pycache__/win32.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/__pycache__/win32.cpython-310.pyc new file mode 100644 index 0000000..105b510 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/__pycache__/win32.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/__pycache__/winterm.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/__pycache__/winterm.cpython-310.pyc new file mode 100644 index 0000000..c938f82 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/__pycache__/winterm.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/ansi.py b/venv/lib/python3.10/site-packages/colorama/ansi.py new file mode 100644 index 0000000..11ec695 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/ansi.py @@ -0,0 +1,102 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +''' +This module generates ANSI character codes to printing colors to terminals. +See: http://en.wikipedia.org/wiki/ANSI_escape_code +''' + +CSI = '\033[' +OSC = '\033]' +BEL = '\a' + + +def code_to_chars(code): + return CSI + str(code) + 'm' + +def set_title(title): + return OSC + '2;' + title + BEL + +def clear_screen(mode=2): + return CSI + str(mode) + 'J' + +def clear_line(mode=2): + return CSI + str(mode) + 'K' + + +class AnsiCodes(object): + def __init__(self): + # the subclasses declare class attributes which are numbers. + # Upon instantiation we define instance attributes, which are the same + # as the class attributes but wrapped with the ANSI escape sequence + for name in dir(self): + if not name.startswith('_'): + value = getattr(self, name) + setattr(self, name, code_to_chars(value)) + + +class AnsiCursor(object): + def UP(self, n=1): + return CSI + str(n) + 'A' + def DOWN(self, n=1): + return CSI + str(n) + 'B' + def FORWARD(self, n=1): + return CSI + str(n) + 'C' + def BACK(self, n=1): + return CSI + str(n) + 'D' + def POS(self, x=1, y=1): + return CSI + str(y) + ';' + str(x) + 'H' + + +class AnsiFore(AnsiCodes): + BLACK = 30 + RED = 31 + GREEN = 32 + YELLOW = 33 + BLUE = 34 + MAGENTA = 35 + CYAN = 36 + WHITE = 37 + RESET = 39 + + # These are fairly well supported, but not part of the standard. + LIGHTBLACK_EX = 90 + LIGHTRED_EX = 91 + LIGHTGREEN_EX = 92 + LIGHTYELLOW_EX = 93 + LIGHTBLUE_EX = 94 + LIGHTMAGENTA_EX = 95 + LIGHTCYAN_EX = 96 + LIGHTWHITE_EX = 97 + + +class AnsiBack(AnsiCodes): + BLACK = 40 + RED = 41 + GREEN = 42 + YELLOW = 43 + BLUE = 44 + MAGENTA = 45 + CYAN = 46 + WHITE = 47 + RESET = 49 + + # These are fairly well supported, but not part of the standard. + LIGHTBLACK_EX = 100 + LIGHTRED_EX = 101 + LIGHTGREEN_EX = 102 + LIGHTYELLOW_EX = 103 + LIGHTBLUE_EX = 104 + LIGHTMAGENTA_EX = 105 + LIGHTCYAN_EX = 106 + LIGHTWHITE_EX = 107 + + +class AnsiStyle(AnsiCodes): + BRIGHT = 1 + DIM = 2 + NORMAL = 22 + RESET_ALL = 0 + +Fore = AnsiFore() +Back = AnsiBack() +Style = AnsiStyle() +Cursor = AnsiCursor() diff --git a/venv/lib/python3.10/site-packages/colorama/ansitowin32.py b/venv/lib/python3.10/site-packages/colorama/ansitowin32.py new file mode 100644 index 0000000..abf209e --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/ansitowin32.py @@ -0,0 +1,277 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import re +import sys +import os + +from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style, BEL +from .winterm import enable_vt_processing, WinTerm, WinColor, WinStyle +from .win32 import windll, winapi_test + + +winterm = None +if windll is not None: + winterm = WinTerm() + + +class StreamWrapper(object): + ''' + Wraps a stream (such as stdout), acting as a transparent proxy for all + attribute access apart from method 'write()', which is delegated to our + Converter instance. + ''' + def __init__(self, wrapped, converter): + # double-underscore everything to prevent clashes with names of + # attributes on the wrapped stream object. + self.__wrapped = wrapped + self.__convertor = converter + + def __getattr__(self, name): + return getattr(self.__wrapped, name) + + def __enter__(self, *args, **kwargs): + # special method lookup bypasses __getattr__/__getattribute__, see + # https://stackoverflow.com/questions/12632894/why-doesnt-getattr-work-with-exit + # thus, contextlib magic methods are not proxied via __getattr__ + return self.__wrapped.__enter__(*args, **kwargs) + + def __exit__(self, *args, **kwargs): + return self.__wrapped.__exit__(*args, **kwargs) + + def __setstate__(self, state): + self.__dict__ = state + + def __getstate__(self): + return self.__dict__ + + def write(self, text): + self.__convertor.write(text) + + def isatty(self): + stream = self.__wrapped + if 'PYCHARM_HOSTED' in os.environ: + if stream is not None and (stream is sys.__stdout__ or stream is sys.__stderr__): + return True + try: + stream_isatty = stream.isatty + except AttributeError: + return False + else: + return stream_isatty() + + @property + def closed(self): + stream = self.__wrapped + try: + return stream.closed + # AttributeError in the case that the stream doesn't support being closed + # ValueError for the case that the stream has already been detached when atexit runs + except (AttributeError, ValueError): + return True + + +class AnsiToWin32(object): + ''' + Implements a 'write()' method which, on Windows, will strip ANSI character + sequences from the text, and if outputting to a tty, will convert them into + win32 function calls. + ''' + ANSI_CSI_RE = re.compile('\001?\033\\[((?:\\d|;)*)([a-zA-Z])\002?') # Control Sequence Introducer + ANSI_OSC_RE = re.compile('\001?\033\\]([^\a]*)(\a)\002?') # Operating System Command + + def __init__(self, wrapped, convert=None, strip=None, autoreset=False): + # The wrapped stream (normally sys.stdout or sys.stderr) + self.wrapped = wrapped + + # should we reset colors to defaults after every .write() + self.autoreset = autoreset + + # create the proxy wrapping our output stream + self.stream = StreamWrapper(wrapped, self) + + on_windows = os.name == 'nt' + # We test if the WinAPI works, because even if we are on Windows + # we may be using a terminal that doesn't support the WinAPI + # (e.g. Cygwin Terminal). In this case it's up to the terminal + # to support the ANSI codes. + conversion_supported = on_windows and winapi_test() + try: + fd = wrapped.fileno() + except Exception: + fd = -1 + system_has_native_ansi = not on_windows or enable_vt_processing(fd) + have_tty = not self.stream.closed and self.stream.isatty() + need_conversion = conversion_supported and not system_has_native_ansi + + # should we strip ANSI sequences from our output? + if strip is None: + strip = need_conversion or not have_tty + self.strip = strip + + # should we should convert ANSI sequences into win32 calls? + if convert is None: + convert = need_conversion and have_tty + self.convert = convert + + # dict of ansi codes to win32 functions and parameters + self.win32_calls = self.get_win32_calls() + + # are we wrapping stderr? + self.on_stderr = self.wrapped is sys.stderr + + def should_wrap(self): + ''' + True if this class is actually needed. If false, then the output + stream will not be affected, nor will win32 calls be issued, so + wrapping stdout is not actually required. This will generally be + False on non-Windows platforms, unless optional functionality like + autoreset has been requested using kwargs to init() + ''' + return self.convert or self.strip or self.autoreset + + def get_win32_calls(self): + if self.convert and winterm: + return { + AnsiStyle.RESET_ALL: (winterm.reset_all, ), + AnsiStyle.BRIGHT: (winterm.style, WinStyle.BRIGHT), + AnsiStyle.DIM: (winterm.style, WinStyle.NORMAL), + AnsiStyle.NORMAL: (winterm.style, WinStyle.NORMAL), + AnsiFore.BLACK: (winterm.fore, WinColor.BLACK), + AnsiFore.RED: (winterm.fore, WinColor.RED), + AnsiFore.GREEN: (winterm.fore, WinColor.GREEN), + AnsiFore.YELLOW: (winterm.fore, WinColor.YELLOW), + AnsiFore.BLUE: (winterm.fore, WinColor.BLUE), + AnsiFore.MAGENTA: (winterm.fore, WinColor.MAGENTA), + AnsiFore.CYAN: (winterm.fore, WinColor.CYAN), + AnsiFore.WHITE: (winterm.fore, WinColor.GREY), + AnsiFore.RESET: (winterm.fore, ), + AnsiFore.LIGHTBLACK_EX: (winterm.fore, WinColor.BLACK, True), + AnsiFore.LIGHTRED_EX: (winterm.fore, WinColor.RED, True), + AnsiFore.LIGHTGREEN_EX: (winterm.fore, WinColor.GREEN, True), + AnsiFore.LIGHTYELLOW_EX: (winterm.fore, WinColor.YELLOW, True), + AnsiFore.LIGHTBLUE_EX: (winterm.fore, WinColor.BLUE, True), + AnsiFore.LIGHTMAGENTA_EX: (winterm.fore, WinColor.MAGENTA, True), + AnsiFore.LIGHTCYAN_EX: (winterm.fore, WinColor.CYAN, True), + AnsiFore.LIGHTWHITE_EX: (winterm.fore, WinColor.GREY, True), + AnsiBack.BLACK: (winterm.back, WinColor.BLACK), + AnsiBack.RED: (winterm.back, WinColor.RED), + AnsiBack.GREEN: (winterm.back, WinColor.GREEN), + AnsiBack.YELLOW: (winterm.back, WinColor.YELLOW), + AnsiBack.BLUE: (winterm.back, WinColor.BLUE), + AnsiBack.MAGENTA: (winterm.back, WinColor.MAGENTA), + AnsiBack.CYAN: (winterm.back, WinColor.CYAN), + AnsiBack.WHITE: (winterm.back, WinColor.GREY), + AnsiBack.RESET: (winterm.back, ), + AnsiBack.LIGHTBLACK_EX: (winterm.back, WinColor.BLACK, True), + AnsiBack.LIGHTRED_EX: (winterm.back, WinColor.RED, True), + AnsiBack.LIGHTGREEN_EX: (winterm.back, WinColor.GREEN, True), + AnsiBack.LIGHTYELLOW_EX: (winterm.back, WinColor.YELLOW, True), + AnsiBack.LIGHTBLUE_EX: (winterm.back, WinColor.BLUE, True), + AnsiBack.LIGHTMAGENTA_EX: (winterm.back, WinColor.MAGENTA, True), + AnsiBack.LIGHTCYAN_EX: (winterm.back, WinColor.CYAN, True), + AnsiBack.LIGHTWHITE_EX: (winterm.back, WinColor.GREY, True), + } + return dict() + + def write(self, text): + if self.strip or self.convert: + self.write_and_convert(text) + else: + self.wrapped.write(text) + self.wrapped.flush() + if self.autoreset: + self.reset_all() + + + def reset_all(self): + if self.convert: + self.call_win32('m', (0,)) + elif not self.strip and not self.stream.closed: + self.wrapped.write(Style.RESET_ALL) + + + def write_and_convert(self, text): + ''' + Write the given text to our wrapped stream, stripping any ANSI + sequences from the text, and optionally converting them into win32 + calls. + ''' + cursor = 0 + text = self.convert_osc(text) + for match in self.ANSI_CSI_RE.finditer(text): + start, end = match.span() + self.write_plain_text(text, cursor, start) + self.convert_ansi(*match.groups()) + cursor = end + self.write_plain_text(text, cursor, len(text)) + + + def write_plain_text(self, text, start, end): + if start < end: + self.wrapped.write(text[start:end]) + self.wrapped.flush() + + + def convert_ansi(self, paramstring, command): + if self.convert: + params = self.extract_params(command, paramstring) + self.call_win32(command, params) + + + def extract_params(self, command, paramstring): + if command in 'Hf': + params = tuple(int(p) if len(p) != 0 else 1 for p in paramstring.split(';')) + while len(params) < 2: + # defaults: + params = params + (1,) + else: + params = tuple(int(p) for p in paramstring.split(';') if len(p) != 0) + if len(params) == 0: + # defaults: + if command in 'JKm': + params = (0,) + elif command in 'ABCD': + params = (1,) + + return params + + + def call_win32(self, command, params): + if command == 'm': + for param in params: + if param in self.win32_calls: + func_args = self.win32_calls[param] + func = func_args[0] + args = func_args[1:] + kwargs = dict(on_stderr=self.on_stderr) + func(*args, **kwargs) + elif command in 'J': + winterm.erase_screen(params[0], on_stderr=self.on_stderr) + elif command in 'K': + winterm.erase_line(params[0], on_stderr=self.on_stderr) + elif command in 'Hf': # cursor position - absolute + winterm.set_cursor_position(params, on_stderr=self.on_stderr) + elif command in 'ABCD': # cursor position - relative + n = params[0] + # A - up, B - down, C - forward, D - back + x, y = {'A': (0, -n), 'B': (0, n), 'C': (n, 0), 'D': (-n, 0)}[command] + winterm.cursor_adjust(x, y, on_stderr=self.on_stderr) + + + def convert_osc(self, text): + for match in self.ANSI_OSC_RE.finditer(text): + start, end = match.span() + text = text[:start] + text[end:] + paramstring, command = match.groups() + if command == BEL: + if paramstring.count(";") == 1: + params = paramstring.split(";") + # 0 - change title and icon (we will only change title) + # 1 - change icon (we don't support this) + # 2 - change title + if params[0] in '02': + winterm.set_title(params[1]) + return text + + + def flush(self): + self.wrapped.flush() diff --git a/venv/lib/python3.10/site-packages/colorama/initialise.py b/venv/lib/python3.10/site-packages/colorama/initialise.py new file mode 100644 index 0000000..d5fd4b7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/initialise.py @@ -0,0 +1,121 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import atexit +import contextlib +import sys + +from .ansitowin32 import AnsiToWin32 + + +def _wipe_internal_state_for_tests(): + global orig_stdout, orig_stderr + orig_stdout = None + orig_stderr = None + + global wrapped_stdout, wrapped_stderr + wrapped_stdout = None + wrapped_stderr = None + + global atexit_done + atexit_done = False + + global fixed_windows_console + fixed_windows_console = False + + try: + # no-op if it wasn't registered + atexit.unregister(reset_all) + except AttributeError: + # python 2: no atexit.unregister. Oh well, we did our best. + pass + + +def reset_all(): + if AnsiToWin32 is not None: # Issue #74: objects might become None at exit + AnsiToWin32(orig_stdout).reset_all() + + +def init(autoreset=False, convert=None, strip=None, wrap=True): + + if not wrap and any([autoreset, convert, strip]): + raise ValueError('wrap=False conflicts with any other arg=True') + + global wrapped_stdout, wrapped_stderr + global orig_stdout, orig_stderr + + orig_stdout = sys.stdout + orig_stderr = sys.stderr + + if sys.stdout is None: + wrapped_stdout = None + else: + sys.stdout = wrapped_stdout = \ + wrap_stream(orig_stdout, convert, strip, autoreset, wrap) + if sys.stderr is None: + wrapped_stderr = None + else: + sys.stderr = wrapped_stderr = \ + wrap_stream(orig_stderr, convert, strip, autoreset, wrap) + + global atexit_done + if not atexit_done: + atexit.register(reset_all) + atexit_done = True + + +def deinit(): + if orig_stdout is not None: + sys.stdout = orig_stdout + if orig_stderr is not None: + sys.stderr = orig_stderr + + +def just_fix_windows_console(): + global fixed_windows_console + + if sys.platform != "win32": + return + if fixed_windows_console: + return + if wrapped_stdout is not None or wrapped_stderr is not None: + # Someone already ran init() and it did stuff, so we won't second-guess them + return + + # On newer versions of Windows, AnsiToWin32.__init__ will implicitly enable the + # native ANSI support in the console as a side-effect. We only need to actually + # replace sys.stdout/stderr if we're in the old-style conversion mode. + new_stdout = AnsiToWin32(sys.stdout, convert=None, strip=None, autoreset=False) + if new_stdout.convert: + sys.stdout = new_stdout + new_stderr = AnsiToWin32(sys.stderr, convert=None, strip=None, autoreset=False) + if new_stderr.convert: + sys.stderr = new_stderr + + fixed_windows_console = True + +@contextlib.contextmanager +def colorama_text(*args, **kwargs): + init(*args, **kwargs) + try: + yield + finally: + deinit() + + +def reinit(): + if wrapped_stdout is not None: + sys.stdout = wrapped_stdout + if wrapped_stderr is not None: + sys.stderr = wrapped_stderr + + +def wrap_stream(stream, convert, strip, autoreset, wrap): + if wrap: + wrapper = AnsiToWin32(stream, + convert=convert, strip=strip, autoreset=autoreset) + if wrapper.should_wrap(): + stream = wrapper.stream + return stream + + +# Use this for initial setup as well, to reduce code duplication +_wipe_internal_state_for_tests() diff --git a/venv/lib/python3.10/site-packages/colorama/tests/__init__.py b/venv/lib/python3.10/site-packages/colorama/tests/__init__.py new file mode 100644 index 0000000..8c5661e --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/tests/__init__.py @@ -0,0 +1 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. diff --git a/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..5a775f7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/ansi_test.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/ansi_test.cpython-310.pyc new file mode 100644 index 0000000..ab8adbf Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/ansi_test.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/ansitowin32_test.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/ansitowin32_test.cpython-310.pyc new file mode 100644 index 0000000..aaf08f7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/ansitowin32_test.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/initialise_test.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/initialise_test.cpython-310.pyc new file mode 100644 index 0000000..0dac367 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/initialise_test.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/isatty_test.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/isatty_test.cpython-310.pyc new file mode 100644 index 0000000..76f169a Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/isatty_test.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/utils.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..e109720 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/utils.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/winterm_test.cpython-310.pyc b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/winterm_test.cpython-310.pyc new file mode 100644 index 0000000..64296e6 Binary files /dev/null and b/venv/lib/python3.10/site-packages/colorama/tests/__pycache__/winterm_test.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/colorama/tests/ansi_test.py b/venv/lib/python3.10/site-packages/colorama/tests/ansi_test.py new file mode 100644 index 0000000..0a20c80 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/tests/ansi_test.py @@ -0,0 +1,76 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import sys +from unittest import TestCase, main + +from ..ansi import Back, Fore, Style +from ..ansitowin32 import AnsiToWin32 + +stdout_orig = sys.stdout +stderr_orig = sys.stderr + + +class AnsiTest(TestCase): + + def setUp(self): + # sanity check: stdout should be a file or StringIO object. + # It will only be AnsiToWin32 if init() has previously wrapped it + self.assertNotEqual(type(sys.stdout), AnsiToWin32) + self.assertNotEqual(type(sys.stderr), AnsiToWin32) + + def tearDown(self): + sys.stdout = stdout_orig + sys.stderr = stderr_orig + + + def testForeAttributes(self): + self.assertEqual(Fore.BLACK, '\033[30m') + self.assertEqual(Fore.RED, '\033[31m') + self.assertEqual(Fore.GREEN, '\033[32m') + self.assertEqual(Fore.YELLOW, '\033[33m') + self.assertEqual(Fore.BLUE, '\033[34m') + self.assertEqual(Fore.MAGENTA, '\033[35m') + self.assertEqual(Fore.CYAN, '\033[36m') + self.assertEqual(Fore.WHITE, '\033[37m') + self.assertEqual(Fore.RESET, '\033[39m') + + # Check the light, extended versions. + self.assertEqual(Fore.LIGHTBLACK_EX, '\033[90m') + self.assertEqual(Fore.LIGHTRED_EX, '\033[91m') + self.assertEqual(Fore.LIGHTGREEN_EX, '\033[92m') + self.assertEqual(Fore.LIGHTYELLOW_EX, '\033[93m') + self.assertEqual(Fore.LIGHTBLUE_EX, '\033[94m') + self.assertEqual(Fore.LIGHTMAGENTA_EX, '\033[95m') + self.assertEqual(Fore.LIGHTCYAN_EX, '\033[96m') + self.assertEqual(Fore.LIGHTWHITE_EX, '\033[97m') + + + def testBackAttributes(self): + self.assertEqual(Back.BLACK, '\033[40m') + self.assertEqual(Back.RED, '\033[41m') + self.assertEqual(Back.GREEN, '\033[42m') + self.assertEqual(Back.YELLOW, '\033[43m') + self.assertEqual(Back.BLUE, '\033[44m') + self.assertEqual(Back.MAGENTA, '\033[45m') + self.assertEqual(Back.CYAN, '\033[46m') + self.assertEqual(Back.WHITE, '\033[47m') + self.assertEqual(Back.RESET, '\033[49m') + + # Check the light, extended versions. + self.assertEqual(Back.LIGHTBLACK_EX, '\033[100m') + self.assertEqual(Back.LIGHTRED_EX, '\033[101m') + self.assertEqual(Back.LIGHTGREEN_EX, '\033[102m') + self.assertEqual(Back.LIGHTYELLOW_EX, '\033[103m') + self.assertEqual(Back.LIGHTBLUE_EX, '\033[104m') + self.assertEqual(Back.LIGHTMAGENTA_EX, '\033[105m') + self.assertEqual(Back.LIGHTCYAN_EX, '\033[106m') + self.assertEqual(Back.LIGHTWHITE_EX, '\033[107m') + + + def testStyleAttributes(self): + self.assertEqual(Style.DIM, '\033[2m') + self.assertEqual(Style.NORMAL, '\033[22m') + self.assertEqual(Style.BRIGHT, '\033[1m') + + +if __name__ == '__main__': + main() diff --git a/venv/lib/python3.10/site-packages/colorama/tests/ansitowin32_test.py b/venv/lib/python3.10/site-packages/colorama/tests/ansitowin32_test.py new file mode 100644 index 0000000..91ca551 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/tests/ansitowin32_test.py @@ -0,0 +1,294 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +from io import StringIO, TextIOWrapper +from unittest import TestCase, main +try: + from contextlib import ExitStack +except ImportError: + # python 2 + from contextlib2 import ExitStack + +try: + from unittest.mock import MagicMock, Mock, patch +except ImportError: + from mock import MagicMock, Mock, patch + +from ..ansitowin32 import AnsiToWin32, StreamWrapper +from ..win32 import ENABLE_VIRTUAL_TERMINAL_PROCESSING +from .utils import osname + + +class StreamWrapperTest(TestCase): + + def testIsAProxy(self): + mockStream = Mock() + wrapper = StreamWrapper(mockStream, None) + self.assertTrue( wrapper.random_attr is mockStream.random_attr ) + + def testDelegatesWrite(self): + mockStream = Mock() + mockConverter = Mock() + wrapper = StreamWrapper(mockStream, mockConverter) + wrapper.write('hello') + self.assertTrue(mockConverter.write.call_args, (('hello',), {})) + + def testDelegatesContext(self): + mockConverter = Mock() + s = StringIO() + with StreamWrapper(s, mockConverter) as fp: + fp.write(u'hello') + self.assertTrue(s.closed) + + def testProxyNoContextManager(self): + mockStream = MagicMock() + mockStream.__enter__.side_effect = AttributeError() + mockConverter = Mock() + with self.assertRaises(AttributeError) as excinfo: + with StreamWrapper(mockStream, mockConverter) as wrapper: + wrapper.write('hello') + + def test_closed_shouldnt_raise_on_closed_stream(self): + stream = StringIO() + stream.close() + wrapper = StreamWrapper(stream, None) + self.assertEqual(wrapper.closed, True) + + def test_closed_shouldnt_raise_on_detached_stream(self): + stream = TextIOWrapper(StringIO()) + stream.detach() + wrapper = StreamWrapper(stream, None) + self.assertEqual(wrapper.closed, True) + +class AnsiToWin32Test(TestCase): + + def testInit(self): + mockStdout = Mock() + auto = Mock() + stream = AnsiToWin32(mockStdout, autoreset=auto) + self.assertEqual(stream.wrapped, mockStdout) + self.assertEqual(stream.autoreset, auto) + + @patch('colorama.ansitowin32.winterm', None) + @patch('colorama.ansitowin32.winapi_test', lambda *_: True) + def testStripIsTrueOnWindows(self): + with osname('nt'): + mockStdout = Mock() + stream = AnsiToWin32(mockStdout) + self.assertTrue(stream.strip) + + def testStripIsFalseOffWindows(self): + with osname('posix'): + mockStdout = Mock(closed=False) + stream = AnsiToWin32(mockStdout) + self.assertFalse(stream.strip) + + def testWriteStripsAnsi(self): + mockStdout = Mock() + stream = AnsiToWin32(mockStdout) + stream.wrapped = Mock() + stream.write_and_convert = Mock() + stream.strip = True + + stream.write('abc') + + self.assertFalse(stream.wrapped.write.called) + self.assertEqual(stream.write_and_convert.call_args, (('abc',), {})) + + def testWriteDoesNotStripAnsi(self): + mockStdout = Mock() + stream = AnsiToWin32(mockStdout) + stream.wrapped = Mock() + stream.write_and_convert = Mock() + stream.strip = False + stream.convert = False + + stream.write('abc') + + self.assertFalse(stream.write_and_convert.called) + self.assertEqual(stream.wrapped.write.call_args, (('abc',), {})) + + def assert_autoresets(self, convert, autoreset=True): + stream = AnsiToWin32(Mock()) + stream.convert = convert + stream.reset_all = Mock() + stream.autoreset = autoreset + stream.winterm = Mock() + + stream.write('abc') + + self.assertEqual(stream.reset_all.called, autoreset) + + def testWriteAutoresets(self): + self.assert_autoresets(convert=True) + self.assert_autoresets(convert=False) + self.assert_autoresets(convert=True, autoreset=False) + self.assert_autoresets(convert=False, autoreset=False) + + def testWriteAndConvertWritesPlainText(self): + stream = AnsiToWin32(Mock()) + stream.write_and_convert( 'abc' ) + self.assertEqual( stream.wrapped.write.call_args, (('abc',), {}) ) + + def testWriteAndConvertStripsAllValidAnsi(self): + stream = AnsiToWin32(Mock()) + stream.call_win32 = Mock() + data = [ + 'abc\033[mdef', + 'abc\033[0mdef', + 'abc\033[2mdef', + 'abc\033[02mdef', + 'abc\033[002mdef', + 'abc\033[40mdef', + 'abc\033[040mdef', + 'abc\033[0;1mdef', + 'abc\033[40;50mdef', + 'abc\033[50;30;40mdef', + 'abc\033[Adef', + 'abc\033[0Gdef', + 'abc\033[1;20;128Hdef', + ] + for datum in data: + stream.wrapped.write.reset_mock() + stream.write_and_convert( datum ) + self.assertEqual( + [args[0] for args in stream.wrapped.write.call_args_list], + [ ('abc',), ('def',) ] + ) + + def testWriteAndConvertSkipsEmptySnippets(self): + stream = AnsiToWin32(Mock()) + stream.call_win32 = Mock() + stream.write_and_convert( '\033[40m\033[41m' ) + self.assertFalse( stream.wrapped.write.called ) + + def testWriteAndConvertCallsWin32WithParamsAndCommand(self): + stream = AnsiToWin32(Mock()) + stream.convert = True + stream.call_win32 = Mock() + stream.extract_params = Mock(return_value='params') + data = { + 'abc\033[adef': ('a', 'params'), + 'abc\033[;;bdef': ('b', 'params'), + 'abc\033[0cdef': ('c', 'params'), + 'abc\033[;;0;;Gdef': ('G', 'params'), + 'abc\033[1;20;128Hdef': ('H', 'params'), + } + for datum, expected in data.items(): + stream.call_win32.reset_mock() + stream.write_and_convert( datum ) + self.assertEqual( stream.call_win32.call_args[0], expected ) + + def test_reset_all_shouldnt_raise_on_closed_orig_stdout(self): + stream = StringIO() + converter = AnsiToWin32(stream) + stream.close() + + converter.reset_all() + + def test_wrap_shouldnt_raise_on_closed_orig_stdout(self): + stream = StringIO() + stream.close() + with \ + patch("colorama.ansitowin32.os.name", "nt"), \ + patch("colorama.ansitowin32.winapi_test", lambda: True): + converter = AnsiToWin32(stream) + self.assertTrue(converter.strip) + self.assertFalse(converter.convert) + + def test_wrap_shouldnt_raise_on_missing_closed_attr(self): + with \ + patch("colorama.ansitowin32.os.name", "nt"), \ + patch("colorama.ansitowin32.winapi_test", lambda: True): + converter = AnsiToWin32(object()) + self.assertTrue(converter.strip) + self.assertFalse(converter.convert) + + def testExtractParams(self): + stream = AnsiToWin32(Mock()) + data = { + '': (0,), + ';;': (0,), + '2': (2,), + ';;002;;': (2,), + '0;1': (0, 1), + ';;003;;456;;': (3, 456), + '11;22;33;44;55': (11, 22, 33, 44, 55), + } + for datum, expected in data.items(): + self.assertEqual(stream.extract_params('m', datum), expected) + + def testCallWin32UsesLookup(self): + listener = Mock() + stream = AnsiToWin32(listener) + stream.win32_calls = { + 1: (lambda *_, **__: listener(11),), + 2: (lambda *_, **__: listener(22),), + 3: (lambda *_, **__: listener(33),), + } + stream.call_win32('m', (3, 1, 99, 2)) + self.assertEqual( + [a[0][0] for a in listener.call_args_list], + [33, 11, 22] ) + + def test_osc_codes(self): + mockStdout = Mock() + stream = AnsiToWin32(mockStdout, convert=True) + with patch('colorama.ansitowin32.winterm') as winterm: + data = [ + '\033]0\x07', # missing arguments + '\033]0;foo\x08', # wrong OSC command + '\033]0;colorama_test_title\x07', # should work + '\033]1;colorama_test_title\x07', # wrong set command + '\033]2;colorama_test_title\x07', # should work + '\033]' + ';' * 64 + '\x08', # see issue #247 + ] + for code in data: + stream.write(code) + self.assertEqual(winterm.set_title.call_count, 2) + + def test_native_windows_ansi(self): + with ExitStack() as stack: + def p(a, b): + stack.enter_context(patch(a, b, create=True)) + # Pretend to be on Windows + p("colorama.ansitowin32.os.name", "nt") + p("colorama.ansitowin32.winapi_test", lambda: True) + p("colorama.win32.winapi_test", lambda: True) + p("colorama.winterm.win32.windll", "non-None") + p("colorama.winterm.get_osfhandle", lambda _: 1234) + + # Pretend that our mock stream has native ANSI support + p( + "colorama.winterm.win32.GetConsoleMode", + lambda _: ENABLE_VIRTUAL_TERMINAL_PROCESSING, + ) + SetConsoleMode = Mock() + p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode) + + stdout = Mock() + stdout.closed = False + stdout.isatty.return_value = True + stdout.fileno.return_value = 1 + + # Our fake console says it has native vt support, so AnsiToWin32 should + # enable that support and do nothing else. + stream = AnsiToWin32(stdout) + SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING) + self.assertFalse(stream.strip) + self.assertFalse(stream.convert) + self.assertFalse(stream.should_wrap()) + + # Now let's pretend we're on an old Windows console, that doesn't have + # native ANSI support. + p("colorama.winterm.win32.GetConsoleMode", lambda _: 0) + SetConsoleMode = Mock() + p("colorama.winterm.win32.SetConsoleMode", SetConsoleMode) + + stream = AnsiToWin32(stdout) + SetConsoleMode.assert_called_with(1234, ENABLE_VIRTUAL_TERMINAL_PROCESSING) + self.assertTrue(stream.strip) + self.assertTrue(stream.convert) + self.assertTrue(stream.should_wrap()) + + +if __name__ == '__main__': + main() diff --git a/venv/lib/python3.10/site-packages/colorama/tests/initialise_test.py b/venv/lib/python3.10/site-packages/colorama/tests/initialise_test.py new file mode 100644 index 0000000..89f9b07 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/tests/initialise_test.py @@ -0,0 +1,189 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import sys +from unittest import TestCase, main, skipUnless + +try: + from unittest.mock import patch, Mock +except ImportError: + from mock import patch, Mock + +from ..ansitowin32 import StreamWrapper +from ..initialise import init, just_fix_windows_console, _wipe_internal_state_for_tests +from .utils import osname, replace_by + +orig_stdout = sys.stdout +orig_stderr = sys.stderr + + +class InitTest(TestCase): + + @skipUnless(sys.stdout.isatty(), "sys.stdout is not a tty") + def setUp(self): + # sanity check + self.assertNotWrapped() + + def tearDown(self): + _wipe_internal_state_for_tests() + sys.stdout = orig_stdout + sys.stderr = orig_stderr + + def assertWrapped(self): + self.assertIsNot(sys.stdout, orig_stdout, 'stdout should be wrapped') + self.assertIsNot(sys.stderr, orig_stderr, 'stderr should be wrapped') + self.assertTrue(isinstance(sys.stdout, StreamWrapper), + 'bad stdout wrapper') + self.assertTrue(isinstance(sys.stderr, StreamWrapper), + 'bad stderr wrapper') + + def assertNotWrapped(self): + self.assertIs(sys.stdout, orig_stdout, 'stdout should not be wrapped') + self.assertIs(sys.stderr, orig_stderr, 'stderr should not be wrapped') + + @patch('colorama.initialise.reset_all') + @patch('colorama.ansitowin32.winapi_test', lambda *_: True) + @patch('colorama.ansitowin32.enable_vt_processing', lambda *_: False) + def testInitWrapsOnWindows(self, _): + with osname("nt"): + init() + self.assertWrapped() + + @patch('colorama.initialise.reset_all') + @patch('colorama.ansitowin32.winapi_test', lambda *_: False) + def testInitDoesntWrapOnEmulatedWindows(self, _): + with osname("nt"): + init() + self.assertNotWrapped() + + def testInitDoesntWrapOnNonWindows(self): + with osname("posix"): + init() + self.assertNotWrapped() + + def testInitDoesntWrapIfNone(self): + with replace_by(None): + init() + # We can't use assertNotWrapped here because replace_by(None) + # changes stdout/stderr already. + self.assertIsNone(sys.stdout) + self.assertIsNone(sys.stderr) + + def testInitAutoresetOnWrapsOnAllPlatforms(self): + with osname("posix"): + init(autoreset=True) + self.assertWrapped() + + def testInitWrapOffDoesntWrapOnWindows(self): + with osname("nt"): + init(wrap=False) + self.assertNotWrapped() + + def testInitWrapOffIncompatibleWithAutoresetOn(self): + self.assertRaises(ValueError, lambda: init(autoreset=True, wrap=False)) + + @patch('colorama.win32.SetConsoleTextAttribute') + @patch('colorama.initialise.AnsiToWin32') + def testAutoResetPassedOn(self, mockATW32, _): + with osname("nt"): + init(autoreset=True) + self.assertEqual(len(mockATW32.call_args_list), 2) + self.assertEqual(mockATW32.call_args_list[1][1]['autoreset'], True) + self.assertEqual(mockATW32.call_args_list[0][1]['autoreset'], True) + + @patch('colorama.initialise.AnsiToWin32') + def testAutoResetChangeable(self, mockATW32): + with osname("nt"): + init() + + init(autoreset=True) + self.assertEqual(len(mockATW32.call_args_list), 4) + self.assertEqual(mockATW32.call_args_list[2][1]['autoreset'], True) + self.assertEqual(mockATW32.call_args_list[3][1]['autoreset'], True) + + init() + self.assertEqual(len(mockATW32.call_args_list), 6) + self.assertEqual( + mockATW32.call_args_list[4][1]['autoreset'], False) + self.assertEqual( + mockATW32.call_args_list[5][1]['autoreset'], False) + + + @patch('colorama.initialise.atexit.register') + def testAtexitRegisteredOnlyOnce(self, mockRegister): + init() + self.assertTrue(mockRegister.called) + mockRegister.reset_mock() + init() + self.assertFalse(mockRegister.called) + + +class JustFixWindowsConsoleTest(TestCase): + def _reset(self): + _wipe_internal_state_for_tests() + sys.stdout = orig_stdout + sys.stderr = orig_stderr + + def tearDown(self): + self._reset() + + @patch("colorama.ansitowin32.winapi_test", lambda: True) + def testJustFixWindowsConsole(self): + if sys.platform != "win32": + # just_fix_windows_console should be a no-op + just_fix_windows_console() + self.assertIs(sys.stdout, orig_stdout) + self.assertIs(sys.stderr, orig_stderr) + else: + def fake_std(): + # Emulate stdout=not a tty, stderr=tty + # to check that we handle both cases correctly + stdout = Mock() + stdout.closed = False + stdout.isatty.return_value = False + stdout.fileno.return_value = 1 + sys.stdout = stdout + + stderr = Mock() + stderr.closed = False + stderr.isatty.return_value = True + stderr.fileno.return_value = 2 + sys.stderr = stderr + + for native_ansi in [False, True]: + with patch( + 'colorama.ansitowin32.enable_vt_processing', + lambda *_: native_ansi + ): + self._reset() + fake_std() + + # Regular single-call test + prev_stdout = sys.stdout + prev_stderr = sys.stderr + just_fix_windows_console() + self.assertIs(sys.stdout, prev_stdout) + if native_ansi: + self.assertIs(sys.stderr, prev_stderr) + else: + self.assertIsNot(sys.stderr, prev_stderr) + + # second call without resetting is always a no-op + prev_stdout = sys.stdout + prev_stderr = sys.stderr + just_fix_windows_console() + self.assertIs(sys.stdout, prev_stdout) + self.assertIs(sys.stderr, prev_stderr) + + self._reset() + fake_std() + + # If init() runs first, just_fix_windows_console should be a no-op + init() + prev_stdout = sys.stdout + prev_stderr = sys.stderr + just_fix_windows_console() + self.assertIs(prev_stdout, sys.stdout) + self.assertIs(prev_stderr, sys.stderr) + + +if __name__ == '__main__': + main() diff --git a/venv/lib/python3.10/site-packages/colorama/tests/isatty_test.py b/venv/lib/python3.10/site-packages/colorama/tests/isatty_test.py new file mode 100644 index 0000000..0f84e4b --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/tests/isatty_test.py @@ -0,0 +1,57 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import sys +from unittest import TestCase, main + +from ..ansitowin32 import StreamWrapper, AnsiToWin32 +from .utils import pycharm, replace_by, replace_original_by, StreamTTY, StreamNonTTY + + +def is_a_tty(stream): + return StreamWrapper(stream, None).isatty() + +class IsattyTest(TestCase): + + def test_TTY(self): + tty = StreamTTY() + self.assertTrue(is_a_tty(tty)) + with pycharm(): + self.assertTrue(is_a_tty(tty)) + + def test_nonTTY(self): + non_tty = StreamNonTTY() + self.assertFalse(is_a_tty(non_tty)) + with pycharm(): + self.assertFalse(is_a_tty(non_tty)) + + def test_withPycharm(self): + with pycharm(): + self.assertTrue(is_a_tty(sys.stderr)) + self.assertTrue(is_a_tty(sys.stdout)) + + def test_withPycharmTTYOverride(self): + tty = StreamTTY() + with pycharm(), replace_by(tty): + self.assertTrue(is_a_tty(tty)) + + def test_withPycharmNonTTYOverride(self): + non_tty = StreamNonTTY() + with pycharm(), replace_by(non_tty): + self.assertFalse(is_a_tty(non_tty)) + + def test_withPycharmNoneOverride(self): + with pycharm(): + with replace_by(None), replace_original_by(None): + self.assertFalse(is_a_tty(None)) + self.assertFalse(is_a_tty(StreamNonTTY())) + self.assertTrue(is_a_tty(StreamTTY())) + + def test_withPycharmStreamWrapped(self): + with pycharm(): + self.assertTrue(AnsiToWin32(StreamTTY()).stream.isatty()) + self.assertFalse(AnsiToWin32(StreamNonTTY()).stream.isatty()) + self.assertTrue(AnsiToWin32(sys.stdout).stream.isatty()) + self.assertTrue(AnsiToWin32(sys.stderr).stream.isatty()) + + +if __name__ == '__main__': + main() diff --git a/venv/lib/python3.10/site-packages/colorama/tests/utils.py b/venv/lib/python3.10/site-packages/colorama/tests/utils.py new file mode 100644 index 0000000..472fafb --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/tests/utils.py @@ -0,0 +1,49 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +from contextlib import contextmanager +from io import StringIO +import sys +import os + + +class StreamTTY(StringIO): + def isatty(self): + return True + +class StreamNonTTY(StringIO): + def isatty(self): + return False + +@contextmanager +def osname(name): + orig = os.name + os.name = name + yield + os.name = orig + +@contextmanager +def replace_by(stream): + orig_stdout = sys.stdout + orig_stderr = sys.stderr + sys.stdout = stream + sys.stderr = stream + yield + sys.stdout = orig_stdout + sys.stderr = orig_stderr + +@contextmanager +def replace_original_by(stream): + orig_stdout = sys.__stdout__ + orig_stderr = sys.__stderr__ + sys.__stdout__ = stream + sys.__stderr__ = stream + yield + sys.__stdout__ = orig_stdout + sys.__stderr__ = orig_stderr + +@contextmanager +def pycharm(): + os.environ["PYCHARM_HOSTED"] = "1" + non_tty = StreamNonTTY() + with replace_by(non_tty), replace_original_by(non_tty): + yield + del os.environ["PYCHARM_HOSTED"] diff --git a/venv/lib/python3.10/site-packages/colorama/tests/winterm_test.py b/venv/lib/python3.10/site-packages/colorama/tests/winterm_test.py new file mode 100644 index 0000000..d0955f9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/tests/winterm_test.py @@ -0,0 +1,131 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +import sys +from unittest import TestCase, main, skipUnless + +try: + from unittest.mock import Mock, patch +except ImportError: + from mock import Mock, patch + +from ..winterm import WinColor, WinStyle, WinTerm + + +class WinTermTest(TestCase): + + @patch('colorama.winterm.win32') + def testInit(self, mockWin32): + mockAttr = Mock() + mockAttr.wAttributes = 7 + 6 * 16 + 8 + mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr + term = WinTerm() + self.assertEqual(term._fore, 7) + self.assertEqual(term._back, 6) + self.assertEqual(term._style, 8) + + @skipUnless(sys.platform.startswith("win"), "requires Windows") + def testGetAttrs(self): + term = WinTerm() + + term._fore = 0 + term._back = 0 + term._style = 0 + self.assertEqual(term.get_attrs(), 0) + + term._fore = WinColor.YELLOW + self.assertEqual(term.get_attrs(), WinColor.YELLOW) + + term._back = WinColor.MAGENTA + self.assertEqual( + term.get_attrs(), + WinColor.YELLOW + WinColor.MAGENTA * 16) + + term._style = WinStyle.BRIGHT + self.assertEqual( + term.get_attrs(), + WinColor.YELLOW + WinColor.MAGENTA * 16 + WinStyle.BRIGHT) + + @patch('colorama.winterm.win32') + def testResetAll(self, mockWin32): + mockAttr = Mock() + mockAttr.wAttributes = 1 + 2 * 16 + 8 + mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr + term = WinTerm() + + term.set_console = Mock() + term._fore = -1 + term._back = -1 + term._style = -1 + + term.reset_all() + + self.assertEqual(term._fore, 1) + self.assertEqual(term._back, 2) + self.assertEqual(term._style, 8) + self.assertEqual(term.set_console.called, True) + + @skipUnless(sys.platform.startswith("win"), "requires Windows") + def testFore(self): + term = WinTerm() + term.set_console = Mock() + term._fore = 0 + + term.fore(5) + + self.assertEqual(term._fore, 5) + self.assertEqual(term.set_console.called, True) + + @skipUnless(sys.platform.startswith("win"), "requires Windows") + def testBack(self): + term = WinTerm() + term.set_console = Mock() + term._back = 0 + + term.back(5) + + self.assertEqual(term._back, 5) + self.assertEqual(term.set_console.called, True) + + @skipUnless(sys.platform.startswith("win"), "requires Windows") + def testStyle(self): + term = WinTerm() + term.set_console = Mock() + term._style = 0 + + term.style(22) + + self.assertEqual(term._style, 22) + self.assertEqual(term.set_console.called, True) + + @patch('colorama.winterm.win32') + def testSetConsole(self, mockWin32): + mockAttr = Mock() + mockAttr.wAttributes = 0 + mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr + term = WinTerm() + term.windll = Mock() + + term.set_console() + + self.assertEqual( + mockWin32.SetConsoleTextAttribute.call_args, + ((mockWin32.STDOUT, term.get_attrs()), {}) + ) + + @patch('colorama.winterm.win32') + def testSetConsoleOnStderr(self, mockWin32): + mockAttr = Mock() + mockAttr.wAttributes = 0 + mockWin32.GetConsoleScreenBufferInfo.return_value = mockAttr + term = WinTerm() + term.windll = Mock() + + term.set_console(on_stderr=True) + + self.assertEqual( + mockWin32.SetConsoleTextAttribute.call_args, + ((mockWin32.STDERR, term.get_attrs()), {}) + ) + + +if __name__ == '__main__': + main() diff --git a/venv/lib/python3.10/site-packages/colorama/win32.py b/venv/lib/python3.10/site-packages/colorama/win32.py new file mode 100644 index 0000000..841b0e2 --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/win32.py @@ -0,0 +1,180 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. + +# from winbase.h +STDOUT = -11 +STDERR = -12 + +ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004 + +try: + import ctypes + from ctypes import LibraryLoader + windll = LibraryLoader(ctypes.WinDLL) + from ctypes import wintypes +except (AttributeError, ImportError): + windll = None + SetConsoleTextAttribute = lambda *_: None + winapi_test = lambda *_: None +else: + from ctypes import byref, Structure, c_char, POINTER + + COORD = wintypes._COORD + + class CONSOLE_SCREEN_BUFFER_INFO(Structure): + """struct in wincon.h.""" + _fields_ = [ + ("dwSize", COORD), + ("dwCursorPosition", COORD), + ("wAttributes", wintypes.WORD), + ("srWindow", wintypes.SMALL_RECT), + ("dwMaximumWindowSize", COORD), + ] + def __str__(self): + return '(%d,%d,%d,%d,%d,%d,%d,%d,%d,%d,%d)' % ( + self.dwSize.Y, self.dwSize.X + , self.dwCursorPosition.Y, self.dwCursorPosition.X + , self.wAttributes + , self.srWindow.Top, self.srWindow.Left, self.srWindow.Bottom, self.srWindow.Right + , self.dwMaximumWindowSize.Y, self.dwMaximumWindowSize.X + ) + + _GetStdHandle = windll.kernel32.GetStdHandle + _GetStdHandle.argtypes = [ + wintypes.DWORD, + ] + _GetStdHandle.restype = wintypes.HANDLE + + _GetConsoleScreenBufferInfo = windll.kernel32.GetConsoleScreenBufferInfo + _GetConsoleScreenBufferInfo.argtypes = [ + wintypes.HANDLE, + POINTER(CONSOLE_SCREEN_BUFFER_INFO), + ] + _GetConsoleScreenBufferInfo.restype = wintypes.BOOL + + _SetConsoleTextAttribute = windll.kernel32.SetConsoleTextAttribute + _SetConsoleTextAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + ] + _SetConsoleTextAttribute.restype = wintypes.BOOL + + _SetConsoleCursorPosition = windll.kernel32.SetConsoleCursorPosition + _SetConsoleCursorPosition.argtypes = [ + wintypes.HANDLE, + COORD, + ] + _SetConsoleCursorPosition.restype = wintypes.BOOL + + _FillConsoleOutputCharacterA = windll.kernel32.FillConsoleOutputCharacterA + _FillConsoleOutputCharacterA.argtypes = [ + wintypes.HANDLE, + c_char, + wintypes.DWORD, + COORD, + POINTER(wintypes.DWORD), + ] + _FillConsoleOutputCharacterA.restype = wintypes.BOOL + + _FillConsoleOutputAttribute = windll.kernel32.FillConsoleOutputAttribute + _FillConsoleOutputAttribute.argtypes = [ + wintypes.HANDLE, + wintypes.WORD, + wintypes.DWORD, + COORD, + POINTER(wintypes.DWORD), + ] + _FillConsoleOutputAttribute.restype = wintypes.BOOL + + _SetConsoleTitleW = windll.kernel32.SetConsoleTitleW + _SetConsoleTitleW.argtypes = [ + wintypes.LPCWSTR + ] + _SetConsoleTitleW.restype = wintypes.BOOL + + _GetConsoleMode = windll.kernel32.GetConsoleMode + _GetConsoleMode.argtypes = [ + wintypes.HANDLE, + POINTER(wintypes.DWORD) + ] + _GetConsoleMode.restype = wintypes.BOOL + + _SetConsoleMode = windll.kernel32.SetConsoleMode + _SetConsoleMode.argtypes = [ + wintypes.HANDLE, + wintypes.DWORD + ] + _SetConsoleMode.restype = wintypes.BOOL + + def _winapi_test(handle): + csbi = CONSOLE_SCREEN_BUFFER_INFO() + success = _GetConsoleScreenBufferInfo( + handle, byref(csbi)) + return bool(success) + + def winapi_test(): + return any(_winapi_test(h) for h in + (_GetStdHandle(STDOUT), _GetStdHandle(STDERR))) + + def GetConsoleScreenBufferInfo(stream_id=STDOUT): + handle = _GetStdHandle(stream_id) + csbi = CONSOLE_SCREEN_BUFFER_INFO() + success = _GetConsoleScreenBufferInfo( + handle, byref(csbi)) + return csbi + + def SetConsoleTextAttribute(stream_id, attrs): + handle = _GetStdHandle(stream_id) + return _SetConsoleTextAttribute(handle, attrs) + + def SetConsoleCursorPosition(stream_id, position, adjust=True): + position = COORD(*position) + # If the position is out of range, do nothing. + if position.Y <= 0 or position.X <= 0: + return + # Adjust for Windows' SetConsoleCursorPosition: + # 1. being 0-based, while ANSI is 1-based. + # 2. expecting (x,y), while ANSI uses (y,x). + adjusted_position = COORD(position.Y - 1, position.X - 1) + if adjust: + # Adjust for viewport's scroll position + sr = GetConsoleScreenBufferInfo(STDOUT).srWindow + adjusted_position.Y += sr.Top + adjusted_position.X += sr.Left + # Resume normal processing + handle = _GetStdHandle(stream_id) + return _SetConsoleCursorPosition(handle, adjusted_position) + + def FillConsoleOutputCharacter(stream_id, char, length, start): + handle = _GetStdHandle(stream_id) + char = c_char(char.encode()) + length = wintypes.DWORD(length) + num_written = wintypes.DWORD(0) + # Note that this is hard-coded for ANSI (vs wide) bytes. + success = _FillConsoleOutputCharacterA( + handle, char, length, start, byref(num_written)) + return num_written.value + + def FillConsoleOutputAttribute(stream_id, attr, length, start): + ''' FillConsoleOutputAttribute( hConsole, csbi.wAttributes, dwConSize, coordScreen, &cCharsWritten )''' + handle = _GetStdHandle(stream_id) + attribute = wintypes.WORD(attr) + length = wintypes.DWORD(length) + num_written = wintypes.DWORD(0) + # Note that this is hard-coded for ANSI (vs wide) bytes. + return _FillConsoleOutputAttribute( + handle, attribute, length, start, byref(num_written)) + + def SetConsoleTitle(title): + return _SetConsoleTitleW(title) + + def GetConsoleMode(handle): + mode = wintypes.DWORD() + success = _GetConsoleMode(handle, byref(mode)) + if not success: + raise ctypes.WinError() + return mode.value + + def SetConsoleMode(handle, mode): + success = _SetConsoleMode(handle, mode) + if not success: + raise ctypes.WinError() diff --git a/venv/lib/python3.10/site-packages/colorama/winterm.py b/venv/lib/python3.10/site-packages/colorama/winterm.py new file mode 100644 index 0000000..aad867e --- /dev/null +++ b/venv/lib/python3.10/site-packages/colorama/winterm.py @@ -0,0 +1,195 @@ +# Copyright Jonathan Hartley 2013. BSD 3-Clause license, see LICENSE file. +try: + from msvcrt import get_osfhandle +except ImportError: + def get_osfhandle(_): + raise OSError("This isn't windows!") + + +from . import win32 + +# from wincon.h +class WinColor(object): + BLACK = 0 + BLUE = 1 + GREEN = 2 + CYAN = 3 + RED = 4 + MAGENTA = 5 + YELLOW = 6 + GREY = 7 + +# from wincon.h +class WinStyle(object): + NORMAL = 0x00 # dim text, dim background + BRIGHT = 0x08 # bright text, dim background + BRIGHT_BACKGROUND = 0x80 # dim text, bright background + +class WinTerm(object): + + def __init__(self): + self._default = win32.GetConsoleScreenBufferInfo(win32.STDOUT).wAttributes + self.set_attrs(self._default) + self._default_fore = self._fore + self._default_back = self._back + self._default_style = self._style + # In order to emulate LIGHT_EX in windows, we borrow the BRIGHT style. + # So that LIGHT_EX colors and BRIGHT style do not clobber each other, + # we track them separately, since LIGHT_EX is overwritten by Fore/Back + # and BRIGHT is overwritten by Style codes. + self._light = 0 + + def get_attrs(self): + return self._fore + self._back * 16 + (self._style | self._light) + + def set_attrs(self, value): + self._fore = value & 7 + self._back = (value >> 4) & 7 + self._style = value & (WinStyle.BRIGHT | WinStyle.BRIGHT_BACKGROUND) + + def reset_all(self, on_stderr=None): + self.set_attrs(self._default) + self.set_console(attrs=self._default) + self._light = 0 + + def fore(self, fore=None, light=False, on_stderr=False): + if fore is None: + fore = self._default_fore + self._fore = fore + # Emulate LIGHT_EX with BRIGHT Style + if light: + self._light |= WinStyle.BRIGHT + else: + self._light &= ~WinStyle.BRIGHT + self.set_console(on_stderr=on_stderr) + + def back(self, back=None, light=False, on_stderr=False): + if back is None: + back = self._default_back + self._back = back + # Emulate LIGHT_EX with BRIGHT_BACKGROUND Style + if light: + self._light |= WinStyle.BRIGHT_BACKGROUND + else: + self._light &= ~WinStyle.BRIGHT_BACKGROUND + self.set_console(on_stderr=on_stderr) + + def style(self, style=None, on_stderr=False): + if style is None: + style = self._default_style + self._style = style + self.set_console(on_stderr=on_stderr) + + def set_console(self, attrs=None, on_stderr=False): + if attrs is None: + attrs = self.get_attrs() + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + win32.SetConsoleTextAttribute(handle, attrs) + + def get_position(self, handle): + position = win32.GetConsoleScreenBufferInfo(handle).dwCursorPosition + # Because Windows coordinates are 0-based, + # and win32.SetConsoleCursorPosition expects 1-based. + position.X += 1 + position.Y += 1 + return position + + def set_cursor_position(self, position=None, on_stderr=False): + if position is None: + # I'm not currently tracking the position, so there is no default. + # position = self.get_position() + return + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + win32.SetConsoleCursorPosition(handle, position) + + def cursor_adjust(self, x, y, on_stderr=False): + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + position = self.get_position(handle) + adjusted_position = (position.Y + y, position.X + x) + win32.SetConsoleCursorPosition(handle, adjusted_position, adjust=False) + + def erase_screen(self, mode=0, on_stderr=False): + # 0 should clear from the cursor to the end of the screen. + # 1 should clear from the cursor to the beginning of the screen. + # 2 should clear the entire screen, and move cursor to (1,1) + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + csbi = win32.GetConsoleScreenBufferInfo(handle) + # get the number of character cells in the current buffer + cells_in_screen = csbi.dwSize.X * csbi.dwSize.Y + # get number of character cells before current cursor position + cells_before_cursor = csbi.dwSize.X * csbi.dwCursorPosition.Y + csbi.dwCursorPosition.X + if mode == 0: + from_coord = csbi.dwCursorPosition + cells_to_erase = cells_in_screen - cells_before_cursor + elif mode == 1: + from_coord = win32.COORD(0, 0) + cells_to_erase = cells_before_cursor + elif mode == 2: + from_coord = win32.COORD(0, 0) + cells_to_erase = cells_in_screen + else: + # invalid mode + return + # fill the entire screen with blanks + win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord) + # now set the buffer's attributes accordingly + win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord) + if mode == 2: + # put the cursor where needed + win32.SetConsoleCursorPosition(handle, (1, 1)) + + def erase_line(self, mode=0, on_stderr=False): + # 0 should clear from the cursor to the end of the line. + # 1 should clear from the cursor to the beginning of the line. + # 2 should clear the entire line. + handle = win32.STDOUT + if on_stderr: + handle = win32.STDERR + csbi = win32.GetConsoleScreenBufferInfo(handle) + if mode == 0: + from_coord = csbi.dwCursorPosition + cells_to_erase = csbi.dwSize.X - csbi.dwCursorPosition.X + elif mode == 1: + from_coord = win32.COORD(0, csbi.dwCursorPosition.Y) + cells_to_erase = csbi.dwCursorPosition.X + elif mode == 2: + from_coord = win32.COORD(0, csbi.dwCursorPosition.Y) + cells_to_erase = csbi.dwSize.X + else: + # invalid mode + return + # fill the entire screen with blanks + win32.FillConsoleOutputCharacter(handle, ' ', cells_to_erase, from_coord) + # now set the buffer's attributes accordingly + win32.FillConsoleOutputAttribute(handle, self.get_attrs(), cells_to_erase, from_coord) + + def set_title(self, title): + win32.SetConsoleTitle(title) + + +def enable_vt_processing(fd): + if win32.windll is None or not win32.winapi_test(): + return False + + try: + handle = get_osfhandle(fd) + mode = win32.GetConsoleMode(handle) + win32.SetConsoleMode( + handle, + mode | win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING, + ) + + mode = win32.GetConsoleMode(handle) + if mode & win32.ENABLE_VIRTUAL_TERMINAL_PROCESSING: + return True + # Can get TypeError in testsuite where 'fd' is a Mock() + except (OSError, TypeError): + return False diff --git a/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/LICENSE.txt b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/LICENSE.txt new file mode 100644 index 0000000..3e01d05 --- /dev/null +++ b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/LICENSE.txt @@ -0,0 +1,27 @@ +Copyright (c) 2005-2025, Michele Simionato +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +* Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in + the documentation and/or other materials provided with the + distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +DAMAGE. diff --git a/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/METADATA b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/METADATA new file mode 100644 index 0000000..be5072e --- /dev/null +++ b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/METADATA @@ -0,0 +1,124 @@ +Metadata-Version: 2.2 +Name: decorator +Version: 5.2.1 +Summary: Decorators for Humans +Author-email: Michele Simionato +License: BSD-2-Clause +Keywords: decorators +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Utilities +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +License-File: LICENSE.txt + +Decorators for Humans +===================== + +The goal of the decorator module is to make it easy to define +signature-preserving function decorators and decorator factories. +It also includes an implementation of multiple dispatch and other niceties +(please check the docs). It is released under a two-clauses +BSD license, i.e. basically you can do whatever you want with it but I am not +responsible. + +Installation +------------- + +If you are lazy, just perform + + ``$ pip install decorator`` + +which will install just the module on your system. + +If you prefer to install the full distribution from source, including +the documentation, clone the `GitHub repo`_ or download the tarball_, unpack it and run + + ``$ pip install .`` + +in the main directory, possibly as superuser. + +.. _tarball: https://pypi.org/project/decorator/#files +.. _GitHub repo: https://github.com/micheles/decorator + +Testing +-------- + +If you have the source code installation you can run the tests with + + `$ python src/tests/test.py -v` + +or (if you have setuptools installed) + + `$ python setup.py test` + +Notice that you may run into trouble if in your system there +is an older version of the decorator module; in such a case remove the +old version. It is safe even to copy the module `decorator.py` over +an existing one, since we kept backward-compatibility for a long time. + +Repository +--------------- + +The project is hosted on GitHub. You can look at the source here: + + https://github.com/micheles/decorator + +Documentation +--------------- + +The documentation has been moved to https://github.com/micheles/decorator/blob/master/docs/documentation.md + +From there you can get a PDF version by simply using the print +functionality of your browser. + +Here is the documentation for previous versions of the module: + +https://github.com/micheles/decorator/blob/4.3.2/docs/tests.documentation.rst +https://github.com/micheles/decorator/blob/4.2.1/docs/tests.documentation.rst +https://github.com/micheles/decorator/blob/4.1.2/docs/tests.documentation.rst +https://github.com/micheles/decorator/blob/4.0.0/documentation.rst +https://github.com/micheles/decorator/blob/3.4.2/documentation.rst + +For the impatient +----------------- + +Here is an example of how to define a family of decorators tracing slow +operations: + +.. code-block:: python + + from decorator import decorator + + @decorator + def warn_slow(func, timelimit=60, *args, **kw): + t0 = time.time() + result = func(*args, **kw) + dt = time.time() - t0 + if dt > timelimit: + logging.warning('%s took %d seconds', func.__name__, dt) + else: + logging.info('%s took %d seconds', func.__name__, dt) + return result + + @warn_slow # warn if it takes more than 1 minute + def preprocess_input_files(inputdir, tempdir): + ... + + @warn_slow(timelimit=600) # warn if it takes more than 10 minutes + def run_calculation(tempdir, outdir): + ... + +Enjoy! diff --git a/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/RECORD b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/RECORD new file mode 100644 index 0000000..9305e88 --- /dev/null +++ b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/RECORD @@ -0,0 +1,9 @@ +__pycache__/decorator.cpython-310.pyc,, +decorator-5.2.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +decorator-5.2.1.dist-info/LICENSE.txt,sha256=kU7m7Xil78FzvaaY8nCERMqdFA_MQIDGDQUD1A2znaY,1308 +decorator-5.2.1.dist-info/METADATA,sha256=i76fp7lNo0QbinX3NBJjSu_GhVp9A8iuab--m-8mKB4,3934 +decorator-5.2.1.dist-info/RECORD,, +decorator-5.2.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91 +decorator-5.2.1.dist-info/pbr.json,sha256=AL84oUUWQHwkd8OCPhLRo2NJjU5MDdmXMqRHv-posqs,47 +decorator-5.2.1.dist-info/top_level.txt,sha256=Kn6eQjo83ctWxXVyBMOYt0_YpjRjBznKYVuNyuC_DSI,10 +decorator.py,sha256=cWnO6cKKohQrXNEG1-Fauiw8djwE3f2PA2gJGofwgXA,17067 diff --git a/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/WHEEL b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/WHEEL new file mode 100644 index 0000000..505164b --- /dev/null +++ b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.8.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/pbr.json b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/pbr.json new file mode 100644 index 0000000..cd04599 --- /dev/null +++ b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/pbr.json @@ -0,0 +1 @@ +{"is_release": false, "git_version": "8608a46"} \ No newline at end of file diff --git a/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/top_level.txt b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/top_level.txt new file mode 100644 index 0000000..3fe18a4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/decorator-5.2.1.dist-info/top_level.txt @@ -0,0 +1 @@ +decorator diff --git a/venv/lib/python3.10/site-packages/decorator.py b/venv/lib/python3.10/site-packages/decorator.py new file mode 100644 index 0000000..40a39f2 --- /dev/null +++ b/venv/lib/python3.10/site-packages/decorator.py @@ -0,0 +1,459 @@ +# ######################### LICENSE ############################ # + +# Copyright (c) 2005-2025, Michele Simionato +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: + +# Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# Redistributions in bytecode form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in +# the documentation and/or other materials provided with the +# distribution. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS +# OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR +# TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE +# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH +# DAMAGE. + +""" +Decorator module, see +https://github.com/micheles/decorator/blob/master/docs/documentation.md +for the documentation. +""" +import re +import sys +import inspect +import operator +import itertools +import functools +from contextlib import _GeneratorContextManager +from inspect import getfullargspec, iscoroutinefunction, isgeneratorfunction + +__version__ = '5.2.1' + +DEF = re.compile(r'\s*def\s*([_\w][_\w\d]*)\s*\(') +POS = inspect.Parameter.POSITIONAL_OR_KEYWORD +EMPTY = inspect.Parameter.empty + + +# this is not used anymore in the core, but kept for backward compatibility +class FunctionMaker(object): + """ + An object with the ability to create functions with a given signature. + It has attributes name, doc, module, signature, defaults, dict and + methods update and make. + """ + + # Atomic get-and-increment provided by the GIL + _compile_count = itertools.count() + + # make pylint happy + args = varargs = varkw = defaults = kwonlyargs = kwonlydefaults = () + + def __init__(self, func=None, name=None, signature=None, + defaults=None, doc=None, module=None, funcdict=None): + self.shortsignature = signature + if func: + # func can be a class or a callable, but not an instance method + self.name = func.__name__ + if self.name == '': # small hack for lambda functions + self.name = '_lambda_' + self.doc = func.__doc__ + self.module = func.__module__ + if inspect.isroutine(func) or isinstance(func, functools.partial): + argspec = getfullargspec(func) + self.annotations = getattr(func, '__annotations__', {}) + for a in ('args', 'varargs', 'varkw', 'defaults', 'kwonlyargs', + 'kwonlydefaults'): + setattr(self, a, getattr(argspec, a)) + for i, arg in enumerate(self.args): + setattr(self, 'arg%d' % i, arg) + allargs = list(self.args) + allshortargs = list(self.args) + if self.varargs: + allargs.append('*' + self.varargs) + allshortargs.append('*' + self.varargs) + elif self.kwonlyargs: + allargs.append('*') # single star syntax + for a in self.kwonlyargs: + allargs.append('%s=None' % a) + allshortargs.append('%s=%s' % (a, a)) + if self.varkw: + allargs.append('**' + self.varkw) + allshortargs.append('**' + self.varkw) + self.signature = ', '.join(allargs) + self.shortsignature = ', '.join(allshortargs) + self.dict = func.__dict__.copy() + # func=None happens when decorating a caller + if name: + self.name = name + if signature is not None: + self.signature = signature + if defaults: + self.defaults = defaults + if doc: + self.doc = doc + if module: + self.module = module + if funcdict: + self.dict = funcdict + # check existence required attributes + assert hasattr(self, 'name') + if not hasattr(self, 'signature'): + raise TypeError('You are decorating a non function: %s' % func) + + def update(self, func, **kw): + """ + Update the signature of func with the data in self + """ + func.__name__ = self.name + func.__doc__ = getattr(self, 'doc', None) + func.__dict__ = getattr(self, 'dict', {}) + func.__defaults__ = self.defaults + func.__kwdefaults__ = self.kwonlydefaults or None + func.__annotations__ = getattr(self, 'annotations', None) + try: + frame = sys._getframe(3) + except AttributeError: # for IronPython and similar implementations + callermodule = '?' + else: + callermodule = frame.f_globals.get('__name__', '?') + func.__module__ = getattr(self, 'module', callermodule) + func.__dict__.update(kw) + + def make(self, src_templ, evaldict=None, addsource=False, **attrs): + """ + Make a new function from a given template and update the signature + """ + src = src_templ % vars(self) # expand name and signature + evaldict = evaldict or {} + mo = DEF.search(src) + if mo is None: + raise SyntaxError('not a valid function template\n%s' % src) + name = mo.group(1) # extract the function name + names = set([name] + [arg.strip(' *') for arg in + self.shortsignature.split(',')]) + for n in names: + if n in ('_func_', '_call_'): + raise NameError('%s is overridden in\n%s' % (n, src)) + + if not src.endswith('\n'): # add a newline for old Pythons + src += '\n' + + # Ensure each generated function has a unique filename for profilers + # (such as cProfile) that depend on the tuple of (, + # , ) being unique. + filename = '' % next(self._compile_count) + try: + code = compile(src, filename, 'single') + exec(code, evaldict) + except Exception: + print('Error in generated code:', file=sys.stderr) + print(src, file=sys.stderr) + raise + func = evaldict[name] + if addsource: + attrs['__source__'] = src + self.update(func, **attrs) + return func + + @classmethod + def create(cls, obj, body, evaldict, defaults=None, + doc=None, module=None, addsource=True, **attrs): + """ + Create a function from the strings name, signature and body. + evaldict is the evaluation dictionary. If addsource is true an + attribute __source__ is added to the result. The attributes attrs + are added, if any. + """ + if isinstance(obj, str): # "name(signature)" + name, rest = obj.strip().split('(', 1) + signature = rest[:-1] # strip a right parens + func = None + else: # a function + name = None + signature = None + func = obj + self = cls(func, name, signature, defaults, doc, module) + ibody = '\n'.join(' ' + line for line in body.splitlines()) + caller = evaldict.get('_call_') # when called from `decorate` + if caller and iscoroutinefunction(caller): + body = ('async def %(name)s(%(signature)s):\n' + ibody).replace( + 'return', 'return await') + else: + body = 'def %(name)s(%(signature)s):\n' + ibody + return self.make(body, evaldict, addsource, **attrs) + + +def fix(args, kwargs, sig): + """ + Fix args and kwargs to be consistent with the signature + """ + ba = sig.bind(*args, **kwargs) + ba.apply_defaults() # needed for test_dan_schult + return ba.args, ba.kwargs + + +def decorate(func, caller, extras=(), kwsyntax=False): + """ + Decorates a function/generator/coroutine using a caller. + If kwsyntax is True calling the decorated functions with keyword + syntax will pass the named arguments inside the ``kw`` dictionary, + even if such argument are positional, similarly to what functools.wraps + does. By default kwsyntax is False and the the arguments are untouched. + """ + sig = inspect.signature(func) + if isinstance(func, functools.partial): + func = functools.update_wrapper(func, func.func) + if iscoroutinefunction(caller): + async def fun(*args, **kw): + if not kwsyntax: + args, kw = fix(args, kw, sig) + return await caller(func, *(extras + args), **kw) + elif isgeneratorfunction(caller): + def fun(*args, **kw): + if not kwsyntax: + args, kw = fix(args, kw, sig) + for res in caller(func, *(extras + args), **kw): + yield res + else: + def fun(*args, **kw): + if not kwsyntax: + args, kw = fix(args, kw, sig) + return caller(func, *(extras + args), **kw) + + fun.__name__ = func.__name__ + fun.__doc__ = func.__doc__ + fun.__wrapped__ = func + fun.__signature__ = sig + fun.__qualname__ = func.__qualname__ + # builtin functions like defaultdict.__setitem__ lack many attributes + try: + fun.__defaults__ = func.__defaults__ + except AttributeError: + pass + try: + fun.__kwdefaults__ = func.__kwdefaults__ + except AttributeError: + pass + try: + fun.__annotations__ = func.__annotations__ + except AttributeError: + pass + try: + fun.__module__ = func.__module__ + except AttributeError: + pass + try: + fun.__name__ = func.__name__ + except AttributeError: # happens with old versions of numpy.vectorize + func.__name__ == 'noname' + try: + fun.__dict__.update(func.__dict__) + except AttributeError: + pass + return fun + + +def decoratorx(caller): + """ + A version of "decorator" implemented via "exec" and not via the + Signature object. Use this if you are want to preserve the `.__code__` + object properties (https://github.com/micheles/decorator/issues/129). + """ + def dec(func): + return FunctionMaker.create( + func, + "return _call_(_func_, %(shortsignature)s)", + dict(_call_=caller, _func_=func), + __wrapped__=func, __qualname__=func.__qualname__) + return dec + + +def decorator(caller, _func=None, kwsyntax=False): + """ + decorator(caller) converts a caller function into a decorator + """ + if _func is not None: # return a decorated function + # this is obsolete behavior; you should use decorate instead + return decorate(_func, caller, (), kwsyntax) + # else return a decorator function + sig = inspect.signature(caller) + dec_params = [p for p in sig.parameters.values() if p.kind is POS] + + def dec(func=None, *args, **kw): + na = len(args) + 1 + extras = args + tuple(kw.get(p.name, p.default) + for p in dec_params[na:] + if p.default is not EMPTY) + if func is None: + return lambda func: decorate(func, caller, extras, kwsyntax) + else: + return decorate(func, caller, extras, kwsyntax) + dec.__signature__ = sig.replace(parameters=dec_params) + dec.__name__ = caller.__name__ + dec.__doc__ = caller.__doc__ + dec.__wrapped__ = caller + dec.__qualname__ = caller.__qualname__ + dec.__kwdefaults__ = getattr(caller, '__kwdefaults__', None) + dec.__dict__.update(caller.__dict__) + return dec + + +# ####################### contextmanager ####################### # + + +class ContextManager(_GeneratorContextManager): + def __init__(self, g, *a, **k): + _GeneratorContextManager.__init__(self, g, a, k) + + def __call__(self, func): + def caller(f, *a, **k): + with self.__class__(self.func, *self.args, **self.kwds): + return f(*a, **k) + return decorate(func, caller) + + +_contextmanager = decorator(ContextManager) + + +def contextmanager(func): + # Enable Pylint config: contextmanager-decorators=decorator.contextmanager + return _contextmanager(func) + + +# ############################ dispatch_on ############################ # + +def append(a, vancestors): + """ + Append ``a`` to the list of the virtual ancestors, unless it is already + included. + """ + add = True + for j, va in enumerate(vancestors): + if issubclass(va, a): + add = False + break + if issubclass(a, va): + vancestors[j] = a + add = False + if add: + vancestors.append(a) + + +# inspired from simplegeneric by P.J. Eby and functools.singledispatch +def dispatch_on(*dispatch_args): + """ + Factory of decorators turning a function into a generic function + dispatching on the given arguments. + """ + assert dispatch_args, 'No dispatch args passed' + dispatch_str = '(%s,)' % ', '.join(dispatch_args) + + def check(arguments, wrong=operator.ne, msg=''): + """Make sure one passes the expected number of arguments""" + if wrong(len(arguments), len(dispatch_args)): + raise TypeError('Expected %d arguments, got %d%s' % + (len(dispatch_args), len(arguments), msg)) + + def gen_func_dec(func): + """Decorator turning a function into a generic function""" + + # first check the dispatch arguments + argset = set(getfullargspec(func).args) + if not set(dispatch_args) <= argset: + raise NameError('Unknown dispatch arguments %s' % dispatch_str) + + typemap = {} + + def vancestors(*types): + """ + Get a list of sets of virtual ancestors for the given types + """ + check(types) + ras = [[] for _ in range(len(dispatch_args))] + for types_ in typemap: + for t, type_, ra in zip(types, types_, ras): + if issubclass(t, type_) and type_ not in t.mro(): + append(type_, ra) + return [set(ra) for ra in ras] + + def ancestors(*types): + """ + Get a list of virtual MROs, one for each type + """ + check(types) + lists = [] + for t, vas in zip(types, vancestors(*types)): + n_vas = len(vas) + if n_vas > 1: + raise RuntimeError( + 'Ambiguous dispatch for %s: %s' % (t, vas)) + elif n_vas == 1: + va, = vas + mro = type('t', (t, va), {}).mro()[1:] + else: + mro = t.mro() + lists.append(mro[:-1]) # discard t and object + return lists + + def register(*types): + """ + Decorator to register an implementation for the given types + """ + check(types) + + def dec(f): + check(getfullargspec(f).args, operator.lt, ' in ' + f.__name__) + typemap[types] = f + return f + return dec + + def dispatch_info(*types): + """ + An utility to introspect the dispatch algorithm + """ + check(types) + lst = [] + for ancs in itertools.product(*ancestors(*types)): + lst.append(tuple(a.__name__ for a in ancs)) + return lst + + def _dispatch(dispatch_args, *args, **kw): + types = tuple(type(arg) for arg in dispatch_args) + try: # fast path + f = typemap[types] + except KeyError: + pass + else: + return f(*args, **kw) + combinations = itertools.product(*ancestors(*types)) + next(combinations) # the first one has been already tried + for types_ in combinations: + f = typemap.get(types_) + if f is not None: + return f(*args, **kw) + + # else call the default implementation + return func(*args, **kw) + + return FunctionMaker.create( + func, 'return _f_(%s, %%(shortsignature)s)' % dispatch_str, + dict(_f_=_dispatch), register=register, default=func, + typemap=typemap, vancestors=vancestors, ancestors=ancestors, + dispatch_info=dispatch_info, __wrapped__=func) + + gen_func_dec.__name__ = 'dispatch_on' + dispatch_str + return gen_func_dec diff --git a/venv/lib/python3.10/site-packages/distutils-precedence.pth b/venv/lib/python3.10/site-packages/distutils-precedence.pth new file mode 100644 index 0000000..6de4198 --- /dev/null +++ b/venv/lib/python3.10/site-packages/distutils-precedence.pth @@ -0,0 +1 @@ +import os; var = 'SETUPTOOLS_USE_DISTUTILS'; enabled = os.environ.get(var, 'stdlib') == 'local'; enabled and __import__('_distutils_hack').add_shim(); diff --git a/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/METADATA b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/METADATA new file mode 100644 index 0000000..46028fb --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/METADATA @@ -0,0 +1,91 @@ +Metadata-Version: 2.4 +Name: Flask +Version: 3.1.2 +Summary: A simple framework for building complex web applications. +Maintainer-email: Pallets +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-Expression: BSD-3-Clause +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Framework :: Flask +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Classifier: Typing :: Typed +License-File: LICENSE.txt +Requires-Dist: blinker>=1.9.0 +Requires-Dist: click>=8.1.3 +Requires-Dist: importlib-metadata>=3.6.0; python_version < '3.10' +Requires-Dist: itsdangerous>=2.2.0 +Requires-Dist: jinja2>=3.1.2 +Requires-Dist: markupsafe>=2.1.1 +Requires-Dist: werkzeug>=3.1.0 +Requires-Dist: asgiref>=3.2 ; extra == "async" +Requires-Dist: python-dotenv ; extra == "dotenv" +Project-URL: Changes, https://flask.palletsprojects.com/page/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://flask.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/flask/ +Provides-Extra: async +Provides-Extra: dotenv + +
+ +# Flask + +Flask is a lightweight [WSGI] web application framework. It is designed +to make getting started quick and easy, with the ability to scale up to +complex applications. It began as a simple wrapper around [Werkzeug] +and [Jinja], and has become one of the most popular Python web +application frameworks. + +Flask offers suggestions, but doesn't enforce any dependencies or +project layout. It is up to the developer to choose the tools and +libraries they want to use. There are many extensions provided by the +community that make adding new functionality easy. + +[WSGI]: https://wsgi.readthedocs.io/ +[Werkzeug]: https://werkzeug.palletsprojects.com/ +[Jinja]: https://jinja.palletsprojects.com/ + +## A Simple Example + +```python +# save this as app.py +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello(): + return "Hello, World!" +``` + +``` +$ flask run + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) +``` + +## Donate + +The Pallets organization develops and supports Flask and the libraries +it uses. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today]. + +[please donate today]: https://palletsprojects.com/donate + +## Contributing + +See our [detailed contributing documentation][contrib] for many ways to +contribute, including reporting issues, requesting features, asking or answering +questions, and making PRs. + +[contrib]: https://palletsprojects.com/contributing/ + diff --git a/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/RECORD b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/RECORD new file mode 100644 index 0000000..0e53a1c --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/RECORD @@ -0,0 +1,58 @@ +../../../bin/flask,sha256=MbTYUNx70KphT3LNltAXh68MDHsHgrFNyFRP5AEj2Q4,241 +flask-3.1.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +flask-3.1.2.dist-info/METADATA,sha256=oRg63DAAIcoLAr7kzTgIEKfm8_4HMTRpmWmIptdY_js,3167 +flask-3.1.2.dist-info/RECORD,, +flask-3.1.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask-3.1.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +flask-3.1.2.dist-info/entry_points.txt,sha256=bBP7hTOS5fz9zLtC7sPofBZAlMkEvBxu7KqS6l5lvc4,40 +flask-3.1.2.dist-info/licenses/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +flask/__init__.py,sha256=mHvJN9Swtl1RDtjCqCIYyIniK_SZ_l_hqUynOzgpJ9o,2701 +flask/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30 +flask/__pycache__/__init__.cpython-310.pyc,, +flask/__pycache__/__main__.cpython-310.pyc,, +flask/__pycache__/app.cpython-310.pyc,, +flask/__pycache__/blueprints.cpython-310.pyc,, +flask/__pycache__/cli.cpython-310.pyc,, +flask/__pycache__/config.cpython-310.pyc,, +flask/__pycache__/ctx.cpython-310.pyc,, +flask/__pycache__/debughelpers.cpython-310.pyc,, +flask/__pycache__/globals.cpython-310.pyc,, +flask/__pycache__/helpers.cpython-310.pyc,, +flask/__pycache__/logging.cpython-310.pyc,, +flask/__pycache__/sessions.cpython-310.pyc,, +flask/__pycache__/signals.cpython-310.pyc,, +flask/__pycache__/templating.cpython-310.pyc,, +flask/__pycache__/testing.cpython-310.pyc,, +flask/__pycache__/typing.cpython-310.pyc,, +flask/__pycache__/views.cpython-310.pyc,, +flask/__pycache__/wrappers.cpython-310.pyc,, +flask/app.py,sha256=XGqgFRsLgBhzIoB2HSftoMTIM3hjDiH6rdV7c3g3IKc,61744 +flask/blueprints.py,sha256=p5QE2lY18GItbdr_RKRpZ8Do17g0PvQGIgZkSUDhX2k,4541 +flask/cli.py,sha256=Pfh72-BxlvoH0QHCDOc1HvXG7Kq5Xetf3zzNz2kNSHk,37184 +flask/config.py,sha256=PiqF0DPam6HW0FH4CH1hpXTBe30NSzjPEOwrz1b6kt0,13219 +flask/ctx.py,sha256=sPKzahqtgxaS7O0y9E_NzUJNUDyTD6M4GkDrVu2fU3Y,15064 +flask/debughelpers.py,sha256=PGIDhStW_efRjpaa3zHIpo-htStJOR41Ip3OJWPYBwo,6080 +flask/globals.py,sha256=XdQZmStBmPIs8t93tjx6pO7Bm3gobAaONWkFcUHaGas,1713 +flask/helpers.py,sha256=rJZge7_J288J1UQv5-kNf4oEaw332PP8NTW0QRIBbXE,23517 +flask/json/__init__.py,sha256=hLNR898paqoefdeAhraa5wyJy-bmRB2k2dV4EgVy2Z8,5602 +flask/json/__pycache__/__init__.cpython-310.pyc,, +flask/json/__pycache__/provider.cpython-310.pyc,, +flask/json/__pycache__/tag.cpython-310.pyc,, +flask/json/provider.py,sha256=5imEzY5HjV2HoUVrQbJLqXCzMNpZXfD0Y1XqdLV2XBA,7672 +flask/json/tag.py,sha256=DhaNwuIOhdt2R74oOC9Y4Z8ZprxFYiRb5dUP5byyINw,9281 +flask/logging.py,sha256=8sM3WMTubi1cBb2c_lPkWpN0J8dMAqrgKRYLLi1dCVI,2377 +flask/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask/sansio/README.md,sha256=-0X1tECnilmz1cogx-YhNw5d7guK7GKrq_DEV2OzlU0,228 +flask/sansio/__pycache__/app.cpython-310.pyc,, +flask/sansio/__pycache__/blueprints.cpython-310.pyc,, +flask/sansio/__pycache__/scaffold.cpython-310.pyc,, +flask/sansio/app.py,sha256=5EbxwHOchgcpZqQyalA9vyDBopknOvDg6BVwXFyFD2s,38099 +flask/sansio/blueprints.py,sha256=Tqe-7EkZ-tbWchm8iDoCfD848f0_3nLv6NNjeIPvHwM,24637 +flask/sansio/scaffold.py,sha256=wSASXYdFRWJmqcL0Xq-T7N-PDVUSiFGvjO9kPZg58bk,30371 +flask/sessions.py,sha256=duvYGmCGh_H3cgMuy2oeSjrCsCvLylF4CBKOXpN0Qms,15480 +flask/signals.py,sha256=V7lMUww7CqgJ2ThUBn1PiatZtQanOyt7OZpu2GZI-34,750 +flask/templating.py,sha256=IHsdsF-eBJPCJE0AJLCi1VhhnytOGdzHCn3yThz87c4,7536 +flask/testing.py,sha256=zzC7XxhBWOP9H697IV_4SG7Lg3Lzb5PWiyEP93_KQXE,10117 +flask/typing.py,sha256=L-L5t2jKgS0aOmVhioQ_ylqcgiVFnA6yxO-RLNhq-GU,3293 +flask/views.py,sha256=xzJx6oJqGElThtEghZN7ZQGMw5TDFyuRxUkecwRuAoA,6962 +flask/wrappers.py,sha256=jUkv4mVek2Iq4hwxd4RvqrIMb69Bv0PElDgWLmd5ORo,9406 diff --git a/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/REQUESTED b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/WHEEL b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/entry_points.txt b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/entry_points.txt new file mode 100644 index 0000000..eec6733 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +flask=flask.cli:main + diff --git a/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/licenses/LICENSE.txt b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask-3.1.2.dist-info/licenses/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/venv/lib/python3.10/site-packages/flask/__init__.py b/venv/lib/python3.10/site-packages/flask/__init__.py new file mode 100644 index 0000000..1fdc50c --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/__init__.py @@ -0,0 +1,61 @@ +from __future__ import annotations + +import typing as t + +from . import json as json +from .app import Flask as Flask +from .blueprints import Blueprint as Blueprint +from .config import Config as Config +from .ctx import after_this_request as after_this_request +from .ctx import copy_current_request_context as copy_current_request_context +from .ctx import has_app_context as has_app_context +from .ctx import has_request_context as has_request_context +from .globals import current_app as current_app +from .globals import g as g +from .globals import request as request +from .globals import session as session +from .helpers import abort as abort +from .helpers import flash as flash +from .helpers import get_flashed_messages as get_flashed_messages +from .helpers import get_template_attribute as get_template_attribute +from .helpers import make_response as make_response +from .helpers import redirect as redirect +from .helpers import send_file as send_file +from .helpers import send_from_directory as send_from_directory +from .helpers import stream_with_context as stream_with_context +from .helpers import url_for as url_for +from .json import jsonify as jsonify +from .signals import appcontext_popped as appcontext_popped +from .signals import appcontext_pushed as appcontext_pushed +from .signals import appcontext_tearing_down as appcontext_tearing_down +from .signals import before_render_template as before_render_template +from .signals import got_request_exception as got_request_exception +from .signals import message_flashed as message_flashed +from .signals import request_finished as request_finished +from .signals import request_started as request_started +from .signals import request_tearing_down as request_tearing_down +from .signals import template_rendered as template_rendered +from .templating import render_template as render_template +from .templating import render_template_string as render_template_string +from .templating import stream_template as stream_template +from .templating import stream_template_string as stream_template_string +from .wrappers import Request as Request +from .wrappers import Response as Response + +if not t.TYPE_CHECKING: + + def __getattr__(name: str) -> t.Any: + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " Flask 3.2. Use feature detection or" + " 'importlib.metadata.version(\"flask\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("flask") + + raise AttributeError(name) diff --git a/venv/lib/python3.10/site-packages/flask/__main__.py b/venv/lib/python3.10/site-packages/flask/__main__.py new file mode 100644 index 0000000..4e28416 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +main() diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..dc63c8e Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/__main__.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/__main__.cpython-310.pyc new file mode 100644 index 0000000..5b6306e Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/__main__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/app.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..8c0b14d Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/app.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/blueprints.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/blueprints.cpython-310.pyc new file mode 100644 index 0000000..a6a99a8 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/blueprints.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/cli.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/cli.cpython-310.pyc new file mode 100644 index 0000000..a9180bb Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/cli.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/config.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..8d1a5c9 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/config.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/ctx.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/ctx.cpython-310.pyc new file mode 100644 index 0000000..a5366d2 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/ctx.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/debughelpers.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/debughelpers.cpython-310.pyc new file mode 100644 index 0000000..36e0817 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/debughelpers.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/globals.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/globals.cpython-310.pyc new file mode 100644 index 0000000..8af4fa7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/globals.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/helpers.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/helpers.cpython-310.pyc new file mode 100644 index 0000000..dfa15ee Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/helpers.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/logging.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/logging.cpython-310.pyc new file mode 100644 index 0000000..9ae1b1b Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/logging.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/sessions.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/sessions.cpython-310.pyc new file mode 100644 index 0000000..0a7b819 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/sessions.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/signals.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/signals.cpython-310.pyc new file mode 100644 index 0000000..77662bb Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/signals.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/templating.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/templating.cpython-310.pyc new file mode 100644 index 0000000..5519f9f Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/templating.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/testing.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/testing.cpython-310.pyc new file mode 100644 index 0000000..73f4a6a Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/testing.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/typing.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/typing.cpython-310.pyc new file mode 100644 index 0000000..3a1b359 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/typing.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/views.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/views.cpython-310.pyc new file mode 100644 index 0000000..fab0abd Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/views.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/__pycache__/wrappers.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/__pycache__/wrappers.cpython-310.pyc new file mode 100644 index 0000000..41915fb Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/__pycache__/wrappers.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/app.py b/venv/lib/python3.10/site-packages/flask/app.py new file mode 100644 index 0000000..1232b03 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/app.py @@ -0,0 +1,1536 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import sys +import typing as t +import weakref +from datetime import timedelta +from inspect import iscoroutinefunction +from itertools import chain +from types import TracebackType +from urllib.parse import quote as _url_quote + +import click +from werkzeug.datastructures import Headers +from werkzeug.datastructures import ImmutableDict +from werkzeug.exceptions import BadRequestKeyError +from werkzeug.exceptions import HTTPException +from werkzeug.exceptions import InternalServerError +from werkzeug.routing import BuildError +from werkzeug.routing import MapAdapter +from werkzeug.routing import RequestRedirect +from werkzeug.routing import RoutingException +from werkzeug.routing import Rule +from werkzeug.serving import is_running_from_reloader +from werkzeug.wrappers import Response as BaseResponse +from werkzeug.wsgi import get_host + +from . import cli +from . import typing as ft +from .ctx import AppContext +from .ctx import RequestContext +from .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import g +from .globals import request +from .globals import request_ctx +from .globals import session +from .helpers import get_debug_flag +from .helpers import get_flashed_messages +from .helpers import get_load_dotenv +from .helpers import send_from_directory +from .sansio.app import App +from .sansio.scaffold import _sentinel +from .sessions import SecureCookieSessionInterface +from .sessions import SessionInterface +from .signals import appcontext_tearing_down +from .signals import got_request_exception +from .signals import request_finished +from .signals import request_started +from .signals import request_tearing_down +from .templating import Environment +from .wrappers import Request +from .wrappers import Response + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIEnvironment + + from .testing import FlaskClient + from .testing import FlaskCliRunner + from .typing import HeadersValue + +T_shell_context_processor = t.TypeVar( + "T_shell_context_processor", bound=ft.ShellContextProcessorCallable +) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) + + +def _make_timedelta(value: timedelta | int | None) -> timedelta | None: + if value is None or isinstance(value, timedelta): + return value + + return timedelta(seconds=value) + + +class Flask(App): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the :file:`__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea of what + belongs to your application. This name is used to find resources + on the filesystem, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in :file:`yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + .. versionadded:: 0.11 + The `root_path` parameter was added. + + .. versionadded:: 1.0 + The ``host_matching`` and ``static_host`` parameters were added. + + .. versionadded:: 1.0 + The ``subdomain_matching`` parameter was added. Subdomain + matching needs to be enabled manually now. Setting + :data:`SERVER_NAME` does not implicitly enable it. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: The folder with static files that is served at + ``static_url_path``. Relative to the application ``root_path`` + or an absolute path. Defaults to ``'static'``. + :param static_host: the host to use when adding the static route. + Defaults to None. Required when using ``host_matching=True`` + with a ``static_folder`` configured. + :param host_matching: set ``url_map.host_matching`` attribute. + Defaults to False. + :param subdomain_matching: consider the subdomain relative to + :data:`SERVER_NAME` when matching routes. Defaults to False. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to ``True`` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + :param root_path: The path to the root of the application files. + This should only be set manually when it can't be detected + automatically, such as for namespace packages. + """ + + default_config = ImmutableDict( + { + "DEBUG": None, + "TESTING": False, + "PROPAGATE_EXCEPTIONS": None, + "SECRET_KEY": None, + "SECRET_KEY_FALLBACKS": None, + "PERMANENT_SESSION_LIFETIME": timedelta(days=31), + "USE_X_SENDFILE": False, + "TRUSTED_HOSTS": None, + "SERVER_NAME": None, + "APPLICATION_ROOT": "/", + "SESSION_COOKIE_NAME": "session", + "SESSION_COOKIE_DOMAIN": None, + "SESSION_COOKIE_PATH": None, + "SESSION_COOKIE_HTTPONLY": True, + "SESSION_COOKIE_SECURE": False, + "SESSION_COOKIE_PARTITIONED": False, + "SESSION_COOKIE_SAMESITE": None, + "SESSION_REFRESH_EACH_REQUEST": True, + "MAX_CONTENT_LENGTH": None, + "MAX_FORM_MEMORY_SIZE": 500_000, + "MAX_FORM_PARTS": 1_000, + "SEND_FILE_MAX_AGE_DEFAULT": None, + "TRAP_BAD_REQUEST_ERRORS": None, + "TRAP_HTTP_EXCEPTIONS": False, + "EXPLAIN_TEMPLATE_LOADING": False, + "PREFERRED_URL_SCHEME": "http", + "TEMPLATES_AUTO_RELOAD": None, + "MAX_COOKIE_SIZE": 4093, + "PROVIDE_AUTOMATIC_OPTIONS": True, + } + ) + + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class: type[Request] = Request + + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class: type[Response] = Response + + #: the session interface to use. By default an instance of + #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. + #: + #: .. versionadded:: 0.8 + session_interface: SessionInterface = SecureCookieSessionInterface() + + def __init__( + self, + import_name: str, + static_url_path: str | None = None, + static_folder: str | os.PathLike[str] | None = "static", + static_host: str | None = None, + host_matching: bool = False, + subdomain_matching: bool = False, + template_folder: str | os.PathLike[str] | None = "templates", + instance_path: str | None = None, + instance_relative_config: bool = False, + root_path: str | None = None, + ): + super().__init__( + import_name=import_name, + static_url_path=static_url_path, + static_folder=static_folder, + static_host=static_host, + host_matching=host_matching, + subdomain_matching=subdomain_matching, + template_folder=template_folder, + instance_path=instance_path, + instance_relative_config=instance_relative_config, + root_path=root_path, + ) + + #: The Click command group for registering CLI commands for this + #: object. The commands are available from the ``flask`` command + #: once the application has been discovered and blueprints have + #: been registered. + self.cli = cli.AppGroup() + + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name + + # Add a static route using the provided static_url_path, static_host, + # and static_folder if there is a configured static_folder. + # Note we do this without checking if static_folder exists. + # For one, it might be created while the server is running (e.g. during + # development). Also, Google App Engine stores static files somewhere + if self.has_static_folder: + assert bool(static_host) == host_matching, ( + "Invalid static_host/host_matching combination" + ) + # Use a weakref to avoid creating a reference cycle between the app + # and the view function (see #3761). + self_ref = weakref.ref(self) + self.add_url_rule( + f"{self.static_url_path}/", + endpoint="static", + host=static_host, + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 + ) + + def get_send_file_max_age(self, filename: str | None) -> int | None: + """Used by :func:`send_file` to determine the ``max_age`` cache + value for a given file path if it wasn't passed. + + By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from + the configuration of :data:`~flask.current_app`. This defaults + to ``None``, which tells the browser to use conditional requests + instead of a timed cache, which is usually preferable. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionchanged:: 2.0 + The default configuration is ``None`` instead of 12 hours. + + .. versionadded:: 0.9 + """ + value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] + + if value is None: + return None + + if isinstance(value, timedelta): + return int(value.total_seconds()) + + return value # type: ignore[no-any-return] + + def send_static_file(self, filename: str) -> Response: + """The view function used to serve files from + :attr:`static_folder`. A route is automatically registered for + this view at :attr:`static_url_path` if :attr:`static_folder` is + set. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionadded:: 0.5 + + """ + if not self.has_static_folder: + raise RuntimeError("'static_folder' must be set to serve static_files.") + + # send_file only knows to call get_send_file_max_age on the app, + # call it here so it works for blueprints too. + max_age = self.get_send_file_max_age(filename) + return send_from_directory( + t.cast(str, self.static_folder), filename, max_age=max_age + ) + + def open_resource( + self, resource: str, mode: str = "rb", encoding: str | None = None + ) -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for reading. + + For example, if the file ``schema.sql`` is next to the file + ``app.py`` where the ``Flask`` app is defined, it can be opened + with: + + .. code-block:: python + + with app.open_resource("schema.sql") as f: + conn.executescript(f.read()) + + :param resource: Path to the resource relative to :attr:`root_path`. + :param mode: Open the file in this mode. Only reading is supported, + valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. + :param encoding: Open the file with this encoding when opening in text + mode. This is ignored when opening in binary mode. + + .. versionchanged:: 3.1 + Added the ``encoding`` parameter. + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + path = os.path.join(self.root_path, resource) + + if mode == "rb": + return open(path, mode) # pyright: ignore + + return open(path, mode, encoding=encoding) + + def open_instance_resource( + self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" + ) -> t.IO[t.AnyStr]: + """Open a resource file relative to the application's instance folder + :attr:`instance_path`. Unlike :meth:`open_resource`, files in the + instance folder can be opened for writing. + + :param resource: Path to the resource relative to :attr:`instance_path`. + :param mode: Open the file in this mode. + :param encoding: Open the file with this encoding when opening in text + mode. This is ignored when opening in binary mode. + + .. versionchanged:: 3.1 + Added the ``encoding`` parameter. + """ + path = os.path.join(self.instance_path, resource) + + if "b" in mode: + return open(path, mode) + + return open(path, mode, encoding=encoding) + + def create_jinja_environment(self) -> Environment: + """Create the Jinja environment based on :attr:`jinja_options` + and the various Jinja-related methods of the app. Changing + :attr:`jinja_options` after this will have no effect. Also adds + Flask-related globals and filters to the environment. + + .. versionchanged:: 0.11 + ``Environment.auto_reload`` set in accordance with + ``TEMPLATES_AUTO_RELOAD`` configuration option. + + .. versionadded:: 0.5 + """ + options = dict(self.jinja_options) + + if "autoescape" not in options: + options["autoescape"] = self.select_jinja_autoescape + + if "auto_reload" not in options: + auto_reload = self.config["TEMPLATES_AUTO_RELOAD"] + + if auto_reload is None: + auto_reload = self.debug + + options["auto_reload"] = auto_reload + + rv = self.jinja_environment(self, **options) + rv.globals.update( + url_for=self.url_for, + get_flashed_messages=get_flashed_messages, + config=self.config, + # request, session and g are normally added with the + # context processor for efficiency reasons but for imported + # templates we also want the proxies in there. + request=request, + session=session, + g=g, + ) + rv.policies["json.dumps_function"] = self.json.dumps + return rv + + def create_url_adapter(self, request: Request | None) -> MapAdapter | None: + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set + up so the request is passed explicitly. + + .. versionchanged:: 3.1 + If :data:`SERVER_NAME` is set, it does not restrict requests to + only that domain, for both ``subdomain_matching`` and + ``host_matching``. + + .. versionchanged:: 1.0 + :data:`SERVER_NAME` no longer implicitly enables subdomain + matching. Use :attr:`subdomain_matching` instead. + + .. versionchanged:: 0.9 + This can be called outside a request when the URL adapter is created + for an application context. + + .. versionadded:: 0.6 + """ + if request is not None: + if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None: + request.trusted_hosts = trusted_hosts + + # Check trusted_hosts here until bind_to_environ does. + request.host = get_host(request.environ, request.trusted_hosts) # pyright: ignore + subdomain = None + server_name = self.config["SERVER_NAME"] + + if self.url_map.host_matching: + # Don't pass SERVER_NAME, otherwise it's used and the actual + # host is ignored, which breaks host matching. + server_name = None + elif not self.subdomain_matching: + # Werkzeug doesn't implement subdomain matching yet. Until then, + # disable it by forcing the current subdomain to the default, or + # the empty string. + subdomain = self.url_map.default_subdomain or "" + + return self.url_map.bind_to_environ( + request.environ, server_name=server_name, subdomain=subdomain + ) + + # Need at least SERVER_NAME to match/build outside a request. + if self.config["SERVER_NAME"] is not None: + return self.url_map.bind( + self.config["SERVER_NAME"], + script_name=self.config["APPLICATION_ROOT"], + url_scheme=self.config["PREFERRED_URL_SCHEME"], + ) + + return None + + def raise_routing_exception(self, request: Request) -> t.NoReturn: + """Intercept routing exceptions and possibly do something else. + + In debug mode, intercept a routing redirect and replace it with + an error if the body will be discarded. + + With modern Werkzeug this shouldn't occur, since it now uses a + 308 status which tells the browser to resend the method and + body. + + .. versionchanged:: 2.1 + Don't intercept 307 and 308 redirects. + + :meta private: + :internal: + """ + if ( + not self.debug + or not isinstance(request.routing_exception, RequestRedirect) + or request.routing_exception.code in {307, 308} + or request.method in {"GET", "HEAD", "OPTIONS"} + ): + raise request.routing_exception # type: ignore[misc] + + from .debughelpers import FormDataRoutingRedirect + + raise FormDataRoutingRedirect(request) + + def update_template_context(self, context: dict[str, t.Any]) -> None: + """Update the template context with some commonly used variables. + This injects request, session, config and g into the template + context as well as everything template context processors want + to inject. Note that the as of Flask 0.6, the original values + in the context will not be overridden if a context processor + decides to return a value with the same key. + + :param context: the context as a dictionary that is updated in place + to add extra variables. + """ + names: t.Iterable[str | None] = (None,) + + # A template may be rendered outside a request context. + if request: + names = chain(names, reversed(request.blueprints)) + + # The values passed to render_template take precedence. Keep a + # copy to re-apply after all context functions. + orig_ctx = context.copy() + + for name in names: + if name in self.template_context_processors: + for func in self.template_context_processors[name]: + context.update(self.ensure_sync(func)()) + + context.update(orig_ctx) + + def make_shell_context(self) -> dict[str, t.Any]: + """Returns the shell context for an interactive shell for this + application. This runs all the registered shell context + processors. + + .. versionadded:: 0.11 + """ + rv = {"app": self, "g": g} + for processor in self.shell_context_processors: + rv.update(processor()) + return rv + + def run( + self, + host: str | None = None, + port: int | None = None, + debug: bool | None = None, + load_dotenv: bool = True, + **options: t.Any, + ) -> None: + """Runs the application on a local development server. + + Do not use ``run()`` in a production setting. It is not intended to + meet security and performance requirements for a production server. + Instead, see :doc:`/deploying/index` for WSGI server recommendations. + + If the :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + + If you want to run the application in debug mode, but disable the + code execution on the interactive debugger, you can pass + ``use_evalex=False`` as parameter. This will keep the debugger's + traceback screen active, but disable code execution. + + It is not recommended to use this function for development with + automatic reloading as this is badly supported. Instead you should + be using the :command:`flask` command line script's ``run`` support. + + .. admonition:: Keep in Mind + + Flask will suppress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you have to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to ``True`` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + + :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to + have the server available externally as well. Defaults to + ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable + if present. + :param port: the port of the webserver. Defaults to ``5000`` or the + port defined in the ``SERVER_NAME`` config variable if present. + :param debug: if given, enable or disable debug mode. See + :attr:`debug`. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param options: the options to be forwarded to the underlying Werkzeug + server. See :func:`werkzeug.serving.run_simple` for more + information. + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment + variables from :file:`.env` and :file:`.flaskenv` files. + + The :envvar:`FLASK_DEBUG` environment variable will override :attr:`debug`. + + Threaded mode is enabled by default. + + .. versionchanged:: 0.10 + The default port is now picked from the ``SERVER_NAME`` + variable. + """ + # Ignore this call so that it doesn't start another server if + # the 'flask run' command is used. + if os.environ.get("FLASK_RUN_FROM_CLI") == "true": + if not is_running_from_reloader(): + click.secho( + " * Ignoring a call to 'app.run()' that would block" + " the current 'flask' CLI command.\n" + " Only call 'app.run()' in an 'if __name__ ==" + ' "__main__"\' guard.', + fg="red", + ) + + return + + if get_load_dotenv(load_dotenv): + cli.load_dotenv() + + # if set, env var overrides existing value + if "FLASK_DEBUG" in os.environ: + self.debug = get_debug_flag() + + # debug passed to method overrides all other sources + if debug is not None: + self.debug = bool(debug) + + server_name = self.config.get("SERVER_NAME") + sn_host = sn_port = None + + if server_name: + sn_host, _, sn_port = server_name.partition(":") + + if not host: + if sn_host: + host = sn_host + else: + host = "127.0.0.1" + + if port or port == 0: + port = int(port) + elif sn_port: + port = int(sn_port) + else: + port = 5000 + + options.setdefault("use_reloader", self.debug) + options.setdefault("use_debugger", self.debug) + options.setdefault("threaded", True) + + cli.show_server_banner(self.debug, self.name) + + from werkzeug.serving import run_simple + + try: + run_simple(t.cast(str, host), port, self, **options) + finally: + # reset the first request information if the development server + # reset normally. This makes it possible to restart the server + # without reloader and that stuff from an interactive shell. + self._got_first_request = False + + def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient: + """Creates a test client for this application. For information + about unit testing head over to :doc:`/testing`. + + Note that if you are testing for assertions or exceptions in your + application code, you must set ``app.testing = True`` in order for the + exceptions to propagate to the test client. Otherwise, the exception + will be handled by the application (not visible to the test client) and + the only indication of an AssertionError or other exception will be a + 500 status code response to the test client. See the :attr:`testing` + attribute. For example:: + + app.testing = True + client = app.test_client() + + The test client can be used in a ``with`` block to defer the closing down + of the context until the end of the ``with`` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' + + Additionally, you may pass optional keyword arguments that will then + be passed to the application's :attr:`test_client_class` constructor. + For example:: + + from flask.testing import FlaskClient + + class CustomClient(FlaskClient): + def __init__(self, *args, **kwargs): + self._authentication = kwargs.pop("authentication") + super(CustomClient,self).__init__( *args, **kwargs) + + app.test_client_class = CustomClient + client = app.test_client(authentication='Basic ....') + + See :class:`~flask.testing.FlaskClient` for more information. + + .. versionchanged:: 0.4 + added support for ``with`` block usage for the client. + + .. versionadded:: 0.7 + The `use_cookies` parameter was added as well as the ability + to override the client to be used by setting the + :attr:`test_client_class` attribute. + + .. versionchanged:: 0.11 + Added `**kwargs` to support passing additional keyword arguments to + the constructor of :attr:`test_client_class`. + """ + cls = self.test_client_class + if cls is None: + from .testing import FlaskClient as cls + return cls( # type: ignore + self, self.response_class, use_cookies=use_cookies, **kwargs + ) + + def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner: + """Create a CLI runner for testing CLI commands. + See :ref:`testing-cli`. + + Returns an instance of :attr:`test_cli_runner_class`, by default + :class:`~flask.testing.FlaskCliRunner`. The Flask app object is + passed as the first argument. + + .. versionadded:: 1.0 + """ + cls = self.test_cli_runner_class + + if cls is None: + from .testing import FlaskCliRunner as cls + + return cls(self, **kwargs) # type: ignore + + def handle_http_exception( + self, e: HTTPException + ) -> HTTPException | ft.ResponseReturnValue: + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionchanged:: 1.0.3 + ``RoutingException``, used internally for actions such as + slash redirects during routing, is not passed to error + handlers. + + .. versionchanged:: 1.0 + Exceptions are looked up by code *and* by MRO, so + ``HTTPException`` subclasses can be handled with a catch-all + handler for the base ``HTTPException``. + + .. versionadded:: 0.3 + """ + # Proxy exceptions don't have error codes. We want to always return + # those unchanged as errors + if e.code is None: + return e + + # RoutingExceptions are used internally to trigger routing + # actions, such as slash redirects raising RequestRedirect. They + # are not raised or handled in user code. + if isinstance(e, RoutingException): + return e + + handler = self._find_error_handler(e, request.blueprints) + if handler is None: + return e + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + + def handle_user_exception( + self, e: Exception + ) -> HTTPException | ft.ResponseReturnValue: + """This method is called whenever an exception occurs that + should be handled. A special case is :class:`~werkzeug + .exceptions.HTTPException` which is forwarded to the + :meth:`handle_http_exception` method. This function will either + return a response value or reraise the exception with the same + traceback. + + .. versionchanged:: 1.0 + Key errors raised from request data like ``form`` show the + bad key in debug mode rather than a generic bad request + message. + + .. versionadded:: 0.7 + """ + if isinstance(e, BadRequestKeyError) and ( + self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] + ): + e.show_exception = True + + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + + handler = self._find_error_handler(e, request.blueprints) + + if handler is None: + raise + + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + + def handle_exception(self, e: Exception) -> Response: + """Handle an exception that did not have an error handler + associated with it, or that was raised from an error handler. + This always causes a 500 ``InternalServerError``. + + Always sends the :data:`got_request_exception` signal. + + If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug + mode, the error will be re-raised so that the debugger can + display it. Otherwise, the original exception is logged, and + an :exc:`~werkzeug.exceptions.InternalServerError` is returned. + + If an error handler is registered for ``InternalServerError`` or + ``500``, it will be used. For consistency, the handler will + always receive the ``InternalServerError``. The original + unhandled exception is available as ``e.original_exception``. + + .. versionchanged:: 1.1.0 + Always passes the ``InternalServerError`` instance to the + handler, setting ``original_exception`` to the unhandled + error. + + .. versionchanged:: 1.1.0 + ``after_request`` functions and other finalization is done + even for the default 500 response when there is no handler. + + .. versionadded:: 0.3 + """ + exc_info = sys.exc_info() + got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) + propagate = self.config["PROPAGATE_EXCEPTIONS"] + + if propagate is None: + propagate = self.testing or self.debug + + if propagate: + # Re-raise if called with an active exception, otherwise + # raise the passed in exception. + if exc_info[1] is e: + raise + + raise e + + self.log_exception(exc_info) + server_error: InternalServerError | ft.ResponseReturnValue + server_error = InternalServerError(original_exception=e) + handler = self._find_error_handler(server_error, request.blueprints) + + if handler is not None: + server_error = self.ensure_sync(handler)(server_error) + + return self.finalize_request(server_error, from_error_handler=True) + + def log_exception( + self, + exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]), + ) -> None: + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error( + f"Exception on {request.path} [{request.method}]", exc_info=exc_info + ) + + def dispatch_request(self) -> ft.ResponseReturnValue: + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. + """ + req = request_ctx.request + if req.routing_exception is not None: + self.raise_routing_exception(req) + rule: Rule = req.url_rule # type: ignore[assignment] + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if ( + getattr(rule, "provide_automatic_options", False) + and req.method == "OPTIONS" + ): + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] + + def full_dispatch_request(self) -> Response: + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + + .. versionadded:: 0.7 + """ + self._got_first_request = True + + try: + request_started.send(self, _async_wrapper=self.ensure_sync) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + except Exception as e: + rv = self.handle_user_exception(e) + return self.finalize_request(rv) + + def finalize_request( + self, + rv: ft.ResponseReturnValue | HTTPException, + from_error_handler: bool = False, + ) -> Response: + """Given the return value from a view function this finalizes + the request by converting it into a response and invoking the + postprocessing functions. This is invoked for both normal + request dispatching as well as error handlers. + + Because this means that it might be called as a result of a + failure a special safe mode is available which can be enabled + with the `from_error_handler` flag. If enabled, failures in + response processing will be logged and otherwise ignored. + + :internal: + """ + response = self.make_response(rv) + try: + response = self.process_response(response) + request_finished.send( + self, _async_wrapper=self.ensure_sync, response=response + ) + except Exception: + if not from_error_handler: + raise + self.logger.exception( + "Request finalizing failed with an error while handling an error" + ) + return response + + def make_default_options_response(self) -> Response: + """This method is called to create the default ``OPTIONS`` response. + This can be changed through subclassing to change the default + behavior of ``OPTIONS`` responses. + + .. versionadded:: 0.7 + """ + adapter = request_ctx.url_adapter + methods = adapter.allowed_methods() # type: ignore[union-attr] + rv = self.response_class() + rv.allow.update(methods) + return rv + + def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + """Ensure that the function is synchronous for WSGI workers. + Plain ``def`` functions are returned as-is. ``async def`` + functions are wrapped to run and wait for the response. + + Override this method to change how the app runs async views. + + .. versionadded:: 2.0 + """ + if iscoroutinefunction(func): + return self.async_to_sync(func) + + return func + + def async_to_sync( + self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]] + ) -> t.Callable[..., t.Any]: + """Return a sync function that will run the coroutine function. + + .. code-block:: python + + result = app.async_to_sync(func)(*args, **kwargs) + + Override this method to change how the app converts async code + to be synchronously callable. + + .. versionadded:: 2.0 + """ + try: + from asgiref.sync import async_to_sync as asgiref_async_to_sync + except ImportError: + raise RuntimeError( + "Install Flask with the 'async' extra in order to use async views." + ) from None + + return asgiref_async_to_sync(func) + + def url_for( + self, + /, + endpoint: str, + *, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, + **values: t.Any, + ) -> str: + """Generate a URL to the given endpoint with the given values. + + This is called by :func:`flask.url_for`, and can be called + directly as well. + + An *endpoint* is the name of a URL rule, usually added with + :meth:`@app.route() `, and usually the same name as the + view function. A route defined in a :class:`~flask.Blueprint` + will prepend the blueprint's name separated by a ``.`` to the + endpoint. + + In some cases, such as email messages, you want URLs to include + the scheme and domain, like ``https://example.com/hello``. When + not in an active request, URLs will be external by default, but + this requires setting :data:`SERVER_NAME` so Flask knows what + domain to use. :data:`APPLICATION_ROOT` and + :data:`PREFERRED_URL_SCHEME` should also be configured as + needed. This config is only used when not in an active request. + + Functions can be decorated with :meth:`url_defaults` to modify + keyword arguments before the URL is built. + + If building fails for some reason, such as an unknown endpoint + or incorrect values, the app's :meth:`handle_url_build_error` + method is called. If that returns a string, that is returned, + otherwise a :exc:`~werkzeug.routing.BuildError` is raised. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it + is external. + :param _external: If given, prefer the URL to be internal + (False) or require it to be external (True). External URLs + include the scheme and domain. When not in an active + request, URLs are external by default. + :param values: Values to use for the variable parts of the URL + rule. Unknown keys are appended as query string arguments, + like ``?a=b&c=d``. + + .. versionadded:: 2.2 + Moved from ``flask.url_for``, which calls this method. + """ + req_ctx = _cv_request.get(None) + + if req_ctx is not None: + url_adapter = req_ctx.url_adapter + blueprint_name = req_ctx.request.blueprint + + # If the endpoint starts with "." and the request matches a + # blueprint, the endpoint is relative to the blueprint. + if endpoint[:1] == ".": + if blueprint_name is not None: + endpoint = f"{blueprint_name}{endpoint}" + else: + endpoint = endpoint[1:] + + # When in a request, generate a URL without scheme and + # domain by default, unless a scheme is given. + if _external is None: + _external = _scheme is not None + else: + app_ctx = _cv_app.get(None) + + # If called by helpers.url_for, an app context is active, + # use its url_adapter. Otherwise, app.url_for was called + # directly, build an adapter. + if app_ctx is not None: + url_adapter = app_ctx.url_adapter + else: + url_adapter = self.create_url_adapter(None) + + if url_adapter is None: + raise RuntimeError( + "Unable to build URLs outside an active request" + " without 'SERVER_NAME' configured. Also configure" + " 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as" + " needed." + ) + + # When outside a request, generate a URL with scheme and + # domain by default. + if _external is None: + _external = True + + # It is an error to set _scheme when _external=False, in order + # to avoid accidental insecure URLs. + if _scheme is not None and not _external: + raise ValueError("When specifying '_scheme', '_external' must be True.") + + self.inject_url_defaults(endpoint, values) + + try: + rv = url_adapter.build( # type: ignore[union-attr] + endpoint, + values, + method=_method, + url_scheme=_scheme, + force_external=_external, + ) + except BuildError as error: + values.update( + _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external + ) + return self.handle_url_build_error(error, endpoint, values) + + if _anchor is not None: + _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@") + rv = f"{rv}#{_anchor}" + + return rv + + def make_response(self, rv: ft.ResponseReturnValue) -> Response: + """Convert the return value from a view function to an instance of + :attr:`response_class`. + + :param rv: the return value from the view function. The view function + must return a response. Returning ``None``, or the view ending + without returning, is not allowed. The following types are allowed + for ``view_rv``: + + ``str`` + A response object is created with the string encoded to UTF-8 + as the body. + + ``bytes`` + A response object is created with the bytes as the body. + + ``dict`` + A dictionary that will be jsonify'd before being returned. + + ``list`` + A list that will be jsonify'd before being returned. + + ``generator`` or ``iterator`` + A generator that returns ``str`` or ``bytes`` to be + streamed as the response. + + ``tuple`` + Either ``(body, status, headers)``, ``(body, status)``, or + ``(body, headers)``, where ``body`` is any of the other types + allowed here, ``status`` is a string or an integer, and + ``headers`` is a dictionary or a list of ``(key, value)`` + tuples. If ``body`` is a :attr:`response_class` instance, + ``status`` overwrites the exiting value and ``headers`` are + extended. + + :attr:`response_class` + The object is returned unchanged. + + other :class:`~werkzeug.wrappers.Response` class + The object is coerced to :attr:`response_class`. + + :func:`callable` + The function is called as a WSGI application. The result is + used to create a response object. + + .. versionchanged:: 2.2 + A generator will be converted to a streaming response. + A list will be converted to a JSON response. + + .. versionchanged:: 1.1 + A dict will be converted to a JSON response. + + .. versionchanged:: 0.9 + Previously a tuple was interpreted as the arguments for the + response object. + """ + + status: int | None = None + headers: HeadersValue | None = None + + # unpack tuple returns + if isinstance(rv, tuple): + len_rv = len(rv) + + # a 3-tuple is unpacked directly + if len_rv == 3: + rv, status, headers = rv # type: ignore[misc] + # decide if a 2-tuple has status or headers + elif len_rv == 2: + if isinstance(rv[1], (Headers, dict, tuple, list)): + rv, headers = rv # pyright: ignore + else: + rv, status = rv # type: ignore[assignment,misc] + # other sized tuples are not allowed + else: + raise TypeError( + "The view function did not return a valid response tuple." + " The tuple must have the form (body, status, headers)," + " (body, status), or (body, headers)." + ) + + # the body must not be None + if rv is None: + raise TypeError( + f"The view function for {request.endpoint!r} did not" + " return a valid response. The function either returned" + " None or ended without a return statement." + ) + + # make sure the body is an instance of the response class + if not isinstance(rv, self.response_class): + if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, cabc.Iterator): + # let the response class set the status and headers instead of + # waiting to do it manually, so that the class can handle any + # special logic + rv = self.response_class( + rv, # pyright: ignore + status=status, + headers=headers, # type: ignore[arg-type] + ) + status = headers = None + elif isinstance(rv, (dict, list)): + rv = self.json.response(rv) + elif isinstance(rv, BaseResponse) or callable(rv): + # evaluate a WSGI callable, or coerce a different response + # class to the correct type + try: + rv = self.response_class.force_type( + rv, # type: ignore[arg-type] + request.environ, + ) + except TypeError as e: + raise TypeError( + f"{e}\nThe view function did not return a valid" + " response. The return type must be a string," + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it" + f" was a {type(rv).__name__}." + ).with_traceback(sys.exc_info()[2]) from None + else: + raise TypeError( + "The view function did not return a valid" + " response. The return type must be a string," + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it was a" + f" {type(rv).__name__}." + ) + + rv = t.cast(Response, rv) + # prefer the status if it was provided + if status is not None: + if isinstance(status, (str, bytes, bytearray)): + rv.status = status + else: + rv.status_code = status + + # extend existing headers with provided headers + if headers: + rv.headers.update(headers) + + return rv + + def preprocess_request(self) -> ft.ResponseReturnValue | None: + """Called before the request is dispatched. Calls + :attr:`url_value_preprocessors` registered with the app and the + current blueprint (if any). Then calls :attr:`before_request_funcs` + registered with the app and the blueprint. + + If any :meth:`before_request` handler returns a non-None value, the + value is handled as if it was the return value from the view, and + further request handling is stopped. + """ + names = (None, *reversed(request.blueprints)) + + for name in names: + if name in self.url_value_preprocessors: + for url_func in self.url_value_preprocessors[name]: + url_func(request.endpoint, request.view_args) + + for name in names: + if name in self.before_request_funcs: + for before_func in self.before_request_funcs[name]: + rv = self.ensure_sync(before_func)() + + if rv is not None: + return rv # type: ignore[no-any-return] + + return None + + def process_response(self, response: Response) -> Response: + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. + + .. versionchanged:: 0.5 + As of Flask 0.5 the functions registered for after request + execution are called in reverse order of registration. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. + """ + ctx = request_ctx._get_current_object() # type: ignore[attr-defined] + + for func in ctx._after_request_functions: + response = self.ensure_sync(func)(response) + + for name in chain(request.blueprints, (None,)): + if name in self.after_request_funcs: + for func in reversed(self.after_request_funcs[name]): + response = self.ensure_sync(func)(response) + + if not self.session_interface.is_null_session(ctx.session): + self.session_interface.save_session(self, ctx.session, response) + + return response + + def do_teardown_request( + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] + ) -> None: + """Called after the request is dispatched and the response is + returned, right before the request context is popped. + + This calls all functions decorated with + :meth:`teardown_request`, and :meth:`Blueprint.teardown_request` + if a blueprint handled the request. Finally, the + :data:`request_tearing_down` signal is sent. + + This is called by + :meth:`RequestContext.pop() `, + which may be delayed during testing to maintain access to + resources. + + :param exc: An unhandled exception raised while dispatching the + request. Detected from the current exception information if + not passed. Passed to each teardown function. + + .. versionchanged:: 0.9 + Added the ``exc`` argument. + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for name in chain(request.blueprints, (None,)): + if name in self.teardown_request_funcs: + for func in reversed(self.teardown_request_funcs[name]): + self.ensure_sync(func)(exc) + + request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def do_teardown_appcontext( + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] + ) -> None: + """Called right before the application context is popped. + + When handling a request, the application context is popped + after the request context. See :meth:`do_teardown_request`. + + This calls all functions decorated with + :meth:`teardown_appcontext`. Then the + :data:`appcontext_tearing_down` signal is sent. + + This is called by + :meth:`AppContext.pop() `. + + .. versionadded:: 0.9 + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for func in reversed(self.teardown_appcontext_funcs): + self.ensure_sync(func)(exc) + + appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def app_context(self) -> AppContext: + """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` + block to push the context, which will make :data:`current_app` + point at this application. + + An application context is automatically pushed by + :meth:`RequestContext.push() ` + when handling a request, and when running a CLI command. Use + this to manually create a context outside of these situations. + + :: + + with app.app_context(): + init_db() + + See :doc:`/appcontext`. + + .. versionadded:: 0.9 + """ + return AppContext(self) + + def request_context(self, environ: WSGIEnvironment) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` representing a + WSGI environment. Use a ``with`` block to push the context, + which will make :data:`request` point at this request. + + See :doc:`/reqcontext`. + + Typically you should not call this from your own code. A request + context is automatically pushed by the :meth:`wsgi_app` when + handling a request. Use :meth:`test_request_context` to create + an environment and context instead of this method. + + :param environ: a WSGI environment + """ + return RequestContext(self, environ) + + def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` for a WSGI + environment created from the given values. This is mostly useful + during testing, where you may want to run a function that uses + request data without dispatching a full request. + + See :doc:`/reqcontext`. + + Use a ``with`` block to push the context, which will make + :data:`request` point at the request for the created + environment. :: + + with app.test_request_context(...): + generate_report() + + When using the shell, it may be easier to push and pop the + context manually to avoid indentation. :: + + ctx = app.test_request_context(...) + ctx.push() + ... + ctx.pop() + + Takes the same arguments as Werkzeug's + :class:`~werkzeug.test.EnvironBuilder`, with some defaults from + the application. See the linked Werkzeug docs for most of the + available arguments. Flask-specific behavior is listed here. + + :param path: URL path being requested. + :param base_url: Base URL where the app is being served, which + ``path`` is relative to. If not given, built from + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to + :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param data: The request body, either as a string or a dict of + form keys and values. + :param json: If given, this is serialized as JSON and passed as + ``data``. Also defaults ``content_type`` to + ``application/json``. + :param args: other positional arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + :param kwargs: other keyword arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + """ + from .testing import EnvironBuilder + + builder = EnvironBuilder(self, *args, **kwargs) + + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() + + def wsgi_app( + self, environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: + """The actual WSGI application. This is not implemented in + :meth:`__call__` so that middlewares can be applied without + losing a reference to the app object. Instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.7 + Teardown events for the request and app contexts are called + even if an unhandled error occurs. Other events may not be + called depending on when an error occurs during dispatch. + See :ref:`callbacks-and-errors`. + + :param environ: A WSGI environment. + :param start_response: A callable accepting a status code, + a list of headers, and an optional exception context to + start the response. + """ + ctx = self.request_context(environ) + error: BaseException | None = None + try: + try: + ctx.push() + response = self.full_dispatch_request() + except Exception as e: + error = e + response = self.handle_exception(e) + except: # noqa: B001 + error = sys.exc_info()[1] + raise + return response(environ, start_response) + finally: + if "werkzeug.debug.preserve_context" in environ: + environ["werkzeug.debug.preserve_context"](_cv_app.get()) + environ["werkzeug.debug.preserve_context"](_cv_request.get()) + + if error is not None and self.should_ignore_error(error): + error = None + + ctx.pop(error) + + def __call__( + self, environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: + """The WSGI server calls the Flask application object as the + WSGI application. This calls :meth:`wsgi_app`, which can be + wrapped to apply middleware. + """ + return self.wsgi_app(environ, start_response) diff --git a/venv/lib/python3.10/site-packages/flask/blueprints.py b/venv/lib/python3.10/site-packages/flask/blueprints.py new file mode 100644 index 0000000..b6d4e43 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/blueprints.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import os +import typing as t +from datetime import timedelta + +from .cli import AppGroup +from .globals import current_app +from .helpers import send_from_directory +from .sansio.blueprints import Blueprint as SansioBlueprint +from .sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa +from .sansio.scaffold import _sentinel + +if t.TYPE_CHECKING: # pragma: no cover + from .wrappers import Response + + +class Blueprint(SansioBlueprint): + def __init__( + self, + name: str, + import_name: str, + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + url_prefix: str | None = None, + subdomain: str | None = None, + url_defaults: dict[str, t.Any] | None = None, + root_path: str | None = None, + cli_group: str | None = _sentinel, # type: ignore + ) -> None: + super().__init__( + name, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_group, + ) + + #: The Click command group for registering CLI commands for this + #: object. The commands are available from the ``flask`` command + #: once the application has been discovered and blueprints have + #: been registered. + self.cli = AppGroup() + + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name + + def get_send_file_max_age(self, filename: str | None) -> int | None: + """Used by :func:`send_file` to determine the ``max_age`` cache + value for a given file path if it wasn't passed. + + By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from + the configuration of :data:`~flask.current_app`. This defaults + to ``None``, which tells the browser to use conditional requests + instead of a timed cache, which is usually preferable. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionchanged:: 2.0 + The default configuration is ``None`` instead of 12 hours. + + .. versionadded:: 0.9 + """ + value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] + + if value is None: + return None + + if isinstance(value, timedelta): + return int(value.total_seconds()) + + return value # type: ignore[no-any-return] + + def send_static_file(self, filename: str) -> Response: + """The view function used to serve files from + :attr:`static_folder`. A route is automatically registered for + this view at :attr:`static_url_path` if :attr:`static_folder` is + set. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionadded:: 0.5 + + """ + if not self.has_static_folder: + raise RuntimeError("'static_folder' must be set to serve static_files.") + + # send_file only knows to call get_send_file_max_age on the app, + # call it here so it works for blueprints too. + max_age = self.get_send_file_max_age(filename) + return send_from_directory( + t.cast(str, self.static_folder), filename, max_age=max_age + ) + + def open_resource( + self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" + ) -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for reading. The + blueprint-relative equivalent of the app's :meth:`~.Flask.open_resource` + method. + + :param resource: Path to the resource relative to :attr:`root_path`. + :param mode: Open the file in this mode. Only reading is supported, + valid values are ``"r"`` (or ``"rt"``) and ``"rb"``. + :param encoding: Open the file with this encoding when opening in text + mode. This is ignored when opening in binary mode. + + .. versionchanged:: 3.1 + Added the ``encoding`` parameter. + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + path = os.path.join(self.root_path, resource) + + if mode == "rb": + return open(path, mode) # pyright: ignore + + return open(path, mode, encoding=encoding) diff --git a/venv/lib/python3.10/site-packages/flask/cli.py b/venv/lib/python3.10/site-packages/flask/cli.py new file mode 100644 index 0000000..ed11f25 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/cli.py @@ -0,0 +1,1135 @@ +from __future__ import annotations + +import ast +import collections.abc as cabc +import importlib.metadata +import inspect +import os +import platform +import re +import sys +import traceback +import typing as t +from functools import update_wrapper +from operator import itemgetter +from types import ModuleType + +import click +from click.core import ParameterSource +from werkzeug import run_simple +from werkzeug.serving import is_running_from_reloader +from werkzeug.utils import import_string + +from .globals import current_app +from .helpers import get_debug_flag +from .helpers import get_load_dotenv + +if t.TYPE_CHECKING: + import ssl + + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + from .app import Flask + + +class NoAppException(click.UsageError): + """Raised if an application cannot be found or loaded.""" + + +def find_best_app(module: ModuleType) -> Flask: + """Given a module instance this tries to find the best possible + application in the module or raises an exception. + """ + from . import Flask + + # Search for the most common names first. + for attr_name in ("app", "application"): + app = getattr(module, attr_name, None) + + if isinstance(app, Flask): + return app + + # Otherwise find the only object that is a Flask instance. + matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] + + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + raise NoAppException( + "Detected multiple Flask applications in module" + f" '{module.__name__}'. Use '{module.__name__}:name'" + " to specify the correct one." + ) + + # Search for app factory functions. + for attr_name in ("create_app", "make_app"): + app_factory = getattr(module, attr_name, None) + + if inspect.isfunction(app_factory): + try: + app = app_factory() + + if isinstance(app, Flask): + return app + except TypeError as e: + if not _called_with_wrong_args(app_factory): + raise + + raise NoAppException( + f"Detected factory '{attr_name}' in module '{module.__name__}'," + " but could not call it without arguments. Use" + f" '{module.__name__}:{attr_name}(args)'" + " to specify arguments." + ) from e + + raise NoAppException( + "Failed to find Flask application or factory in module" + f" '{module.__name__}'. Use '{module.__name__}:name'" + " to specify one." + ) + + +def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool: + """Check whether calling a function raised a ``TypeError`` because + the call failed or because something in the factory raised the + error. + + :param f: The function that was called. + :return: ``True`` if the call failed. + """ + tb = sys.exc_info()[2] + + try: + while tb is not None: + if tb.tb_frame.f_code is f.__code__: + # In the function, it was called successfully. + return False + + tb = tb.tb_next + + # Didn't reach the function. + return True + finally: + # Delete tb to break a circular reference. + # https://docs.python.org/2/library/sys.html#sys.exc_info + del tb + + +def find_app_by_string(module: ModuleType, app_name: str) -> Flask: + """Check if the given string is a variable name or a function. Call + a function to get the app instance, or return the variable directly. + """ + from . import Flask + + # Parse app_name as a single expression to determine if it's a valid + # attribute name or function call. + try: + expr = ast.parse(app_name.strip(), mode="eval").body + except SyntaxError: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) from None + + if isinstance(expr, ast.Name): + name = expr.id + args = [] + kwargs = {} + elif isinstance(expr, ast.Call): + # Ensure the function name is an attribute name only. + if not isinstance(expr.func, ast.Name): + raise NoAppException( + f"Function reference must be a simple name: {app_name!r}." + ) + + name = expr.func.id + + # Parse the positional and keyword arguments as literals. + try: + args = [ast.literal_eval(arg) for arg in expr.args] + kwargs = { + kw.arg: ast.literal_eval(kw.value) + for kw in expr.keywords + if kw.arg is not None + } + except ValueError: + # literal_eval gives cryptic error messages, show a generic + # message with the full expression instead. + raise NoAppException( + f"Failed to parse arguments as literal values: {app_name!r}." + ) from None + else: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) + + try: + attr = getattr(module, name) + except AttributeError as e: + raise NoAppException( + f"Failed to find attribute {name!r} in {module.__name__!r}." + ) from e + + # If the attribute is a function, call it with any args and kwargs + # to get the real application. + if inspect.isfunction(attr): + try: + app = attr(*args, **kwargs) + except TypeError as e: + if not _called_with_wrong_args(attr): + raise + + raise NoAppException( + f"The factory {app_name!r} in module" + f" {module.__name__!r} could not be called with the" + " specified arguments." + ) from e + else: + app = attr + + if isinstance(app, Flask): + return app + + raise NoAppException( + "A valid Flask application was not obtained from" + f" '{module.__name__}:{app_name}'." + ) + + +def prepare_import(path: str) -> str: + """Given a filename this will try to calculate the python path, add it + to the search path and return the actual module name that is expected. + """ + path = os.path.realpath(path) + + fname, ext = os.path.splitext(path) + if ext == ".py": + path = fname + + if os.path.basename(path) == "__init__": + path = os.path.dirname(path) + + module_name = [] + + # move up until outside package structure (no __init__.py) + while True: + path, name = os.path.split(path) + module_name.append(name) + + if not os.path.exists(os.path.join(path, "__init__.py")): + break + + if sys.path[0] != path: + sys.path.insert(0, path) + + return ".".join(module_name[::-1]) + + +@t.overload +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True +) -> Flask: ... + + +@t.overload +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ... +) -> Flask | None: ... + + +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: bool = True +) -> Flask | None: + try: + __import__(module_name) + except ImportError: + # Reraise the ImportError if it occurred within the imported module. + # Determine this by checking whether the trace has a depth > 1. + if sys.exc_info()[2].tb_next: # type: ignore[union-attr] + raise NoAppException( + f"While importing {module_name!r}, an ImportError was" + f" raised:\n\n{traceback.format_exc()}" + ) from None + elif raise_if_not_found: + raise NoAppException(f"Could not import {module_name!r}.") from None + else: + return None + + module = sys.modules[module_name] + + if app_name is None: + return find_best_app(module) + else: + return find_app_by_string(module, app_name) + + +def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None: + if not value or ctx.resilient_parsing: + return + + flask_version = importlib.metadata.version("flask") + werkzeug_version = importlib.metadata.version("werkzeug") + + click.echo( + f"Python {platform.python_version()}\n" + f"Flask {flask_version}\n" + f"Werkzeug {werkzeug_version}", + color=ctx.color, + ) + ctx.exit() + + +version_option = click.Option( + ["--version"], + help="Show the Flask version.", + expose_value=False, + callback=get_version, + is_flag=True, + is_eager=True, +) + + +class ScriptInfo: + """Helper object to deal with Flask applications. This is usually not + necessary to interface with as it's used internally in the dispatching + to click. In future versions of Flask this object will most likely play + a bigger role. Typically it's created automatically by the + :class:`FlaskGroup` but you can also manually create it and pass it + onwards as click object. + + .. versionchanged:: 3.1 + Added the ``load_dotenv_defaults`` parameter and attribute. + """ + + def __init__( + self, + app_import_path: str | None = None, + create_app: t.Callable[..., Flask] | None = None, + set_debug_flag: bool = True, + load_dotenv_defaults: bool = True, + ) -> None: + #: Optionally the import path for the Flask application. + self.app_import_path = app_import_path + #: Optionally a function that is passed the script info to create + #: the instance of the application. + self.create_app = create_app + #: A dictionary with arbitrary data that can be associated with + #: this script info. + self.data: dict[t.Any, t.Any] = {} + self.set_debug_flag = set_debug_flag + + self.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults) + """Whether default ``.flaskenv`` and ``.env`` files should be loaded. + + ``ScriptInfo`` doesn't load anything, this is for reference when doing + the load elsewhere during processing. + + .. versionadded:: 3.1 + """ + + self._loaded_app: Flask | None = None + + def load_app(self) -> Flask: + """Loads the Flask app (if not yet loaded) and returns it. Calling + this multiple times will just result in the already loaded app to + be returned. + """ + if self._loaded_app is not None: + return self._loaded_app + app: Flask | None = None + if self.create_app is not None: + app = self.create_app() + else: + if self.app_import_path: + path, name = ( + re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None] + )[:2] + import_name = prepare_import(path) + app = locate_app(import_name, name) + else: + for path in ("wsgi.py", "app.py"): + import_name = prepare_import(path) + app = locate_app(import_name, None, raise_if_not_found=False) + + if app is not None: + break + + if app is None: + raise NoAppException( + "Could not locate a Flask application. Use the" + " 'flask --app' option, 'FLASK_APP' environment" + " variable, or a 'wsgi.py' or 'app.py' file in the" + " current directory." + ) + + if self.set_debug_flag: + # Update the app's debug flag through the descriptor so that + # other values repopulate as well. + app.debug = get_debug_flag() + + self._loaded_app = app + return app + + +pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + + +def with_appcontext(f: F) -> F: + """Wraps a callback so that it's guaranteed to be executed with the + script's application context. + + Custom commands (and their options) registered under ``app.cli`` or + ``blueprint.cli`` will always have an app context available, this + decorator is not required in that case. + + .. versionchanged:: 2.2 + The app context is active for subcommands as well as the + decorated callback. The app context is always available to + ``app.cli`` command and parameter callbacks. + """ + + @click.pass_context + def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + if not current_app: + app = ctx.ensure_object(ScriptInfo).load_app() + ctx.with_resource(app.app_context()) + + return ctx.invoke(f, *args, **kwargs) + + return update_wrapper(decorator, f) # type: ignore[return-value] + + +class AppGroup(click.Group): + """This works similar to a regular click :class:`~click.Group` but it + changes the behavior of the :meth:`command` decorator so that it + automatically wraps the functions in :func:`with_appcontext`. + + Not to be confused with :class:`FlaskGroup`. + """ + + def command( # type: ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]: + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` + unless it's disabled by passing ``with_appcontext=False``. + """ + wrap_for_ctx = kwargs.pop("with_appcontext", True) + + def decorator(f: t.Callable[..., t.Any]) -> click.Command: + if wrap_for_ctx: + f = with_appcontext(f) + return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return] + + return decorator + + def group( # type: ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]: + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it defaults the group class to + :class:`AppGroup`. + """ + kwargs.setdefault("cls", AppGroup) + return super().group(*args, **kwargs) # type: ignore[no-any-return] + + +def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: + if value is None: + return None + + info = ctx.ensure_object(ScriptInfo) + info.app_import_path = value + return value + + +# This option is eager so the app will be available if --help is given. +# --help is also eager, so --app must be before it in the param list. +# no_args_is_help bypasses eager processing, so this option must be +# processed manually in that case to ensure FLASK_APP gets picked up. +_app_option = click.Option( + ["-A", "--app"], + metavar="IMPORT", + help=( + "The Flask application or factory function to load, in the form 'module:name'." + " Module can be a dotted import or file path. Name is not required if it is" + " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to" + " pass arguments." + ), + is_eager=True, + expose_value=False, + callback=_set_app, +) + + +def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: + # If the flag isn't provided, it will default to False. Don't use + # that, let debug be set by env in that case. + source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] + + if source is not None and source in ( + ParameterSource.DEFAULT, + ParameterSource.DEFAULT_MAP, + ): + return None + + # Set with env var instead of ScriptInfo.load so that it can be + # accessed early during a factory function. + os.environ["FLASK_DEBUG"] = "1" if value else "0" + return value + + +_debug_option = click.Option( + ["--debug/--no-debug"], + help="Set debug mode.", + expose_value=False, + callback=_set_debug, +) + + +def _env_file_callback( + ctx: click.Context, param: click.Option, value: str | None +) -> str | None: + try: + import dotenv # noqa: F401 + except ImportError: + # Only show an error if a value was passed, otherwise we still want to + # call load_dotenv and show a message without exiting. + if value is not None: + raise click.BadParameter( + "python-dotenv must be installed to load an env file.", + ctx=ctx, + param=param, + ) from None + + # Load if a value was passed, or we want to load default files, or both. + if value is not None or ctx.obj.load_dotenv_defaults: + load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults) + + return value + + +# This option is eager so env vars are loaded as early as possible to be +# used by other options. +_env_file_option = click.Option( + ["-e", "--env-file"], + type=click.Path(exists=True, dir_okay=False), + help=( + "Load environment variables from this file, taking precedence over" + " those set by '.env' and '.flaskenv'. Variables set directly in the" + " environment take highest precedence. python-dotenv must be installed." + ), + is_eager=True, + expose_value=False, + callback=_env_file_callback, +) + + +class FlaskGroup(AppGroup): + """Special subclass of the :class:`AppGroup` group that supports + loading more commands from the configured Flask app. Normally a + developer does not have to interface with this class but there are + some very advanced use cases for which it makes sense to create an + instance of this. see :ref:`custom-scripts`. + + :param add_default_commands: if this is True then the default run and + shell commands will be added. + :param add_version_option: adds the ``--version`` option. + :param create_app: an optional callback that is passed the script info and + returns the loaded app. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param set_debug_flag: Set the app's debug flag. + + .. versionchanged:: 3.1 + ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files. + + .. versionchanged:: 2.2 + Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options. + + .. versionchanged:: 2.2 + An app context is pushed when running ``app.cli`` commands, so + ``@with_appcontext`` is no longer required for those commands. + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment variables + from :file:`.env` and :file:`.flaskenv` files. + """ + + def __init__( + self, + add_default_commands: bool = True, + create_app: t.Callable[..., Flask] | None = None, + add_version_option: bool = True, + load_dotenv: bool = True, + set_debug_flag: bool = True, + **extra: t.Any, + ) -> None: + params: list[click.Parameter] = list(extra.pop("params", None) or ()) + # Processing is done with option callbacks instead of a group + # callback. This allows users to make a custom group callback + # without losing the behavior. --env-file must come first so + # that it is eagerly evaluated before --app. + params.extend((_env_file_option, _app_option, _debug_option)) + + if add_version_option: + params.append(version_option) + + if "context_settings" not in extra: + extra["context_settings"] = {} + + extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK") + + super().__init__(params=params, **extra) + + self.create_app = create_app + self.load_dotenv = load_dotenv + self.set_debug_flag = set_debug_flag + + if add_default_commands: + self.add_command(run_command) + self.add_command(shell_command) + self.add_command(routes_command) + + self._loaded_plugin_commands = False + + def _load_plugin_commands(self) -> None: + if self._loaded_plugin_commands: + return + + if sys.version_info >= (3, 10): + from importlib import metadata + else: + # Use a backport on Python < 3.10. We technically have + # importlib.metadata on 3.8+, but the API changed in 3.10, + # so use the backport for consistency. + import importlib_metadata as metadata # pyright: ignore + + for ep in metadata.entry_points(group="flask.commands"): + self.add_command(ep.load(), ep.name) + + self._loaded_plugin_commands = True + + def get_command(self, ctx: click.Context, name: str) -> click.Command | None: + self._load_plugin_commands() + # Look up built-in and plugin commands, which should be + # available even if the app fails to load. + rv = super().get_command(ctx, name) + + if rv is not None: + return rv + + info = ctx.ensure_object(ScriptInfo) + + # Look up commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + app = info.load_app() + except NoAppException as e: + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + return None + + # Push an app context for the loaded app unless it is already + # active somehow. This makes the context available to parameter + # and command callbacks without needing @with_appcontext. + if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined] + ctx.with_resource(app.app_context()) + + return app.cli.get_command(ctx, name) + + def list_commands(self, ctx: click.Context) -> list[str]: + self._load_plugin_commands() + # Start with the built-in and plugin commands. + rv = set(super().list_commands(ctx)) + info = ctx.ensure_object(ScriptInfo) + + # Add commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + rv.update(info.load_app().cli.list_commands(ctx)) + except NoAppException as e: + # When an app couldn't be loaded, show the error message + # without the traceback. + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + except Exception: + # When any other errors occurred during loading, show the + # full traceback. + click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") + + return sorted(rv) + + def make_context( + self, + info_name: str | None, + args: list[str], + parent: click.Context | None = None, + **extra: t.Any, + ) -> click.Context: + # Set a flag to tell app.run to become a no-op. If app.run was + # not in a __name__ == __main__ guard, it would start the server + # when importing, blocking whatever command is being called. + os.environ["FLASK_RUN_FROM_CLI"] = "true" + + if "obj" not in extra and "obj" not in self.context_settings: + extra["obj"] = ScriptInfo( + create_app=self.create_app, + set_debug_flag=self.set_debug_flag, + load_dotenv_defaults=self.load_dotenv, + ) + + return super().make_context(info_name, args, parent=parent, **extra) + + def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: + if (not args and self.no_args_is_help) or ( + len(args) == 1 and args[0] in self.get_help_option_names(ctx) + ): + # Attempt to load --env-file and --app early in case they + # were given as env vars. Otherwise no_args_is_help will not + # see commands from app.cli. + _env_file_option.handle_parse_result(ctx, {}, []) + _app_option.handle_parse_result(ctx, {}, []) + + return super().parse_args(ctx, args) + + +def _path_is_ancestor(path: str, other: str) -> bool: + """Take ``other`` and remove the length of ``path`` from it. Then join it + to ``path``. If it is the original value, ``path`` is an ancestor of + ``other``.""" + return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other + + +def load_dotenv( + path: str | os.PathLike[str] | None = None, load_defaults: bool = True +) -> bool: + """Load "dotenv" files to set environment variables. A given path takes + precedence over ``.env``, which takes precedence over ``.flaskenv``. After + loading and combining these files, values are only set if the key is not + already set in ``os.environ``. + + This is a no-op if `python-dotenv`_ is not installed. + + .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme + + :param path: Load the file at this location. + :param load_defaults: Search for and load the default ``.flaskenv`` and + ``.env`` files. + :return: ``True`` if at least one env var was loaded. + + .. versionchanged:: 3.1 + Added the ``load_defaults`` parameter. A given path takes precedence + over default files. + + .. versionchanged:: 2.0 + The current directory is not changed to the location of the + loaded file. + + .. versionchanged:: 2.0 + When loading the env files, set the default encoding to UTF-8. + + .. versionchanged:: 1.1.0 + Returns ``False`` when python-dotenv is not installed, or when + the given path isn't a file. + + .. versionadded:: 1.0 + """ + try: + import dotenv + except ImportError: + if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): + click.secho( + " * Tip: There are .env files present. Install python-dotenv" + " to use them.", + fg="yellow", + err=True, + ) + + return False + + data: dict[str, str | None] = {} + + if load_defaults: + for default_name in (".flaskenv", ".env"): + if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)): + continue + + data |= dotenv.dotenv_values(default_path, encoding="utf-8") + + if path is not None and os.path.isfile(path): + data |= dotenv.dotenv_values(path, encoding="utf-8") + + for key, value in data.items(): + if key in os.environ or value is None: + continue + + os.environ[key] = value + + return bool(data) # True if at least one env var was loaded. + + +def show_server_banner(debug: bool, app_import_path: str | None) -> None: + """Show extra startup messages the first time the server is run, + ignoring the reloader. + """ + if is_running_from_reloader(): + return + + if app_import_path is not None: + click.echo(f" * Serving Flask app '{app_import_path}'") + + if debug is not None: + click.echo(f" * Debug mode: {'on' if debug else 'off'}") + + +class CertParamType(click.ParamType): + """Click option type for the ``--cert`` option. Allows either an + existing file, the string ``'adhoc'``, or an import for a + :class:`~ssl.SSLContext` object. + """ + + name = "path" + + def __init__(self) -> None: + self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) + + def convert( + self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: + try: + import ssl + except ImportError: + raise click.BadParameter( + 'Using "--cert" requires Python to be compiled with SSL support.', + ctx, + param, + ) from None + + try: + return self.path_type(value, param, ctx) + except click.BadParameter: + value = click.STRING(value, param, ctx).lower() + + if value == "adhoc": + try: + import cryptography # noqa: F401 + except ImportError: + raise click.BadParameter( + "Using ad-hoc certificates requires the cryptography library.", + ctx, + param, + ) from None + + return value + + obj = import_string(value, silent=True) + + if isinstance(obj, ssl.SSLContext): + return obj + + raise + + +def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any: + """The ``--key`` option must be specified when ``--cert`` is a file. + Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. + """ + cert = ctx.params.get("cert") + is_adhoc = cert == "adhoc" + + try: + import ssl + except ImportError: + is_context = False + else: + is_context = isinstance(cert, ssl.SSLContext) + + if value is not None: + if is_adhoc: + raise click.BadParameter( + 'When "--cert" is "adhoc", "--key" is not used.', ctx, param + ) + + if is_context: + raise click.BadParameter( + 'When "--cert" is an SSLContext object, "--key" is not used.', + ctx, + param, + ) + + if not cert: + raise click.BadParameter('"--cert" must also be specified.', ctx, param) + + ctx.params["cert"] = cert, value + + else: + if cert and not (is_adhoc or is_context): + raise click.BadParameter('Required when using "--cert".', ctx, param) + + return value + + +class SeparatedPathType(click.Path): + """Click option type that accepts a list of values separated by the + OS's path separator (``:``, ``;`` on Windows). Each value is + validated as a :class:`click.Path` type. + """ + + def convert( + self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: + items = self.split_envvar_value(value) + # can't call no-arg super() inside list comprehension until Python 3.12 + super_convert = super().convert + return [super_convert(item, param, ctx) for item in items] + + +@click.command("run", short_help="Run a development server.") +@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") +@click.option("--port", "-p", default=5000, help="The port to bind to.") +@click.option( + "--cert", + type=CertParamType(), + help="Specify a certificate file to use HTTPS.", + is_eager=True, +) +@click.option( + "--key", + type=click.Path(exists=True, dir_okay=False, resolve_path=True), + callback=_validate_key, + expose_value=False, + help="The key file to use when specifying a certificate.", +) +@click.option( + "--reload/--no-reload", + default=None, + help="Enable or disable the reloader. By default the reloader " + "is active if debug is enabled.", +) +@click.option( + "--debugger/--no-debugger", + default=None, + help="Enable or disable the debugger. By default the debugger " + "is active if debug is enabled.", +) +@click.option( + "--with-threads/--without-threads", + default=True, + help="Enable or disable multithreading.", +) +@click.option( + "--extra-files", + default=None, + type=SeparatedPathType(), + help=( + "Extra files that trigger a reload on change. Multiple paths" + f" are separated by {os.path.pathsep!r}." + ), +) +@click.option( + "--exclude-patterns", + default=None, + type=SeparatedPathType(), + help=( + "Files matching these fnmatch patterns will not trigger a reload" + " on change. Multiple patterns are separated by" + f" {os.path.pathsep!r}." + ), +) +@pass_script_info +def run_command( + info: ScriptInfo, + host: str, + port: int, + reload: bool, + debugger: bool, + with_threads: bool, + cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None, + extra_files: list[str] | None, + exclude_patterns: list[str] | None, +) -> None: + """Run a local development server. + + This server is for development purposes only. It does not provide + the stability, security, or performance of production WSGI servers. + + The reloader and debugger are enabled by default with the '--debug' + option. + """ + try: + app: WSGIApplication = info.load_app() # pyright: ignore + except Exception as e: + if is_running_from_reloader(): + # When reloading, print out the error immediately, but raise + # it later so the debugger or server can handle it. + traceback.print_exc() + err = e + + def app( + environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: + raise err from None + + else: + # When not reloading, raise the error immediately so the + # command fails. + raise e from None + + debug = get_debug_flag() + + if reload is None: + reload = debug + + if debugger is None: + debugger = debug + + show_server_banner(debug, info.app_import_path) + + run_simple( + host, + port, + app, + use_reloader=reload, + use_debugger=debugger, + threaded=with_threads, + ssl_context=cert, + extra_files=extra_files, + exclude_patterns=exclude_patterns, + ) + + +run_command.params.insert(0, _debug_option) + + +@click.command("shell", short_help="Run a shell in the app context.") +@with_appcontext +def shell_command() -> None: + """Run an interactive Python shell in the context of a given + Flask application. The application will populate the default + namespace of this shell according to its configuration. + + This is useful for executing small snippets of management code + without having to manually configure the application. + """ + import code + + banner = ( + f"Python {sys.version} on {sys.platform}\n" + f"App: {current_app.import_name}\n" + f"Instance: {current_app.instance_path}" + ) + ctx: dict[str, t.Any] = {} + + # Support the regular Python interpreter startup script if someone + # is using it. + startup = os.environ.get("PYTHONSTARTUP") + if startup and os.path.isfile(startup): + with open(startup) as f: + eval(compile(f.read(), startup, "exec"), ctx) + + ctx.update(current_app.make_shell_context()) + + # Site, customize, or startup script can set a hook to call when + # entering interactive mode. The default one sets up readline with + # tab and history completion. + interactive_hook = getattr(sys, "__interactivehook__", None) + + if interactive_hook is not None: + try: + import readline + from rlcompleter import Completer + except ImportError: + pass + else: + # rlcompleter uses __main__.__dict__ by default, which is + # flask.__main__. Use the shell context instead. + readline.set_completer(Completer(ctx).complete) + + interactive_hook() + + code.interact(banner=banner, local=ctx) + + +@click.command("routes", short_help="Show the routes for the app.") +@click.option( + "--sort", + "-s", + type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), + default="endpoint", + help=( + "Method to sort routes by. 'match' is the order that Flask will match routes" + " when dispatching a request." + ), +) +@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") +@with_appcontext +def routes_command(sort: str, all_methods: bool) -> None: + """Show all registered routes with endpoints and methods.""" + rules = list(current_app.url_map.iter_rules()) + + if not rules: + click.echo("No routes were registered.") + return + + ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"} + host_matching = current_app.url_map.host_matching + has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) + rows = [] + + for rule in rules: + row = [ + rule.endpoint, + ", ".join(sorted((rule.methods or set()) - ignored_methods)), + ] + + if has_domain: + row.append((rule.host if host_matching else rule.subdomain) or "") + + row.append(rule.rule) + rows.append(row) + + headers = ["Endpoint", "Methods"] + sorts = ["endpoint", "methods"] + + if has_domain: + headers.append("Host" if host_matching else "Subdomain") + sorts.append("domain") + + headers.append("Rule") + sorts.append("rule") + + try: + rows.sort(key=itemgetter(sorts.index(sort))) + except ValueError: + pass + + rows.insert(0, headers) + widths = [max(len(row[i]) for row in rows) for i in range(len(headers))] + rows.insert(1, ["-" * w for w in widths]) + template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths)) + + for row in rows: + click.echo(template.format(*row)) + + +cli = FlaskGroup( + name="flask", + help="""\ +A general utility script for Flask applications. + +An application to load must be given with the '--app' option, +'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file +in the current directory. +""", +) + + +def main() -> None: + cli.main() + + +if __name__ == "__main__": + main() diff --git a/venv/lib/python3.10/site-packages/flask/config.py b/venv/lib/python3.10/site-packages/flask/config.py new file mode 100644 index 0000000..34ef1a5 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/config.py @@ -0,0 +1,367 @@ +from __future__ import annotations + +import errno +import json +import os +import types +import typing as t + +from werkzeug.utils import import_string + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .sansio.app import App + + +T = t.TypeVar("T") + + +class ConfigAttribute(t.Generic[T]): + """Makes an attribute forward to the config""" + + def __init__( + self, name: str, get_converter: t.Callable[[t.Any], T] | None = None + ) -> None: + self.__name__ = name + self.get_converter = get_converter + + @t.overload + def __get__(self, obj: None, owner: None) -> te.Self: ... + + @t.overload + def __get__(self, obj: App, owner: type[App]) -> T: ... + + def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self: + if obj is None: + return self + + rv = obj.config[self.__name__] + + if self.get_converter is not None: + rv = self.get_converter(rv) + + return rv # type: ignore[no-any-return] + + def __set__(self, obj: App, value: t.Any) -> None: + obj.config[self.__name__] = value + + +class Config(dict): # type: ignore[type-arg] + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__( + self, + root_path: str | os.PathLike[str], + defaults: dict[str, t.Any] | None = None, + ) -> None: + super().__init__(defaults or {}) + self.root_path = root_path + + def from_envvar(self, variable_name: str, silent: bool = False) -> bool: + """Loads a configuration from an environment variable pointing to + a configuration file. This is basically just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError( + f"The environment variable {variable_name!r} is not set" + " and as such configuration could not be loaded. Set" + " this variable and make it point to a configuration" + " file" + ) + return self.from_pyfile(rv, silent=silent) + + def from_prefixed_env( + self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads + ) -> bool: + """Load any environment variables that start with ``FLASK_``, + dropping the prefix from the env key for the config key. Values + are passed through a loading function to attempt to convert them + to more specific types than strings. + + Keys are loaded in :func:`sorted` order. + + The default loading function attempts to parse values as any + valid JSON type, including dicts and lists. + + Specific items in nested dicts can be set by separating the + keys with double underscores (``__``). If an intermediate key + doesn't exist, it will be initialized to an empty dict. + + :param prefix: Load env vars that start with this prefix, + separated with an underscore (``_``). + :param loads: Pass each string value to this function and use + the returned value as the config value. If any error is + raised it is ignored and the value remains a string. The + default is :func:`json.loads`. + + .. versionadded:: 2.1 + """ + prefix = f"{prefix}_" + + for key in sorted(os.environ): + if not key.startswith(prefix): + continue + + value = os.environ[key] + key = key.removeprefix(prefix) + + try: + value = loads(value) + except Exception: + # Keep the value as a string if loading failed. + pass + + if "__" not in key: + # A non-nested key, set directly. + self[key] = value + continue + + # Traverse nested dictionaries with keys separated by "__". + current = self + *parts, tail = key.split("__") + + for part in parts: + # If an intermediate dict does not exist, create it. + if part not in current: + current[part] = {} + + current = current[part] + + current[tail] = value + + return True + + def from_pyfile( + self, filename: str | os.PathLike[str], silent: bool = False + ) -> bool: + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + + .. versionadded:: 0.7 + `silent` parameter. + """ + filename = os.path.join(self.root_path, filename) + d = types.ModuleType("config") + d.__file__ = filename + try: + with open(filename, mode="rb") as config_file: + exec(compile(config_file.read(), filename, "exec"), d.__dict__) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): + return False + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + self.from_object(d) + return True + + def from_object(self, obj: object | str) -> None: + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. :meth:`from_object` + loads only the uppercase attributes of the module/class. A ``dict`` + object will not work with :meth:`from_object` because the keys of a + ``dict`` are not attributes of the ``dict`` class. + + Example of module-based configuration:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + Nothing is done to the object before loading. If the object is a + class and has ``@property`` attributes, it needs to be + instantiated before being passed to this method. + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + See :ref:`config-dev-prod` for an example of class-based configuration + using :meth:`from_object`. + + :param obj: an import name or object + """ + if isinstance(obj, str): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def from_file( + self, + filename: str | os.PathLike[str], + load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]], + silent: bool = False, + text: bool = True, + ) -> bool: + """Update the values in the config from a file that is loaded + using the ``load`` parameter. The loaded data is passed to the + :meth:`from_mapping` method. + + .. code-block:: python + + import json + app.config.from_file("config.json", load=json.load) + + import tomllib + app.config.from_file("config.toml", load=tomllib.load, text=False) + + :param filename: The path to the data file. This can be an + absolute path or relative to the config root path. + :param load: A callable that takes a file handle and returns a + mapping of loaded data from the file. + :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` + implements a ``read`` method. + :param silent: Ignore the file if it doesn't exist. + :param text: Open the file in text or binary mode. + :return: ``True`` if the file was loaded successfully. + + .. versionchanged:: 2.3 + The ``text`` parameter was added. + + .. versionadded:: 2.0 + """ + filename = os.path.join(self.root_path, filename) + + try: + with open(filename, "r" if text else "rb") as f: + obj = load(f) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + + return self.from_mapping(obj) + + def from_mapping( + self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any + ) -> bool: + """Updates the config like :meth:`update` ignoring items with + non-upper keys. + + :return: Always returns ``True``. + + .. versionadded:: 0.11 + """ + mappings: dict[str, t.Any] = {} + if mapping is not None: + mappings.update(mapping) + mappings.update(kwargs) + for key, value in mappings.items(): + if key.isupper(): + self[key] = value + return True + + def get_namespace( + self, namespace: str, lowercase: bool = True, trim_namespace: bool = True + ) -> dict[str, t.Any]: + """Returns a dictionary containing a subset of configuration options + that match the specified namespace/prefix. Example usage:: + + app.config['IMAGE_STORE_TYPE'] = 'fs' + app.config['IMAGE_STORE_PATH'] = '/var/app/images' + app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' + image_store_config = app.config.get_namespace('IMAGE_STORE_') + + The resulting dictionary `image_store_config` would look like:: + + { + 'type': 'fs', + 'path': '/var/app/images', + 'base_url': 'http://img.website.com' + } + + This is often useful when configuration options map directly to + keyword arguments in functions or class constructors. + + :param namespace: a configuration namespace + :param lowercase: a flag indicating if the keys of the resulting + dictionary should be lowercase + :param trim_namespace: a flag indicating if the keys of the resulting + dictionary should not include the namespace + + .. versionadded:: 0.11 + """ + rv = {} + for k, v in self.items(): + if not k.startswith(namespace): + continue + if trim_namespace: + key = k[len(namespace) :] + else: + key = k + if lowercase: + key = key.lower() + rv[key] = v + return rv + + def __repr__(self) -> str: + return f"<{type(self).__name__} {dict.__repr__(self)}>" diff --git a/venv/lib/python3.10/site-packages/flask/ctx.py b/venv/lib/python3.10/site-packages/flask/ctx.py new file mode 100644 index 0000000..222e818 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/ctx.py @@ -0,0 +1,449 @@ +from __future__ import annotations + +import contextvars +import sys +import typing as t +from functools import update_wrapper +from types import TracebackType + +from werkzeug.exceptions import HTTPException + +from . import typing as ft +from .globals import _cv_app +from .globals import _cv_request +from .signals import appcontext_popped +from .signals import appcontext_pushed + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIEnvironment + + from .app import Flask + from .sessions import SessionMixin + from .wrappers import Request + + +# a singleton sentinel value for parameter defaults +_sentinel = object() + + +class _AppCtxGlobals: + """A plain object. Used as a namespace for storing data during an + application context. + + Creating an app context automatically creates this object, which is + made available as the :data:`g` proxy. + + .. describe:: 'key' in g + + Check whether an attribute is present. + + .. versionadded:: 0.10 + + .. describe:: iter(g) + + Return an iterator over the attribute names. + + .. versionadded:: 0.10 + """ + + # Define attr methods to let mypy know this is a namespace object + # that has arbitrary attributes. + + def __getattr__(self, name: str) -> t.Any: + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def __setattr__(self, name: str, value: t.Any) -> None: + self.__dict__[name] = value + + def __delattr__(self, name: str) -> None: + try: + del self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def get(self, name: str, default: t.Any | None = None) -> t.Any: + """Get an attribute by name, or a default value. Like + :meth:`dict.get`. + + :param name: Name of attribute to get. + :param default: Value to return if the attribute is not present. + + .. versionadded:: 0.10 + """ + return self.__dict__.get(name, default) + + def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: + """Get and remove an attribute by name. Like :meth:`dict.pop`. + + :param name: Name of attribute to pop. + :param default: Value to return if the attribute is not present, + instead of raising a ``KeyError``. + + .. versionadded:: 0.11 + """ + if default is _sentinel: + return self.__dict__.pop(name) + else: + return self.__dict__.pop(name, default) + + def setdefault(self, name: str, default: t.Any = None) -> t.Any: + """Get the value of an attribute if it is present, otherwise + set and return a default value. Like :meth:`dict.setdefault`. + + :param name: Name of attribute to get. + :param default: Value to set and return if the attribute is not + present. + + .. versionadded:: 0.11 + """ + return self.__dict__.setdefault(name, default) + + def __contains__(self, item: str) -> bool: + return item in self.__dict__ + + def __iter__(self) -> t.Iterator[str]: + return iter(self.__dict__) + + def __repr__(self) -> str: + ctx = _cv_app.get(None) + if ctx is not None: + return f"" + return object.__repr__(self) + + +def after_this_request( + f: ft.AfterRequestCallable[t.Any], +) -> ft.AfterRequestCallable[t.Any]: + """Executes a function after this request. This is useful to modify + response objects. The function is passed the response object and has + to return the same or a new one. + + Example:: + + @app.route('/') + def index(): + @after_this_request + def add_header(response): + response.headers['X-Foo'] = 'Parachute' + return response + return 'Hello World!' + + This is more useful if a function other than the view function wants to + modify a response. For instance think of a decorator that wants to add + some headers without converting the return value into a response object. + + .. versionadded:: 0.9 + """ + ctx = _cv_request.get(None) + + if ctx is None: + raise RuntimeError( + "'after_this_request' can only be used when a request" + " context is active, such as in a view function." + ) + + ctx._after_request_functions.append(f) + return f + + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + + +def copy_current_request_context(f: F) -> F: + """A helper function that decorates a function to retain the current + request context. This is useful when working with greenlets. The moment + the function is decorated a copy of the request context is created and + then pushed when the function is called. The current session is also + included in the copied request context. + + Example:: + + import gevent + from flask import copy_current_request_context + + @app.route('/') + def index(): + @copy_current_request_context + def do_some_work(): + # do some work here, it can access flask.request or + # flask.session like you would otherwise in the view function. + ... + gevent.spawn(do_some_work) + return 'Regular response' + + .. versionadded:: 0.10 + """ + ctx = _cv_request.get(None) + + if ctx is None: + raise RuntimeError( + "'copy_current_request_context' can only be used when a" + " request context is active, such as in a view function." + ) + + ctx = ctx.copy() + + def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: + with ctx: + return ctx.app.ensure_sync(f)(*args, **kwargs) + + return update_wrapper(wrapper, f) # type: ignore[return-value] + + +def has_request_context() -> bool: + """If you have code that wants to test if a request context is there or + not this function can be used. For instance, you may want to take advantage + of request information if the request object is available, but fail + silently if it is unavailable. + + :: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and has_request_context(): + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + Alternatively you can also just test any of the context bound objects + (such as :class:`request` or :class:`g`) for truthness:: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and request: + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + .. versionadded:: 0.7 + """ + return _cv_request.get(None) is not None + + +def has_app_context() -> bool: + """Works like :func:`has_request_context` but for the application + context. You can also just do a boolean check on the + :data:`current_app` object instead. + + .. versionadded:: 0.9 + """ + return _cv_app.get(None) is not None + + +class AppContext: + """The app context contains application-specific information. An app + context is created and pushed at the beginning of each request if + one is not already active. An app context is also pushed when + running CLI commands. + """ + + def __init__(self, app: Flask) -> None: + self.app = app + self.url_adapter = app.create_url_adapter(None) + self.g: _AppCtxGlobals = app.app_ctx_globals_class() + self._cv_tokens: list[contextvars.Token[AppContext]] = [] + + def push(self) -> None: + """Binds the app context to the current context.""" + self._cv_tokens.append(_cv_app.set(self)) + appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) + + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore + """Pops the app context.""" + try: + if len(self._cv_tokens) == 1: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) + finally: + ctx = _cv_app.get() + _cv_app.reset(self._cv_tokens.pop()) + + if ctx is not self: + raise AssertionError( + f"Popped wrong app context. ({ctx!r} instead of {self!r})" + ) + + appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) + + def __enter__(self) -> AppContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + +class RequestContext: + """The request context contains per-request information. The Flask + app creates and pushes it at the beginning of the request, then pops + it at the end of the request. It will create the URL adapter and + request object for the WSGI environment provided. + + Do not attempt to use this class directly, instead use + :meth:`~flask.Flask.test_request_context` and + :meth:`~flask.Flask.request_context` to create this object. + + When the request context is popped, it will evaluate all the + functions registered on the application for teardown execution + (:meth:`~flask.Flask.teardown_request`). + + The request context is automatically popped at the end of the + request. When using the interactive debugger, the context will be + restored so ``request`` is still accessible. Similarly, the test + client can preserve the context after the request ends. However, + teardown functions may already have closed some resources such as + database connections. + """ + + def __init__( + self, + app: Flask, + environ: WSGIEnvironment, + request: Request | None = None, + session: SessionMixin | None = None, + ) -> None: + self.app = app + if request is None: + request = app.request_class(environ) + request.json_module = app.json + self.request: Request = request + self.url_adapter = None + try: + self.url_adapter = app.create_url_adapter(self.request) + except HTTPException as e: + self.request.routing_exception = e + self.flashes: list[tuple[str, str]] | None = None + self.session: SessionMixin | None = session + # Functions that should be executed after the request on the response + # object. These will be called before the regular "after_request" + # functions. + self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = [] + + self._cv_tokens: list[ + tuple[contextvars.Token[RequestContext], AppContext | None] + ] = [] + + def copy(self) -> RequestContext: + """Creates a copy of this request context with the same request object. + This can be used to move a request context to a different greenlet. + Because the actual request object is the same this cannot be used to + move a request context to a different thread unless access to the + request object is locked. + + .. versionadded:: 0.10 + + .. versionchanged:: 1.1 + The current session object is used instead of reloading the original + data. This prevents `flask.session` pointing to an out-of-date object. + """ + return self.__class__( + self.app, + environ=self.request.environ, + request=self.request, + session=self.session, + ) + + def match_request(self) -> None: + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + result = self.url_adapter.match(return_rule=True) # type: ignore + self.request.url_rule, self.request.view_args = result # type: ignore + except HTTPException as e: + self.request.routing_exception = e + + def push(self) -> None: + # Before we push the request context we have to ensure that there + # is an application context. + app_ctx = _cv_app.get(None) + + if app_ctx is None or app_ctx.app is not self.app: + app_ctx = self.app.app_context() + app_ctx.push() + else: + app_ctx = None + + self._cv_tokens.append((_cv_request.set(self), app_ctx)) + + # Open the session at the moment that the request context is available. + # This allows a custom open_session method to use the request context. + # Only open a new session if this is the first time the request was + # pushed, otherwise stream_with_context loses the session. + if self.session is None: + session_interface = self.app.session_interface + self.session = session_interface.open_session(self.app, self.request) + + if self.session is None: + self.session = session_interface.make_null_session(self.app) + + # Match the request URL after loading the session, so that the + # session is available in custom URL converters. + if self.url_adapter is not None: + self.match_request() + + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore + """Pops the request context and unbinds it by doing that. This will + also trigger the execution of functions registered by the + :meth:`~flask.Flask.teardown_request` decorator. + + .. versionchanged:: 0.9 + Added the `exc` argument. + """ + clear_request = len(self._cv_tokens) == 1 + + try: + if clear_request: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) + + request_close = getattr(self.request, "close", None) + if request_close is not None: + request_close() + finally: + ctx = _cv_request.get() + token, app_ctx = self._cv_tokens.pop() + _cv_request.reset(token) + + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + if clear_request: + ctx.request.environ["werkzeug.request"] = None + + if app_ctx is not None: + app_ctx.pop(exc) + + if ctx is not self: + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) + + def __enter__(self) -> RequestContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + def __repr__(self) -> str: + return ( + f"<{type(self).__name__} {self.request.url!r}" + f" [{self.request.method}] of {self.app.name}>" + ) diff --git a/venv/lib/python3.10/site-packages/flask/debughelpers.py b/venv/lib/python3.10/site-packages/flask/debughelpers.py new file mode 100644 index 0000000..2c8c4c4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/debughelpers.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +import typing as t + +from jinja2.loaders import BaseLoader +from werkzeug.routing import RequestRedirect + +from .blueprints import Blueprint +from .globals import request_ctx +from .sansio.app import App + +if t.TYPE_CHECKING: + from .sansio.scaffold import Scaffold + from .wrappers import Request + + +class UnexpectedUnicodeError(AssertionError, UnicodeError): + """Raised in places where we want some better error reporting for + unexpected unicode or binary data. + """ + + +class DebugFilesKeyError(KeyError, AssertionError): + """Raised from request.files during debugging. The idea is that it can + provide a better error message than just a generic KeyError/BadRequest. + """ + + def __init__(self, request: Request, key: str) -> None: + form_matches = request.form.getlist(key) + buf = [ + f"You tried to access the file {key!r} in the request.files" + " dictionary but it does not exist. The mimetype for the" + f" request is {request.mimetype!r} instead of" + " 'multipart/form-data' which means that no file contents" + " were transmitted. To fix this error you should provide" + ' enctype="multipart/form-data" in your form.' + ] + if form_matches: + names = ", ".join(repr(x) for x in form_matches) + buf.append( + "\n\nThe browser instead transmitted some file names. " + f"This was submitted: {names}" + ) + self.msg = "".join(buf) + + def __str__(self) -> str: + return self.msg + + +class FormDataRoutingRedirect(AssertionError): + """This exception is raised in debug mode if a routing redirect + would cause the browser to drop the method or body. This happens + when method is not GET, HEAD or OPTIONS and the status code is not + 307 or 308. + """ + + def __init__(self, request: Request) -> None: + exc = request.routing_exception + assert isinstance(exc, RequestRedirect) + buf = [ + f"A request was sent to '{request.url}', but routing issued" + f" a redirect to the canonical URL '{exc.new_url}'." + ] + + if f"{request.base_url}/" == exc.new_url.partition("?")[0]: + buf.append( + " The URL was defined with a trailing slash. Flask" + " will redirect to the URL with a trailing slash if it" + " was accessed without one." + ) + + buf.append( + " Send requests to the canonical URL, or use 307 or 308 for" + " routing redirects. Otherwise, browsers will drop form" + " data.\n\n" + "This exception is only raised in debug mode." + ) + super().__init__("".join(buf)) + + +def attach_enctype_error_multidict(request: Request) -> None: + """Patch ``request.files.__getitem__`` to raise a descriptive error + about ``enctype=multipart/form-data``. + + :param request: The request to patch. + :meta private: + """ + oldcls = request.files.__class__ + + class newcls(oldcls): # type: ignore[valid-type, misc] + def __getitem__(self, key: str) -> t.Any: + try: + return super().__getitem__(key) + except KeyError as e: + if key not in request.form: + raise + + raise DebugFilesKeyError(request, key).with_traceback( + e.__traceback__ + ) from None + + newcls.__name__ = oldcls.__name__ + newcls.__module__ = oldcls.__module__ + request.files.__class__ = newcls + + +def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]: + yield f"class: {type(loader).__module__}.{type(loader).__name__}" + for key, value in sorted(loader.__dict__.items()): + if key.startswith("_"): + continue + if isinstance(value, (tuple, list)): + if not all(isinstance(x, str) for x in value): + continue + yield f"{key}:" + for item in value: + yield f" - {item}" + continue + elif not isinstance(value, (str, int, float, bool)): + continue + yield f"{key}: {value!r}" + + +def explain_template_loading_attempts( + app: App, + template: str, + attempts: list[ + tuple[ + BaseLoader, + Scaffold, + tuple[str, str | None, t.Callable[[], bool] | None] | None, + ] + ], +) -> None: + """This should help developers understand what failed""" + info = [f"Locating template {template!r}:"] + total_found = 0 + blueprint = None + if request_ctx and request_ctx.request.blueprint is not None: + blueprint = request_ctx.request.blueprint + + for idx, (loader, srcobj, triple) in enumerate(attempts): + if isinstance(srcobj, App): + src_info = f"application {srcobj.import_name!r}" + elif isinstance(srcobj, Blueprint): + src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" + else: + src_info = repr(srcobj) + + info.append(f"{idx + 1:5}: trying loader of {src_info}") + + for line in _dump_loader_info(loader): + info.append(f" {line}") + + if triple is None: + detail = "no match" + else: + detail = f"found ({triple[1] or ''!r})" + total_found += 1 + info.append(f" -> {detail}") + + seems_fishy = False + if total_found == 0: + info.append("Error: the template could not be found.") + seems_fishy = True + elif total_found > 1: + info.append("Warning: multiple loaders returned a match for the template.") + seems_fishy = True + + if blueprint is not None and seems_fishy: + info.append( + " The template was looked up from an endpoint that belongs" + f" to the blueprint {blueprint!r}." + ) + info.append(" Maybe you did not place a template in the right folder?") + info.append(" See https://flask.palletsprojects.com/blueprints/#templates") + + app.logger.info("\n".join(info)) diff --git a/venv/lib/python3.10/site-packages/flask/globals.py b/venv/lib/python3.10/site-packages/flask/globals.py new file mode 100644 index 0000000..e2c410c --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/globals.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import typing as t +from contextvars import ContextVar + +from werkzeug.local import LocalProxy + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .ctx import _AppCtxGlobals + from .ctx import AppContext + from .ctx import RequestContext + from .sessions import SessionMixin + from .wrappers import Request + + +_no_app_msg = """\ +Working outside of application context. + +This typically means that you attempted to use functionality that needed +the current application. To solve this, set up an application context +with app.app_context(). See the documentation for more information.\ +""" +_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") +app_ctx: AppContext = LocalProxy( # type: ignore[assignment] + _cv_app, unbound_message=_no_app_msg +) +current_app: Flask = LocalProxy( # type: ignore[assignment] + _cv_app, "app", unbound_message=_no_app_msg +) +g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] + _cv_app, "g", unbound_message=_no_app_msg +) + +_no_req_msg = """\ +Working outside of request context. + +This typically means that you attempted to use functionality that needed +an active HTTP request. Consult the documentation on testing for +information about how to avoid this problem.\ +""" +_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") +request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] + _cv_request, unbound_message=_no_req_msg +) +request: Request = LocalProxy( # type: ignore[assignment] + _cv_request, "request", unbound_message=_no_req_msg +) +session: SessionMixin = LocalProxy( # type: ignore[assignment] + _cv_request, "session", unbound_message=_no_req_msg +) diff --git a/venv/lib/python3.10/site-packages/flask/helpers.py b/venv/lib/python3.10/site-packages/flask/helpers.py new file mode 100644 index 0000000..5d412c9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/helpers.py @@ -0,0 +1,641 @@ +from __future__ import annotations + +import importlib.util +import os +import sys +import typing as t +from datetime import datetime +from functools import cache +from functools import update_wrapper + +import werkzeug.utils +from werkzeug.exceptions import abort as _wz_abort +from werkzeug.utils import redirect as _wz_redirect +from werkzeug.wrappers import Response as BaseResponse + +from .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import request +from .globals import request_ctx +from .globals import session +from .signals import message_flashed + +if t.TYPE_CHECKING: # pragma: no cover + from .wrappers import Response + + +def get_debug_flag() -> bool: + """Get whether debug mode should be enabled for the app, indicated by the + :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. + """ + val = os.environ.get("FLASK_DEBUG") + return bool(val and val.lower() not in {"0", "false", "no"}) + + +def get_load_dotenv(default: bool = True) -> bool: + """Get whether the user has disabled loading default dotenv files by + setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load + the files. + + :param default: What to return if the env var isn't set. + """ + val = os.environ.get("FLASK_SKIP_DOTENV") + + if not val: + return default + + return val.lower() in ("0", "false", "no") + + +@t.overload +def stream_with_context( + generator_or_function: t.Iterator[t.AnyStr], +) -> t.Iterator[t.AnyStr]: ... + + +@t.overload +def stream_with_context( + generator_or_function: t.Callable[..., t.Iterator[t.AnyStr]], +) -> t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: ... + + +def stream_with_context( + generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]], +) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: + """Wrap a response generator function so that it runs inside the current + request context. This keeps :data:`request`, :data:`session`, and :data:`g` + available, even though at the point the generator runs the request context + will typically have ended. + + Use it as a decorator on a generator function: + + .. code-block:: python + + from flask import stream_with_context, request, Response + + @app.get("/stream") + def streamed_response(): + @stream_with_context + def generate(): + yield "Hello " + yield request.args["name"] + yield "!" + + return Response(generate()) + + Or use it as a wrapper around a created generator: + + .. code-block:: python + + from flask import stream_with_context, request, Response + + @app.get("/stream") + def streamed_response(): + def generate(): + yield "Hello " + yield request.args["name"] + yield "!" + + return Response(stream_with_context(generate())) + + .. versionadded:: 0.9 + """ + try: + gen = iter(generator_or_function) # type: ignore[arg-type] + except TypeError: + + def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: + gen = generator_or_function(*args, **kwargs) # type: ignore[operator] + return stream_with_context(gen) + + return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type] + + def generator() -> t.Iterator[t.AnyStr]: + if (req_ctx := _cv_request.get(None)) is None: + raise RuntimeError( + "'stream_with_context' can only be used when a request" + " context is active, such as in a view function." + ) + + app_ctx = _cv_app.get() + # Setup code below will run the generator to this point, so that the + # current contexts are recorded. The contexts must be pushed after, + # otherwise their ContextVar will record the wrong event loop during + # async view functions. + yield None # type: ignore[misc] + + # Push the app context first, so that the request context does not + # automatically create and push a different app context. + with app_ctx, req_ctx: + try: + yield from gen + finally: + # Clean up in case the user wrapped a WSGI iterator. + if hasattr(gen, "close"): + gen.close() + + # Execute the generator to the sentinel value. This ensures the context is + # preserved in the generator's state. Further iteration will push the + # context and yield from the original iterator. + wrapped_g = generator() + next(wrapped_g) + return wrapped_g + + +def make_response(*args: t.Any) -> Response: + """Sometimes it is necessary to set additional headers in a view. Because + views do not have to return response objects but can return a value that + is converted into a response object by Flask itself, it becomes tricky to + add headers to it. This function can be called instead of using a return + and you will get a response object which you can use to attach headers. + + If view looked like this and you want to add a new header:: + + def index(): + return render_template('index.html', foo=42) + + You can now do something like this:: + + def index(): + response = make_response(render_template('index.html', foo=42)) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + + This function accepts the very same arguments you can return from a + view function. This for example creates a response with a 404 error + code:: + + response = make_response(render_template('not_found.html'), 404) + + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + + Internally this function does the following things: + + - if no arguments are passed, it creates a new response argument + - if one argument is passed, :meth:`flask.Flask.make_response` + is invoked with it. + - if more than one argument is passed, the arguments are passed + to the :meth:`flask.Flask.make_response` function as tuple. + + .. versionadded:: 0.6 + """ + if not args: + return current_app.response_class() + if len(args) == 1: + args = args[0] + return current_app.make_response(args) + + +def url_for( + endpoint: str, + *, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, + **values: t.Any, +) -> str: + """Generate a URL to the given endpoint with the given values. + + This requires an active request or application context, and calls + :meth:`current_app.url_for() `. See that method + for full documentation. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it is + external. + :param _external: If given, prefer the URL to be internal (False) or + require it to be external (True). External URLs include the + scheme and domain. When not in an active request, URLs are + external by default. + :param values: Values to use for the variable parts of the URL rule. + Unknown keys are appended as query string arguments, like + ``?a=b&c=d``. + + .. versionchanged:: 2.2 + Calls ``current_app.url_for``, allowing an app to override the + behavior. + + .. versionchanged:: 0.10 + The ``_scheme`` parameter was added. + + .. versionchanged:: 0.9 + The ``_anchor`` and ``_method`` parameters were added. + + .. versionchanged:: 0.9 + Calls ``app.handle_url_build_error`` on build errors. + """ + return current_app.url_for( + endpoint, + _anchor=_anchor, + _method=_method, + _scheme=_scheme, + _external=_external, + **values, + ) + + +def redirect( + location: str, code: int = 302, Response: type[BaseResponse] | None = None +) -> BaseResponse: + """Create a redirect response object. + + If :data:`~flask.current_app` is available, it will use its + :meth:`~flask.Flask.redirect` method, otherwise it will use + :func:`werkzeug.utils.redirect`. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. + :param Response: The response class to use. Not used when + ``current_app`` is active, which uses ``app.response_class``. + + .. versionadded:: 2.2 + Calls ``current_app.redirect`` if available instead of always + using Werkzeug's default ``redirect``. + """ + if current_app: + return current_app.redirect(location, code=code) + + return _wz_redirect(location, code=code, Response=Response) + + +def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: + """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given + status code. + + If :data:`~flask.current_app` is available, it will call its + :attr:`~flask.Flask.aborter` object, otherwise it will use + :func:`werkzeug.exceptions.abort`. + + :param code: The status code for the exception, which must be + registered in ``app.aborter``. + :param args: Passed to the exception. + :param kwargs: Passed to the exception. + + .. versionadded:: 2.2 + Calls ``current_app.aborter`` if available instead of always + using Werkzeug's default ``abort``. + """ + if current_app: + current_app.aborter(code, *args, **kwargs) + + _wz_abort(code, *args, **kwargs) + + +def get_template_attribute(template_name: str, attribute: str) -> t.Any: + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named :file:`_cider.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_cider.html', 'hello') + return hello('World') + + .. versionadded:: 0.2 + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to access + """ + return getattr(current_app.jinja_env.get_template(template_name).module, attribute) + + +def flash(message: str, category: str = "message") -> None: + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + + .. versionchanged:: 0.3 + `category` parameter added. + + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string can be used as category. + """ + # Original implementation: + # + # session.setdefault('_flashes', []).append((category, message)) + # + # This assumed that changes made to mutable structures in the session are + # always in sync with the session object, which is not true for session + # implementations that use external storage for keeping their keys/values. + flashes = session.get("_flashes", []) + flashes.append((category, message)) + session["_flashes"] = flashes + app = current_app._get_current_object() # type: ignore + message_flashed.send( + app, + _async_wrapper=app.ensure_sync, + message=message, + category=category, + ) + + +def get_flashed_messages( + with_categories: bool = False, category_filter: t.Iterable[str] = () +) -> list[str] | list[tuple[str, str]]: + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. By default just the messages are returned, + but when `with_categories` is set to ``True``, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Filter the flashed messages to one or more categories by providing those + categories in `category_filter`. This allows rendering categories in + separate html blocks. The `with_categories` and `category_filter` + arguments are distinct: + + * `with_categories` controls whether categories are returned with message + text (``True`` gives a tuple, where ``False`` gives just the message text). + * `category_filter` filters the messages down to only those matching the + provided categories. + + See :doc:`/patterns/flashing` for examples. + + .. versionchanged:: 0.3 + `with_categories` parameter added. + + .. versionchanged:: 0.9 + `category_filter` parameter added. + + :param with_categories: set to ``True`` to also receive categories. + :param category_filter: filter of categories to limit return values. Only + categories in the list will be returned. + """ + flashes = request_ctx.flashes + if flashes is None: + flashes = session.pop("_flashes") if "_flashes" in session else [] + request_ctx.flashes = flashes + if category_filter: + flashes = list(filter(lambda f: f[0] in category_filter, flashes)) + if not with_categories: + return [x[1] for x in flashes] + return flashes + + +def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: + if kwargs.get("max_age") is None: + kwargs["max_age"] = current_app.get_send_file_max_age + + kwargs.update( + environ=request.environ, + use_x_sendfile=current_app.config["USE_X_SENDFILE"], + response_class=current_app.response_class, + _root_path=current_app.root_path, + ) + return kwargs + + +def send_file( + path_or_file: os.PathLike[t.AnyStr] | str | t.IO[bytes], + mimetype: str | None = None, + as_attachment: bool = False, + download_name: str | None = None, + conditional: bool = True, + etag: bool | str = True, + last_modified: datetime | int | float | None = None, + max_age: None | (int | t.Callable[[str | None], int | None]) = None, +) -> Response: + """Send the contents of a file to the client. + + The first argument can be a file path or a file-like object. Paths + are preferred in most cases because Werkzeug can manage the file and + get extra information from the path. Passing a file-like object + requires that the file is opened in binary mode, and is mostly + useful when building a file in memory with :class:`io.BytesIO`. + + Never pass file paths provided by a user. The path is assumed to be + trusted, so a user could craft a path to access a file you didn't + intend. Use :func:`send_from_directory` to safely serve + user-requested paths from within a directory. + + If the WSGI server sets a ``file_wrapper`` in ``environ``, it is + used, otherwise Werkzeug's built-in wrapper is used. Alternatively, + if the HTTP server supports ``X-Sendfile``, configuring Flask with + ``USE_X_SENDFILE = True`` will tell the server to send the given + path, which is much more efficient than reading it in Python. + + :param path_or_file: The path to the file to send, relative to the + current working directory if a relative path is given. + Alternatively, a file-like object opened in binary mode. Make + sure the file pointer is seeked to the start of the data. + :param mimetype: The MIME type to send for the file. If not + provided, it will try to detect it from the file name. + :param as_attachment: Indicate to a browser that it should offer to + save the file instead of displaying it. + :param download_name: The default name browsers will use when saving + the file. Defaults to the passed file name. + :param conditional: Enable conditional and range responses based on + request headers. Requires passing a file path and ``environ``. + :param etag: Calculate an ETag for the file, which requires passing + a file path. Can also be a string to use instead. + :param last_modified: The last modified time to send for the file, + in seconds. If not provided, it will try to detect it from the + file path. + :param max_age: How long the client should cache the file, in + seconds. If set, ``Cache-Control`` will be ``public``, otherwise + it will be ``no-cache`` to prefer conditional caching. + + .. versionchanged:: 2.0 + ``download_name`` replaces the ``attachment_filename`` + parameter. If ``as_attachment=False``, it is passed with + ``Content-Disposition: inline`` instead. + + .. versionchanged:: 2.0 + ``max_age`` replaces the ``cache_timeout`` parameter. + ``conditional`` is enabled and ``max_age`` is not set by + default. + + .. versionchanged:: 2.0 + ``etag`` replaces the ``add_etags`` parameter. It can be a + string to use instead of generating one. + + .. versionchanged:: 2.0 + Passing a file-like object that inherits from + :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather + than sending an empty file. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionchanged:: 1.1 + ``filename`` may be a :class:`~os.PathLike` object. + + .. versionchanged:: 1.1 + Passing a :class:`~io.BytesIO` object supports range requests. + + .. versionchanged:: 1.0.3 + Filenames are encoded with ASCII instead of Latin-1 for broader + compatibility with WSGI servers. + + .. versionchanged:: 1.0 + UTF-8 filenames as specified in :rfc:`2231` are supported. + + .. versionchanged:: 0.12 + The filename is no longer automatically inferred from file + objects. If you want to use automatic MIME and etag support, + pass a filename via ``filename_or_fp`` or + ``attachment_filename``. + + .. versionchanged:: 0.12 + ``attachment_filename`` is preferred over ``filename`` for MIME + detection. + + .. versionchanged:: 0.9 + ``cache_timeout`` defaults to + :meth:`Flask.get_send_file_max_age`. + + .. versionchanged:: 0.7 + MIME guessing and etag support for file-like objects was + removed because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. + + .. versionchanged:: 0.5 + The ``add_etags``, ``cache_timeout`` and ``conditional`` + parameters were added. The default behavior is to add etags. + + .. versionadded:: 0.2 + """ + return werkzeug.utils.send_file( # type: ignore[return-value] + **_prepare_send_file_kwargs( + path_or_file=path_or_file, + environ=request.environ, + mimetype=mimetype, + as_attachment=as_attachment, + download_name=download_name, + conditional=conditional, + etag=etag, + last_modified=last_modified, + max_age=max_age, + ) + ) + + +def send_from_directory( + directory: os.PathLike[str] | str, + path: os.PathLike[str] | str, + **kwargs: t.Any, +) -> Response: + """Send a file from within a directory using :func:`send_file`. + + .. code-block:: python + + @app.route("/uploads/") + def download_file(name): + return send_from_directory( + app.config['UPLOAD_FOLDER'], name, as_attachment=True + ) + + This is a secure way to serve files from a folder, such as static + files or uploads. Uses :func:`~werkzeug.security.safe_join` to + ensure the path coming from the client is not maliciously crafted to + point outside the specified directory. + + If the final path does not point to an existing regular file, + raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. + + :param directory: The directory that ``path`` must be located under, + relative to the current application's root path. This *must not* + be a value provided by the client, otherwise it becomes insecure. + :param path: The path to the file to send, relative to + ``directory``. + :param kwargs: Arguments to pass to :func:`send_file`. + + .. versionchanged:: 2.0 + ``path`` replaces the ``filename`` parameter. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionadded:: 0.5 + """ + return werkzeug.utils.send_from_directory( # type: ignore[return-value] + directory, path, **_prepare_send_file_kwargs(**kwargs) + ) + + +def get_root_path(import_name: str) -> str: + """Find the root path of a package, or the path that contains a + module. If it cannot be found, returns the current working + directory. + + Not to be confused with the value returned by :func:`find_package`. + + :meta private: + """ + # Module already imported and has a file attribute. Use that first. + mod = sys.modules.get(import_name) + + if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None: + return os.path.dirname(os.path.abspath(mod.__file__)) + + # Next attempt: check the loader. + try: + spec = importlib.util.find_spec(import_name) + + if spec is None: + raise ValueError + except (ImportError, ValueError): + loader = None + else: + loader = spec.loader + + # Loader does not exist or we're referring to an unloaded main + # module or a main module without path (interactive sessions), go + # with the current working directory. + if loader is None: + return os.getcwd() + + if hasattr(loader, "get_filename"): + filepath = loader.get_filename(import_name) # pyright: ignore + else: + # Fall back to imports. + __import__(import_name) + mod = sys.modules[import_name] + filepath = getattr(mod, "__file__", None) + + # If we don't have a file path it might be because it is a + # namespace package. In this case pick the root path from the + # first module that is contained in the package. + if filepath is None: + raise RuntimeError( + "No root path can be found for the provided module" + f" {import_name!r}. This can happen because the module" + " came from an import hook that does not provide file" + " name information or because it's a namespace package." + " In this case the root path needs to be explicitly" + " provided." + ) + + # filepath is import_name.py for a module, or __init__.py for a package. + return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return] + + +@cache +def _split_blueprint_path(name: str) -> list[str]: + out: list[str] = [name] + + if "." in name: + out.extend(_split_blueprint_path(name.rpartition(".")[0])) + + return out diff --git a/venv/lib/python3.10/site-packages/flask/json/__init__.py b/venv/lib/python3.10/site-packages/flask/json/__init__.py new file mode 100644 index 0000000..c0941d0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/json/__init__.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import json as _json +import typing as t + +from ..globals import current_app +from .provider import _default + +if t.TYPE_CHECKING: # pragma: no cover + from ..wrappers import Response + + +def dumps(obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.dumps() ` + method, otherwise it will use :func:`json.dumps`. + + :param obj: The data to serialize. + :param kwargs: Arguments passed to the ``dumps`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.dumps``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + if current_app: + return current_app.json.dumps(obj, **kwargs) + + kwargs.setdefault("default", _default) + return _json.dumps(obj, **kwargs) + + +def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: + """Serialize data as JSON and write to a file. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.dump() ` + method, otherwise it will use :func:`json.dump`. + + :param obj: The data to serialize. + :param fp: A file opened for writing text. Should use the UTF-8 + encoding to be valid JSON. + :param kwargs: Arguments passed to the ``dump`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.dump``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0 + Writing to a binary file, and the ``encoding`` argument, will be + removed in Flask 2.1. + """ + if current_app: + current_app.json.dump(obj, fp, **kwargs) + else: + kwargs.setdefault("default", _default) + _json.dump(obj, fp, **kwargs) + + +def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.loads() ` + method, otherwise it will use :func:`json.loads`. + + :param s: Text or UTF-8 bytes. + :param kwargs: Arguments passed to the ``loads`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.loads``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. The data must be a + string or UTF-8 bytes. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + if current_app: + return current_app.json.loads(s, **kwargs) + + return _json.loads(s, **kwargs) + + +def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON read from a file. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.load() ` + method, otherwise it will use :func:`json.load`. + + :param fp: A file opened for reading text or UTF-8 bytes. + :param kwargs: Arguments passed to the ``load`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.load``, allowing an app to override + the behavior. + + .. versionchanged:: 2.2 + The ``app`` parameter will be removed in Flask 2.3. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. The file must be text + mode, or binary mode with UTF-8 bytes. + """ + if current_app: + return current_app.json.load(fp, **kwargs) + + return _json.load(fp, **kwargs) + + +def jsonify(*args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with the ``application/json`` + mimetype. A dict or list returned from a view will be converted to a + JSON response automatically without needing to call this. + + This requires an active request or application context, and calls + :meth:`app.json.response() `. + + In debug mode, the output is formatted with indentation to make it + easier to read. This may also be controlled by the provider. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + + .. versionchanged:: 2.2 + Calls ``current_app.json.response``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + + .. versionchanged:: 0.11 + Added support for serializing top-level arrays. This was a + security risk in ancient browsers. See :ref:`security-json`. + + .. versionadded:: 0.2 + """ + return current_app.json.response(*args, **kwargs) # type: ignore[return-value] diff --git a/venv/lib/python3.10/site-packages/flask/json/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/json/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..01d3d7f Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/json/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/json/__pycache__/provider.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/json/__pycache__/provider.cpython-310.pyc new file mode 100644 index 0000000..fe5bfee Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/json/__pycache__/provider.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/json/__pycache__/tag.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/json/__pycache__/tag.cpython-310.pyc new file mode 100644 index 0000000..ac9d62b Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/json/__pycache__/tag.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/json/provider.py b/venv/lib/python3.10/site-packages/flask/json/provider.py new file mode 100644 index 0000000..ea7e475 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/json/provider.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import dataclasses +import decimal +import json +import typing as t +import uuid +import weakref +from datetime import date + +from werkzeug.http import http_date + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.sansio.response import Response + + from ..sansio.app import App + + +class JSONProvider: + """A standard set of JSON operations for an application. Subclasses + of this can be used to customize JSON behavior or use different + JSON libraries. + + To implement a provider for a specific library, subclass this base + class and implement at least :meth:`dumps` and :meth:`loads`. All + other methods have default implementations. + + To use a different provider, either subclass ``Flask`` and set + :attr:`~flask.Flask.json_provider_class` to a provider class, or set + :attr:`app.json ` to an instance of the class. + + :param app: An application instance. This will be stored as a + :class:`weakref.proxy` on the :attr:`_app` attribute. + + .. versionadded:: 2.2 + """ + + def __init__(self, app: App) -> None: + self._app: App = weakref.proxy(app) + + def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON. + + :param obj: The data to serialize. + :param kwargs: May be passed to the underlying JSON library. + """ + raise NotImplementedError + + def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: + """Serialize data as JSON and write to a file. + + :param obj: The data to serialize. + :param fp: A file opened for writing text. Should use the UTF-8 + encoding to be valid JSON. + :param kwargs: May be passed to the underlying JSON library. + """ + fp.write(self.dumps(obj, **kwargs)) + + def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON. + + :param s: Text or UTF-8 bytes. + :param kwargs: May be passed to the underlying JSON library. + """ + raise NotImplementedError + + def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON read from a file. + + :param fp: A file opened for reading text or UTF-8 bytes. + :param kwargs: May be passed to the underlying JSON library. + """ + return self.loads(fp.read(), **kwargs) + + def _prepare_response_obj( + self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] + ) -> t.Any: + if args and kwargs: + raise TypeError("app.json.response() takes either args or kwargs, not both") + + if not args and not kwargs: + return None + + if len(args) == 1: + return args[0] + + return args or kwargs + + def response(self, *args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with the ``application/json`` + mimetype. + + The :func:`~flask.json.jsonify` function calls this method for + the current application. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + """ + obj = self._prepare_response_obj(args, kwargs) + return self._app.response_class(self.dumps(obj), mimetype="application/json") + + +def _default(o: t.Any) -> t.Any: + if isinstance(o, date): + return http_date(o) + + if isinstance(o, (decimal.Decimal, uuid.UUID)): + return str(o) + + if dataclasses and dataclasses.is_dataclass(o): + return dataclasses.asdict(o) # type: ignore[arg-type] + + if hasattr(o, "__html__"): + return str(o.__html__()) + + raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") + + +class DefaultJSONProvider(JSONProvider): + """Provide JSON operations using Python's built-in :mod:`json` + library. Serializes the following additional data types: + + - :class:`datetime.datetime` and :class:`datetime.date` are + serialized to :rfc:`822` strings. This is the same as the HTTP + date format. + - :class:`uuid.UUID` is serialized to a string. + - :class:`dataclasses.dataclass` is passed to + :func:`dataclasses.asdict`. + - :class:`~markupsafe.Markup` (or any object with a ``__html__`` + method) will call the ``__html__`` method to get a string. + """ + + default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment] + """Apply this function to any object that :meth:`json.dumps` does + not know how to serialize. It should return a valid JSON type or + raise a ``TypeError``. + """ + + ensure_ascii = True + """Replace non-ASCII characters with escape sequences. This may be + more compatible with some clients, but can be disabled for better + performance and size. + """ + + sort_keys = True + """Sort the keys in any serialized dicts. This may be useful for + some caching situations, but can be disabled for better performance. + When enabled, keys must all be strings, they are not converted + before sorting. + """ + + compact: bool | None = None + """If ``True``, or ``None`` out of debug mode, the :meth:`response` + output will not add indentation, newlines, or spaces. If ``False``, + or ``None`` in debug mode, it will use a non-compact representation. + """ + + mimetype = "application/json" + """The mimetype set in :meth:`response`.""" + + def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON to a string. + + Keyword arguments are passed to :func:`json.dumps`. Sets some + parameter defaults from the :attr:`default`, + :attr:`ensure_ascii`, and :attr:`sort_keys` attributes. + + :param obj: The data to serialize. + :param kwargs: Passed to :func:`json.dumps`. + """ + kwargs.setdefault("default", self.default) + kwargs.setdefault("ensure_ascii", self.ensure_ascii) + kwargs.setdefault("sort_keys", self.sort_keys) + return json.dumps(obj, **kwargs) + + def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON from a string or bytes. + + :param s: Text or UTF-8 bytes. + :param kwargs: Passed to :func:`json.loads`. + """ + return json.loads(s, **kwargs) + + def response(self, *args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with it. The response mimetype + will be "application/json" and can be changed with + :attr:`mimetype`. + + If :attr:`compact` is ``False`` or debug mode is enabled, the + output will be formatted to be easier to read. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + """ + obj = self._prepare_response_obj(args, kwargs) + dump_args: dict[str, t.Any] = {} + + if (self.compact is None and self._app.debug) or self.compact is False: + dump_args.setdefault("indent", 2) + else: + dump_args.setdefault("separators", (",", ":")) + + return self._app.response_class( + f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype + ) diff --git a/venv/lib/python3.10/site-packages/flask/json/tag.py b/venv/lib/python3.10/site-packages/flask/json/tag.py new file mode 100644 index 0000000..8dc3629 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/json/tag.py @@ -0,0 +1,327 @@ +""" +Tagged JSON +~~~~~~~~~~~ + +A compact representation for lossless serialization of non-standard JSON +types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this +to serialize the session data, but it may be useful in other places. It +can be extended to support other types. + +.. autoclass:: TaggedJSONSerializer + :members: + +.. autoclass:: JSONTag + :members: + +Let's see an example that adds support for +:class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so +to handle this we will dump the items as a list of ``[key, value]`` +pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to +identify the type. The session serializer processes dicts first, so +insert the new tag at the front of the order since ``OrderedDict`` must +be processed before ``dict``. + +.. code-block:: python + + from flask.json.tag import JSONTag + + class TagOrderedDict(JSONTag): + __slots__ = ('serializer',) + key = ' od' + + def check(self, value): + return isinstance(value, OrderedDict) + + def to_json(self, value): + return [[k, self.serializer.tag(v)] for k, v in iteritems(value)] + + def to_python(self, value): + return OrderedDict(value) + + app.session_interface.serializer.register(TagOrderedDict, index=0) +""" + +from __future__ import annotations + +import typing as t +from base64 import b64decode +from base64 import b64encode +from datetime import datetime +from uuid import UUID + +from markupsafe import Markup +from werkzeug.http import http_date +from werkzeug.http import parse_date + +from ..json import dumps +from ..json import loads + + +class JSONTag: + """Base class for defining type tags for :class:`TaggedJSONSerializer`.""" + + __slots__ = ("serializer",) + + #: The tag to mark the serialized object with. If empty, this tag is + #: only used as an intermediate step during tagging. + key: str = "" + + def __init__(self, serializer: TaggedJSONSerializer) -> None: + """Create a tagger for the given serializer.""" + self.serializer = serializer + + def check(self, value: t.Any) -> bool: + """Check if the given value should be tagged by this tag.""" + raise NotImplementedError + + def to_json(self, value: t.Any) -> t.Any: + """Convert the Python object to an object that is a valid JSON type. + The tag will be added later.""" + raise NotImplementedError + + def to_python(self, value: t.Any) -> t.Any: + """Convert the JSON representation back to the correct type. The tag + will already be removed.""" + raise NotImplementedError + + def tag(self, value: t.Any) -> dict[str, t.Any]: + """Convert the value to a valid JSON type and add the tag structure + around it.""" + return {self.key: self.to_json(value)} + + +class TagDict(JSONTag): + """Tag for 1-item dicts whose only key matches a registered tag. + + Internally, the dict key is suffixed with `__`, and the suffix is removed + when deserializing. + """ + + __slots__ = () + key = " di" + + def check(self, value: t.Any) -> bool: + return ( + isinstance(value, dict) + and len(value) == 1 + and next(iter(value)) in self.serializer.tags + ) + + def to_json(self, value: t.Any) -> t.Any: + key = next(iter(value)) + return {f"{key}__": self.serializer.tag(value[key])} + + def to_python(self, value: t.Any) -> t.Any: + key = next(iter(value)) + return {key[:-2]: value[key]} + + +class PassDict(JSONTag): + __slots__ = () + + def check(self, value: t.Any) -> bool: + return isinstance(value, dict) + + def to_json(self, value: t.Any) -> t.Any: + # JSON objects may only have string keys, so don't bother tagging the + # key here. + return {k: self.serializer.tag(v) for k, v in value.items()} + + tag = to_json + + +class TagTuple(JSONTag): + __slots__ = () + key = " t" + + def check(self, value: t.Any) -> bool: + return isinstance(value, tuple) + + def to_json(self, value: t.Any) -> t.Any: + return [self.serializer.tag(item) for item in value] + + def to_python(self, value: t.Any) -> t.Any: + return tuple(value) + + +class PassList(JSONTag): + __slots__ = () + + def check(self, value: t.Any) -> bool: + return isinstance(value, list) + + def to_json(self, value: t.Any) -> t.Any: + return [self.serializer.tag(item) for item in value] + + tag = to_json + + +class TagBytes(JSONTag): + __slots__ = () + key = " b" + + def check(self, value: t.Any) -> bool: + return isinstance(value, bytes) + + def to_json(self, value: t.Any) -> t.Any: + return b64encode(value).decode("ascii") + + def to_python(self, value: t.Any) -> t.Any: + return b64decode(value) + + +class TagMarkup(JSONTag): + """Serialize anything matching the :class:`~markupsafe.Markup` API by + having a ``__html__`` method to the result of that method. Always + deserializes to an instance of :class:`~markupsafe.Markup`.""" + + __slots__ = () + key = " m" + + def check(self, value: t.Any) -> bool: + return callable(getattr(value, "__html__", None)) + + def to_json(self, value: t.Any) -> t.Any: + return str(value.__html__()) + + def to_python(self, value: t.Any) -> t.Any: + return Markup(value) + + +class TagUUID(JSONTag): + __slots__ = () + key = " u" + + def check(self, value: t.Any) -> bool: + return isinstance(value, UUID) + + def to_json(self, value: t.Any) -> t.Any: + return value.hex + + def to_python(self, value: t.Any) -> t.Any: + return UUID(value) + + +class TagDateTime(JSONTag): + __slots__ = () + key = " d" + + def check(self, value: t.Any) -> bool: + return isinstance(value, datetime) + + def to_json(self, value: t.Any) -> t.Any: + return http_date(value) + + def to_python(self, value: t.Any) -> t.Any: + return parse_date(value) + + +class TaggedJSONSerializer: + """Serializer that uses a tag system to compactly represent objects that + are not JSON types. Passed as the intermediate serializer to + :class:`itsdangerous.Serializer`. + + The following extra types are supported: + + * :class:`dict` + * :class:`tuple` + * :class:`bytes` + * :class:`~markupsafe.Markup` + * :class:`~uuid.UUID` + * :class:`~datetime.datetime` + """ + + __slots__ = ("tags", "order") + + #: Tag classes to bind when creating the serializer. Other tags can be + #: added later using :meth:`~register`. + default_tags = [ + TagDict, + PassDict, + TagTuple, + PassList, + TagBytes, + TagMarkup, + TagUUID, + TagDateTime, + ] + + def __init__(self) -> None: + self.tags: dict[str, JSONTag] = {} + self.order: list[JSONTag] = [] + + for cls in self.default_tags: + self.register(cls) + + def register( + self, + tag_class: type[JSONTag], + force: bool = False, + index: int | None = None, + ) -> None: + """Register a new tag with this serializer. + + :param tag_class: tag class to register. Will be instantiated with this + serializer instance. + :param force: overwrite an existing tag. If false (default), a + :exc:`KeyError` is raised. + :param index: index to insert the new tag in the tag order. Useful when + the new tag is a special case of an existing tag. If ``None`` + (default), the tag is appended to the end of the order. + + :raise KeyError: if the tag key is already registered and ``force`` is + not true. + """ + tag = tag_class(self) + key = tag.key + + if key: + if not force and key in self.tags: + raise KeyError(f"Tag '{key}' is already registered.") + + self.tags[key] = tag + + if index is None: + self.order.append(tag) + else: + self.order.insert(index, tag) + + def tag(self, value: t.Any) -> t.Any: + """Convert a value to a tagged representation if necessary.""" + for tag in self.order: + if tag.check(value): + return tag.tag(value) + + return value + + def untag(self, value: dict[str, t.Any]) -> t.Any: + """Convert a tagged representation back to the original type.""" + if len(value) != 1: + return value + + key = next(iter(value)) + + if key not in self.tags: + return value + + return self.tags[key].to_python(value[key]) + + def _untag_scan(self, value: t.Any) -> t.Any: + if isinstance(value, dict): + # untag each item recursively + value = {k: self._untag_scan(v) for k, v in value.items()} + # untag the dict itself + value = self.untag(value) + elif isinstance(value, list): + # untag each item recursively + value = [self._untag_scan(item) for item in value] + + return value + + def dumps(self, value: t.Any) -> str: + """Tag the value and dump it to a compact JSON string.""" + return dumps(self.tag(value), separators=(",", ":")) + + def loads(self, value: str) -> t.Any: + """Load data from a JSON string and deserialized any tagged objects.""" + return self._untag_scan(loads(value)) diff --git a/venv/lib/python3.10/site-packages/flask/logging.py b/venv/lib/python3.10/site-packages/flask/logging.py new file mode 100644 index 0000000..0cb8f43 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/logging.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import logging +import sys +import typing as t + +from werkzeug.local import LocalProxy + +from .globals import request + +if t.TYPE_CHECKING: # pragma: no cover + from .sansio.app import App + + +@LocalProxy +def wsgi_errors_stream() -> t.TextIO: + """Find the most appropriate error stream for the application. If a request + is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``. + + If you configure your own :class:`logging.StreamHandler`, you may want to + use this for the stream. If you are using file or dict configuration and + can't import this directly, you can refer to it as + ``ext://flask.logging.wsgi_errors_stream``. + """ + if request: + return request.environ["wsgi.errors"] # type: ignore[no-any-return] + + return sys.stderr + + +def has_level_handler(logger: logging.Logger) -> bool: + """Check if there is a handler in the logging chain that will handle the + given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`. + """ + level = logger.getEffectiveLevel() + current = logger + + while current: + if any(handler.level <= level for handler in current.handlers): + return True + + if not current.propagate: + break + + current = current.parent # type: ignore + + return False + + +#: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format +#: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``. +default_handler = logging.StreamHandler(wsgi_errors_stream) # type: ignore +default_handler.setFormatter( + logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") +) + + +def create_logger(app: App) -> logging.Logger: + """Get the Flask app's logger and configure it if needed. + + The logger name will be the same as + :attr:`app.import_name `. + + When :attr:`~flask.Flask.debug` is enabled, set the logger level to + :data:`logging.DEBUG` if it is not set. + + If there is no handler for the logger's effective level, add a + :class:`~logging.StreamHandler` for + :func:`~flask.logging.wsgi_errors_stream` with a basic format. + """ + logger = logging.getLogger(app.name) + + if app.debug and not logger.level: + logger.setLevel(logging.DEBUG) + + if not has_level_handler(logger): + logger.addHandler(default_handler) + + return logger diff --git a/venv/lib/python3.10/site-packages/flask/py.typed b/venv/lib/python3.10/site-packages/flask/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/flask/sansio/README.md b/venv/lib/python3.10/site-packages/flask/sansio/README.md new file mode 100644 index 0000000..623ac19 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/sansio/README.md @@ -0,0 +1,6 @@ +# Sansio + +This folder contains code that can be used by alternative Flask +implementations, for example Quart. The code therefore cannot do any +IO, nor be part of a likely IO path. Finally this code cannot use the +Flask globals. diff --git a/venv/lib/python3.10/site-packages/flask/sansio/__pycache__/app.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/sansio/__pycache__/app.cpython-310.pyc new file mode 100644 index 0000000..a7ac375 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/sansio/__pycache__/app.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/sansio/__pycache__/blueprints.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/sansio/__pycache__/blueprints.cpython-310.pyc new file mode 100644 index 0000000..ee28daa Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/sansio/__pycache__/blueprints.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/sansio/__pycache__/scaffold.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask/sansio/__pycache__/scaffold.cpython-310.pyc new file mode 100644 index 0000000..b1d1ac9 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask/sansio/__pycache__/scaffold.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask/sansio/app.py b/venv/lib/python3.10/site-packages/flask/sansio/app.py new file mode 100644 index 0000000..a2592fe --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/sansio/app.py @@ -0,0 +1,964 @@ +from __future__ import annotations + +import logging +import os +import sys +import typing as t +from datetime import timedelta +from itertools import chain + +from werkzeug.exceptions import Aborter +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import BadRequestKeyError +from werkzeug.routing import BuildError +from werkzeug.routing import Map +from werkzeug.routing import Rule +from werkzeug.sansio.response import Response +from werkzeug.utils import cached_property +from werkzeug.utils import redirect as _wz_redirect + +from .. import typing as ft +from ..config import Config +from ..config import ConfigAttribute +from ..ctx import _AppCtxGlobals +from ..helpers import _split_blueprint_path +from ..helpers import get_debug_flag +from ..json.provider import DefaultJSONProvider +from ..json.provider import JSONProvider +from ..logging import create_logger +from ..templating import DispatchingJinjaLoader +from ..templating import Environment +from .scaffold import _endpoint_from_view_func +from .scaffold import find_package +from .scaffold import Scaffold +from .scaffold import setupmethod + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.wrappers import Response as BaseResponse + + from ..testing import FlaskClient + from ..testing import FlaskCliRunner + from .blueprints import Blueprint + +T_shell_context_processor = t.TypeVar( + "T_shell_context_processor", bound=ft.ShellContextProcessorCallable +) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) + + +def _make_timedelta(value: timedelta | int | None) -> timedelta | None: + if value is None or isinstance(value, timedelta): + return value + + return timedelta(seconds=value) + + +class App(Scaffold): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the :file:`__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea of what + belongs to your application. This name is used to find resources + on the filesystem, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in :file:`yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + .. versionadded:: 0.11 + The `root_path` parameter was added. + + .. versionadded:: 1.0 + The ``host_matching`` and ``static_host`` parameters were added. + + .. versionadded:: 1.0 + The ``subdomain_matching`` parameter was added. Subdomain + matching needs to be enabled manually now. Setting + :data:`SERVER_NAME` does not implicitly enable it. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: The folder with static files that is served at + ``static_url_path``. Relative to the application ``root_path`` + or an absolute path. Defaults to ``'static'``. + :param static_host: the host to use when adding the static route. + Defaults to None. Required when using ``host_matching=True`` + with a ``static_folder`` configured. + :param host_matching: set ``url_map.host_matching`` attribute. + Defaults to False. + :param subdomain_matching: consider the subdomain relative to + :data:`SERVER_NAME` when matching routes. Defaults to False. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to ``True`` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + :param root_path: The path to the root of the application files. + This should only be set manually when it can't be detected + automatically, such as for namespace packages. + """ + + #: The class of the object assigned to :attr:`aborter`, created by + #: :meth:`create_aborter`. That object is called by + #: :func:`flask.abort` to raise HTTP errors, and can be + #: called directly as well. + #: + #: Defaults to :class:`werkzeug.exceptions.Aborter`. + #: + #: .. versionadded:: 2.2 + aborter_class = Aborter + + #: The class that is used for the Jinja environment. + #: + #: .. versionadded:: 0.11 + jinja_environment = Environment + + #: The class that is used for the :data:`~flask.g` instance. + #: + #: Example use cases for a custom class: + #: + #: 1. Store arbitrary attributes on flask.g. + #: 2. Add a property for lazy per-request database connectors. + #: 3. Return None instead of AttributeError on unexpected attributes. + #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. + #: + #: In Flask 0.9 this property was called `request_globals_class` but it + #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the + #: flask.g object is now application context scoped. + #: + #: .. versionadded:: 0.10 + app_ctx_globals_class = _AppCtxGlobals + + #: The class that is used for the ``config`` attribute of this app. + #: Defaults to :class:`~flask.Config`. + #: + #: Example use cases for a custom class: + #: + #: 1. Default values for certain config options. + #: 2. Access to config values through attributes in addition to keys. + #: + #: .. versionadded:: 0.11 + config_class = Config + + #: The testing flag. Set this to ``True`` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate test helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: + #: This attribute can also be configured from the config with the + #: ``TESTING`` configuration key. Defaults to ``False``. + testing = ConfigAttribute[bool]("TESTING") + + #: If a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. + secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY") + + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute[timedelta]( + "PERMANENT_SESSION_LIFETIME", + get_converter=_make_timedelta, # type: ignore[arg-type] + ) + + json_provider_class: type[JSONProvider] = DefaultJSONProvider + """A subclass of :class:`~flask.json.provider.JSONProvider`. An + instance is created and assigned to :attr:`app.json` when creating + the app. + + The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses + Python's built-in :mod:`json` library. A different provider can use + a different JSON library. + + .. versionadded:: 2.2 + """ + + #: Options that are passed to the Jinja environment in + #: :meth:`create_jinja_environment`. Changing these options after + #: the environment is created (accessing :attr:`jinja_env`) will + #: have no effect. + #: + #: .. versionchanged:: 1.1.0 + #: This is a ``dict`` instead of an ``ImmutableDict`` to allow + #: easier configuration. + #: + jinja_options: dict[str, t.Any] = {} + + #: The rule object to use for URL rules created. This is used by + #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. + #: + #: .. versionadded:: 0.7 + url_rule_class = Rule + + #: The map object to use for storing the URL rules and routing + #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. + #: + #: .. versionadded:: 1.1.0 + url_map_class = Map + + #: The :meth:`test_client` method creates an instance of this test + #: client class. Defaults to :class:`~flask.testing.FlaskClient`. + #: + #: .. versionadded:: 0.7 + test_client_class: type[FlaskClient] | None = None + + #: The :class:`~click.testing.CliRunner` subclass, by default + #: :class:`~flask.testing.FlaskCliRunner` that is used by + #: :meth:`test_cli_runner`. Its ``__init__`` method should take a + #: Flask app object as the first argument. + #: + #: .. versionadded:: 1.0 + test_cli_runner_class: type[FlaskCliRunner] | None = None + + default_config: dict[str, t.Any] + response_class: type[Response] + + def __init__( + self, + import_name: str, + static_url_path: str | None = None, + static_folder: str | os.PathLike[str] | None = "static", + static_host: str | None = None, + host_matching: bool = False, + subdomain_matching: bool = False, + template_folder: str | os.PathLike[str] | None = "templates", + instance_path: str | None = None, + instance_relative_config: bool = False, + root_path: str | None = None, + ) -> None: + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if instance_path is None: + instance_path = self.auto_find_instance_path() + elif not os.path.isabs(instance_path): + raise ValueError( + "If an instance path is provided it must be absolute." + " A relative path was given instead." + ) + + #: Holds the path to the instance folder. + #: + #: .. versionadded:: 0.8 + self.instance_path = instance_path + + #: The configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = self.make_config(instance_relative_config) + + #: An instance of :attr:`aborter_class` created by + #: :meth:`make_aborter`. This is called by :func:`flask.abort` + #: to raise HTTP errors, and can be called directly as well. + #: + #: .. versionadded:: 2.2 + #: Moved from ``flask.abort``, which calls this object. + self.aborter = self.make_aborter() + + self.json: JSONProvider = self.json_provider_class(self) + """Provides access to JSON methods. Functions in ``flask.json`` + will call methods on this provider when the application context + is active. Used for handling JSON requests and responses. + + An instance of :attr:`json_provider_class`. Can be customized by + changing that attribute on a subclass, or by assigning to this + attribute afterwards. + + The default, :class:`~flask.json.provider.DefaultJSONProvider`, + uses Python's built-in :mod:`json` library. A different provider + can use a different JSON library. + + .. versionadded:: 2.2 + """ + + #: A list of functions that are called by + #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a + #: :exc:`~werkzeug.routing.BuildError`. Each function is called + #: with ``error``, ``endpoint`` and ``values``. If a function + #: returns ``None`` or raises a ``BuildError``, it is skipped. + #: Otherwise, its return value is returned by ``url_for``. + #: + #: .. versionadded:: 0.9 + self.url_build_error_handlers: list[ + t.Callable[[Exception, str, dict[str, t.Any]], str] + ] = [] + + #: A list of functions that are called when the application context + #: is destroyed. Since the application context is also torn down + #: if the request ends this is the place to store code that disconnects + #: from databases. + #: + #: .. versionadded:: 0.9 + self.teardown_appcontext_funcs: list[ft.TeardownCallable] = [] + + #: A list of shell context processor functions that should be run + #: when a shell context is created. + #: + #: .. versionadded:: 0.11 + self.shell_context_processors: list[ft.ShellContextProcessorCallable] = [] + + #: Maps registered blueprint names to blueprint objects. The + #: dict retains the order the blueprints were registered in. + #: Blueprints can be registered multiple times, this dict does + #: not track how often they were attached. + #: + #: .. versionadded:: 0.7 + self.blueprints: dict[str, Blueprint] = {} + + #: a place where extensions can store application specific state. For + #: example this is where an extension could store database engines and + #: similar things. + #: + #: The key must match the name of the extension module. For example in + #: case of a "Flask-Foo" extension in `flask_foo`, the key would be + #: ``'foo'``. + #: + #: .. versionadded:: 0.7 + self.extensions: dict[str, t.Any] = {} + + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug.routing import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(super(ListConverter, self).to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter + self.url_map = self.url_map_class(host_matching=host_matching) + + self.subdomain_matching = subdomain_matching + + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + + def _check_setup_finished(self, f_name: str) -> None: + if self._got_first_request: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called" + " on the application. It has already handled its first" + " request, any changes will not be applied" + " consistently.\n" + "Make sure all imports, decorators, functions, etc." + " needed to set up the application are done before" + " running it." + ) + + @cached_property + def name(self) -> str: + """The name of the application. This is usually the import name + with the difference that it's guessed from the run file if the + import name is main. This name is used as a display name when + Flask needs the name of the application. It can be set and overridden + to change the value. + + .. versionadded:: 0.8 + """ + if self.import_name == "__main__": + fn: str | None = getattr(sys.modules["__main__"], "__file__", None) + if fn is None: + return "__main__" + return os.path.splitext(os.path.basename(fn))[0] + return self.import_name + + @cached_property + def logger(self) -> logging.Logger: + """A standard Python :class:`~logging.Logger` for the app, with + the same name as :attr:`name`. + + In debug mode, the logger's :attr:`~logging.Logger.level` will + be set to :data:`~logging.DEBUG`. + + If there are no handlers configured, a default handler will be + added. See :doc:`/logging` for more information. + + .. versionchanged:: 1.1.0 + The logger takes the same name as :attr:`name` rather than + hard-coding ``"flask.app"``. + + .. versionchanged:: 1.0.0 + Behavior was simplified. The logger is always named + ``"flask.app"``. The level is only set during configuration, + it doesn't check ``app.debug`` each time. Only one format is + used, not different ones depending on ``app.debug``. No + handlers are removed, and a handler is only added if no + handlers are already configured. + + .. versionadded:: 0.3 + """ + return create_logger(self) + + @cached_property + def jinja_env(self) -> Environment: + """The Jinja environment used to load templates. + + The environment is created the first time this property is + accessed. Changing :attr:`jinja_options` after that will have no + effect. + """ + return self.create_jinja_environment() + + def create_jinja_environment(self) -> Environment: + raise NotImplementedError() + + def make_config(self, instance_relative: bool = False) -> Config: + """Used to create the config attribute by the Flask constructor. + The `instance_relative` parameter is passed in from the constructor + of Flask (there named `instance_relative_config`) and indicates if + the config should be relative to the instance path or the root path + of the application. + + .. versionadded:: 0.8 + """ + root_path = self.root_path + if instance_relative: + root_path = self.instance_path + defaults = dict(self.default_config) + defaults["DEBUG"] = get_debug_flag() + return self.config_class(root_path, defaults) + + def make_aborter(self) -> Aborter: + """Create the object to assign to :attr:`aborter`. That object + is called by :func:`flask.abort` to raise HTTP errors, and can + be called directly as well. + + By default, this creates an instance of :attr:`aborter_class`, + which defaults to :class:`werkzeug.exceptions.Aborter`. + + .. versionadded:: 2.2 + """ + return self.aborter_class() + + def auto_find_instance_path(self) -> str: + """Tries to locate the instance path if it was not provided to the + constructor of the application class. It will basically calculate + the path to a folder named ``instance`` next to your main file or + the package. + + .. versionadded:: 0.8 + """ + prefix, package_path = find_package(self.import_name) + if prefix is None: + return os.path.join(package_path, "instance") + return os.path.join(prefix, "var", f"{self.name}-instance") + + def create_global_jinja_loader(self) -> DispatchingJinjaLoader: + """Creates the loader for the Jinja environment. Can be used to + override just the loader and keeping the rest unchanged. It's + discouraged to override this function. Instead one should override + the :meth:`jinja_loader` function instead. + + The global loader dispatches between the loaders of the application + and the individual blueprints. + + .. versionadded:: 0.7 + """ + return DispatchingJinjaLoader(self) + + def select_jinja_autoescape(self, filename: str) -> bool: + """Returns ``True`` if autoescaping should be active for the given + template name. If no template name is given, returns `True`. + + .. versionchanged:: 2.2 + Autoescaping is now enabled by default for ``.svg`` files. + + .. versionadded:: 0.5 + """ + if filename is None: + return True + return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) + + @property + def debug(self) -> bool: + """Whether debug mode is enabled. When using ``flask run`` to start the + development server, an interactive debugger will be shown for unhandled + exceptions, and the server will be reloaded when code changes. This maps to the + :data:`DEBUG` config key. It may not behave as expected if set late. + + **Do not enable debug mode when deploying in production.** + + Default: ``False`` + """ + return self.config["DEBUG"] # type: ignore[no-any-return] + + @debug.setter + def debug(self, value: bool) -> None: + self.config["DEBUG"] = value + + if self.config["TEMPLATES_AUTO_RELOAD"] is None: + self.jinja_env.auto_reload = value + + @setupmethod + def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on the application. Keyword + arguments passed to this method will override the defaults set on the + blueprint. + + Calls the blueprint's :meth:`~flask.Blueprint.register` method after + recording the blueprint in the application's :attr:`blueprints`. + + :param blueprint: The blueprint to register. + :param url_prefix: Blueprint routes will be prefixed with this. + :param subdomain: Blueprint routes will match on this subdomain. + :param url_defaults: Blueprint routes will use these default values for + view arguments. + :param options: Additional keyword arguments are passed to + :class:`~flask.blueprints.BlueprintSetupState`. They can be + accessed in :meth:`~flask.Blueprint.record` callbacks. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 0.7 + """ + blueprint.register(self, options) + + def iter_blueprints(self) -> t.ValuesView[Blueprint]: + """Iterates over all blueprints by the order they were registered. + + .. versionadded:: 0.11 + """ + return self.blueprints.values() + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + options["endpoint"] = endpoint + methods = options.pop("methods", None) + + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only ``GET`` as default. + if methods is None: + methods = getattr(view_func, "methods", None) or ("GET",) + if isinstance(methods, str): + raise TypeError( + "Allowed methods must be a list of strings, for" + ' example: @app.route(..., methods=["POST"])' + ) + methods = {item.upper() for item in methods} + + # Methods that should always be added + required_methods: set[str] = set(getattr(view_func, "required_methods", ())) + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + if provide_automatic_options is None: + provide_automatic_options = getattr( + view_func, "provide_automatic_options", None + ) + + if provide_automatic_options is None: + if "OPTIONS" not in methods and self.config["PROVIDE_AUTOMATIC_OPTIONS"]: + provide_automatic_options = True + required_methods.add("OPTIONS") + else: + provide_automatic_options = False + + # Add the required methods now. + methods |= required_methods + + rule_obj = self.url_rule_class(rule, methods=methods, **options) + rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined] + + self.url_map.add(rule_obj) + if view_func is not None: + old_func = self.view_functions.get(endpoint) + if old_func is not None and old_func != view_func: + raise AssertionError( + "View function mapping is overwriting an existing" + f" endpoint function: {endpoint}" + ) + self.view_functions[endpoint] = view_func + + @setupmethod + def template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter() + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: T_template_filter) -> T_template_filter: + self.add_template_filter(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_filter( + self, f: ft.TemplateFilterCallable, name: str | None = None + ) -> None: + """Register a custom template filter. Works exactly like the + :meth:`template_filter` decorator. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + self.jinja_env.filters[name or f.__name__] = f + + @setupmethod + def template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """A decorator that is used to register custom template test. + You can specify a name for the test, otherwise the function + name will be used. Example:: + + @app.template_test() + def is_prime(n): + if n == 2: + return True + for i in range(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False + return True + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: T_template_test) -> T_template_test: + self.add_template_test(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_test( + self, f: ft.TemplateTestCallable, name: str | None = None + ) -> None: + """Register a custom template test. Works exactly like the + :meth:`template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + self.jinja_env.tests[name or f.__name__] = f + + @setupmethod + def template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """A decorator that is used to register a custom template global function. + You can specify a name for the global function, otherwise the function + name will be used. Example:: + + @app.template_global() + def double(n): + return 2 * n + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + + def decorator(f: T_template_global) -> T_template_global: + self.add_template_global(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_global( + self, f: ft.TemplateGlobalCallable, name: str | None = None + ) -> None: + """Register a custom template global function. Works exactly like the + :meth:`template_global` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + self.jinja_env.globals[name or f.__name__] = f + + @setupmethod + def teardown_appcontext(self, f: T_teardown) -> T_teardown: + """Registers a function to be called when the application + context is popped. The application context is typically popped + after the request context for each request, at the end of CLI + commands, or after a manually pushed context ends. + + .. code-block:: python + + with app.app_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the app context is + made inactive. Since a request context typically also manages an + application context it would also be called when you pop a + request context. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. + + The return values of teardown functions are ignored. + + .. versionadded:: 0.9 + """ + self.teardown_appcontext_funcs.append(f) + return f + + @setupmethod + def shell_context_processor( + self, f: T_shell_context_processor + ) -> T_shell_context_processor: + """Registers a shell context processor function. + + .. versionadded:: 0.11 + """ + self.shell_context_processors.append(f) + return f + + def _find_error_handler( + self, e: Exception, blueprints: list[str] + ) -> ft.ErrorHandlerCallable | None: + """Return a registered error handler for an exception in this order: + blueprint handler for a specific code, app handler for a specific code, + blueprint handler for an exception class, app handler for an exception + class, or ``None`` if a suitable handler is not found. + """ + exc_class, code = self._get_exc_class_and_code(type(e)) + names = (*blueprints, None) + + for c in (code, None) if code is not None else (None,): + for name in names: + handler_map = self.error_handler_spec[name][c] + + if not handler_map: + continue + + for cls in exc_class.__mro__: + handler = handler_map.get(cls) + + if handler is not None: + return handler + return None + + def trap_http_exception(self, e: Exception) -> bool: + """Checks if an HTTP exception should be trapped or not. By default + this will return ``False`` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It + also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. + + This is called for all HTTP exceptions raised by a view function. + If it returns ``True`` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + + .. versionchanged:: 1.0 + Bad request errors are not trapped by default in debug mode. + + .. versionadded:: 0.8 + """ + if self.config["TRAP_HTTP_EXCEPTIONS"]: + return True + + trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] + + # if unset, trap key errors in debug mode + if ( + trap_bad_request is None + and self.debug + and isinstance(e, BadRequestKeyError) + ): + return True + + if trap_bad_request: + return isinstance(e, BadRequest) + + return False + + def should_ignore_error(self, error: BaseException | None) -> bool: + """This is called to figure out if an error should be ignored + or not as far as the teardown system is concerned. If this + function returns ``True`` then the teardown handlers will not be + passed the error. + + .. versionadded:: 0.10 + """ + return False + + def redirect(self, location: str, code: int = 302) -> BaseResponse: + """Create a redirect response object. + + This is called by :func:`flask.redirect`, and can be called + directly as well. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. + + .. versionadded:: 2.2 + Moved from ``flask.redirect``, which calls this method. + """ + return _wz_redirect( + location, + code=code, + Response=self.response_class, # type: ignore[arg-type] + ) + + def inject_url_defaults(self, endpoint: str, values: dict[str, t.Any]) -> None: + """Injects the URL defaults for the given endpoint directly into + the values dictionary passed. This is used internally and + automatically called on URL building. + + .. versionadded:: 0.7 + """ + names: t.Iterable[str | None] = (None,) + + # url_for may be called outside a request context, parse the + # passed endpoint instead of using request.blueprints. + if "." in endpoint: + names = chain( + names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) + ) + + for name in names: + if name in self.url_default_functions: + for func in self.url_default_functions[name]: + func(endpoint, values) + + def handle_url_build_error( + self, error: BuildError, endpoint: str, values: dict[str, t.Any] + ) -> str: + """Called by :meth:`.url_for` if a + :exc:`~werkzeug.routing.BuildError` was raised. If this returns + a value, it will be returned by ``url_for``, otherwise the error + will be re-raised. + + Each function in :attr:`url_build_error_handlers` is called with + ``error``, ``endpoint`` and ``values``. If a function returns + ``None`` or raises a ``BuildError``, it is skipped. Otherwise, + its return value is returned by ``url_for``. + + :param error: The active ``BuildError`` being handled. + :param endpoint: The endpoint being built. + :param values: The keyword arguments passed to ``url_for``. + """ + for handler in self.url_build_error_handlers: + try: + rv = handler(error, endpoint, values) + except BuildError as e: + # make error available outside except block + error = e + else: + if rv is not None: + return rv + + # Re-raise if called with an active exception, otherwise raise + # the passed in exception. + if error is sys.exc_info()[1]: + raise + + raise error diff --git a/venv/lib/python3.10/site-packages/flask/sansio/blueprints.py b/venv/lib/python3.10/site-packages/flask/sansio/blueprints.py new file mode 100644 index 0000000..4f912cc --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/sansio/blueprints.py @@ -0,0 +1,632 @@ +from __future__ import annotations + +import os +import typing as t +from collections import defaultdict +from functools import update_wrapper + +from .. import typing as ft +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import Scaffold +from .scaffold import setupmethod + +if t.TYPE_CHECKING: # pragma: no cover + from .app import App + +DeferredSetupFunction = t.Callable[["BlueprintSetupState"], None] +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) + + +class BlueprintSetupState: + """Temporary holder object for registering a blueprint with the + application. An instance of this class is created by the + :meth:`~flask.Blueprint.make_setup_state` method and later passed + to all register callback functions. + """ + + def __init__( + self, + blueprint: Blueprint, + app: App, + options: t.Any, + first_registration: bool, + ) -> None: + #: a reference to the current application + self.app = app + + #: a reference to the blueprint that created this setup state. + self.blueprint = blueprint + + #: a dictionary with all options that were passed to the + #: :meth:`~flask.Flask.register_blueprint` method. + self.options = options + + #: as blueprints can be registered multiple times with the + #: application and not everything wants to be registered + #: multiple times on it, this attribute can be used to figure + #: out if the blueprint was registered in the past already. + self.first_registration = first_registration + + subdomain = self.options.get("subdomain") + if subdomain is None: + subdomain = self.blueprint.subdomain + + #: The subdomain that the blueprint should be active for, ``None`` + #: otherwise. + self.subdomain = subdomain + + url_prefix = self.options.get("url_prefix") + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + #: The prefix that should be used for all URLs defined on the + #: blueprint. + self.url_prefix = url_prefix + + self.name = self.options.get("name", blueprint.name) + self.name_prefix = self.options.get("name_prefix", "") + + #: A dictionary with URL defaults that is added to each and every + #: URL that was defined with the blueprint. + self.url_defaults = dict(self.blueprint.url_values_defaults) + self.url_defaults.update(self.options.get("url_defaults", ())) + + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + **options: t.Any, + ) -> None: + """A helper method to register a rule (and optionally a view function) + to the application. The endpoint is automatically prefixed with the + blueprint's name. + """ + if self.url_prefix is not None: + if rule: + rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) + else: + rule = self.url_prefix + options.setdefault("subdomain", self.subdomain) + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + defaults = self.url_defaults + if "defaults" in options: + defaults = dict(defaults, **options.pop("defaults")) + + self.app.add_url_rule( + rule, + f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), + view_func, + defaults=defaults, + **options, + ) + + +class Blueprint(Scaffold): + """Represents a blueprint, a collection of routes and other + app-related functions that can be registered on a real application + later. + + A blueprint is an object that allows defining application functions + without requiring an application object ahead of time. It uses the + same decorators as :class:`~flask.Flask`, but defers the need for an + application by recording them for later registration. + + Decorating a function with a blueprint creates a deferred function + that is called with :class:`~flask.blueprints.BlueprintSetupState` + when the blueprint is registered on an application. + + See :doc:`/blueprints` for more information. + + :param name: The name of the blueprint. Will be prepended to each + endpoint name. + :param import_name: The name of the blueprint package, usually + ``__name__``. This helps locate the ``root_path`` for the + blueprint. + :param static_folder: A folder with static files that should be + served by the blueprint's static route. The path is relative to + the blueprint's root path. Blueprint static files are disabled + by default. + :param static_url_path: The url to serve static files from. + Defaults to ``static_folder``. If the blueprint does not have + a ``url_prefix``, the app's static route will take precedence, + and the blueprint's static files won't be accessible. + :param template_folder: A folder with templates that should be added + to the app's template search path. The path is relative to the + blueprint's root path. Blueprint templates are disabled by + default. Blueprint templates have a lower precedence than those + in the app's templates folder. + :param url_prefix: A path to prepend to all of the blueprint's URLs, + to make them distinct from the rest of the app's routes. + :param subdomain: A subdomain that blueprint routes will match on by + default. + :param url_defaults: A dict of default values that blueprint routes + will receive by default. + :param root_path: By default, the blueprint will automatically set + this based on ``import_name``. In certain situations this + automatic detection can fail, so the path can be specified + manually instead. + + .. versionchanged:: 1.1.0 + Blueprints have a ``cli`` group to register nested CLI commands. + The ``cli_group`` parameter controls the name of the group under + the ``flask`` command. + + .. versionadded:: 0.7 + """ + + _got_registered_once = False + + def __init__( + self, + name: str, + import_name: str, + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + url_prefix: str | None = None, + subdomain: str | None = None, + url_defaults: dict[str, t.Any] | None = None, + root_path: str | None = None, + cli_group: str | None = _sentinel, # type: ignore[assignment] + ): + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if not name: + raise ValueError("'name' may not be empty.") + + if "." in name: + raise ValueError("'name' may not contain a dot '.' character.") + + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.deferred_functions: list[DeferredSetupFunction] = [] + + if url_defaults is None: + url_defaults = {} + + self.url_values_defaults = url_defaults + self.cli_group = cli_group + self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = [] + + def _check_setup_finished(self, f_name: str) -> None: + if self._got_registered_once: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called on the blueprint" + f" '{self.name}'. It has already been registered at least once, any" + " changes will not be applied consistently.\n" + "Make sure all imports, decorators, functions, etc. needed to set up" + " the blueprint are done before registering it." + ) + + @setupmethod + def record(self, func: DeferredSetupFunction) -> None: + """Registers a function that is called when the blueprint is + registered on the application. This function is called with the + state as argument as returned by the :meth:`make_setup_state` + method. + """ + self.deferred_functions.append(func) + + @setupmethod + def record_once(self, func: DeferredSetupFunction) -> None: + """Works like :meth:`record` but wraps the function in another + function that will ensure the function is only called once. If the + blueprint is registered a second time on the application, the + function passed is not called. + """ + + def wrapper(state: BlueprintSetupState) -> None: + if state.first_registration: + func(state) + + self.record(update_wrapper(wrapper, func)) + + def make_setup_state( + self, app: App, options: dict[str, t.Any], first_registration: bool = False + ) -> BlueprintSetupState: + """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` + object that is later passed to the register callback functions. + Subclasses can override this to return a subclass of the setup state. + """ + return BlueprintSetupState(self, app, options, first_registration) + + @setupmethod + def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on this blueprint. Keyword + arguments passed to this method will override the defaults set + on the blueprint. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 2.0 + """ + if blueprint is self: + raise ValueError("Cannot register a blueprint on itself") + self._blueprints.append((blueprint, options)) + + def register(self, app: App, options: dict[str, t.Any]) -> None: + """Called by :meth:`Flask.register_blueprint` to register all + views and callbacks registered on the blueprint with the + application. Creates a :class:`.BlueprintSetupState` and calls + each :meth:`record` callback with it. + + :param app: The application this blueprint is being registered + with. + :param options: Keyword arguments forwarded from + :meth:`~Flask.register_blueprint`. + + .. versionchanged:: 2.3 + Nested blueprints now correctly apply subdomains. + + .. versionchanged:: 2.1 + Registering the same blueprint with the same name multiple + times is an error. + + .. versionchanged:: 2.0.1 + Nested blueprints are registered with their dotted name. + This allows different blueprints with the same name to be + nested at different locations. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + """ + name_prefix = options.get("name_prefix", "") + self_name = options.get("name", self.name) + name = f"{name_prefix}.{self_name}".lstrip(".") + + if name in app.blueprints: + bp_desc = "this" if app.blueprints[name] is self else "a different" + existing_at = f" '{name}'" if self_name != name else "" + + raise ValueError( + f"The name '{self_name}' is already registered for" + f" {bp_desc} blueprint{existing_at}. Use 'name=' to" + f" provide a unique name." + ) + + first_bp_registration = not any(bp is self for bp in app.blueprints.values()) + first_name_registration = name not in app.blueprints + + app.blueprints[name] = self + self._got_registered_once = True + state = self.make_setup_state(app, options, first_bp_registration) + + if self.has_static_folder: + state.add_url_rule( + f"{self.static_url_path}/", + view_func=self.send_static_file, # type: ignore[attr-defined] + endpoint="static", + ) + + # Merge blueprint data into parent. + if first_bp_registration or first_name_registration: + self._merge_blueprint_funcs(app, name) + + for deferred in self.deferred_functions: + deferred(state) + + cli_resolved_group = options.get("cli_group", self.cli_group) + + if self.cli.commands: + if cli_resolved_group is None: + app.cli.commands.update(self.cli.commands) + elif cli_resolved_group is _sentinel: + self.cli.name = name + app.cli.add_command(self.cli) + else: + self.cli.name = cli_resolved_group + app.cli.add_command(self.cli) + + for blueprint, bp_options in self._blueprints: + bp_options = bp_options.copy() + bp_url_prefix = bp_options.get("url_prefix") + bp_subdomain = bp_options.get("subdomain") + + if bp_subdomain is None: + bp_subdomain = blueprint.subdomain + + if state.subdomain is not None and bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + "." + state.subdomain + elif bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + elif state.subdomain is not None: + bp_options["subdomain"] = state.subdomain + + if bp_url_prefix is None: + bp_url_prefix = blueprint.url_prefix + + if state.url_prefix is not None and bp_url_prefix is not None: + bp_options["url_prefix"] = ( + state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") + ) + elif bp_url_prefix is not None: + bp_options["url_prefix"] = bp_url_prefix + elif state.url_prefix is not None: + bp_options["url_prefix"] = state.url_prefix + + bp_options["name_prefix"] = name + blueprint.register(app, bp_options) + + def _merge_blueprint_funcs(self, app: App, name: str) -> None: + def extend( + bp_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], + parent_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], + ) -> None: + for key, values in bp_dict.items(): + key = name if key is None else f"{name}.{key}" + parent_dict[key].extend(values) + + for key, value in self.error_handler_spec.items(): + key = name if key is None else f"{name}.{key}" + value = defaultdict( + dict, + { + code: {exc_class: func for exc_class, func in code_values.items()} + for code, code_values in value.items() + }, + ) + app.error_handler_spec[key] = value + + for endpoint, func in self.view_functions.items(): + app.view_functions[endpoint] = func + + extend(self.before_request_funcs, app.before_request_funcs) + extend(self.after_request_funcs, app.after_request_funcs) + extend( + self.teardown_request_funcs, + app.teardown_request_funcs, + ) + extend(self.url_default_functions, app.url_default_functions) + extend(self.url_value_preprocessors, app.url_value_preprocessors) + extend(self.template_context_processors, app.template_context_processors) + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for + full documentation. + + The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, + used with :func:`url_for`, is prefixed with the blueprint's name. + """ + if endpoint and "." in endpoint: + raise ValueError("'endpoint' may not contain a dot '.' character.") + + if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: + raise ValueError("'view_func' name may not contain a dot '.' character.") + + self.record( + lambda s: s.add_url_rule( + rule, + endpoint, + view_func, + provide_automatic_options=provide_automatic_options, + **options, + ) + ) + + @setupmethod + def app_template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """Register a template filter, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: T_template_filter) -> T_template_filter: + self.add_app_template_filter(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_filter( + self, f: ft.TemplateFilterCallable, name: str | None = None + ) -> None: + """Register a template filter, available in any template rendered by the + application. Works like the :meth:`app_template_filter` decorator. Equivalent to + :meth:`.Flask.add_template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.filters[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """Register a template test, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: T_template_test) -> T_template_test: + self.add_app_template_test(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_test( + self, f: ft.TemplateTestCallable, name: str | None = None + ) -> None: + """Register a template test, available in any template rendered by the + application. Works like the :meth:`app_template_test` decorator. Equivalent to + :meth:`.Flask.add_template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.tests[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """Register a template global, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def decorator(f: T_template_global) -> T_template_global: + self.add_app_template_global(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_global( + self, f: ft.TemplateGlobalCallable, name: str | None = None + ) -> None: + """Register a template global, available in any template rendered by the + application. Works like the :meth:`app_template_global` decorator. Equivalent to + :meth:`.Flask.add_template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.globals[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def before_app_request(self, f: T_before_request) -> T_before_request: + """Like :meth:`before_request`, but before every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.before_request`. + """ + self.record_once( + lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def after_app_request(self, f: T_after_request) -> T_after_request: + """Like :meth:`after_request`, but after every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.after_request`. + """ + self.record_once( + lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def teardown_app_request(self, f: T_teardown) -> T_teardown: + """Like :meth:`teardown_request`, but after every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. + """ + self.record_once( + lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_context_processor( + self, f: T_template_context_processor + ) -> T_template_context_processor: + """Like :meth:`context_processor`, but for templates rendered by every view, not + only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. + """ + self.record_once( + lambda s: s.app.template_context_processors.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_errorhandler( + self, code: type[Exception] | int + ) -> t.Callable[[T_error_handler], T_error_handler]: + """Like :meth:`errorhandler`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.errorhandler`. + """ + + def decorator(f: T_error_handler) -> T_error_handler: + def from_blueprint(state: BlueprintSetupState) -> None: + state.app.errorhandler(code)(f) + + self.record_once(from_blueprint) + return f + + return decorator + + @setupmethod + def app_url_value_preprocessor( + self, f: T_url_value_preprocessor + ) -> T_url_value_preprocessor: + """Like :meth:`url_value_preprocessor`, but for every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. + """ + self.record_once( + lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: + """Like :meth:`url_defaults`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.url_defaults`. + """ + self.record_once( + lambda s: s.app.url_default_functions.setdefault(None, []).append(f) + ) + return f diff --git a/venv/lib/python3.10/site-packages/flask/sansio/scaffold.py b/venv/lib/python3.10/site-packages/flask/sansio/scaffold.py new file mode 100644 index 0000000..0e96f15 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/sansio/scaffold.py @@ -0,0 +1,792 @@ +from __future__ import annotations + +import importlib.util +import os +import pathlib +import sys +import typing as t +from collections import defaultdict +from functools import update_wrapper + +from jinja2 import BaseLoader +from jinja2 import FileSystemLoader +from werkzeug.exceptions import default_exceptions +from werkzeug.exceptions import HTTPException +from werkzeug.utils import cached_property + +from .. import typing as ft +from ..helpers import get_root_path +from ..templating import _default_template_ctx_processor + +if t.TYPE_CHECKING: # pragma: no cover + from click import Group + +# a singleton sentinel value for parameter defaults +_sentinel = object() + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) +T_route = t.TypeVar("T_route", bound=ft.RouteCallable) + + +def setupmethod(f: F) -> F: + f_name = f.__name__ + + def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any: + self._check_setup_finished(f_name) + return f(self, *args, **kwargs) + + return t.cast(F, update_wrapper(wrapper_func, f)) + + +class Scaffold: + """Common behavior shared between :class:`~flask.Flask` and + :class:`~flask.blueprints.Blueprint`. + + :param import_name: The import name of the module where this object + is defined. Usually :attr:`__name__` should be used. + :param static_folder: Path to a folder of static files to serve. + If this is set, a static route will be added. + :param static_url_path: URL prefix for the static route. + :param template_folder: Path to a folder containing template files. + for rendering. If this is set, a Jinja loader will be added. + :param root_path: The path that static, template, and resource files + are relative to. Typically not set, it is discovered based on + the ``import_name``. + + .. versionadded:: 2.0 + """ + + cli: Group + name: str + _static_folder: str | None = None + _static_url_path: str | None = None + + def __init__( + self, + import_name: str, + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + root_path: str | None = None, + ): + #: The name of the package or module that this object belongs + #: to. Do not change this once it is set by the constructor. + self.import_name = import_name + + self.static_folder = static_folder + self.static_url_path = static_url_path + + #: The path to the templates folder, relative to + #: :attr:`root_path`, to add to the template loader. ``None`` if + #: templates should not be added. + self.template_folder = template_folder + + if root_path is None: + root_path = get_root_path(self.import_name) + + #: Absolute path to the package on the filesystem. Used to look + #: up resources contained in the package. + self.root_path = root_path + + #: A dictionary mapping endpoint names to view functions. + #: + #: To register a view function, use the :meth:`route` decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.view_functions: dict[str, ft.RouteCallable] = {} + + #: A data structure of registered error handlers, in the format + #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is + #: the name of a blueprint the handlers are active for, or + #: ``None`` for all requests. The ``code`` key is the HTTP + #: status code for ``HTTPException``, or ``None`` for + #: other exceptions. The innermost dictionary maps exception + #: classes to handler functions. + #: + #: To register an error handler, use the :meth:`errorhandler` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.error_handler_spec: dict[ + ft.AppOrBlueprintKey, + dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]], + ] = defaultdict(lambda: defaultdict(dict)) + + #: A data structure of functions to call at the beginning of + #: each request, in the format ``{scope: [functions]}``. The + #: ``scope`` key is the name of a blueprint the functions are + #: active for, or ``None`` for all requests. + #: + #: To register a function, use the :meth:`before_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.before_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable] + ] = defaultdict(list) + + #: A data structure of functions to call at the end of each + #: request, in the format ``{scope: [functions]}``. The + #: ``scope`` key is the name of a blueprint the functions are + #: active for, or ``None`` for all requests. + #: + #: To register a function, use the :meth:`after_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.after_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.AfterRequestCallable[t.Any]] + ] = defaultdict(list) + + #: A data structure of functions to call at the end of each + #: request even if an exception is raised, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`teardown_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.teardown_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.TeardownCallable] + ] = defaultdict(list) + + #: A data structure of functions to call to pass extra context + #: values when rendering templates, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`context_processor` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.template_context_processors: dict[ + ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable] + ] = defaultdict(list, {None: [_default_template_ctx_processor]}) + + #: A data structure of functions to call to modify the keyword + #: arguments passed to the view function, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the + #: :meth:`url_value_preprocessor` decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.url_value_preprocessors: dict[ + ft.AppOrBlueprintKey, + list[ft.URLValuePreprocessorCallable], + ] = defaultdict(list) + + #: A data structure of functions to call to modify the keyword + #: arguments when generating URLs, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`url_defaults` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.url_default_functions: dict[ + ft.AppOrBlueprintKey, list[ft.URLDefaultCallable] + ] = defaultdict(list) + + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.name!r}>" + + def _check_setup_finished(self, f_name: str) -> None: + raise NotImplementedError + + @property + def static_folder(self) -> str | None: + """The absolute path to the configured static folder. ``None`` + if no static folder is set. + """ + if self._static_folder is not None: + return os.path.join(self.root_path, self._static_folder) + else: + return None + + @static_folder.setter + def static_folder(self, value: str | os.PathLike[str] | None) -> None: + if value is not None: + value = os.fspath(value).rstrip(r"\/") + + self._static_folder = value + + @property + def has_static_folder(self) -> bool: + """``True`` if :attr:`static_folder` is set. + + .. versionadded:: 0.5 + """ + return self.static_folder is not None + + @property + def static_url_path(self) -> str | None: + """The URL prefix that the static route will be accessible from. + + If it was not configured during init, it is derived from + :attr:`static_folder`. + """ + if self._static_url_path is not None: + return self._static_url_path + + if self.static_folder is not None: + basename = os.path.basename(self.static_folder) + return f"/{basename}".rstrip("/") + + return None + + @static_url_path.setter + def static_url_path(self, value: str | None) -> None: + if value is not None: + value = value.rstrip("/") + + self._static_url_path = value + + @cached_property + def jinja_loader(self) -> BaseLoader | None: + """The Jinja loader for this object's templates. By default this + is a class :class:`jinja2.loaders.FileSystemLoader` to + :attr:`template_folder` if it is set. + + .. versionadded:: 0.5 + """ + if self.template_folder is not None: + return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) + else: + return None + + def _method_route( + self, + method: str, + rule: str, + options: dict[str, t.Any], + ) -> t.Callable[[T_route], T_route]: + if "methods" in options: + raise TypeError("Use the 'route' decorator to use the 'methods' argument.") + + return self.route(rule, methods=[method], **options) + + @setupmethod + def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["GET"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("GET", rule, options) + + @setupmethod + def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["POST"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("POST", rule, options) + + @setupmethod + def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["PUT"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("PUT", rule, options) + + @setupmethod + def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["DELETE"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("DELETE", rule, options) + + @setupmethod + def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["PATCH"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("PATCH", rule, options) + + @setupmethod + def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Decorate a view function to register it with the given URL + rule and options. Calls :meth:`add_url_rule`, which has more + details about the implementation. + + .. code-block:: python + + @app.route("/") + def index(): + return "Hello, World!" + + See :ref:`url-route-registrations`. + + The endpoint name for the route defaults to the name of the view + function if the ``endpoint`` parameter isn't passed. + + The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and + ``OPTIONS`` are added automatically. + + :param rule: The URL rule string. + :param options: Extra options passed to the + :class:`~werkzeug.routing.Rule` object. + """ + + def decorator(f: T_route) -> T_route: + endpoint = options.pop("endpoint", None) + self.add_url_rule(rule, endpoint, f, **options) + return f + + return decorator + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + """Register a rule for routing incoming requests and building + URLs. The :meth:`route` decorator is a shortcut to call this + with the ``view_func`` argument. These are equivalent: + + .. code-block:: python + + @app.route("/") + def index(): + ... + + .. code-block:: python + + def index(): + ... + + app.add_url_rule("/", view_func=index) + + See :ref:`url-route-registrations`. + + The endpoint name for the route defaults to the name of the view + function if the ``endpoint`` parameter isn't passed. An error + will be raised if a function has already been registered for the + endpoint. + + The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is + always added automatically, and ``OPTIONS`` is added + automatically by default. + + ``view_func`` does not necessarily need to be passed, but if the + rule should participate in routing an endpoint name must be + associated with a view function at some point with the + :meth:`endpoint` decorator. + + .. code-block:: python + + app.add_url_rule("/", endpoint="index") + + @app.endpoint("index") + def index(): + ... + + If ``view_func`` has a ``required_methods`` attribute, those + methods are added to the passed and automatic methods. If it + has a ``provide_automatic_methods`` attribute, it is used as the + default if the parameter is not passed. + + :param rule: The URL rule string. + :param endpoint: The endpoint name to associate with the rule + and view function. Used when routing and building URLs. + Defaults to ``view_func.__name__``. + :param view_func: The view function to associate with the + endpoint name. + :param provide_automatic_options: Add the ``OPTIONS`` method and + respond to ``OPTIONS`` requests automatically. + :param options: Extra options passed to the + :class:`~werkzeug.routing.Rule` object. + """ + raise NotImplementedError + + @setupmethod + def endpoint(self, endpoint: str) -> t.Callable[[F], F]: + """Decorate a view function to register it for the given + endpoint. Used if a rule is added without a ``view_func`` with + :meth:`add_url_rule`. + + .. code-block:: python + + app.add_url_rule("/ex", endpoint="example") + + @app.endpoint("example") + def example(): + ... + + :param endpoint: The endpoint name to associate with the view + function. + """ + + def decorator(f: F) -> F: + self.view_functions[endpoint] = f + return f + + return decorator + + @setupmethod + def before_request(self, f: T_before_request) -> T_before_request: + """Register a function to run before each request. + + For example, this can be used to open a database connection, or + to load the logged in user from the session. + + .. code-block:: python + + @app.before_request + def load_user(): + if "user_id" in session: + g.user = db.session.get(session["user_id"]) + + The function will be called without any arguments. If it returns + a non-``None`` value, the value is handled as if it was the + return value from the view, and further request handling is + stopped. + + This is available on both app and blueprint objects. When used on an app, this + executes before every request. When used on a blueprint, this executes before + every request that the blueprint handles. To register with a blueprint and + execute before every request, use :meth:`.Blueprint.before_app_request`. + """ + self.before_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def after_request(self, f: T_after_request) -> T_after_request: + """Register a function to run after each request to this object. + + The function is called with the response object, and must return + a response object. This allows the functions to modify or + replace the response before it is sent. + + If a function raises an exception, any remaining + ``after_request`` functions will not be called. Therefore, this + should not be used for actions that must execute, such as to + close resources. Use :meth:`teardown_request` for that. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.after_app_request`. + """ + self.after_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def teardown_request(self, f: T_teardown) -> T_teardown: + """Register a function to be called when the request context is + popped. Typically this happens at the end of each request, but + contexts may be pushed manually as well during testing. + + .. code-block:: python + + with app.test_request_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the request context is + made inactive. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. + + The return values of teardown functions are ignored. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.teardown_app_request`. + """ + self.teardown_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def context_processor( + self, + f: T_template_context_processor, + ) -> T_template_context_processor: + """Registers a template context processor function. These functions run before + rendering a template. The keys of the returned dict are added as variables + available in the template. + + This is available on both app and blueprint objects. When used on an app, this + is called for every rendered template. When used on a blueprint, this is called + for templates rendered from the blueprint's views. To register with a blueprint + and affect every template, use :meth:`.Blueprint.app_context_processor`. + """ + self.template_context_processors[None].append(f) + return f + + @setupmethod + def url_value_preprocessor( + self, + f: T_url_value_preprocessor, + ) -> T_url_value_preprocessor: + """Register a URL value preprocessor function for all view + functions in the application. These functions will be called before the + :meth:`before_request` functions. + + The function can modify the values captured from the matched url before + they are passed to the view. For example, this can be used to pop a + common language code value and place it in ``g`` rather than pass it to + every view. + + The function is passed the endpoint name and values dict. The return + value is ignored. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_value_preprocessor`. + """ + self.url_value_preprocessors[None].append(f) + return f + + @setupmethod + def url_defaults(self, f: T_url_defaults) -> T_url_defaults: + """Callback function for URL defaults for all view functions of the + application. It's called with the endpoint and values and should + update the values passed in place. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_defaults`. + """ + self.url_default_functions[None].append(f) + return f + + @setupmethod + def errorhandler( + self, code_or_exception: type[Exception] | int + ) -> t.Callable[[T_error_handler], T_error_handler]: + """Register a function to handle errors by code or exception class. + + A decorator that is used to register a function given an + error code. Example:: + + @app.errorhandler(404) + def page_not_found(error): + return 'This page does not exist', 404 + + You can also register handlers for arbitrary exceptions:: + + @app.errorhandler(DatabaseError) + def special_exception_handler(error): + return 'Database connection failed', 500 + + This is available on both app and blueprint objects. When used on an app, this + can handle errors from every request. When used on a blueprint, this can handle + errors from requests that the blueprint handles. To register with a blueprint + and affect every request, use :meth:`.Blueprint.app_errorhandler`. + + .. versionadded:: 0.7 + Use :meth:`register_error_handler` instead of modifying + :attr:`error_handler_spec` directly, for application wide error + handlers. + + .. versionadded:: 0.7 + One can now additionally also register custom exception types + that do not necessarily have to be a subclass of the + :class:`~werkzeug.exceptions.HTTPException` class. + + :param code_or_exception: the code as integer for the handler, or + an arbitrary exception + """ + + def decorator(f: T_error_handler) -> T_error_handler: + self.register_error_handler(code_or_exception, f) + return f + + return decorator + + @setupmethod + def register_error_handler( + self, + code_or_exception: type[Exception] | int, + f: ft.ErrorHandlerCallable, + ) -> None: + """Alternative error attach function to the :meth:`errorhandler` + decorator that is more straightforward to use for non decorator + usage. + + .. versionadded:: 0.7 + """ + exc_class, code = self._get_exc_class_and_code(code_or_exception) + self.error_handler_spec[None][code][exc_class] = f + + @staticmethod + def _get_exc_class_and_code( + exc_class_or_code: type[Exception] | int, + ) -> tuple[type[Exception], int | None]: + """Get the exception class being handled. For HTTP status codes + or ``HTTPException`` subclasses, return both the exception and + status code. + + :param exc_class_or_code: Any exception class, or an HTTP status + code as an integer. + """ + exc_class: type[Exception] + + if isinstance(exc_class_or_code, int): + try: + exc_class = default_exceptions[exc_class_or_code] + except KeyError: + raise ValueError( + f"'{exc_class_or_code}' is not a recognized HTTP" + " error code. Use a subclass of HTTPException with" + " that code instead." + ) from None + else: + exc_class = exc_class_or_code + + if isinstance(exc_class, Exception): + raise TypeError( + f"{exc_class!r} is an instance, not a class. Handlers" + " can only be registered for Exception classes or HTTP" + " error codes." + ) + + if not issubclass(exc_class, Exception): + raise ValueError( + f"'{exc_class.__name__}' is not a subclass of Exception." + " Handlers can only be registered for Exception classes" + " or HTTP error codes." + ) + + if issubclass(exc_class, HTTPException): + return exc_class, exc_class.code + else: + return exc_class, None + + +def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str: + """Internal helper that returns the default endpoint for a given + function. This always is the function name. + """ + assert view_func is not None, "expected view func if endpoint is not provided." + return view_func.__name__ + + +def _find_package_path(import_name: str) -> str: + """Find the path that contains the package or module.""" + root_mod_name, _, _ = import_name.partition(".") + + try: + root_spec = importlib.util.find_spec(root_mod_name) + + if root_spec is None: + raise ValueError("not found") + except (ImportError, ValueError): + # ImportError: the machinery told us it does not exist + # ValueError: + # - the module name was invalid + # - the module name is __main__ + # - we raised `ValueError` due to `root_spec` being `None` + return os.getcwd() + + if root_spec.submodule_search_locations: + if root_spec.origin is None or root_spec.origin == "namespace": + # namespace package + package_spec = importlib.util.find_spec(import_name) + + if package_spec is not None and package_spec.submodule_search_locations: + # Pick the path in the namespace that contains the submodule. + package_path = pathlib.Path( + os.path.commonpath(package_spec.submodule_search_locations) + ) + search_location = next( + location + for location in root_spec.submodule_search_locations + if package_path.is_relative_to(location) + ) + else: + # Pick the first path. + search_location = root_spec.submodule_search_locations[0] + + return os.path.dirname(search_location) + else: + # package with __init__.py + return os.path.dirname(os.path.dirname(root_spec.origin)) + else: + # module + return os.path.dirname(root_spec.origin) # type: ignore[type-var, return-value] + + +def find_package(import_name: str) -> tuple[str | None, str]: + """Find the prefix that a package is installed under, and the path + that it would be imported from. + + The prefix is the directory containing the standard directory + hierarchy (lib, bin, etc.). If the package is not installed to the + system (:attr:`sys.prefix`) or a virtualenv (``site-packages``), + ``None`` is returned. + + The path is the entry in :attr:`sys.path` that contains the package + for import. If the package is not installed, it's assumed that the + package was imported from the current working directory. + """ + package_path = _find_package_path(import_name) + py_prefix = os.path.abspath(sys.prefix) + + # installed to the system + if pathlib.PurePath(package_path).is_relative_to(py_prefix): + return py_prefix, package_path + + site_parent, site_folder = os.path.split(package_path) + + # installed to a virtualenv + if site_folder.lower() == "site-packages": + parent, folder = os.path.split(site_parent) + + # Windows (prefix/lib/site-packages) + if folder.lower() == "lib": + return parent, package_path + + # Unix (prefix/lib/pythonX.Y/site-packages) + if os.path.basename(parent).lower() == "lib": + return os.path.dirname(parent), package_path + + # something else (prefix/site-packages) + return site_parent, package_path + + # not installed + return None, package_path diff --git a/venv/lib/python3.10/site-packages/flask/sessions.py b/venv/lib/python3.10/site-packages/flask/sessions.py new file mode 100644 index 0000000..0a357d9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/sessions.py @@ -0,0 +1,399 @@ +from __future__ import annotations + +import collections.abc as c +import hashlib +import typing as t +from collections.abc import MutableMapping +from datetime import datetime +from datetime import timezone + +from itsdangerous import BadSignature +from itsdangerous import URLSafeTimedSerializer +from werkzeug.datastructures import CallbackDict + +from .json.tag import TaggedJSONSerializer + +if t.TYPE_CHECKING: # pragma: no cover + import typing_extensions as te + + from .app import Flask + from .wrappers import Request + from .wrappers import Response + + +class SessionMixin(MutableMapping[str, t.Any]): + """Expands a basic dictionary with session attributes.""" + + @property + def permanent(self) -> bool: + """This reflects the ``'_permanent'`` key in the dict.""" + return self.get("_permanent", False) + + @permanent.setter + def permanent(self, value: bool) -> None: + self["_permanent"] = bool(value) + + #: Some implementations can detect whether a session is newly + #: created, but that is not guaranteed. Use with caution. The mixin + # default is hard-coded ``False``. + new = False + + #: Some implementations can detect changes to the session and set + #: this when that happens. The mixin default is hard coded to + #: ``True``. + modified = True + + #: Some implementations can detect when session data is read or + #: written and set this when that happens. The mixin default is hard + #: coded to ``True``. + accessed = True + + +class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): + """Base class for sessions based on signed cookies. + + This session backend will set the :attr:`modified` and + :attr:`accessed` attributes. It cannot reliably track whether a + session is new (vs. empty), so :attr:`new` remains hard coded to + ``False``. + """ + + #: When data is changed, this is set to ``True``. Only the session + #: dictionary itself is tracked; if the session contains mutable + #: data (for example a nested dict) then this must be set to + #: ``True`` manually when modifying that data. The session cookie + #: will only be written to the response if this is ``True``. + modified = False + + #: When data is read or written, this is set to ``True``. Used by + # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` + #: header, which allows caching proxies to cache different pages for + #: different users. + accessed = False + + def __init__( + self, + initial: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = None, + ) -> None: + def on_update(self: te.Self) -> None: + self.modified = True + self.accessed = True + + super().__init__(initial, on_update) + + def __getitem__(self, key: str) -> t.Any: + self.accessed = True + return super().__getitem__(key) + + def get(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().get(key, default) + + def setdefault(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().setdefault(key, default) + + +class NullSession(SecureCookieSession): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: + raise RuntimeError( + "The session is unavailable because no secret " + "key was set. Set the secret_key on the " + "application to something unique and secret." + ) + + __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # noqa: B950 + del _fail + + +class SessionInterface: + """The basic interface you have to implement in order to replace the + default session interface which uses werkzeug's securecookie + implementation. The only methods you have to implement are + :meth:`open_session` and :meth:`save_session`, the others have + useful defaults which you don't need to change. + + The session object returned by the :meth:`open_session` method has to + provide a dictionary like interface plus the properties and methods + from the :class:`SessionMixin`. We recommend just subclassing a dict + and adding that mixin:: + + class Session(dict, SessionMixin): + pass + + If :meth:`open_session` returns ``None`` Flask will call into + :meth:`make_null_session` to create a session that acts as replacement + if the session support cannot work because some requirement is not + fulfilled. The default :class:`NullSession` class that is created + will complain that the secret key was not set. + + To replace the session interface on an application all you have to do + is to assign :attr:`flask.Flask.session_interface`:: + + app = Flask(__name__) + app.session_interface = MySessionInterface() + + Multiple requests with the same session may be sent and handled + concurrently. When implementing a new session interface, consider + whether reads or writes to the backing store must be synchronized. + There is no guarantee on the order in which the session for each + request is opened or saved, it will occur in the order that requests + begin and end processing. + + .. versionadded:: 0.8 + """ + + #: :meth:`make_null_session` will look here for the class that should + #: be created when a null session is requested. Likewise the + #: :meth:`is_null_session` method will perform a typecheck against + #: this type. + null_session_class = NullSession + + #: A flag that indicates if the session interface is pickle based. + #: This can be used by Flask extensions to make a decision in regards + #: to how to deal with the session object. + #: + #: .. versionadded:: 0.10 + pickle_based = False + + def make_null_session(self, app: Flask) -> NullSession: + """Creates a null session which acts as a replacement object if the + real session support could not be loaded due to a configuration + error. This mainly aids the user experience because the job of the + null session is to still support lookup without complaining but + modifications are answered with a helpful error message of what + failed. + + This creates an instance of :attr:`null_session_class` by default. + """ + return self.null_session_class() + + def is_null_session(self, obj: object) -> bool: + """Checks if a given object is a null session. Null sessions are + not asked to be saved. + + This checks if the object is an instance of :attr:`null_session_class` + by default. + """ + return isinstance(obj, self.null_session_class) + + def get_cookie_name(self, app: Flask) -> str: + """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" + return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] + + def get_cookie_domain(self, app: Flask) -> str | None: + """The value of the ``Domain`` parameter on the session cookie. If not set, + browsers will only send the cookie to the exact domain it was set from. + Otherwise, they will send it to any subdomain of the given value as well. + + Uses the :data:`SESSION_COOKIE_DOMAIN` config. + + .. versionchanged:: 2.3 + Not set by default, does not fall back to ``SERVER_NAME``. + """ + return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] + + def get_cookie_path(self, app: Flask) -> str: + """Returns the path for which the cookie should be valid. The + default implementation uses the value from the ``SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's ``None``. + """ + return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] + + def get_cookie_httponly(self, app: Flask) -> bool: + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] + + def get_cookie_secure(self, app: Flask) -> bool: + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] + + def get_cookie_samesite(self, app: Flask) -> str | None: + """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the + ``SameSite`` attribute. This currently just returns the value of + the :data:`SESSION_COOKIE_SAMESITE` setting. + """ + return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] + + def get_cookie_partitioned(self, app: Flask) -> bool: + """Returns True if the cookie should be partitioned. By default, uses + the value of :data:`SESSION_COOKIE_PARTITIONED`. + + .. versionadded:: 3.1 + """ + return app.config["SESSION_COOKIE_PARTITIONED"] # type: ignore[no-any-return] + + def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: + """A helper method that returns an expiration date for the session + or ``None`` if the session is linked to the browser session. The + default implementation returns now + the permanent session + lifetime configured on the application. + """ + if session.permanent: + return datetime.now(timezone.utc) + app.permanent_session_lifetime + return None + + def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: + """Used by session backends to determine if a ``Set-Cookie`` header + should be set for this session cookie for this response. If the session + has been modified, the cookie is set. If the session is permanent and + the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is + always set. + + This check is usually skipped if the session was deleted. + + .. versionadded:: 0.11 + """ + + return session.modified or ( + session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] + ) + + def open_session(self, app: Flask, request: Request) -> SessionMixin | None: + """This is called at the beginning of each request, after + pushing the request context, before matching the URL. + + This must return an object which implements a dictionary-like + interface as well as the :class:`SessionMixin` interface. + + This will return ``None`` to indicate that loading failed in + some way that is not immediately an error. The request + context will fall back to using :meth:`make_null_session` + in this case. + """ + raise NotImplementedError() + + def save_session( + self, app: Flask, session: SessionMixin, response: Response + ) -> None: + """This is called at the end of each request, after generating + a response, before removing the request context. It is skipped + if :meth:`is_null_session` returns ``True``. + """ + raise NotImplementedError() + + +session_json_serializer = TaggedJSONSerializer() + + +def _lazy_sha1(string: bytes = b"") -> t.Any: + """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include + SHA-1, in which case the import and use as a default would fail before the + developer can configure something else. + """ + return hashlib.sha1(string) + + +class SecureCookieSessionInterface(SessionInterface): + """The default session interface that stores sessions in signed cookies + through the :mod:`itsdangerous` module. + """ + + #: the salt that should be applied on top of the secret key for the + #: signing of cookie based sessions. + salt = "cookie-session" + #: the hash function to use for the signature. The default is sha1 + digest_method = staticmethod(_lazy_sha1) + #: the name of the itsdangerous supported key derivation. The default + #: is hmac. + key_derivation = "hmac" + #: A python serializer for the payload. The default is a compact + #: JSON derived serializer with support for some extra Python types + #: such as datetime objects or tuples. + serializer = session_json_serializer + session_class = SecureCookieSession + + def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: + if not app.secret_key: + return None + + keys: list[str | bytes] = [] + + if fallbacks := app.config["SECRET_KEY_FALLBACKS"]: + keys.extend(fallbacks) + + keys.append(app.secret_key) # itsdangerous expects current key at top + return URLSafeTimedSerializer( + keys, # type: ignore[arg-type] + salt=self.salt, + serializer=self.serializer, + signer_kwargs={ + "key_derivation": self.key_derivation, + "digest_method": self.digest_method, + }, + ) + + def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: + s = self.get_signing_serializer(app) + if s is None: + return None + val = request.cookies.get(self.get_cookie_name(app)) + if not val: + return self.session_class() + max_age = int(app.permanent_session_lifetime.total_seconds()) + try: + data = s.loads(val, max_age=max_age) + return self.session_class(data) + except BadSignature: + return self.session_class() + + def save_session( + self, app: Flask, session: SessionMixin, response: Response + ) -> None: + name = self.get_cookie_name(app) + domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + secure = self.get_cookie_secure(app) + partitioned = self.get_cookie_partitioned(app) + samesite = self.get_cookie_samesite(app) + httponly = self.get_cookie_httponly(app) + + # Add a "Vary: Cookie" header if the session was accessed at all. + if session.accessed: + response.vary.add("Cookie") + + # If the session is modified to be empty, remove the cookie. + # If the session is empty, return without setting the cookie. + if not session: + if session.modified: + response.delete_cookie( + name, + domain=domain, + path=path, + secure=secure, + partitioned=partitioned, + samesite=samesite, + httponly=httponly, + ) + response.vary.add("Cookie") + + return + + if not self.should_set_cookie(app, session): + return + + expires = self.get_expiration_time(app, session) + val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore[union-attr] + response.set_cookie( + name, + val, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + partitioned=partitioned, + samesite=samesite, + ) + response.vary.add("Cookie") diff --git a/venv/lib/python3.10/site-packages/flask/signals.py b/venv/lib/python3.10/site-packages/flask/signals.py new file mode 100644 index 0000000..444fda9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/signals.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from blinker import Namespace + +# This namespace is only for signals provided by Flask itself. +_signals = Namespace() + +template_rendered = _signals.signal("template-rendered") +before_render_template = _signals.signal("before-render-template") +request_started = _signals.signal("request-started") +request_finished = _signals.signal("request-finished") +request_tearing_down = _signals.signal("request-tearing-down") +got_request_exception = _signals.signal("got-request-exception") +appcontext_tearing_down = _signals.signal("appcontext-tearing-down") +appcontext_pushed = _signals.signal("appcontext-pushed") +appcontext_popped = _signals.signal("appcontext-popped") +message_flashed = _signals.signal("message-flashed") diff --git a/venv/lib/python3.10/site-packages/flask/templating.py b/venv/lib/python3.10/site-packages/flask/templating.py new file mode 100644 index 0000000..16d480f --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/templating.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +import typing as t + +from jinja2 import BaseLoader +from jinja2 import Environment as BaseEnvironment +from jinja2 import Template +from jinja2 import TemplateNotFound + +from .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import request +from .helpers import stream_with_context +from .signals import before_render_template +from .signals import template_rendered + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .sansio.app import App + from .sansio.scaffold import Scaffold + + +def _default_template_ctx_processor() -> dict[str, t.Any]: + """Default template context processor. Injects `request`, + `session` and `g`. + """ + appctx = _cv_app.get(None) + reqctx = _cv_request.get(None) + rv: dict[str, t.Any] = {} + if appctx is not None: + rv["g"] = appctx.g + if reqctx is not None: + rv["request"] = reqctx.request + rv["session"] = reqctx.session + return rv + + +class Environment(BaseEnvironment): + """Works like a regular Jinja environment but has some additional + knowledge of how Flask's blueprint works so that it can prepend the + name of the blueprint to referenced templates if necessary. + """ + + def __init__(self, app: App, **options: t.Any) -> None: + if "loader" not in options: + options["loader"] = app.create_global_jinja_loader() + BaseEnvironment.__init__(self, **options) + self.app = app + + +class DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the blueprint folders. + """ + + def __init__(self, app: App) -> None: + self.app = app + + def get_source( + self, environment: BaseEnvironment, template: str + ) -> tuple[str, str | None, t.Callable[[], bool] | None]: + if self.app.config["EXPLAIN_TEMPLATE_LOADING"]: + return self._get_source_explained(environment, template) + return self._get_source_fast(environment, template) + + def _get_source_explained( + self, environment: BaseEnvironment, template: str + ) -> tuple[str, str | None, t.Callable[[], bool] | None]: + attempts = [] + rv: tuple[str, str | None, t.Callable[[], bool] | None] | None + trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None + + for srcobj, loader in self._iter_loaders(template): + try: + rv = loader.get_source(environment, template) + if trv is None: + trv = rv + except TemplateNotFound: + rv = None + attempts.append((loader, srcobj, rv)) + + from .debughelpers import explain_template_loading_attempts + + explain_template_loading_attempts(self.app, template, attempts) + + if trv is not None: + return trv + raise TemplateNotFound(template) + + def _get_source_fast( + self, environment: BaseEnvironment, template: str + ) -> tuple[str, str | None, t.Callable[[], bool] | None]: + for _srcobj, loader in self._iter_loaders(template): + try: + return loader.get_source(environment, template) + except TemplateNotFound: + continue + raise TemplateNotFound(template) + + def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]: + loader = self.app.jinja_loader + if loader is not None: + yield self.app, loader + + for blueprint in self.app.iter_blueprints(): + loader = blueprint.jinja_loader + if loader is not None: + yield blueprint, loader + + def list_templates(self) -> list[str]: + result = set() + loader = self.app.jinja_loader + if loader is not None: + result.update(loader.list_templates()) + + for blueprint in self.app.iter_blueprints(): + loader = blueprint.jinja_loader + if loader is not None: + for template in loader.list_templates(): + result.add(template) + + return list(result) + + +def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str: + app.update_template_context(context) + before_render_template.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + rv = template.render(context) + template_rendered.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + return rv + + +def render_template( + template_name_or_list: str | Template | list[str | Template], + **context: t.Any, +) -> str: + """Render a template by name with the given context. + + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _render(app, template, context) + + +def render_template_string(source: str, **context: t.Any) -> str: + """Render a template from the given source string with the given + context. + + :param source: The source code of the template to render. + :param context: The variables to make available in the template. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _render(app, template, context) + + +def _stream( + app: Flask, template: Template, context: dict[str, t.Any] +) -> t.Iterator[str]: + app.update_template_context(context) + before_render_template.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + + def generate() -> t.Iterator[str]: + yield from template.generate(context) + template_rendered.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + + rv = generate() + + # If a request context is active, keep it while generating. + if request: + rv = stream_with_context(rv) + + return rv + + +def stream_template( + template_name_or_list: str | Template | list[str | Template], + **context: t.Any, +) -> t.Iterator[str]: + """Render a template by name with the given context as a stream. + This returns an iterator of strings, which can be used as a + streaming response from a view. + + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _stream(app, template, context) + + +def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: + """Render a template from the given source string with the given + context as a stream. This returns an iterator of strings, which can + be used as a streaming response from a view. + + :param source: The source code of the template to render. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _stream(app, template, context) diff --git a/venv/lib/python3.10/site-packages/flask/testing.py b/venv/lib/python3.10/site-packages/flask/testing.py new file mode 100644 index 0000000..55eb12f --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/testing.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import importlib.metadata +import typing as t +from contextlib import contextmanager +from contextlib import ExitStack +from copy import copy +from types import TracebackType +from urllib.parse import urlsplit + +import werkzeug.test +from click.testing import CliRunner +from click.testing import Result +from werkzeug.test import Client +from werkzeug.wrappers import Request as BaseRequest + +from .cli import ScriptInfo +from .sessions import SessionMixin + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIEnvironment + from werkzeug.test import TestResponse + + from .app import Flask + + +class EnvironBuilder(werkzeug.test.EnvironBuilder): + """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the + application. + + :param app: The Flask application to configure the environment from. + :param path: URL path being requested. + :param base_url: Base URL where the app is being served, which + ``path`` is relative to. If not given, built from + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param json: If given, this is serialized as JSON and passed as + ``data``. Also defaults ``content_type`` to + ``application/json``. + :param args: other positional arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + :param kwargs: other keyword arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + """ + + def __init__( + self, + app: Flask, + path: str = "/", + base_url: str | None = None, + subdomain: str | None = None, + url_scheme: str | None = None, + *args: t.Any, + **kwargs: t.Any, + ) -> None: + assert not (base_url or subdomain or url_scheme) or ( + base_url is not None + ) != bool(subdomain or url_scheme), ( + 'Cannot pass "subdomain" or "url_scheme" with "base_url".' + ) + + if base_url is None: + http_host = app.config.get("SERVER_NAME") or "localhost" + app_root = app.config["APPLICATION_ROOT"] + + if subdomain: + http_host = f"{subdomain}.{http_host}" + + if url_scheme is None: + url_scheme = app.config["PREFERRED_URL_SCHEME"] + + url = urlsplit(path) + base_url = ( + f"{url.scheme or url_scheme}://{url.netloc or http_host}" + f"/{app_root.lstrip('/')}" + ) + path = url.path + + if url.query: + path = f"{path}?{url.query}" + + self.app = app + super().__init__(path, base_url, *args, **kwargs) + + def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """Serialize ``obj`` to a JSON-formatted string. + + The serialization will be configured according to the config associated + with this EnvironBuilder's ``app``. + """ + return self.app.json.dumps(obj, **kwargs) + + +_werkzeug_version = "" + + +def _get_werkzeug_version() -> str: + global _werkzeug_version + + if not _werkzeug_version: + _werkzeug_version = importlib.metadata.version("werkzeug") + + return _werkzeug_version + + +class FlaskClient(Client): + """Works like a regular Werkzeug test client but has knowledge about + Flask's contexts to defer the cleanup of the request context until + the end of a ``with`` block. For general information about how to + use this class refer to :class:`werkzeug.test.Client`. + + .. versionchanged:: 0.12 + `app.test_client()` includes preset default environment, which can be + set after instantiation of the `app.test_client()` object in + `client.environ_base`. + + Basic usage is outlined in the :doc:`/testing` chapter. + """ + + application: Flask + + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + super().__init__(*args, **kwargs) + self.preserve_context = False + self._new_contexts: list[t.ContextManager[t.Any]] = [] + self._context_stack = ExitStack() + self.environ_base = { + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}", + } + + @contextmanager + def session_transaction( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Iterator[SessionMixin]: + """When used in combination with a ``with`` statement this opens a + session transaction. This can be used to modify the session that + the test client uses. Once the ``with`` block is left the session is + stored back. + + :: + + with client.session_transaction() as session: + session['value'] = 42 + + Internally this is implemented by going through a temporary test + request context and since session handling could depend on + request variables this function accepts the same arguments as + :meth:`~flask.Flask.test_request_context` which are directly + passed through. + """ + if self._cookies is None: + raise TypeError( + "Cookies are disabled. Create a client with 'use_cookies=True'." + ) + + app = self.application + ctx = app.test_request_context(*args, **kwargs) + self._add_cookies_to_wsgi(ctx.request.environ) + + with ctx: + sess = app.session_interface.open_session(app, ctx.request) + + if sess is None: + raise RuntimeError("Session backend did not open a session.") + + yield sess + resp = app.response_class() + + if app.session_interface.is_null_session(sess): + return + + with ctx: + app.session_interface.save_session(app, sess, resp) + + self._update_cookies_from_response( + ctx.request.host.partition(":")[0], + ctx.request.path, + resp.headers.getlist("Set-Cookie"), + ) + + def _copy_environ(self, other: WSGIEnvironment) -> WSGIEnvironment: + out = {**self.environ_base, **other} + + if self.preserve_context: + out["werkzeug.debug.preserve_context"] = self._new_contexts.append + + return out + + def _request_from_builder_args( + self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] + ) -> BaseRequest: + kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {})) + builder = EnvironBuilder(self.application, *args, **kwargs) + + try: + return builder.get_request() + finally: + builder.close() + + def open( + self, + *args: t.Any, + buffered: bool = False, + follow_redirects: bool = False, + **kwargs: t.Any, + ) -> TestResponse: + if args and isinstance( + args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest) + ): + if isinstance(args[0], werkzeug.test.EnvironBuilder): + builder = copy(args[0]) + builder.environ_base = self._copy_environ(builder.environ_base or {}) # type: ignore[arg-type] + request = builder.get_request() + elif isinstance(args[0], dict): + request = EnvironBuilder.from_environ( + args[0], app=self.application, environ_base=self._copy_environ({}) + ).get_request() + else: + # isinstance(args[0], BaseRequest) + request = copy(args[0]) + request.environ = self._copy_environ(request.environ) + else: + # request is None + request = self._request_from_builder_args(args, kwargs) + + # Pop any previously preserved contexts. This prevents contexts + # from being preserved across redirects or multiple requests + # within a single block. + self._context_stack.close() + + response = super().open( + request, + buffered=buffered, + follow_redirects=follow_redirects, + ) + response.json_module = self.application.json # type: ignore[assignment] + + # Re-push contexts that were preserved during the request. + for cm in self._new_contexts: + self._context_stack.enter_context(cm) + + self._new_contexts.clear() + return response + + def __enter__(self) -> FlaskClient: + if self.preserve_context: + raise RuntimeError("Cannot nest client invocations") + self.preserve_context = True + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.preserve_context = False + self._context_stack.close() + + +class FlaskCliRunner(CliRunner): + """A :class:`~click.testing.CliRunner` for testing a Flask app's + CLI commands. Typically created using + :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`. + """ + + def __init__(self, app: Flask, **kwargs: t.Any) -> None: + self.app = app + super().__init__(**kwargs) + + def invoke( # type: ignore + self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any + ) -> Result: + """Invokes a CLI command in an isolated environment. See + :meth:`CliRunner.invoke ` for + full method documentation. See :ref:`testing-cli` for examples. + + If the ``obj`` argument is not given, passes an instance of + :class:`~flask.cli.ScriptInfo` that knows how to load the Flask + app being tested. + + :param cli: Command object to invoke. Default is the app's + :attr:`~flask.app.Flask.cli` group. + :param args: List of strings to invoke the command with. + + :return: a :class:`~click.testing.Result` object. + """ + if cli is None: + cli = self.app.cli + + if "obj" not in kwargs: + kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) + + return super().invoke(cli, args, **kwargs) diff --git a/venv/lib/python3.10/site-packages/flask/typing.py b/venv/lib/python3.10/site-packages/flask/typing.py new file mode 100644 index 0000000..6b70c40 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/typing.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import collections.abc as cabc +import typing as t + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIApplication # noqa: F401 + from werkzeug.datastructures import Headers # noqa: F401 + from werkzeug.sansio.response import Response # noqa: F401 + +# The possible types that are directly convertible or are a Response object. +ResponseValue = t.Union[ + "Response", + str, + bytes, + list[t.Any], + # Only dict is actually accepted, but Mapping allows for TypedDict. + t.Mapping[str, t.Any], + t.Iterator[str], + t.Iterator[bytes], + cabc.AsyncIterable[str], # for Quart, until App is generic. + cabc.AsyncIterable[bytes], +] + +# the possible types for an individual HTTP header +# This should be a Union, but mypy doesn't pass unless it's a TypeVar. +HeaderValue = t.Union[str, list[str], tuple[str, ...]] + +# the possible types for HTTP headers +HeadersValue = t.Union[ + "Headers", + t.Mapping[str, HeaderValue], + t.Sequence[tuple[str, HeaderValue]], +] + +# The possible types returned by a route function. +ResponseReturnValue = t.Union[ + ResponseValue, + tuple[ResponseValue, HeadersValue], + tuple[ResponseValue, int], + tuple[ResponseValue, int, HeadersValue], + "WSGIApplication", +] + +# Allow any subclass of werkzeug.Response, such as the one from Flask, +# as a callback argument. Using werkzeug.Response directly makes a +# callback annotated with flask.Response fail type checking. +ResponseClass = t.TypeVar("ResponseClass", bound="Response") + +AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named +AfterRequestCallable = t.Union[ + t.Callable[[ResponseClass], ResponseClass], + t.Callable[[ResponseClass], t.Awaitable[ResponseClass]], +] +BeforeFirstRequestCallable = t.Union[ + t.Callable[[], None], t.Callable[[], t.Awaitable[None]] +] +BeforeRequestCallable = t.Union[ + t.Callable[[], t.Optional[ResponseReturnValue]], + t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]], +] +ShellContextProcessorCallable = t.Callable[[], dict[str, t.Any]] +TeardownCallable = t.Union[ + t.Callable[[t.Optional[BaseException]], None], + t.Callable[[t.Optional[BaseException]], t.Awaitable[None]], +] +TemplateContextProcessorCallable = t.Union[ + t.Callable[[], dict[str, t.Any]], + t.Callable[[], t.Awaitable[dict[str, t.Any]]], +] +TemplateFilterCallable = t.Callable[..., t.Any] +TemplateGlobalCallable = t.Callable[..., t.Any] +TemplateTestCallable = t.Callable[..., bool] +URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None] +URLValuePreprocessorCallable = t.Callable[ + [t.Optional[str], t.Optional[dict[str, t.Any]]], None +] + +# This should take Exception, but that either breaks typing the argument +# with a specific exception, or decorating multiple times with different +# exceptions (and using a union type on the argument). +# https://github.com/pallets/flask/issues/4095 +# https://github.com/pallets/flask/issues/4295 +# https://github.com/pallets/flask/issues/4297 +ErrorHandlerCallable = t.Union[ + t.Callable[[t.Any], ResponseReturnValue], + t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]], +] + +RouteCallable = t.Union[ + t.Callable[..., ResponseReturnValue], + t.Callable[..., t.Awaitable[ResponseReturnValue]], +] diff --git a/venv/lib/python3.10/site-packages/flask/views.py b/venv/lib/python3.10/site-packages/flask/views.py new file mode 100644 index 0000000..53fe976 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/views.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import typing as t + +from . import typing as ft +from .globals import current_app +from .globals import request + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + +http_method_funcs = frozenset( + ["get", "post", "head", "options", "delete", "put", "trace", "patch"] +) + + +class View: + """Subclass this class and override :meth:`dispatch_request` to + create a generic class-based view. Call :meth:`as_view` to create a + view function that creates an instance of the class with the given + arguments and calls its ``dispatch_request`` method with any URL + variables. + + See :doc:`views` for a detailed guide. + + .. code-block:: python + + class Hello(View): + init_every_request = False + + def dispatch_request(self, name): + return f"Hello, {name}!" + + app.add_url_rule( + "/hello/", view_func=Hello.as_view("hello") + ) + + Set :attr:`methods` on the class to change what methods the view + accepts. + + Set :attr:`decorators` on the class to apply a list of decorators to + the generated view function. Decorators applied to the class itself + will not be applied to the generated view function! + + Set :attr:`init_every_request` to ``False`` for efficiency, unless + you need to store request-global data on ``self``. + """ + + #: The methods this view is registered for. Uses the same default + #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and + #: ``add_url_rule`` by default. + methods: t.ClassVar[t.Collection[str] | None] = None + + #: Control whether the ``OPTIONS`` method is handled automatically. + #: Uses the same default (``True``) as ``route`` and + #: ``add_url_rule`` by default. + provide_automatic_options: t.ClassVar[bool | None] = None + + #: A list of decorators to apply, in order, to the generated view + #: function. Remember that ``@decorator`` syntax is applied bottom + #: to top, so the first decorator in the list would be the bottom + #: decorator. + #: + #: .. versionadded:: 0.8 + decorators: t.ClassVar[list[t.Callable[..., t.Any]]] = [] + + #: Create a new instance of this view class for every request by + #: default. If a view subclass sets this to ``False``, the same + #: instance is used for every request. + #: + #: A single instance is more efficient, especially if complex setup + #: is done during init. However, storing data on ``self`` is no + #: longer safe across requests, and :data:`~flask.g` should be used + #: instead. + #: + #: .. versionadded:: 2.2 + init_every_request: t.ClassVar[bool] = True + + def dispatch_request(self) -> ft.ResponseReturnValue: + """The actual view function behavior. Subclasses must override + this and return a valid response. Any variables from the URL + rule are passed as keyword arguments. + """ + raise NotImplementedError() + + @classmethod + def as_view( + cls, name: str, *class_args: t.Any, **class_kwargs: t.Any + ) -> ft.RouteCallable: + """Convert the class into a view function that can be registered + for a route. + + By default, the generated view will create a new instance of the + view class for every request and call its + :meth:`dispatch_request` method. If the view class sets + :attr:`init_every_request` to ``False``, the same instance will + be used for every request. + + Except for ``name``, all other arguments passed to this method + are forwarded to the view class ``__init__`` method. + + .. versionchanged:: 2.2 + Added the ``init_every_request`` class attribute. + """ + if cls.init_every_request: + + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + self = view.view_class( # type: ignore[attr-defined] + *class_args, **class_kwargs + ) + return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] + + else: + self = cls(*class_args, **class_kwargs) # pyright: ignore + + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] + + if cls.decorators: + view.__name__ = name + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + + # We attach the view class to the view function for two reasons: + # first of all it allows us to easily figure out what class-based + # view this thing came from, secondly it's also used for instantiating + # the view class so you can actually replace it with something else + # for testing purposes and debugging. + view.view_class = cls # type: ignore + view.__name__ = name + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + view.methods = cls.methods # type: ignore + view.provide_automatic_options = cls.provide_automatic_options # type: ignore + return view + + +class MethodView(View): + """Dispatches request methods to the corresponding instance methods. + For example, if you implement a ``get`` method, it will be used to + handle ``GET`` requests. + + This can be useful for defining a REST API. + + :attr:`methods` is automatically set based on the methods defined on + the class. + + See :doc:`views` for a detailed guide. + + .. code-block:: python + + class CounterAPI(MethodView): + def get(self): + return str(session.get("counter", 0)) + + def post(self): + session["counter"] = session.get("counter", 0) + 1 + return redirect(url_for("counter")) + + app.add_url_rule( + "/counter", view_func=CounterAPI.as_view("counter") + ) + """ + + def __init_subclass__(cls, **kwargs: t.Any) -> None: + super().__init_subclass__(**kwargs) + + if "methods" not in cls.__dict__: + methods = set() + + for base in cls.__bases__: + if getattr(base, "methods", None): + methods.update(base.methods) # type: ignore[attr-defined] + + for key in http_method_funcs: + if hasattr(cls, key): + methods.add(key.upper()) + + if methods: + cls.methods = methods + + def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: + meth = getattr(self, request.method.lower(), None) + + # If the request method is HEAD and we don't have a handler for it + # retry with GET. + if meth is None and request.method == "HEAD": + meth = getattr(self, "get", None) + + assert meth is not None, f"Unimplemented method {request.method!r}" + return current_app.ensure_sync(meth)(**kwargs) # type: ignore[no-any-return] diff --git a/venv/lib/python3.10/site-packages/flask/wrappers.py b/venv/lib/python3.10/site-packages/flask/wrappers.py new file mode 100644 index 0000000..bab6102 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask/wrappers.py @@ -0,0 +1,257 @@ +from __future__ import annotations + +import typing as t + +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import HTTPException +from werkzeug.wrappers import Request as RequestBase +from werkzeug.wrappers import Response as ResponseBase + +from . import json +from .globals import current_app +from .helpers import _split_blueprint_path + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.routing import Rule + + +class Request(RequestBase): + """The request object used by default in Flask. Remembers the + matched endpoint and view arguments. + + It is what ends up as :class:`~flask.request`. If you want to replace + the request object used you can subclass this and set + :attr:`~flask.Flask.request_class` to your subclass. + + The request object is a :class:`~werkzeug.wrappers.Request` subclass and + provides all of the attributes Werkzeug defines plus a few Flask + specific ones. + """ + + json_module: t.Any = json + + #: The internal URL rule that matched the request. This can be + #: useful to inspect which methods are allowed for the URL from + #: a before/after handler (``request.url_rule.methods``) etc. + #: Though if the request's method was invalid for the URL rule, + #: the valid list is available in ``routing_exception.valid_methods`` + #: instead (an attribute of the Werkzeug exception + #: :exc:`~werkzeug.exceptions.MethodNotAllowed`) + #: because the request was never internally bound. + #: + #: .. versionadded:: 0.6 + url_rule: Rule | None = None + + #: A dict of view arguments that matched the request. If an exception + #: happened when matching, this will be ``None``. + view_args: dict[str, t.Any] | None = None + + #: If matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception: HTTPException | None = None + + _max_content_length: int | None = None + _max_form_memory_size: int | None = None + _max_form_parts: int | None = None + + @property + def max_content_length(self) -> int | None: + """The maximum number of bytes that will be read during this request. If + this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` + error is raised. If it is set to ``None``, no limit is enforced at the + Flask application level. However, if it is ``None`` and the request has + no ``Content-Length`` header and the WSGI server does not indicate that + it terminates the stream, then no data is read to avoid an infinite + stream. + + Each request defaults to the :data:`MAX_CONTENT_LENGTH` config, which + defaults to ``None``. It can be set on a specific ``request`` to apply + the limit to that specific view. This should be set appropriately based + on an application's or view's specific needs. + + .. versionchanged:: 3.1 + This can be set per-request. + + .. versionchanged:: 0.6 + This is configurable through Flask config. + """ + if self._max_content_length is not None: + return self._max_content_length + + if not current_app: + return super().max_content_length + + return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return] + + @max_content_length.setter + def max_content_length(self, value: int | None) -> None: + self._max_content_length = value + + @property + def max_form_memory_size(self) -> int | None: + """The maximum size in bytes any non-file form field may be in a + ``multipart/form-data`` body. If this limit is exceeded, a 413 + :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it + is set to ``None``, no limit is enforced at the Flask application level. + + Each request defaults to the :data:`MAX_FORM_MEMORY_SIZE` config, which + defaults to ``500_000``. It can be set on a specific ``request`` to + apply the limit to that specific view. This should be set appropriately + based on an application's or view's specific needs. + + .. versionchanged:: 3.1 + This is configurable through Flask config. + """ + if self._max_form_memory_size is not None: + return self._max_form_memory_size + + if not current_app: + return super().max_form_memory_size + + return current_app.config["MAX_FORM_MEMORY_SIZE"] # type: ignore[no-any-return] + + @max_form_memory_size.setter + def max_form_memory_size(self, value: int | None) -> None: + self._max_form_memory_size = value + + @property # type: ignore[override] + def max_form_parts(self) -> int | None: + """The maximum number of fields that may be present in a + ``multipart/form-data`` body. If this limit is exceeded, a 413 + :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it + is set to ``None``, no limit is enforced at the Flask application level. + + Each request defaults to the :data:`MAX_FORM_PARTS` config, which + defaults to ``1_000``. It can be set on a specific ``request`` to apply + the limit to that specific view. This should be set appropriately based + on an application's or view's specific needs. + + .. versionchanged:: 3.1 + This is configurable through Flask config. + """ + if self._max_form_parts is not None: + return self._max_form_parts + + if not current_app: + return super().max_form_parts + + return current_app.config["MAX_FORM_PARTS"] # type: ignore[no-any-return] + + @max_form_parts.setter + def max_form_parts(self, value: int | None) -> None: + self._max_form_parts = value + + @property + def endpoint(self) -> str | None: + """The endpoint that matched the request URL. + + This will be ``None`` if matching failed or has not been + performed yet. + + This in combination with :attr:`view_args` can be used to + reconstruct the same URL or a modified URL. + """ + if self.url_rule is not None: + return self.url_rule.endpoint # type: ignore[no-any-return] + + return None + + @property + def blueprint(self) -> str | None: + """The registered name of the current blueprint. + + This will be ``None`` if the endpoint is not part of a + blueprint, or if URL matching failed or has not been performed + yet. + + This does not necessarily match the name the blueprint was + created with. It may have been nested, or registered with a + different name. + """ + endpoint = self.endpoint + + if endpoint is not None and "." in endpoint: + return endpoint.rpartition(".")[0] + + return None + + @property + def blueprints(self) -> list[str]: + """The registered names of the current blueprint upwards through + parent blueprints. + + This will be an empty list if there is no current blueprint, or + if URL matching failed. + + .. versionadded:: 2.0.1 + """ + name = self.blueprint + + if name is None: + return [] + + return _split_blueprint_path(name) + + def _load_form_data(self) -> None: + super()._load_form_data() + + # In debug mode we're replacing the files multidict with an ad-hoc + # subclass that raises a different error for key errors. + if ( + current_app + and current_app.debug + and self.mimetype != "multipart/form-data" + and not self.files + ): + from .debughelpers import attach_enctype_error_multidict + + attach_enctype_error_multidict(self) + + def on_json_loading_failed(self, e: ValueError | None) -> t.Any: + try: + return super().on_json_loading_failed(e) + except BadRequest as ebr: + if current_app and current_app.debug: + raise + + raise BadRequest() from ebr + + +class Response(ResponseBase): + """The response object that is used by default in Flask. Works like the + response object from Werkzeug but is set to have an HTML mimetype by + default. Quite often you don't have to create this object yourself because + :meth:`~flask.Flask.make_response` will take care of that for you. + + If you want to replace the response object used you can subclass this and + set :attr:`~flask.Flask.response_class` to your subclass. + + .. versionchanged:: 1.0 + JSON support is added to the response, like the request. This is useful + when testing to get the test client response data as JSON. + + .. versionchanged:: 1.0 + + Added :attr:`max_cookie_size`. + """ + + default_mimetype: str | None = "text/html" + + json_module = json + + autocorrect_location_header = False + + @property + def max_cookie_size(self) -> int: # type: ignore + """Read-only view of the :data:`MAX_COOKIE_SIZE` config key. + + See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in + Werkzeug's docs. + """ + if current_app: + return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return] + + # return Werkzeug's default when not in an app context + return super().max_cookie_size diff --git a/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/METADATA b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/METADATA new file mode 100644 index 0000000..44bd264 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/METADATA @@ -0,0 +1,144 @@ +Metadata-Version: 2.4 +Name: flask-cors +Version: 6.0.2 +Summary: A Flask extension simplifying CORS support +Author-email: Cory Dolphin +License-Expression: MIT +Project-URL: Homepage, https://corydolphin.github.io/flask-cors/ +Project-URL: Repository, https://github.com/corydolphin/flask-cors +Project-URL: Documentation, https://corydolphin.github.io/flask-cors/ +Keywords: python +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: <4.0,>=3.9 +Description-Content-Type: text/x-rst +Requires-Dist: flask>=0.9 +Requires-Dist: Werkzeug>=0.7 + +Flask-CORS +========== + +|Build Status| |Latest Version| |Supported Python versions| +|License| + +A Flask extension for handling Cross Origin Resource Sharing (CORS), making cross-origin AJAX possible. + +This package has a simple philosophy: when you want to enable CORS, you wish to enable it for all use cases on a domain. +This means no mucking around with different allowed headers, methods, etc. + +By default, submission of cookies across domains is disabled due to the security implications. +Please see the documentation for how to enable credential'ed requests, and please make sure you add some sort of `CSRF `__ protection before doing so! + +Installation +------------ + +Install the extension with using pip, or easy\_install. + +.. code:: bash + + $ pip install -U flask-cors + +Usage +----- + +This package exposes a Flask extension which by default enables CORS support on all routes, for all origins and methods. +It allows parameterization of all CORS headers on a per-resource level. +The package also contains a decorator, for those who prefer this approach. + +Simple Usage +~~~~~~~~~~~~ + +In the simplest case, initialize the Flask-Cors extension with default arguments in order to allow CORS for all domains on all routes. +See the full list of options in the `documentation `__. + +.. code:: python + + + from flask import Flask + from flask_cors import CORS + + app = Flask(__name__) + CORS(app) + + @app.route("/") + def helloWorld(): + return "Hello, cross-origin-world!" + +Resource specific CORS +^^^^^^^^^^^^^^^^^^^^^^ + +Alternatively, you can specify CORS options on a resource and origin level of granularity by passing a dictionary as the `resources` option, mapping paths to a set of options. +See the full list of options in the `documentation `__. + +.. code:: python + + app = Flask(__name__) + cors = CORS(app, resources={r"/api/*": {"origins": "*"}}) + + @app.route("/api/v1/users") + def list_users(): + return "user example" + +Route specific CORS via decorator +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +This extension also exposes a simple decorator to decorate flask routes with. +Simply add ``@cross_origin()`` below a call to Flask's ``@app.route(..)`` to allow CORS on a given route. +See the full list of options in the `decorator documentation `__. + +.. code:: python + + @app.route("/") + @cross_origin() + def helloWorld(): + return "Hello, cross-origin-world!" + +Documentation +------------- + +For a full list of options, please see the full `documentation `__ + +Troubleshooting +--------------- + +If things aren't working as you expect, enable logging to help understand what is going on under the hood, and why. + +.. code:: python + + logging.getLogger('flask_cors').level = logging.DEBUG + + + +Set Up Your Development Environment +--- +The development environment uses `uv` for Python version management as well as dependency management. +There are helpful Makefile targets to do everything you need. Use `make test` to get started! + + +Contributing +------------ + +Questions, comments or improvements? +Please create an issue on `Github `__, tweet at `@corydolphin `__ or send me an email. +I do my best to include every contribution proposed in any way that I can. + +Credits +------- + +This Flask extension is based upon the `Decorator for the HTTP Access Control `__ written by Armin Ronacher. + +.. |Build Status| image:: https://github.com/corydolphin/flask-cors/actions/workflows/unittests.yaml/badge.svg + :target: https://travis-ci.org/corydolphin/flask-cors +.. |Latest Version| image:: https://img.shields.io/pypi/v/Flask-Cors.svg + :target: https://pypi.python.org/pypi/Flask-Cors/ +.. |Supported Python versions| image:: https://img.shields.io/pypi/pyversions/Flask-Cors.svg + :target: https://img.shields.io/pypi/pyversions/Flask-Cors.svg +.. |License| image:: http://img.shields.io/:license-mit-blue.svg + :target: https://pypi.python.org/pypi/Flask-Cors/ diff --git a/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/RECORD b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/RECORD new file mode 100644 index 0000000..06db143 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/RECORD @@ -0,0 +1,16 @@ +flask_cors-6.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +flask_cors-6.0.2.dist-info/METADATA,sha256=xYYixYpXPBqR21WrRmV6PMSlRh7OQ5J3A4G9WD6k_-M,5327 +flask_cors-6.0.2.dist-info/RECORD,, +flask_cors-6.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask_cors-6.0.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91 +flask_cors-6.0.2.dist-info/top_level.txt,sha256=aWye_0QNZPp_QtPF4ZluLHqnyVLT9CPJsfiGhwqkWuo,11 +flask_cors/__init__.py,sha256=Hnwpy4nxEz4tlcB3enWNXUBctcjgxRnUqa5B5zDabQ4,522 +flask_cors/__pycache__/__init__.cpython-310.pyc,, +flask_cors/__pycache__/core.cpython-310.pyc,, +flask_cors/__pycache__/decorator.cpython-310.pyc,, +flask_cors/__pycache__/extension.cpython-310.pyc,, +flask_cors/__pycache__/version.cpython-310.pyc,, +flask_cors/core.py,sha256=S8Wv7dz9e0FcR386TkBT-T8_VRsjFQGhNiR0jIpUYiY,14416 +flask_cors/decorator.py,sha256=YnPIDyWAfz60t2yxLgoOZLzgWWjZEvxQAPTTV4qfsss,4706 +flask_cors/extension.py,sha256=ONk6eNJiYT7HPEtmmySG8KgjzvAs9xtcJHNYvWLAS9s,8268 +flask_cors/version.py,sha256=lze97Y7ToUnpENKGAhOoSKx1zlZQe6ZfCSr8M50_K1g,22 diff --git a/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/REQUESTED b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/WHEEL b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/WHEEL new file mode 100644 index 0000000..e7fa31b --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/top_level.txt b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/top_level.txt new file mode 100644 index 0000000..27af988 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors-6.0.2.dist-info/top_level.txt @@ -0,0 +1 @@ +flask_cors diff --git a/venv/lib/python3.10/site-packages/flask_cors/__init__.py b/venv/lib/python3.10/site-packages/flask_cors/__init__.py new file mode 100644 index 0000000..732d401 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors/__init__.py @@ -0,0 +1,17 @@ +from .decorator import cross_origin +from .extension import CORS +from .version import __version__ + +__all__ = ["CORS", "__version__", "cross_origin"] + +# Set default logging handler to avoid "No handler found" warnings. +import logging +from logging import NullHandler + +# Set initial level to WARN. Users must manually enable logging for +# flask_cors to see our logging. +rootlogger = logging.getLogger(__name__) +rootlogger.addHandler(NullHandler()) + +if rootlogger.level == logging.NOTSET: + rootlogger.setLevel(logging.WARN) diff --git a/venv/lib/python3.10/site-packages/flask_cors/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..a7b3154 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask_cors/__pycache__/core.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/core.cpython-310.pyc new file mode 100644 index 0000000..32d55a2 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/core.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask_cors/__pycache__/decorator.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/decorator.cpython-310.pyc new file mode 100644 index 0000000..7d7644d Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/decorator.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask_cors/__pycache__/extension.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/extension.cpython-310.pyc new file mode 100644 index 0000000..5e666c4 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/extension.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask_cors/__pycache__/version.cpython-310.pyc b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/version.cpython-310.pyc new file mode 100644 index 0000000..a410946 Binary files /dev/null and b/venv/lib/python3.10/site-packages/flask_cors/__pycache__/version.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/flask_cors/core.py b/venv/lib/python3.10/site-packages/flask_cors/core.py new file mode 100644 index 0000000..8df343c --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors/core.py @@ -0,0 +1,399 @@ +import logging +import re +from collections.abc import Iterable +from datetime import timedelta + +from flask import current_app, request +from werkzeug.datastructures import Headers, MultiDict + +LOG = logging.getLogger(__name__) + +# Response Headers +ACL_ORIGIN = "Access-Control-Allow-Origin" +ACL_METHODS = "Access-Control-Allow-Methods" +ACL_ALLOW_HEADERS = "Access-Control-Allow-Headers" +ACL_EXPOSE_HEADERS = "Access-Control-Expose-Headers" +ACL_CREDENTIALS = "Access-Control-Allow-Credentials" +ACL_MAX_AGE = "Access-Control-Max-Age" +ACL_RESPONSE_PRIVATE_NETWORK = "Access-Control-Allow-Private-Network" + +# Request Header +ACL_REQUEST_METHOD = "Access-Control-Request-Method" +ACL_REQUEST_HEADERS = "Access-Control-Request-Headers" +ACL_REQUEST_HEADER_PRIVATE_NETWORK = "Access-Control-Request-Private-Network" + +ALL_METHODS = ["GET", "HEAD", "POST", "OPTIONS", "PUT", "PATCH", "DELETE"] +CONFIG_OPTIONS = [ + "CORS_ORIGINS", + "CORS_METHODS", + "CORS_ALLOW_HEADERS", + "CORS_EXPOSE_HEADERS", + "CORS_SUPPORTS_CREDENTIALS", + "CORS_MAX_AGE", + "CORS_SEND_WILDCARD", + "CORS_AUTOMATIC_OPTIONS", + "CORS_VARY_HEADER", + "CORS_RESOURCES", + "CORS_INTERCEPT_EXCEPTIONS", + "CORS_ALWAYS_SEND", + "CORS_ALLOW_PRIVATE_NETWORK", +] +# Attribute added to request object by decorator to indicate that CORS +# was evaluated, in case the decorator and extension are both applied +# to a view. +FLASK_CORS_EVALUATED = "_FLASK_CORS_EVALUATED" + +# Strange, but this gets the type of a compiled regex, which is otherwise not +# exposed in a public API. +RegexObject = type(re.compile("")) +DEFAULT_OPTIONS = dict( + origins="*", + methods=ALL_METHODS, + allow_headers="*", + expose_headers=None, + supports_credentials=False, + max_age=None, + send_wildcard=False, + automatic_options=True, + vary_header=True, + resources=r"/*", + intercept_exceptions=True, + always_send=True, + allow_private_network=False, +) + + +def parse_resources(resources): + if isinstance(resources, dict): + # To make the API more consistent with the decorator, allow a + # resource of '*', which is not actually a valid regexp. + resources = [(re_fix(k), v) for k, v in resources.items()] + + # Sort patterns with static (literal) paths first, then by regex specificity + def sort_key(pair): + pattern, _ = pair + if isinstance(pattern, RegexObject): + return (1, 0, -pattern.pattern.count("/"), -len(pattern.pattern)) + elif probably_regex(pattern): + return (1, 1, -pattern.count("/"), -len(pattern)) + else: + return (0, 0, -pattern.count("/"), -len(pattern)) + + return sorted(resources, key=sort_key) + + elif isinstance(resources, str): + return [(re_fix(resources), {})] + + elif isinstance(resources, Iterable): + return [(re_fix(r), {}) for r in resources] + + # Type of compiled regex is not part of the public API. Test for this + # at runtime. + elif isinstance(resources, RegexObject): + return [(re_fix(resources), {})] + + else: + raise ValueError("Unexpected value for resources argument.") + + +def get_regexp_pattern(regexp): + """ + Helper that returns regexp pattern from given value. + + :param regexp: regular expression to stringify + :type regexp: _sre.SRE_Pattern or str + :returns: string representation of given regexp pattern + :rtype: str + """ + try: + return regexp.pattern + except AttributeError: + return str(regexp) + + +def get_cors_origins(options, request_origin): + origins = options.get("origins") + wildcard = r".*" in origins + + # If the Origin header is not present terminate this set of steps. + # The request is outside the scope of this specification.-- W3Spec + if request_origin: + LOG.debug("CORS request received with 'Origin' %s", request_origin) + + # If the allowed origins is an asterisk or 'wildcard', always match + if wildcard and options.get("send_wildcard"): + LOG.debug("Allowed origins are set to '*'. Sending wildcard CORS header.") + return ["*"] + # If the value of the Origin header is a case-insensitive match + # for any of the values in list of origins. + # NOTE: Per RFC 1035 and RFC 4343 schemes and hostnames are case insensitive. + elif try_match_any_pattern(request_origin, origins, caseSensitive=False): + LOG.debug( + "The request's Origin header matches. Sending CORS headers.", + ) + # Add a single Access-Control-Allow-Origin header, with either + # the value of the Origin header or the string "*" as value. + # -- W3Spec + return [request_origin] + else: + LOG.debug("The request's Origin header does not match any of allowed origins.") + return None + + elif options.get("always_send"): + if wildcard: + # If wildcard is in the origins, even if 'send_wildcard' is False, + # simply send the wildcard. Unless supports_credentials is True, + # since that is forbidden by the spec.. + # It is the most-likely to be correct thing to do (the only other + # option is to return nothing, which almost certainly not what + # the developer wants if the '*' origin was specified. + if options.get("supports_credentials"): + return None + else: + return ["*"] + else: + # Return all origins that are not regexes. + return sorted([o for o in origins if not probably_regex(o)]) + + # Terminate these steps, return the original request untouched. + else: + LOG.debug( + "The request did not contain an 'Origin' header. This means the browser or client did not request CORS, ensure the Origin Header is set." + ) + return None + + +def get_allow_headers(options, acl_request_headers): + if acl_request_headers: + request_headers = [h.strip() for h in acl_request_headers.split(",")] + + # any header that matches in the allow_headers + matching_headers = filter(lambda h: try_match_any_pattern(h, options.get("allow_headers"), caseSensitive=False), request_headers) + + return ", ".join(sorted(matching_headers)) + + return None + + +def get_cors_headers(options, request_headers, request_method): + origins_to_set = get_cors_origins(options, request_headers.get("Origin")) + headers = MultiDict() + + if not origins_to_set: # CORS is not enabled for this route + return headers + + for origin in origins_to_set: + headers.add(ACL_ORIGIN, origin) + + headers[ACL_EXPOSE_HEADERS] = options.get("expose_headers") + + if options.get("supports_credentials"): + headers[ACL_CREDENTIALS] = "true" # case sensitive + + if ( + ACL_REQUEST_HEADER_PRIVATE_NETWORK in request_headers + and request_headers.get(ACL_REQUEST_HEADER_PRIVATE_NETWORK) == "true" + ): + allow_private_network = "true" if options.get("allow_private_network") else "false" + headers[ACL_RESPONSE_PRIVATE_NETWORK] = allow_private_network + + # This is a preflight request + # http://www.w3.org/TR/cors/#resource-preflight-requests + if request_method == "OPTIONS": + acl_request_method = request_headers.get(ACL_REQUEST_METHOD, "").upper() + + # If there is no Access-Control-Request-Method header or if parsing + # failed, do not set any additional headers + if acl_request_method and acl_request_method in options.get("methods"): + # If method is not a case-sensitive match for any of the values in + # list of methods do not set any additional headers and terminate + # this set of steps. + headers[ACL_ALLOW_HEADERS] = get_allow_headers(options, request_headers.get(ACL_REQUEST_HEADERS)) + headers[ACL_MAX_AGE] = options.get("max_age") + headers[ACL_METHODS] = options.get("methods") + else: + LOG.info( + "The request's Access-Control-Request-Method header does not match allowed methods. CORS headers will not be applied." + ) + + # http://www.w3.org/TR/cors/#resource-implementation + if options.get("vary_header"): + # Only set header if the origin returned will vary dynamically, + # i.e. if we are not returning an asterisk, and there are multiple + # origins that can be matched. + if headers[ACL_ORIGIN] == "*": + pass + elif ( + len(options.get("origins")) > 1 + or len(origins_to_set) > 1 + or any(map(probably_regex, options.get("origins"))) + ): + headers.add("Vary", "Origin") + + return MultiDict((k, v) for k, v in headers.items() if v) + + +def set_cors_headers(resp, options): + """ + Performs the actual evaluation of Flask-CORS options and actually + modifies the response object. + + This function is used both in the decorator and the after_request + callback + """ + + # If CORS has already been evaluated via the decorator, skip + if hasattr(resp, FLASK_CORS_EVALUATED): + LOG.debug("CORS have been already evaluated, skipping") + return resp + + # Some libraries, like OAuthlib, set resp.headers to non Multidict + # objects (Werkzeug Headers work as well). This is a problem because + # headers allow repeated values. + if not isinstance(resp.headers, Headers) and not isinstance(resp.headers, MultiDict): + resp.headers = MultiDict(resp.headers) + + headers_to_set = get_cors_headers(options, request.headers, request.method) + + LOG.debug("Settings CORS headers: %s", str(headers_to_set)) + + for k, v in headers_to_set.items(): + resp.headers.add(k, v) + + return resp + + +def probably_regex(maybe_regex): + if isinstance(maybe_regex, RegexObject): + return True + else: + common_regex_chars = ["*", "\\", "]", "?", "$", "^", "[", "]", "(", ")"] + # Use common characters used in regular expressions as a proxy + # for if this string is in fact a regex. + return any(c in maybe_regex for c in common_regex_chars) + + +def re_fix(reg): + """ + Replace the invalid regex r'*' with the valid, wildcard regex r'/.*' to + enable the CORS app extension to have a more user friendly api. + """ + return r".*" if reg == r"*" else reg + + +def try_match_any_pattern(inst, patterns, caseSensitive=True): + return any(try_match_pattern(inst, pattern, caseSensitive) for pattern in patterns) + +def try_match_pattern(value, pattern, caseSensitive=True): + """ + Safely attempts to match a pattern or string to a value. This + function can be used to match request origins, headers, or paths. + The value of caseSensitive should be set in accordance to the + data being compared e.g. origins and headers are case insensitive + whereas paths are case-sensitive + """ + if isinstance(pattern, RegexObject): + return re.match(pattern, value) + if probably_regex(pattern): + flags = 0 if caseSensitive else re.IGNORECASE + try: + return re.match(pattern, value, flags=flags) + except re.error: + return False + try: + v = str(value) + p = str(pattern) + return v == p if caseSensitive else v.casefold() == p.casefold() + except Exception: + return value == pattern + +def get_cors_options(appInstance, *dicts): + """ + Compute CORS options for an application by combining the DEFAULT_OPTIONS, + the app's configuration-specified options and any dictionaries passed. The + last specified option wins. + """ + options = DEFAULT_OPTIONS.copy() + options.update(get_app_kwarg_dict(appInstance)) + if dicts: + for d in dicts: + options.update(d) + + return serialize_options(options) + + +def get_app_kwarg_dict(appInstance=None): + """Returns the dictionary of CORS specific app configurations.""" + app = appInstance or current_app + + # In order to support blueprints which do not have a config attribute + app_config = getattr(app, "config", {}) + + return {k.lower().replace("cors_", ""): app_config.get(k) for k in CONFIG_OPTIONS if app_config.get(k) is not None} + + +def flexible_str(obj): + """ + A more flexible str function which intelligently handles stringifying + strings, lists and other iterables. The results are lexographically sorted + to ensure generated responses are consistent when iterables such as Set + are used. + """ + if obj is None: + return None + elif not isinstance(obj, str) and isinstance(obj, Iterable): + return ", ".join(str(item) for item in sorted(obj)) + else: + return str(obj) + + +def serialize_option(options_dict, key, upper=False): + if key in options_dict: + value = flexible_str(options_dict[key]) + options_dict[key] = value.upper() if upper else value + + +def ensure_iterable(inst): + """ + Wraps scalars or string types as a list, or returns the iterable instance. + """ + if isinstance(inst, str) or not isinstance(inst, Iterable): + return [inst] + else: + return inst + + +def sanitize_regex_param(param): + return [re_fix(x) for x in ensure_iterable(param)] + + +def serialize_options(opts): + """ + A helper method to serialize and processes the options dictionary. + """ + options = (opts or {}).copy() + + for key in opts.keys(): + if key not in DEFAULT_OPTIONS: + LOG.warning("Unknown option passed to Flask-CORS: %s", key) + + # Ensure origins is a list of allowed origins with at least one entry. + options["origins"] = sanitize_regex_param(options.get("origins")) + options["allow_headers"] = sanitize_regex_param(options.get("allow_headers")) + + # This is expressly forbidden by the spec. Raise a value error so people + # don't get burned in production. + if r".*" in options["origins"] and options["supports_credentials"] and options["send_wildcard"]: + raise ValueError( + "Cannot use supports_credentials in conjunction with" + "an origin string of '*'. See: " + "http://www.w3.org/TR/cors/#resource-requests" + ) + + serialize_option(options, "expose_headers") + serialize_option(options, "methods", upper=True) + + if isinstance(options.get("max_age"), timedelta): + options["max_age"] = str(int(options["max_age"].total_seconds())) + + return options diff --git a/venv/lib/python3.10/site-packages/flask_cors/decorator.py b/venv/lib/python3.10/site-packages/flask_cors/decorator.py new file mode 100644 index 0000000..f8915b6 --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors/decorator.py @@ -0,0 +1,129 @@ +import logging +from functools import update_wrapper + +from flask import current_app, make_response, request + +from .core import FLASK_CORS_EVALUATED, get_cors_options, set_cors_headers + +LOG = logging.getLogger(__name__) + + +def cross_origin(*args, **kwargs): + """ + This function is the decorator which is used to wrap a Flask route with. + In the simplest case, simply use the default parameters to allow all + origins in what is the most permissive configuration. If this method + modifies state or performs authentication which may be brute-forced, you + should add some degree of protection, such as Cross Site Request Forgery + protection. + + :param origins: + The origin, or list of origins to allow requests from. + The origin(s) may be regular expressions, case-sensitive strings, + or else an asterisk + + Default : '*' + :type origins: list, string or regex + + :param methods: + The method or list of methods which the allowed origins are allowed to + access for non-simple requests. + + Default : [GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE] + :type methods: list or string + + :param expose_headers: + The header or list which are safe to expose to the API of a CORS API + specification. + + Default : None + :type expose_headers: list or string + + :param allow_headers: + The header or list of header field names which can be used when this + resource is accessed by allowed origins. The header(s) may be regular + expressions, case-sensitive strings, or else an asterisk. + + Default : '*', allow all headers + :type allow_headers: list, string or regex + + :param supports_credentials: + Allows users to make authenticated requests. If true, injects the + `Access-Control-Allow-Credentials` header in responses. This allows + cookies and credentials to be submitted across domains. + + :note: This option cannot be used in conjunction with a '*' origin + + Default : False + :type supports_credentials: bool + + :param max_age: + The maximum time for which this CORS request maybe cached. This value + is set as the `Access-Control-Max-Age` header. + + Default : None + :type max_age: timedelta, integer, string or None + + :param send_wildcard: If True, and the origins parameter is `*`, a wildcard + `Access-Control-Allow-Origin` header is sent, rather than the + request's `Origin` header. + + Default : False + :type send_wildcard: bool + + :param vary_header: + If True, the header Vary: Origin will be returned as per the W3 + implementation guidelines. + + Setting this header when the `Access-Control-Allow-Origin` is + dynamically generated (e.g. when there is more than one allowed + origin, and an Origin than '*' is returned) informs CDNs and other + caches that the CORS headers are dynamic, and cannot be cached. + + If False, the Vary header will never be injected or altered. + + Default : True + :type vary_header: bool + + :param automatic_options: + Only applies to the `cross_origin` decorator. If True, Flask-CORS will + override Flask's default OPTIONS handling to return CORS headers for + OPTIONS requests. + + Default : True + :type automatic_options: bool + + """ + _options = kwargs + + def decorator(f): + LOG.debug("Enabling %s for cross_origin using options:%s", f, _options) + + # If True, intercept OPTIONS requests by modifying the view function, + # replicating Flask's default behavior, and wrapping the response with + # CORS headers. + # + # If f.provide_automatic_options is unset or True, Flask's route + # decorator (which is actually wraps the function object we return) + # intercepts OPTIONS handling, and requests will not have CORS headers + if _options.get("automatic_options", True): + f.required_methods = getattr(f, "required_methods", set()) + f.required_methods.add("OPTIONS") + f.provide_automatic_options = False + + def wrapped_function(*args, **kwargs): + # Handle setting of Flask-Cors parameters + options = get_cors_options(current_app, _options) + + if options.get("automatic_options") and request.method == "OPTIONS": + resp = current_app.make_default_options_response() + else: + resp = make_response(f(*args, **kwargs)) + + set_cors_headers(resp, options) + setattr(resp, FLASK_CORS_EVALUATED, True) + return resp + + return update_wrapper(wrapped_function, f) + + return decorator diff --git a/venv/lib/python3.10/site-packages/flask_cors/extension.py b/venv/lib/python3.10/site-packages/flask_cors/extension.py new file mode 100644 index 0000000..434f65e --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors/extension.py @@ -0,0 +1,206 @@ +import logging +from urllib.parse import unquote + +from flask import request + +from .core import ACL_ORIGIN, get_cors_options, get_regexp_pattern, parse_resources, set_cors_headers, try_match_pattern + +LOG = logging.getLogger(__name__) + + +class CORS: + """ + Initializes Cross Origin Resource sharing for the application. The + arguments are identical to `cross_origin`, with the addition of a + `resources` parameter. The resources parameter defines a series of regular + expressions for resource paths to match and optionally, the associated + options to be applied to the particular resource. These options are + identical to the arguments to `cross_origin`. + + The settings for CORS are determined in the following order + + 1. Resource level settings (e.g when passed as a dictionary) + 2. Keyword argument settings + 3. App level configuration settings (e.g. CORS_*) + 4. Default settings + + Note: as it is possible for multiple regular expressions to match a + resource path, the regular expressions are first sorted by length, + from longest to shortest, in order to attempt to match the most + specific regular expression. This allows the definition of a + number of specific resource options, with a wildcard fallback + for all other resources. + + :param resources: + The series of regular expression and (optionally) associated CORS + options to be applied to the given resource path. + + If the argument is a dictionary, it's keys must be regular expressions, + and the values must be a dictionary of kwargs, identical to the kwargs + of this function. + + If the argument is a list, it is expected to be a list of regular + expressions, for which the app-wide configured options are applied. + + If the argument is a string, it is expected to be a regular expression + for which the app-wide configured options are applied. + + Default : Match all and apply app-level configuration + + :type resources: dict, iterable or string + + :param origins: + The origin, or list of origins to allow requests from. + The origin(s) may be regular expressions, case-sensitive strings, + or else an asterisk. + + .. note:: + + origins must include the schema and the port (if not port 80), + e.g., + `CORS(app, origins=["http://localhost:8000", "https://example.com"])`. + + Default : '*' + :type origins: list, string or regex + + :param methods: + The method or list of methods which the allowed origins are allowed to + access for non-simple requests. + + Default : [GET, HEAD, POST, OPTIONS, PUT, PATCH, DELETE] + :type methods: list or string + + :param expose_headers: + The header or list which are safe to expose to the API of a CORS API + specification. + + Default : None + :type expose_headers: list or string + + :param allow_headers: + The header or list of header field names which can be used when this + resource is accessed by allowed origins. The header(s) may be regular + expressions, case-sensitive strings, or else an asterisk. + + Default : '*', allow all headers + :type allow_headers: list, string or regex + + :param supports_credentials: + Allows users to make authenticated requests. If true, injects the + `Access-Control-Allow-Credentials` header in responses. This allows + cookies and credentials to be submitted across domains. + + :note: This option cannot be used in conjunction with a '*' origin + + Default : False + :type supports_credentials: bool + + :param max_age: + The maximum time for which this CORS request maybe cached. This value + is set as the `Access-Control-Max-Age` header. + + Default : None + :type max_age: timedelta, integer, string or None + + :param send_wildcard: If True, and the origins parameter is `*`, a wildcard + `Access-Control-Allow-Origin` header is sent, rather than the + request's `Origin` header. + + Default : False + :type send_wildcard: bool + + :param vary_header: + If True, the header Vary: Origin will be returned as per the W3 + implementation guidelines. + + Setting this header when the `Access-Control-Allow-Origin` is + dynamically generated (e.g. when there is more than one allowed + origin, and an Origin than '*' is returned) informs CDNs and other + caches that the CORS headers are dynamic, and cannot be cached. + + If False, the Vary header will never be injected or altered. + + Default : True + :type vary_header: bool + + :param allow_private_network: + If True, the response header `Access-Control-Allow-Private-Network` + will be set with the value 'true' whenever the request header + `Access-Control-Request-Private-Network` has a value 'true'. + + If False, the response header `Access-Control-Allow-Private-Network` + will be set with the value 'false' whenever the request header + `Access-Control-Request-Private-Network` has a value of 'true'. + + If the request header `Access-Control-Request-Private-Network` is + not present or has a value other than 'true', the response header + `Access-Control-Allow-Private-Network` will not be set. + + Default : True + :type allow_private_network: bool + """ + + def __init__(self, app=None, **kwargs): + self._options = kwargs + if app is not None: + self.init_app(app, **kwargs) + + def init_app(self, app, **kwargs): + # The resources and options may be specified in the App Config, the CORS constructor + # or the kwargs to the call to init_app. + options = get_cors_options(app, self._options, kwargs) + + # Flatten our resources into a list of the form + # (pattern_or_regexp, dictionary_of_options) + resources = parse_resources(options.get("resources")) + + # Compute the options for each resource by combining the options from + # the app's configuration, the constructor, the kwargs to init_app, and + # finally the options specified in the resources dictionary. + resources = [(pattern, get_cors_options(app, options, opts)) for (pattern, opts) in resources] + + # Create a human-readable form of these resources by converting the compiled + # regular expressions into strings. + resources_human = {get_regexp_pattern(pattern): opts for (pattern, opts) in resources} + LOG.debug("Configuring CORS with resources: %s", resources_human) + + cors_after_request = make_after_request_function(resources) + app.after_request(cors_after_request) + + # Wrap exception handlers with cross_origin + # These error handlers will still respect the behavior of the route + if options.get("intercept_exceptions", True): + + def _after_request_decorator(f): + def wrapped_function(*args, **kwargs): + return cors_after_request(app.make_response(f(*args, **kwargs))) + + return wrapped_function + + if hasattr(app, "handle_exception"): + app.handle_exception = _after_request_decorator(app.handle_exception) + app.handle_user_exception = _after_request_decorator(app.handle_user_exception) + + +def make_after_request_function(resources): + def cors_after_request(resp): + # If CORS headers are set in a view decorator, pass + if resp.headers is not None and resp.headers.get(ACL_ORIGIN): + LOG.debug("CORS have been already evaluated, skipping") + return resp + normalized_path = unquote(request.path) + for res_regex, res_options in resources: + if try_match_pattern(normalized_path, res_regex, caseSensitive=True): + LOG.debug( + "Request to '%r' matches CORS resource '%s'. Using options: %s", + request.path, + get_regexp_pattern(res_regex), + res_options, + ) + set_cors_headers(resp, res_options) + break + else: + LOG.debug("No CORS rule matches") + return resp + + return cors_after_request diff --git a/venv/lib/python3.10/site-packages/flask_cors/version.py b/venv/lib/python3.10/site-packages/flask_cors/version.py new file mode 100644 index 0000000..7f229cf --- /dev/null +++ b/venv/lib/python3.10/site-packages/flask_cors/version.py @@ -0,0 +1 @@ +__version__ = "6.0.2" diff --git a/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/LICENSE.txt b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/LICENSE.txt new file mode 100644 index 0000000..275cafd --- /dev/null +++ b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright (c) 2013-2024 Python Charmers, Australia + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/METADATA b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/METADATA new file mode 100644 index 0000000..3d0e551 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/METADATA @@ -0,0 +1,112 @@ +Metadata-Version: 2.1 +Name: future +Version: 1.0.0 +Summary: Clean single-source support for Python 3 and 2 +Home-page: https://python-future.org +Author: Ed Schofield +Author-email: ed@pythoncharmers.com +License: MIT +Project-URL: Source, https://github.com/PythonCharmers/python-future +Keywords: future past python3 migration futurize backport six 2to3 modernize pasteurize 3to2 +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 2 +Classifier: Programming Language :: Python :: 2.6 +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.3 +Classifier: Programming Language :: Python :: 3.4 +Classifier: Programming Language :: Python :: 3.5 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: License :: OSI Approved +Classifier: License :: OSI Approved :: MIT License +Classifier: Development Status :: 6 - Mature +Classifier: Intended Audience :: Developers +Requires-Python: >=2.6, !=3.0.*, !=3.1.*, !=3.2.* +License-File: LICENSE.txt + + +future: Easy, safe support for Python 2/3 compatibility +======================================================= + +``future`` is the missing compatibility layer between Python 2 and Python +3. It allows you to use a single, clean Python 3.x-compatible codebase to +support both Python 2 and Python 3 with minimal overhead. + +It is designed to be used as follows:: + + from __future__ import (absolute_import, division, + print_function, unicode_literals) + from builtins import ( + bytes, dict, int, list, object, range, str, + ascii, chr, hex, input, next, oct, open, + pow, round, super, + filter, map, zip) + +followed by predominantly standard, idiomatic Python 3 code that then runs +similarly on Python 2.6/2.7 and Python 3.3+. + +The imports have no effect on Python 3. On Python 2, they shadow the +corresponding builtins, which normally have different semantics on Python 3 +versus 2, to provide their Python 3 semantics. + + +Standard library reorganization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``future`` supports the standard library reorganization (PEP 3108) through the +following Py3 interfaces: + + >>> # Top-level packages with Py3 names provided on Py2: + >>> import html.parser + >>> import queue + >>> import tkinter.dialog + >>> import xmlrpc.client + >>> # etc. + + >>> # Aliases provided for extensions to existing Py2 module names: + >>> from future.standard_library import install_aliases + >>> install_aliases() + + >>> from collections import Counter, OrderedDict # backported to Py2.6 + >>> from collections import UserDict, UserList, UserString + >>> import urllib.request + >>> from itertools import filterfalse, zip_longest + >>> from subprocess import getoutput, getstatusoutput + + +Automatic conversion +-------------------- + +An included script called `futurize +`_ aids in converting +code (from either Python 2 or Python 3) to code compatible with both +platforms. It is similar to ``python-modernize`` but goes further in +providing Python 3 compatibility through the use of the backported types +and builtin functions in ``future``. + + +Documentation +------------- + +See: https://python-future.org + + +Credits +------- + +:Author: Ed Schofield, Jordan M. Adler, et al +:Sponsor: Python Charmers: https://pythoncharmers.com +:Others: See docs/credits.rst or https://python-future.org/credits.html + + +Licensing +--------- +Copyright 2013-2024 Python Charmers, Australia. +The software is distributed under an MIT licence. See LICENSE.txt. + diff --git a/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/RECORD b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/RECORD new file mode 100644 index 0000000..dab21bb --- /dev/null +++ b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/RECORD @@ -0,0 +1,417 @@ +../../../bin/futurize,sha256=1XI700osjuILaR4Y77YdAkKcZEZJ-u3rwKeYMKNdPdE,248 +../../../bin/pasteurize,sha256=BitBlxDAMLABv_38PcLgTZ25alhlH7I7YWW09dnam4Y,250 +future-1.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +future-1.0.0.dist-info/LICENSE.txt,sha256=vroEEDw6n_-Si1nHfV--sDo-S9DXrD0eH4mUoh4w7js,1075 +future-1.0.0.dist-info/METADATA,sha256=MNNaUKMRbZAmVYq5Am_-1NtYZ1ViECpSa8VsQjXbkT4,3959 +future-1.0.0.dist-info/RECORD,, +future-1.0.0.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92 +future-1.0.0.dist-info/entry_points.txt,sha256=U9LtP60KSNXoj58mzV5TbotBF371gTWzrKrzJIH80Kw,88 +future-1.0.0.dist-info/top_level.txt,sha256=DT0C3az2gb-uJaj-fs0h4WwHYlJVDp0EvLdud1y5Zyw,38 +future/__init__.py,sha256=T9PNLu6ycmVtpETLxRurmufuRAaosICWmWdAExZb5a8,2938 +future/__pycache__/__init__.cpython-310.pyc,, +future/backports/__init__.py,sha256=5QXvQ_jc5Xx6p4dSaHnZXPZazBEunKDKhbUjxZ0XD1I,530 +future/backports/__pycache__/__init__.cpython-310.pyc,, +future/backports/__pycache__/_markupbase.cpython-310.pyc,, +future/backports/__pycache__/datetime.cpython-310.pyc,, +future/backports/__pycache__/misc.cpython-310.pyc,, +future/backports/__pycache__/socket.cpython-310.pyc,, +future/backports/__pycache__/socketserver.cpython-310.pyc,, +future/backports/__pycache__/total_ordering.cpython-310.pyc,, +future/backports/_markupbase.py,sha256=MDPTCykLq4J7Aea3PvYotATEE0CG4R_SjlxfJaLXTJM,16215 +future/backports/datetime.py,sha256=jITCStolfadhCEhejFd99wCo59mBDF0Ruj8l7QcG7Ms,75553 +future/backports/email/__init__.py,sha256=eH3AJr3FkuBy_D6yS1V2K76Q2CQ93y2zmAMWmn8FbHI,2269 +future/backports/email/__pycache__/__init__.cpython-310.pyc,, +future/backports/email/__pycache__/_encoded_words.cpython-310.pyc,, +future/backports/email/__pycache__/_header_value_parser.cpython-310.pyc,, +future/backports/email/__pycache__/_parseaddr.cpython-310.pyc,, +future/backports/email/__pycache__/_policybase.cpython-310.pyc,, +future/backports/email/__pycache__/base64mime.cpython-310.pyc,, +future/backports/email/__pycache__/charset.cpython-310.pyc,, +future/backports/email/__pycache__/encoders.cpython-310.pyc,, +future/backports/email/__pycache__/errors.cpython-310.pyc,, +future/backports/email/__pycache__/feedparser.cpython-310.pyc,, +future/backports/email/__pycache__/generator.cpython-310.pyc,, +future/backports/email/__pycache__/header.cpython-310.pyc,, +future/backports/email/__pycache__/headerregistry.cpython-310.pyc,, +future/backports/email/__pycache__/iterators.cpython-310.pyc,, +future/backports/email/__pycache__/message.cpython-310.pyc,, +future/backports/email/__pycache__/parser.cpython-310.pyc,, +future/backports/email/__pycache__/policy.cpython-310.pyc,, +future/backports/email/__pycache__/quoprimime.cpython-310.pyc,, +future/backports/email/__pycache__/utils.cpython-310.pyc,, +future/backports/email/_encoded_words.py,sha256=m1vTRfxAQdg4VyWO7PF-1ih1mmq97V-BPyHHkuEwSME,8443 +future/backports/email/_header_value_parser.py,sha256=GmSdr5PpG3xzedMiElSJOsQ6IwE3Tl5SNwp4m6ZT4aE,104692 +future/backports/email/_parseaddr.py,sha256=KewEnos0YDM-SYX503z7E1MmVbG5VRaKjxjcl0Ipjbs,17389 +future/backports/email/_policybase.py,sha256=2lJD9xouiz4uHvWGQ6j1nwlwWVQGwwzpy5JZoeQqhUc,14647 +future/backports/email/base64mime.py,sha256=gXZFxh66jk6D2UqAmjRbmmyhOXbGUWZmFcdVOIolaYE,3761 +future/backports/email/charset.py,sha256=CfE4iV2zAq6MQC0CHXHLnwTNW71zmhNITbzOcfxE4vY,17439 +future/backports/email/encoders.py,sha256=Nn4Pcx1rOdRgoSIzB6T5RWHl5zxClbf32wgE6D0tUt8,2800 +future/backports/email/errors.py,sha256=tRX8PP5g7mk2bAxL1jTCYrbfhD2gPZFNrh4_GJRM8OQ,3680 +future/backports/email/feedparser.py,sha256=bvmhb4cdY-ipextPK2K2sDgMsNvTspmuQfYyCxc4zSc,22736 +future/backports/email/generator.py,sha256=lpaLhZHneguvZ2QgRu7Figkjb7zmY28AGhj9iZTdI7s,19520 +future/backports/email/header.py,sha256=uBHbNKO-yx5I9KBflernJpyy3fX4gImCB1QE7ICApLs,24448 +future/backports/email/headerregistry.py,sha256=ZPbvLKXD0NMLSU4jXlVHfGyGcLMrFm-GQVURu_XHj88,20637 +future/backports/email/iterators.py,sha256=kMRYFGy3SVVpo7HG7JJr2ZAlOoaX6CVPzKYwDSvLfV0,2348 +future/backports/email/message.py,sha256=I6WW5cZDza7uwLOGJSvsDhGZC9K_Q570Lk2gt_vDUXM,35237 +future/backports/email/mime/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +future/backports/email/mime/__pycache__/__init__.cpython-310.pyc,, +future/backports/email/mime/__pycache__/application.cpython-310.pyc,, +future/backports/email/mime/__pycache__/audio.cpython-310.pyc,, +future/backports/email/mime/__pycache__/base.cpython-310.pyc,, +future/backports/email/mime/__pycache__/image.cpython-310.pyc,, +future/backports/email/mime/__pycache__/message.cpython-310.pyc,, +future/backports/email/mime/__pycache__/multipart.cpython-310.pyc,, +future/backports/email/mime/__pycache__/nonmultipart.cpython-310.pyc,, +future/backports/email/mime/__pycache__/text.cpython-310.pyc,, +future/backports/email/mime/application.py,sha256=m-5a4mSxu2E32XAImnp9x9eMVX5Vme2iNgn2dMMNyss,1401 +future/backports/email/mime/audio.py,sha256=2ognalFRadcsUYQYMUZbjv5i1xJbFhQN643doMuI7M4,2815 +future/backports/email/mime/base.py,sha256=wV3ClQyMsOqmkXSXbk_wd_zPoPTvBx8kAIzq3rdM4lE,875 +future/backports/email/mime/image.py,sha256=DpQk1sB-IMmO43AF4uadsXyf_y5TdEzJLfyhqR48bIw,1907 +future/backports/email/mime/message.py,sha256=pFsMhXW07aRjsLq1peO847PApWFAl28-Z2Z7BP1Dn74,1429 +future/backports/email/mime/multipart.py,sha256=j4Lf_sJmuwTbfgdQ6R35_t1_ha2DynJBJDvpjwbNObE,1699 +future/backports/email/mime/nonmultipart.py,sha256=Ciba1Z8d2yLDDpxgDJuk3Bb-TqcpE9HCd8KfbW5vgl4,832 +future/backports/email/mime/text.py,sha256=zV98BjoR4S_nX8c47x43LnsnifeGhIfNGwSAh575bs0,1552 +future/backports/email/parser.py,sha256=NpTjmvjv6YDH6eImMJEYiIn_K7qe9-pPz3DmzTdMZUU,5310 +future/backports/email/policy.py,sha256=gpcbhVRXuCohkK6MUqopTs1lv4E4-ZVUO6OVncoGEJE,8823 +future/backports/email/quoprimime.py,sha256=w93W5XgdFpyGaDqDBJrnXF_v_npH5r20WuAxmrAzyQg,10923 +future/backports/email/utils.py,sha256=vpfN0E8UjNbNw-2NFBQGCo4TNgrghMsqzpEYW5C_fBs,14270 +future/backports/html/__init__.py,sha256=FKwqFtWMCoGNkhU97OPnR1fZSh6etAKfN1FU1KvXcV8,924 +future/backports/html/__pycache__/__init__.cpython-310.pyc,, +future/backports/html/__pycache__/entities.cpython-310.pyc,, +future/backports/html/__pycache__/parser.cpython-310.pyc,, +future/backports/html/entities.py,sha256=kzoRnQyGk_3DgoucHLhL5QL1pglK9nvmxhPIGZFDTnc,75428 +future/backports/html/parser.py,sha256=G2tUObvbHSotNt06JLY-BP1swaZNfDYFd_ENWDjPmRg,19770 +future/backports/http/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +future/backports/http/__pycache__/__init__.cpython-310.pyc,, +future/backports/http/__pycache__/client.cpython-310.pyc,, +future/backports/http/__pycache__/cookiejar.cpython-310.pyc,, +future/backports/http/__pycache__/cookies.cpython-310.pyc,, +future/backports/http/__pycache__/server.cpython-310.pyc,, +future/backports/http/client.py,sha256=76EbhEZOtvdHFcU-jrjivoff13oQ9IMbdkZEdf5kQzQ,47602 +future/backports/http/cookiejar.py,sha256=jqb27uvv8wB2mJm6kF9aC0w7B03nO6rzQ0_CF35yArg,76608 +future/backports/http/cookies.py,sha256=DsyDUGDEbCXAA9Jq6suswSc76uSZqUu39adDDNj8XGw,21581 +future/backports/http/server.py,sha256=1CaMxgzHf9lYhmTJyE7topgjRIlIn9cnjgw8YEvwJV4,45523 +future/backports/misc.py,sha256=EGnCVRmU-_7xrzss1rqqCqwqlQVywaPAxxLogBeNpw4,33063 +future/backports/socket.py,sha256=DH1V6IjKPpJ0tln8bYvxvQ7qnvZG-UoQtMA5yVleHiU,15663 +future/backports/socketserver.py,sha256=Twvyk5FqVnOeiNcbVsyMDPTF1mNlkKfyofG7tKxTdD8,24286 +future/backports/test/__init__.py,sha256=9dXxIZnkI095YfHC-XIaVF6d31GjeY1Ag8TEzcFgepM,264 +future/backports/test/__pycache__/__init__.cpython-310.pyc,, +future/backports/test/__pycache__/pystone.cpython-310.pyc,, +future/backports/test/__pycache__/ssl_servers.cpython-310.pyc,, +future/backports/test/__pycache__/support.cpython-310.pyc,, +future/backports/test/badcert.pem,sha256=JioQeRZkHH8hGsWJjAF3U1zQvcWqhyzG6IOEJpTY9SE,1928 +future/backports/test/badkey.pem,sha256=gaBK9px_gG7DmrLKxfD6f6i-toAmARBTVfs-YGFRQF0,2162 +future/backports/test/dh512.pem,sha256=dUTsjtLbK-femrorUrTGF8qvLjhTiT_n4Uo5V6u__Gs,402 +future/backports/test/https_svn_python_org_root.pem,sha256=wOB3Onnc62Iu9kEFd8GcHhd_suucYjpJNA3jyfHeJWA,2569 +future/backports/test/keycert.passwd.pem,sha256=ZBfnVLpbBtAOf_2gCdiQ-yrBHmRsNzSf8VC3UpQZIjg,1830 +future/backports/test/keycert.pem,sha256=xPXi5idPcQVbrhgxBqF2TNGm6sSZ2aLVVEt6DWzplL8,1783 +future/backports/test/keycert2.pem,sha256=DB46FEAYv8BWwQJ-5RzC696FxPN7CON-Qsi-R4poJgc,1795 +future/backports/test/nokia.pem,sha256=s00x0uPDSaa5DHJ_CwzlVhg3OVdJ47f4zgqQdd0SAfQ,1923 +future/backports/test/nullbytecert.pem,sha256=NFRYWhmP_qT3jGfVjR6-iaC-EQdhIFjiXtTLN5ZPKnE,5435 +future/backports/test/nullcert.pem,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +future/backports/test/pystone.py,sha256=fvyoJ_tVovTNaxbJmdJMwr9F6SngY-U4ibULnd_wUqA,7427 +future/backports/test/sha256.pem,sha256=3wB-GQqEc7jq-PYwYAQaPbtTvvr7stk_DVmZxFgehfA,8344 +future/backports/test/ssl_cert.pem,sha256=M607jJNeIeHG9BlTf_jaQkPJI4nOxSJPn-zmEAaW43M,867 +future/backports/test/ssl_key.passwd.pem,sha256=I_WH4sBw9Vs9Z-BvmuXY0aw8tx8avv6rm5UL4S_pP00,963 +future/backports/test/ssl_key.pem,sha256=VKGU-R3UYaZpVTXl7chWl4vEYEDeob69SfvRTQ8aq_4,916 +future/backports/test/ssl_servers.py,sha256=-pd7HMZljuZfFRAbCAiAP_2G04orITJFj-S9ddr6o84,7209 +future/backports/test/support.py,sha256=oTQ09QrLcbmFZXhMGqPz3VrYZddgxpJGEJPQhwfiG2k,69620 +future/backports/total_ordering.py,sha256=O3M57_IisQ-zW5hW20uxkfk4fTGsr0EF2tAKx3BksQo,1929 +future/backports/urllib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +future/backports/urllib/__pycache__/__init__.cpython-310.pyc,, +future/backports/urllib/__pycache__/error.cpython-310.pyc,, +future/backports/urllib/__pycache__/parse.cpython-310.pyc,, +future/backports/urllib/__pycache__/request.cpython-310.pyc,, +future/backports/urllib/__pycache__/response.cpython-310.pyc,, +future/backports/urllib/__pycache__/robotparser.cpython-310.pyc,, +future/backports/urllib/error.py,sha256=ktikuK9ag4lS4f8Z0k5p1F11qF40N2AiOtjbXiF97ew,2715 +future/backports/urllib/parse.py,sha256=67avrYqV1UK7i_22goRUrvJ8SffzjRdTja9wzq_ynXY,35792 +future/backports/urllib/request.py,sha256=aR9ZMzfhV1C2Qk3wFsGvkwxqtdPTdsJVGRt5DUCwgJ8,96276 +future/backports/urllib/response.py,sha256=ooQyswwbb-9N6IVi1Kwjss1aR-Kvm8ZNezoyVEonp8c,3180 +future/backports/urllib/robotparser.py,sha256=pnAGTbKhdbCq_9yMZp7m8hj5q_NJpyQX6oQIZuYcnkw,6865 +future/backports/xmlrpc/__init__.py,sha256=h61ciVTdVvu8oEUXv4dHf_Tc5XUXDH3RKB1-8fQhSsg,38 +future/backports/xmlrpc/__pycache__/__init__.cpython-310.pyc,, +future/backports/xmlrpc/__pycache__/client.cpython-310.pyc,, +future/backports/xmlrpc/__pycache__/server.cpython-310.pyc,, +future/backports/xmlrpc/client.py,sha256=TIHPztKRlrStphmO_PfYOQxsy2xugzWKz77STC1OZ1U,48175 +future/backports/xmlrpc/server.py,sha256=W_RW5hgYbNV2LGbnvngzm7akacRdK-XFY-Cy2HL-qsY,37285 +future/builtins/__init__.py,sha256=rDAHzkhfXHSaye72FgzQz-HPN3yYBu-VXSs5PUJGA6o,1688 +future/builtins/__pycache__/__init__.cpython-310.pyc,, +future/builtins/__pycache__/disabled.cpython-310.pyc,, +future/builtins/__pycache__/iterators.cpython-310.pyc,, +future/builtins/__pycache__/misc.cpython-310.pyc,, +future/builtins/__pycache__/new_min_max.cpython-310.pyc,, +future/builtins/__pycache__/newnext.cpython-310.pyc,, +future/builtins/__pycache__/newround.cpython-310.pyc,, +future/builtins/__pycache__/newsuper.cpython-310.pyc,, +future/builtins/disabled.py,sha256=Ysq74bsmwntpq7dzkwTAD7IHKrkXy66vJlPshVwgVBI,2109 +future/builtins/iterators.py,sha256=l1Zawm2x82oqOuGGtCZRE76Ej98sMlHQwu9fZLK5RrA,1396 +future/builtins/misc.py,sha256=hctlKKWUyN0Eoodxg4ySQHEqARTukOLR4L5K5c6PW9k,4550 +future/builtins/new_min_max.py,sha256=7qQ4iiG4GDgRzjPzzzmg9pdby35Mtt6xNOOsyqHnIGY,1757 +future/builtins/newnext.py,sha256=oxXB8baXqJv29YG40aCS9UXk9zObyoOjya8BJ7NdBJM,2009 +future/builtins/newround.py,sha256=7YTWjBgfIAvSEl7hLCWgemhjqdKtzohbO18yMErKz4E,3190 +future/builtins/newsuper.py,sha256=3Ygqq-8l3wh9gNvGbW5nAiTYT5WxxgSKN6RhNj7qi74,3849 +future/moves/__init__.py,sha256=MsAW69Xp_fqUo4xODufcKM6AZf-ozHaz44WPZdsDFJA,220 +future/moves/__pycache__/__init__.cpython-310.pyc,, +future/moves/__pycache__/_dummy_thread.cpython-310.pyc,, +future/moves/__pycache__/_markupbase.cpython-310.pyc,, +future/moves/__pycache__/_thread.cpython-310.pyc,, +future/moves/__pycache__/builtins.cpython-310.pyc,, +future/moves/__pycache__/collections.cpython-310.pyc,, +future/moves/__pycache__/configparser.cpython-310.pyc,, +future/moves/__pycache__/copyreg.cpython-310.pyc,, +future/moves/__pycache__/itertools.cpython-310.pyc,, +future/moves/__pycache__/multiprocessing.cpython-310.pyc,, +future/moves/__pycache__/pickle.cpython-310.pyc,, +future/moves/__pycache__/queue.cpython-310.pyc,, +future/moves/__pycache__/reprlib.cpython-310.pyc,, +future/moves/__pycache__/socketserver.cpython-310.pyc,, +future/moves/__pycache__/subprocess.cpython-310.pyc,, +future/moves/__pycache__/sys.cpython-310.pyc,, +future/moves/__pycache__/winreg.cpython-310.pyc,, +future/moves/_dummy_thread.py,sha256=ULUtLk1Luw9I1h-YPitnU3gqCbvNPoKC28N_Bk8jkR8,348 +future/moves/_markupbase.py,sha256=W9wh_Gu3jDAMIhVBV1ZnCkJwYLHRk_v_su_HLALBkZQ,171 +future/moves/_thread.py,sha256=rwY7L4BZMFPlrp_i6T2Un4_iKYwnrXJ-yV6FJZN8YDo,163 +future/moves/builtins.py,sha256=4sjjKiylecJeL9da_RaBZjdymX2jtMs84oA9lCqb4Ug,281 +future/moves/collections.py,sha256=OKQ-TfUgms_2bnZRn4hrclLDoiN2i-HSWcjs3BC2iY8,417 +future/moves/configparser.py,sha256=TNy226uCbljjU-DjAVo7j7Effbj5zxXvDh0SdXehbzk,146 +future/moves/copyreg.py,sha256=Y3UjLXIMSOxZggXtvZucE9yv4tkKZtVan45z8eix4sU,438 +future/moves/dbm/__init__.py,sha256=_VkvQHC2UcIgZFPRroiX_P0Fs7HNqS_69flR0-oq2B8,488 +future/moves/dbm/__pycache__/__init__.cpython-310.pyc,, +future/moves/dbm/__pycache__/dumb.cpython-310.pyc,, +future/moves/dbm/__pycache__/gnu.cpython-310.pyc,, +future/moves/dbm/__pycache__/ndbm.cpython-310.pyc,, +future/moves/dbm/dumb.py,sha256=HKdjjtO3EyP9EKi1Hgxh_eUU6yCQ0fBX9NN3n-zb8JE,166 +future/moves/dbm/gnu.py,sha256=XoCSEpZ2QaOgo2h1m80GW7NUgj_b93BKtbcuwgtnaKo,162 +future/moves/dbm/ndbm.py,sha256=OFnreyo_1YHDBl5YUm9gCzKlN1MHgWbfSQAZVls2jaM,162 +future/moves/html/__init__.py,sha256=BSUFSHxXf2kGvHozlnrB1nn6bPE6p4PpN3DwA_Z5geo,1016 +future/moves/html/__pycache__/__init__.cpython-310.pyc,, +future/moves/html/__pycache__/entities.cpython-310.pyc,, +future/moves/html/__pycache__/parser.cpython-310.pyc,, +future/moves/html/entities.py,sha256=lVvchdjK_RzRj759eg4RMvGWHfgBbj0tKGOoZ8dbRyY,177 +future/moves/html/parser.py,sha256=V2XpHLKLCxQum3N9xlO3IUccAD7BIykZMqdEcWET3vY,167 +future/moves/http/__init__.py,sha256=Mx1v_Tcks4udHCtDM8q2xnYUiQ01gD7EpPyeQwsP3-Q,71 +future/moves/http/__pycache__/__init__.cpython-310.pyc,, +future/moves/http/__pycache__/client.cpython-310.pyc,, +future/moves/http/__pycache__/cookiejar.cpython-310.pyc,, +future/moves/http/__pycache__/cookies.cpython-310.pyc,, +future/moves/http/__pycache__/server.cpython-310.pyc,, +future/moves/http/client.py,sha256=hqEBq7GDXZidd1AscKnSyjSoMcuj8rERqGTmD7VheDQ,165 +future/moves/http/cookiejar.py,sha256=Frr9ZZCg-145ymy0VGpiPJhvBEpJtVqRBYPaKhgT1Z4,173 +future/moves/http/cookies.py,sha256=PPrHa1_oDbu3D_BhJGc6PvMgY1KoxyYq1jqeJwEcMvE,233 +future/moves/http/server.py,sha256=8YQlSCShjAsB5rr5foVvZgp3IzwYFvTmGZCHhBSDtaI,606 +future/moves/itertools.py,sha256=PVxFHRlBQl9ElS0cuGFPcUtj53eHX7Z1DmggzGfgQ6c,158 +future/moves/multiprocessing.py,sha256=4L37igVf2NwBhXqmCHRA3slZ7lJeiQLzZdrGSGOOZ08,191 +future/moves/pickle.py,sha256=r8j9skzfE8ZCeHyh_OB-WucOkRTIHN7zpRM7l7V3qS4,229 +future/moves/queue.py,sha256=uxvLCChF-zxWWgrY1a_wxt8rp2jILdwO4PrnkBW6VTE,160 +future/moves/reprlib.py,sha256=Nt5sUgMQ3jeVIukqSHOvB0UIsl6Y5t-mmT_13mpZmiY,161 +future/moves/socketserver.py,sha256=v8ZLurDxHOgsubYm1iefjlpnnJQcx2VuRUGt9FCJB9k,174 +future/moves/subprocess.py,sha256=oqRSMfFZkxM4MXkt3oD5N6eBwmmJ6rQ9KPhvSQKT_hM,251 +future/moves/sys.py,sha256=HOMRX4Loim75FMbWawd3oEwuGNJR-ClMREEFkVpBsRs,132 +future/moves/test/__init__.py,sha256=yB9F-fDQpzu1v8cBoKgIrL2ScUNqjlkqEztYrGVCQ-0,110 +future/moves/test/__pycache__/__init__.cpython-310.pyc,, +future/moves/test/__pycache__/support.cpython-310.pyc,, +future/moves/test/support.py,sha256=TG5h0FVGwyJGtKQEXMhWtD4G9WZagHrMI_CeL9NlZYc,484 +future/moves/tkinter/__init__.py,sha256=jV9vDx3wRl0bsoclU8oSe-5SqHQ3YpCbStmqtXnq1p4,620 +future/moves/tkinter/__pycache__/__init__.cpython-310.pyc,, +future/moves/tkinter/__pycache__/colorchooser.cpython-310.pyc,, +future/moves/tkinter/__pycache__/commondialog.cpython-310.pyc,, +future/moves/tkinter/__pycache__/constants.cpython-310.pyc,, +future/moves/tkinter/__pycache__/dialog.cpython-310.pyc,, +future/moves/tkinter/__pycache__/dnd.cpython-310.pyc,, +future/moves/tkinter/__pycache__/filedialog.cpython-310.pyc,, +future/moves/tkinter/__pycache__/font.cpython-310.pyc,, +future/moves/tkinter/__pycache__/messagebox.cpython-310.pyc,, +future/moves/tkinter/__pycache__/scrolledtext.cpython-310.pyc,, +future/moves/tkinter/__pycache__/simpledialog.cpython-310.pyc,, +future/moves/tkinter/__pycache__/tix.cpython-310.pyc,, +future/moves/tkinter/__pycache__/ttk.cpython-310.pyc,, +future/moves/tkinter/colorchooser.py,sha256=kprlmpRtvDbW5Gq43H1mi2KmNJ2kuzLQOba0a5EwDkU,333 +future/moves/tkinter/commondialog.py,sha256=mdUbq1IZqOGaSA7_8R367IukDCsMfzXiVHrTQQpp7Z0,333 +future/moves/tkinter/constants.py,sha256=0qRUrZLRPdVxueABL9KTzzEWEsk6xM1rOjxK6OHxXtA,324 +future/moves/tkinter/dialog.py,sha256=ksp-zvs-_A90P9RNHS8S27f1k8f48zG2Bel2jwZV5y0,311 +future/moves/tkinter/dnd.py,sha256=C_Ah0Urnyf2XKE5u-oP6mWi16RzMSXgMA1uhBSAwKY8,306 +future/moves/tkinter/filedialog.py,sha256=yNr30k-hDY1aMJHNsKqRqHqOOlzYKCubfQ3HjY1ZlrE,534 +future/moves/tkinter/font.py,sha256=TXarflhJRxqepaRNSDw6JFUVGz5P1T1C4_uF9VRqj3w,309 +future/moves/tkinter/messagebox.py,sha256=WJt4t83kLmr_UnpCWFuLoyazZr3wAUOEl6ADn3osoEA,327 +future/moves/tkinter/scrolledtext.py,sha256=DRzN8aBAlDBUo1B2KDHzdpRSzXBfH4rOOz0iuHXbQcg,329 +future/moves/tkinter/simpledialog.py,sha256=6MhuVhZCJV4XfPpPSUWKlDLLGEi0Y2ZlGQ9TbsmJFL0,329 +future/moves/tkinter/tix.py,sha256=aNeOfbWSGmcN69UmEGf4tJ-QIxLT6SU5ynzm1iWgepA,302 +future/moves/tkinter/ttk.py,sha256=rRrJpDjcP2gjQNukECu4F026P-CkW-3Ca2tN6Oia-Fw,302 +future/moves/urllib/__init__.py,sha256=yB9F-fDQpzu1v8cBoKgIrL2ScUNqjlkqEztYrGVCQ-0,110 +future/moves/urllib/__pycache__/__init__.cpython-310.pyc,, +future/moves/urllib/__pycache__/error.cpython-310.pyc,, +future/moves/urllib/__pycache__/parse.cpython-310.pyc,, +future/moves/urllib/__pycache__/request.cpython-310.pyc,, +future/moves/urllib/__pycache__/response.cpython-310.pyc,, +future/moves/urllib/__pycache__/robotparser.cpython-310.pyc,, +future/moves/urllib/error.py,sha256=gfrKzv-6W5OjzNIfjvJaQkxABRLym2KwjfKFXSdDB60,479 +future/moves/urllib/parse.py,sha256=xLLUMIIB5MreCdYzRZ5zIRWrhTRCoMO8RZEH4WPFQDY,1045 +future/moves/urllib/request.py,sha256=ttIzq60PwjRyrLQUGdAtfYvs4fziVwvcLe2Kw-hvE0g,3496 +future/moves/urllib/response.py,sha256=ZEZML0FpbB--GIeBFPvSzbtlVJ6EsR4tCI4qB7D8sFQ,342 +future/moves/urllib/robotparser.py,sha256=j24p6dMNzUpGZtT3BQxwRoE-F88iWmBpKgu0tRV61FQ,179 +future/moves/winreg.py,sha256=2zNAG59QI7vFlCj7kqDh0JrAYTpexOnI55PEAIjYhqo,163 +future/moves/xmlrpc/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +future/moves/xmlrpc/__pycache__/__init__.cpython-310.pyc,, +future/moves/xmlrpc/__pycache__/client.cpython-310.pyc,, +future/moves/xmlrpc/__pycache__/server.cpython-310.pyc,, +future/moves/xmlrpc/client.py,sha256=2PfnL5IbKVwdKP7C8B1OUviEtuBObwoH4pAPfvHIvQc,143 +future/moves/xmlrpc/server.py,sha256=ESDXdpUgTKyeFmCDSnJmBp8zONjJklsRJOvy4OtaALc,143 +future/standard_library/__init__.py,sha256=Nwbaqikyeh77wSiro-BHNjSsCmSmuLGAe91d4c5q_QE,28065 +future/standard_library/__pycache__/__init__.cpython-310.pyc,, +future/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +future/tests/__pycache__/__init__.cpython-310.pyc,, +future/tests/__pycache__/base.cpython-310.pyc,, +future/tests/base.py,sha256=7LTAKHJgUxOwmffD1kgcErVt2VouKcldPnq4iruqk_k,19956 +future/types/__init__.py,sha256=5fBxWqf_OTQ8jZ7k2TS34rFH14togeR488F4zBHIQ-s,6831 +future/types/__pycache__/__init__.cpython-310.pyc,, +future/types/__pycache__/newbytes.cpython-310.pyc,, +future/types/__pycache__/newdict.cpython-310.pyc,, +future/types/__pycache__/newint.cpython-310.pyc,, +future/types/__pycache__/newlist.cpython-310.pyc,, +future/types/__pycache__/newmemoryview.cpython-310.pyc,, +future/types/__pycache__/newobject.cpython-310.pyc,, +future/types/__pycache__/newopen.cpython-310.pyc,, +future/types/__pycache__/newrange.cpython-310.pyc,, +future/types/__pycache__/newstr.cpython-310.pyc,, +future/types/newbytes.py,sha256=D_kNDD9sbNJir2cUxxePiAuw2OW5irxVnu55uHmuK9E,16303 +future/types/newdict.py,sha256=go-Lbl2MRWZJJRlwTAUlJNJRkg986YYeV0jCqEUEFNc,2011 +future/types/newint.py,sha256=HH90HS2Y1ApS02LDpKzqt9V1Lwtp6tktMIYjavZUIh8,13406 +future/types/newlist.py,sha256=-H5-fXodd-UQgTFnZBJdwE68CrgIL_jViYdv4w7q2rU,2284 +future/types/newmemoryview.py,sha256=LnARgiKqQ2zLwwDZ3owu1atoonPQkOneWMfxJCwB4_o,712 +future/types/newobject.py,sha256=AX_n8GwlDR2IY-xIwZCvu0Olj_Ca2aS57nlTihnFr-I,3358 +future/types/newopen.py,sha256=lcRNHWZ1UjEn_0_XKis1ZA5U6l-4c-CHlC0WX1sY4NI,810 +future/types/newrange.py,sha256=fcCL1imqqH-lqWsY9Lnml9d-WbJOtXrayAUPoUbM7Ck,5296 +future/types/newstr.py,sha256=e0brkurI0IK--4ToQEO4Cz1FECZav4CyUGMKxlrcmK4,15758 +future/utils/__init__.py,sha256=Er_tUl6bS4xp7_M1Z3hZrgM9hAGrRUvCAdcHDRgSOdE,21960 +future/utils/__pycache__/__init__.cpython-310.pyc,, +future/utils/__pycache__/surrogateescape.cpython-310.pyc,, +future/utils/surrogateescape.py,sha256=7u4V4XlW83P5YSAJS2f92YUF8vsWthsiTnmAshOJL_M,6097 +libfuturize/__init__.py,sha256=CZA_KgvTQOPAY1_MrlJeQ6eMh2Eei4_KIv4JuyAkpfw,31 +libfuturize/__pycache__/__init__.cpython-310.pyc,, +libfuturize/__pycache__/fixer_util.cpython-310.pyc,, +libfuturize/__pycache__/main.cpython-310.pyc,, +libfuturize/fixer_util.py,sha256=hOmX8XLnicGJ6RGwlUxslhuhzhPc0cZimlylFQAeDOo,17357 +libfuturize/fixes/__init__.py,sha256=5KEpUnjVsFCCsr_-zrikvJbLf9zslEJnFTH_5pBc33I,5236 +libfuturize/fixes/__pycache__/__init__.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_UserDict.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_absolute_import.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_add__future__imports_except_unicode_literals.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_basestring.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_bytes.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_cmp.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_division.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_division_safe.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_execfile.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_future_builtins.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_future_standard_library.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_future_standard_library_urllib.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_input.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_metaclass.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_next_call.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_object.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_oldstr_wrap.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_order___future__imports.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_print.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_print_with_import.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_raise.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_remove_old__future__imports.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_unicode_keep_u.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_unicode_literals_import.cpython-310.pyc,, +libfuturize/fixes/__pycache__/fix_xrange_with_import.cpython-310.pyc,, +libfuturize/fixes/fix_UserDict.py,sha256=jL4jXnGaUQTkG8RKfGXbU_HVTkB3MWZMQwUkqMAjB6I,3840 +libfuturize/fixes/fix_absolute_import.py,sha256=vkrF2FyQR5lSz2WmdqywzkEJVTC0eq4gh_REWBKHh7w,3140 +libfuturize/fixes/fix_add__future__imports_except_unicode_literals.py,sha256=Fr219VAzR8KWXc2_bfiqLl10EgxAWjL6cI3Mowt--VU,662 +libfuturize/fixes/fix_basestring.py,sha256=bHkKuMzhr5FMXwjXlMOjsod4S3rQkVdbzhoWV4-tl3Y,394 +libfuturize/fixes/fix_bytes.py,sha256=AhzOJes6EnPwgzboDjvURANbWKqciG6ZGaYW07PYQK8,685 +libfuturize/fixes/fix_cmp.py,sha256=Blq_Z0IGkYiKS83QzZ5wUgpJyZfQiZoEsWJ1VPyXgFY,701 +libfuturize/fixes/fix_division.py,sha256=gnrAi7stquiVUyi_De1H8q--43iQaSUX0CjnOmQ6O2w,228 +libfuturize/fixes/fix_division_safe.py,sha256=oz407p0Woc2EKw7jZHUL4CpDs81FFpekRum58NKsNp4,3631 +libfuturize/fixes/fix_execfile.py,sha256=I5AcJ6vPZ7i70TChaq9inxqnZ4C04-yJyfAItGa8E3c,921 +libfuturize/fixes/fix_future_builtins.py,sha256=QBCRpD9XA7tbtfP4wmOF2DXquB4lq-eupkQj-QAxp0s,2027 +libfuturize/fixes/fix_future_standard_library.py,sha256=FVtflFt38efHe_SEX6k3m6IYAtKWjA4rAPZrlCv6yA0,733 +libfuturize/fixes/fix_future_standard_library_urllib.py,sha256=Rf81XcAXA-vwNvrhskf5sLExbR--Wkr5fiUcMYGAKzs,1001 +libfuturize/fixes/fix_input.py,sha256=bhaPNtMrZNbjWIDQCR7Iue5BxBj4rf0RJQ9_jiwvb-s,687 +libfuturize/fixes/fix_metaclass.py,sha256=_CS1NDXYM-Mh6xpogLK_GtYx3rUUptu1-Z0Rx3lC9eQ,9570 +libfuturize/fixes/fix_next_call.py,sha256=01STG86Av9o5QcpQDJ6UbPhvxt9kKrkatiPeddXRgvA,3158 +libfuturize/fixes/fix_object.py,sha256=qalFIjn0VTWXG5sGOOoCvO65omjX5_9d40SUpwUjBdw,407 +libfuturize/fixes/fix_oldstr_wrap.py,sha256=UCR6Q2l-pVqJSrRTnQAWMlaqBoX7oX1VpG_w6Q0XcyY,1214 +libfuturize/fixes/fix_order___future__imports.py,sha256=ACUCw5NEGWvj6XA9rNj8BYha3ktxLvkM5Ssh5cyV644,829 +libfuturize/fixes/fix_print.py,sha256=nbJdv5DbxtWzJIRIQ0tr7FfGkMkHScJTLzvpxv_hSNw,3881 +libfuturize/fixes/fix_print_with_import.py,sha256=hVWn70Q1DPMUiHMyEqgUx-6sM1AylLj78v9pMc4LFw8,735 +libfuturize/fixes/fix_raise.py,sha256=CkjqiSvHHD-enaLxYMkH-Nsi92NGShFLWd3fG-exmI4,3904 +libfuturize/fixes/fix_remove_old__future__imports.py,sha256=j4EC1KEVgXhuQAqhYHnAruUjW6uczPjV_fTCSOLMuAw,851 +libfuturize/fixes/fix_unicode_keep_u.py,sha256=M8fcFxHeFnWVOKoQRpkMsnpd9qmUFubI2oFhO4ZPk7A,779 +libfuturize/fixes/fix_unicode_literals_import.py,sha256=wq-hb-9Yx3Az4ol-ylXZJPEDZ81EaPZeIy5VvpA0CEY,367 +libfuturize/fixes/fix_xrange_with_import.py,sha256=f074qStjMz3OtLjt1bKKZSxQnRbbb7HzEbqHt9wgqdw,479 +libfuturize/main.py,sha256=feICmcv0dzWhutvwz0unnIVxusbSlQZFDaxObkHebs8,13733 +libpasteurize/__init__.py,sha256=CZA_KgvTQOPAY1_MrlJeQ6eMh2Eei4_KIv4JuyAkpfw,31 +libpasteurize/__pycache__/__init__.cpython-310.pyc,, +libpasteurize/__pycache__/main.cpython-310.pyc,, +libpasteurize/fixes/__init__.py,sha256=ccdv-2MGjQMbq8XuEZBndHmbzGRrZnabksjXZLUv044,3719 +libpasteurize/fixes/__pycache__/__init__.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/feature_base.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_add_all__future__imports.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_add_all_future_builtins.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_add_future_standard_library_import.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_annotations.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_division.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_features.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_fullargspec.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_future_builtins.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_getcwd.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_imports.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_imports2.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_kwargs.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_memoryview.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_metaclass.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_newstyle.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_next.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_printfunction.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_raise.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_raise_.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_throw.cpython-310.pyc,, +libpasteurize/fixes/__pycache__/fix_unpacking.cpython-310.pyc,, +libpasteurize/fixes/feature_base.py,sha256=v7yLjBDBUPeNUc-YHGGlIsJDOQzFAM4Vo0RN5F1JHVU,1723 +libpasteurize/fixes/fix_add_all__future__imports.py,sha256=mHet1LgbHn9GfgCYGNZXKo-rseDWreAvUcAjZwdgeTE,676 +libpasteurize/fixes/fix_add_all_future_builtins.py,sha256=scfkY-Sz5j0yDtLYls2ENOcqEMPVxeDm9gFYYPINPB8,1269 +libpasteurize/fixes/fix_add_future_standard_library_import.py,sha256=thTRbkBzy_SJjZ0bJteTp0sBTx8Wr69xFakH4styf7Y,663 +libpasteurize/fixes/fix_annotations.py,sha256=VT_AorKY9AYWYZUZ17_CeUrJlEA7VGkwVLDQlwD1Bxo,1581 +libpasteurize/fixes/fix_division.py,sha256=_TD_c5KniAYqEm11O7NJF0v2WEhYSNkRGcKG_94ZOas,904 +libpasteurize/fixes/fix_features.py,sha256=NZn0n34_MYZpLNwyP1Tf51hOiN58Rg7A8tA9pK1S8-c,2675 +libpasteurize/fixes/fix_fullargspec.py,sha256=VlZuIU6QNrClmRuvC4mtLICL3yMCi-RcGCnS9fD4b-Q,438 +libpasteurize/fixes/fix_future_builtins.py,sha256=SlCK9I9u05m19Lr1wxlJxF8toZ5yu0yXBeDLxUN9_fw,1450 +libpasteurize/fixes/fix_getcwd.py,sha256=uebvTvFboLqsROFCwdnzoP6ThziM0skz9TDXHoJcFsQ,873 +libpasteurize/fixes/fix_imports.py,sha256=KH4Q-qMzsuN5VcfE1ZGS337yHhxbgrmLoRtpHtr2A94,5026 +libpasteurize/fixes/fix_imports2.py,sha256=bs2V5Yv0v_8xLx-lNj9kNEAK2dLYXUXkZ2hxECg01CU,8580 +libpasteurize/fixes/fix_kwargs.py,sha256=NB_Ap8YJk-9ncoJRbOiPY_VMIigFgVB8m8AuY29DDhE,5991 +libpasteurize/fixes/fix_memoryview.py,sha256=Fwayx_ezpr22tbJ0-QrKdJ-FZTpU-m7y78l1h_N4xxc,551 +libpasteurize/fixes/fix_metaclass.py,sha256=IcE2KjaDG8jUR3FYXECzOC_cr2pr5r95W1NTbMrK8Wc,3260 +libpasteurize/fixes/fix_newstyle.py,sha256=78sazKOHm9DUoMyW4VdvQpMXZhicbXzorVPRhBpSUrM,888 +libpasteurize/fixes/fix_next.py,sha256=VHqcyORRNVqKJ51jJ1OkhwxHuXRgp8qaldyqcMvA4J0,1233 +libpasteurize/fixes/fix_printfunction.py,sha256=NDIfqVmUJBG3H9E6nrnN0cWZK8ch9pL4F-nMexdsa38,401 +libpasteurize/fixes/fix_raise.py,sha256=zQ_AcMsGmCbtKMgrxZGcHLYNscw6tqXFvHQxgqtNbU8,1099 +libpasteurize/fixes/fix_raise_.py,sha256=9STp633frUfYASjYzqhwxx_MXePNmMhfJClowRj8FLY,1225 +libpasteurize/fixes/fix_throw.py,sha256=_ZREVre-WttUvk4sWjrqUNqm9Q1uFaATECN0_-PXKbk,835 +libpasteurize/fixes/fix_unpacking.py,sha256=xZqxMYHgdeuIkermtY-evisvcKlGCPi5vg5t5pt-XCY,6041 +libpasteurize/main.py,sha256=dVHYTQQeJonuOFDNrenJZl-rKHgOQKRMPP1OqnJogWQ,8186 +past/__init__.py,sha256=2DxcQt5zgPH-e-TSDS2l7hI94A9eG7pPgD-V5FgH084,2892 +past/__pycache__/__init__.cpython-310.pyc,, +past/builtins/__init__.py,sha256=7j_4OsUlN6q2eKr14do7mRQ1GwXRoXAMUR0A1fJpAls,1805 +past/builtins/__pycache__/__init__.cpython-310.pyc,, +past/builtins/__pycache__/misc.cpython-310.pyc,, +past/builtins/__pycache__/noniterators.cpython-310.pyc,, +past/builtins/misc.py,sha256=I76Mpx_3wnHpJg7Ub9SZOBRqEFo02YgimZJpfoq17_0,5598 +past/builtins/noniterators.py,sha256=LtdELnd7KyYdXg7GkW25cgkEPUC0ggZ5AYMtDe9N95I,9370 +past/translation/__init__.py,sha256=oTtrOHD8ToM9c9RXat_BhjKhN33N7_Vg4HGS0if-UbU,14914 +past/translation/__pycache__/__init__.cpython-310.pyc,, +past/types/__init__.py,sha256=RyJlgqg9uJ8oF-kJT9QlfhfdmhiMh3fShmtvd2CQycY,879 +past/types/__pycache__/__init__.cpython-310.pyc,, +past/types/__pycache__/basestring.cpython-310.pyc,, +past/types/__pycache__/olddict.cpython-310.pyc,, +past/types/__pycache__/oldstr.cpython-310.pyc,, +past/types/basestring.py,sha256=lO66aHgOV02vka6kosnR6GWK0iNC0G28Nugb1MP69-E,774 +past/types/olddict.py,sha256=0YtffZ55VY6AyQ_rwu4DZ4vcRsp6dz-dQzczeyN8hLk,2721 +past/types/oldstr.py,sha256=JuF8VBBI4OGSgZ3PyhU6LxSAiTfEWzdHUx0Hwg13WSY,4333 +past/utils/__init__.py,sha256=e8l1sOfdiDJ3dkckBWLNWvC1ahC5BX5haHC2TGdNgA8,2633 +past/utils/__pycache__/__init__.cpython-310.pyc,, diff --git a/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/WHEEL b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/WHEEL new file mode 100644 index 0000000..7e68873 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.41.2) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/entry_points.txt b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/entry_points.txt new file mode 100644 index 0000000..74aec27 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +futurize = libfuturize.main:main +pasteurize = libpasteurize.main:main diff --git a/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/top_level.txt b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/top_level.txt new file mode 100644 index 0000000..58f5843 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future-1.0.0.dist-info/top_level.txt @@ -0,0 +1,4 @@ +future +libfuturize +libpasteurize +past diff --git a/venv/lib/python3.10/site-packages/future/__init__.py b/venv/lib/python3.10/site-packages/future/__init__.py new file mode 100644 index 0000000..b097fd8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/__init__.py @@ -0,0 +1,92 @@ +""" +future: Easy, safe support for Python 2/3 compatibility +======================================================= + +``future`` is the missing compatibility layer between Python 2 and Python +3. It allows you to use a single, clean Python 3.x-compatible codebase to +support both Python 2 and Python 3 with minimal overhead. + +It is designed to be used as follows:: + + from __future__ import (absolute_import, division, + print_function, unicode_literals) + from builtins import ( + bytes, dict, int, list, object, range, str, + ascii, chr, hex, input, next, oct, open, + pow, round, super, + filter, map, zip) + +followed by predominantly standard, idiomatic Python 3 code that then runs +similarly on Python 2.6/2.7 and Python 3.3+. + +The imports have no effect on Python 3. On Python 2, they shadow the +corresponding builtins, which normally have different semantics on Python 3 +versus 2, to provide their Python 3 semantics. + + +Standard library reorganization +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +``future`` supports the standard library reorganization (PEP 3108) through the +following Py3 interfaces: + + >>> # Top-level packages with Py3 names provided on Py2: + >>> import html.parser + >>> import queue + >>> import tkinter.dialog + >>> import xmlrpc.client + >>> # etc. + + >>> # Aliases provided for extensions to existing Py2 module names: + >>> from future.standard_library import install_aliases + >>> install_aliases() + + >>> from collections import Counter, OrderedDict # backported to Py2.6 + >>> from collections import UserDict, UserList, UserString + >>> import urllib.request + >>> from itertools import filterfalse, zip_longest + >>> from subprocess import getoutput, getstatusoutput + + +Automatic conversion +-------------------- + +An included script called `futurize +`_ aids in converting +code (from either Python 2 or Python 3) to code compatible with both +platforms. It is similar to ``python-modernize`` but goes further in +providing Python 3 compatibility through the use of the backported types +and builtin functions in ``future``. + + +Documentation +------------- + +See: https://python-future.org + + +Credits +------- + +:Author: Ed Schofield, Jordan M. Adler, et al +:Sponsor: Python Charmers: https://pythoncharmers.com +:Others: See docs/credits.rst or https://python-future.org/credits.html + + +Licensing +--------- +Copyright 2013-2024 Python Charmers, Australia. +The software is distributed under an MIT licence. See LICENSE.txt. + +""" + +__title__ = 'future' +__author__ = 'Ed Schofield' +__license__ = 'MIT' +__copyright__ = 'Copyright 2013-2024 Python Charmers (https://pythoncharmers.com)' +__ver_major__ = 1 +__ver_minor__ = 0 +__ver_patch__ = 0 +__ver_sub__ = '' +__version__ = "%d.%d.%d%s" % (__ver_major__, __ver_minor__, + __ver_patch__, __ver_sub__) diff --git a/venv/lib/python3.10/site-packages/future/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f48579d Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/__init__.py b/venv/lib/python3.10/site-packages/future/backports/__init__.py new file mode 100644 index 0000000..c71e065 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/__init__.py @@ -0,0 +1,26 @@ +""" +future.backports package +""" + +from __future__ import absolute_import + +import sys + +__future_module__ = True +from future.standard_library import import_top_level_modules + + +if sys.version_info[0] >= 3: + import_top_level_modules() + + +from .misc import (ceil, + OrderedDict, + Counter, + ChainMap, + check_output, + count, + recursive_repr, + _count_elements, + cmp_to_key + ) diff --git a/venv/lib/python3.10/site-packages/future/backports/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..9a2e516 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/__pycache__/_markupbase.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/__pycache__/_markupbase.cpython-310.pyc new file mode 100644 index 0000000..019cb90 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/__pycache__/_markupbase.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/__pycache__/datetime.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/__pycache__/datetime.cpython-310.pyc new file mode 100644 index 0000000..df6b9f7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/__pycache__/datetime.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/__pycache__/misc.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/__pycache__/misc.cpython-310.pyc new file mode 100644 index 0000000..d15ed89 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/__pycache__/misc.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/__pycache__/socket.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/__pycache__/socket.cpython-310.pyc new file mode 100644 index 0000000..69f5038 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/__pycache__/socket.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/__pycache__/socketserver.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/__pycache__/socketserver.cpython-310.pyc new file mode 100644 index 0000000..ba4b26b Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/__pycache__/socketserver.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/__pycache__/total_ordering.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/__pycache__/total_ordering.cpython-310.pyc new file mode 100644 index 0000000..7a3fc46 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/__pycache__/total_ordering.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/_markupbase.py b/venv/lib/python3.10/site-packages/future/backports/_markupbase.py new file mode 100644 index 0000000..d51bfc7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/_markupbase.py @@ -0,0 +1,422 @@ +"""Shared support for scanning document type declarations in HTML and XHTML. + +Backported for python-future from Python 3.3. Reason: ParserBase is an +old-style class in the Python 2.7 source of markupbase.py, which I suspect +might be the cause of sporadic unit-test failures on travis-ci.org with +test_htmlparser.py. The test failures look like this: + + ====================================================================== + +ERROR: test_attr_entity_replacement (future.tests.test_htmlparser.AttributesStrictTestCase) + +---------------------------------------------------------------------- + +Traceback (most recent call last): + File "/home/travis/build/edschofield/python-future/future/tests/test_htmlparser.py", line 661, in test_attr_entity_replacement + [("starttag", "a", [("b", "&><\"'")])]) + File "/home/travis/build/edschofield/python-future/future/tests/test_htmlparser.py", line 93, in _run_check + collector = self.get_collector() + File "/home/travis/build/edschofield/python-future/future/tests/test_htmlparser.py", line 617, in get_collector + return EventCollector(strict=True) + File "/home/travis/build/edschofield/python-future/future/tests/test_htmlparser.py", line 27, in __init__ + html.parser.HTMLParser.__init__(self, *args, **kw) + File "/home/travis/build/edschofield/python-future/future/backports/html/parser.py", line 135, in __init__ + self.reset() + File "/home/travis/build/edschofield/python-future/future/backports/html/parser.py", line 143, in reset + _markupbase.ParserBase.reset(self) + +TypeError: unbound method reset() must be called with ParserBase instance as first argument (got EventCollector instance instead) + +This module is used as a foundation for the html.parser module. It has no +documented public API and should not be used directly. + +""" + +import re + +_declname_match = re.compile(r'[a-zA-Z][-_.a-zA-Z0-9]*\s*').match +_declstringlit_match = re.compile(r'(\'[^\']*\'|"[^"]*")\s*').match +_commentclose = re.compile(r'--\s*>') +_markedsectionclose = re.compile(r']\s*]\s*>') + +# An analysis of the MS-Word extensions is available at +# http://www.planetpublish.com/xmlarena/xap/Thursday/WordtoXML.pdf + +_msmarkedsectionclose = re.compile(r']\s*>') + +del re + + +class ParserBase(object): + """Parser base class which provides some common support methods used + by the SGML/HTML and XHTML parsers.""" + + def __init__(self): + if self.__class__ is ParserBase: + raise RuntimeError( + "_markupbase.ParserBase must be subclassed") + + def error(self, message): + raise NotImplementedError( + "subclasses of ParserBase must override error()") + + def reset(self): + self.lineno = 1 + self.offset = 0 + + def getpos(self): + """Return current line number and offset.""" + return self.lineno, self.offset + + # Internal -- update line number and offset. This should be + # called for each piece of data exactly once, in order -- in other + # words the concatenation of all the input strings to this + # function should be exactly the entire input. + def updatepos(self, i, j): + if i >= j: + return j + rawdata = self.rawdata + nlines = rawdata.count("\n", i, j) + if nlines: + self.lineno = self.lineno + nlines + pos = rawdata.rindex("\n", i, j) # Should not fail + self.offset = j-(pos+1) + else: + self.offset = self.offset + j-i + return j + + _decl_otherchars = '' + + # Internal -- parse declaration (for use by subclasses). + def parse_declaration(self, i): + # This is some sort of declaration; in "HTML as + # deployed," this should only be the document type + # declaration (""). + # ISO 8879:1986, however, has more complex + # declaration syntax for elements in , including: + # --comment-- + # [marked section] + # name in the following list: ENTITY, DOCTYPE, ELEMENT, + # ATTLIST, NOTATION, SHORTREF, USEMAP, + # LINKTYPE, LINK, IDLINK, USELINK, SYSTEM + rawdata = self.rawdata + j = i + 2 + assert rawdata[i:j] == "": + # the empty comment + return j + 1 + if rawdata[j:j+1] in ("-", ""): + # Start of comment followed by buffer boundary, + # or just a buffer boundary. + return -1 + # A simple, practical version could look like: ((name|stringlit) S*) + '>' + n = len(rawdata) + if rawdata[j:j+2] == '--': #comment + # Locate --.*-- as the body of the comment + return self.parse_comment(i) + elif rawdata[j] == '[': #marked section + # Locate [statusWord [...arbitrary SGML...]] as the body of the marked section + # Where statusWord is one of TEMP, CDATA, IGNORE, INCLUDE, RCDATA + # Note that this is extended by Microsoft Office "Save as Web" function + # to include [if...] and [endif]. + return self.parse_marked_section(i) + else: #all other declaration elements + decltype, j = self._scan_name(j, i) + if j < 0: + return j + if decltype == "doctype": + self._decl_otherchars = '' + while j < n: + c = rawdata[j] + if c == ">": + # end of declaration syntax + data = rawdata[i+2:j] + if decltype == "doctype": + self.handle_decl(data) + else: + # According to the HTML5 specs sections "8.2.4.44 Bogus + # comment state" and "8.2.4.45 Markup declaration open + # state", a comment token should be emitted. + # Calling unknown_decl provides more flexibility though. + self.unknown_decl(data) + return j + 1 + if c in "\"'": + m = _declstringlit_match(rawdata, j) + if not m: + return -1 # incomplete + j = m.end() + elif c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ": + name, j = self._scan_name(j, i) + elif c in self._decl_otherchars: + j = j + 1 + elif c == "[": + # this could be handled in a separate doctype parser + if decltype == "doctype": + j = self._parse_doctype_subset(j + 1, i) + elif decltype in set(["attlist", "linktype", "link", "element"]): + # must tolerate []'d groups in a content model in an element declaration + # also in data attribute specifications of attlist declaration + # also link type declaration subsets in linktype declarations + # also link attribute specification lists in link declarations + self.error("unsupported '[' char in %s declaration" % decltype) + else: + self.error("unexpected '[' char in declaration") + else: + self.error( + "unexpected %r char in declaration" % rawdata[j]) + if j < 0: + return j + return -1 # incomplete + + # Internal -- parse a marked section + # Override this to handle MS-word extension syntax content + def parse_marked_section(self, i, report=1): + rawdata= self.rawdata + assert rawdata[i:i+3] == ' ending + match= _markedsectionclose.search(rawdata, i+3) + elif sectName in set(["if", "else", "endif"]): + # look for MS Office ]> ending + match= _msmarkedsectionclose.search(rawdata, i+3) + else: + self.error('unknown status keyword %r in marked section' % rawdata[i+3:j]) + if not match: + return -1 + if report: + j = match.start(0) + self.unknown_decl(rawdata[i+3: j]) + return match.end(0) + + # Internal -- parse comment, return length or -1 if not terminated + def parse_comment(self, i, report=1): + rawdata = self.rawdata + if rawdata[i:i+4] != ' delimiter transport-padding + # --> CRLF body-part + for body_part in msgtexts: + # delimiter transport-padding CRLF + self.write(self._NL + '--' + boundary + self._NL) + # body-part + self._fp.write(body_part) + # close-delimiter transport-padding + self.write(self._NL + '--' + boundary + '--') + if msg.epilogue is not None: + self.write(self._NL) + if self._mangle_from_: + epilogue = fcre.sub('>From ', msg.epilogue) + else: + epilogue = msg.epilogue + self._write_lines(epilogue) + + def _handle_multipart_signed(self, msg): + # The contents of signed parts has to stay unmodified in order to keep + # the signature intact per RFC1847 2.1, so we disable header wrapping. + # RDM: This isn't enough to completely preserve the part, but it helps. + p = self.policy + self.policy = p.clone(max_line_length=0) + try: + self._handle_multipart(msg) + finally: + self.policy = p + + def _handle_message_delivery_status(self, msg): + # We can't just write the headers directly to self's file object + # because this will leave an extra newline between the last header + # block and the boundary. Sigh. + blocks = [] + for part in msg.get_payload(): + s = self._new_buffer() + g = self.clone(s) + g.flatten(part, unixfrom=False, linesep=self._NL) + text = s.getvalue() + lines = text.split(self._encoded_NL) + # Strip off the unnecessary trailing empty line + if lines and lines[-1] == self._encoded_EMPTY: + blocks.append(self._encoded_NL.join(lines[:-1])) + else: + blocks.append(text) + # Now join all the blocks with an empty line. This has the lovely + # effect of separating each block with an empty line, but not adding + # an extra one after the last one. + self._fp.write(self._encoded_NL.join(blocks)) + + def _handle_message(self, msg): + s = self._new_buffer() + g = self.clone(s) + # The payload of a message/rfc822 part should be a multipart sequence + # of length 1. The zeroth element of the list should be the Message + # object for the subpart. Extract that object, stringify it, and + # write it out. + # Except, it turns out, when it's a string instead, which happens when + # and only when HeaderParser is used on a message of mime type + # message/rfc822. Such messages are generated by, for example, + # Groupwise when forwarding unadorned messages. (Issue 7970.) So + # in that case we just emit the string body. + payload = msg._payload + if isinstance(payload, list): + g.flatten(msg.get_payload(0), unixfrom=False, linesep=self._NL) + payload = s.getvalue() + else: + payload = self._encode(payload) + self._fp.write(payload) + + # This used to be a module level function; we use a classmethod for this + # and _compile_re so we can continue to provide the module level function + # for backward compatibility by doing + # _make_boudary = Generator._make_boundary + # at the end of the module. It *is* internal, so we could drop that... + @classmethod + def _make_boundary(cls, text=None): + # Craft a random boundary. If text is given, ensure that the chosen + # boundary doesn't appear in the text. + token = random.randrange(sys.maxsize) + boundary = ('=' * 15) + (_fmt % token) + '==' + if text is None: + return boundary + b = boundary + counter = 0 + while True: + cre = cls._compile_re('^--' + re.escape(b) + '(--)?$', re.MULTILINE) + if not cre.search(text): + break + b = boundary + '.' + str(counter) + counter += 1 + return b + + @classmethod + def _compile_re(cls, s, flags): + return re.compile(s, flags) + +class BytesGenerator(Generator): + """Generates a bytes version of a Message object tree. + + Functionally identical to the base Generator except that the output is + bytes and not string. When surrogates were used in the input to encode + bytes, these are decoded back to bytes for output. If the policy has + cte_type set to 7bit, then the message is transformed such that the + non-ASCII bytes are properly content transfer encoded, using the charset + unknown-8bit. + + The outfp object must accept bytes in its write method. + """ + + # Bytes versions of this constant for use in manipulating data from + # the BytesIO buffer. + _encoded_EMPTY = b'' + + def write(self, s): + self._fp.write(str(s).encode('ascii', 'surrogateescape')) + + def _new_buffer(self): + return BytesIO() + + def _encode(self, s): + return s.encode('ascii') + + def _write_headers(self, msg): + # This is almost the same as the string version, except for handling + # strings with 8bit bytes. + for h, v in msg.raw_items(): + self._fp.write(self.policy.fold_binary(h, v)) + # A blank line always separates headers from body + self.write(self._NL) + + def _handle_text(self, msg): + # If the string has surrogates the original source was bytes, so + # just write it back out. + if msg._payload is None: + return + if _has_surrogates(msg._payload) and not self.policy.cte_type=='7bit': + if self._mangle_from_: + msg._payload = fcre.sub(">From ", msg._payload) + self._write_lines(msg._payload) + else: + super(BytesGenerator,self)._handle_text(msg) + + # Default body handler + _writeBody = _handle_text + + @classmethod + def _compile_re(cls, s, flags): + return re.compile(s.encode('ascii'), flags) + + +_FMT = '[Non-text (%(type)s) part of message omitted, filename %(filename)s]' + +class DecodedGenerator(Generator): + """Generates a text representation of a message. + + Like the Generator base class, except that non-text parts are substituted + with a format string representing the part. + """ + def __init__(self, outfp, mangle_from_=True, maxheaderlen=78, fmt=None): + """Like Generator.__init__() except that an additional optional + argument is allowed. + + Walks through all subparts of a message. If the subpart is of main + type `text', then it prints the decoded payload of the subpart. + + Otherwise, fmt is a format string that is used instead of the message + payload. fmt is expanded with the following keywords (in + %(keyword)s format): + + type : Full MIME type of the non-text part + maintype : Main MIME type of the non-text part + subtype : Sub-MIME type of the non-text part + filename : Filename of the non-text part + description: Description associated with the non-text part + encoding : Content transfer encoding of the non-text part + + The default value for fmt is None, meaning + + [Non-text (%(type)s) part of message omitted, filename %(filename)s] + """ + Generator.__init__(self, outfp, mangle_from_, maxheaderlen) + if fmt is None: + self._fmt = _FMT + else: + self._fmt = fmt + + def _dispatch(self, msg): + for part in msg.walk(): + maintype = part.get_content_maintype() + if maintype == 'text': + print(part.get_payload(decode=False), file=self) + elif maintype == 'multipart': + # Just skip this + pass + else: + print(self._fmt % { + 'type' : part.get_content_type(), + 'maintype' : part.get_content_maintype(), + 'subtype' : part.get_content_subtype(), + 'filename' : part.get_filename('[no filename]'), + 'description': part.get('Content-Description', + '[no description]'), + 'encoding' : part.get('Content-Transfer-Encoding', + '[no encoding]'), + }, file=self) + + +# Helper used by Generator._make_boundary +_width = len(repr(sys.maxsize-1)) +_fmt = '%%0%dd' % _width + +# Backward compatibility +_make_boundary = Generator._make_boundary diff --git a/venv/lib/python3.10/site-packages/future/backports/email/header.py b/venv/lib/python3.10/site-packages/future/backports/email/header.py new file mode 100644 index 0000000..63bf038 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/header.py @@ -0,0 +1,581 @@ +# Copyright (C) 2002-2007 Python Software Foundation +# Author: Ben Gertzfield, Barry Warsaw +# Contact: email-sig@python.org + +"""Header encoding and decoding functionality.""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import +from future.builtins import bytes, range, str, super, zip + +__all__ = [ + 'Header', + 'decode_header', + 'make_header', + ] + +import re +import binascii + +from future.backports import email +from future.backports.email import base64mime +from future.backports.email.errors import HeaderParseError +import future.backports.email.charset as _charset + +# Helpers +from future.backports.email.quoprimime import _max_append, header_decode + +Charset = _charset.Charset + +NL = '\n' +SPACE = ' ' +BSPACE = b' ' +SPACE8 = ' ' * 8 +EMPTYSTRING = '' +MAXLINELEN = 78 +FWS = ' \t' + +USASCII = Charset('us-ascii') +UTF8 = Charset('utf-8') + +# Match encoded-word strings in the form =?charset?q?Hello_World?= +ecre = re.compile(r''' + =\? # literal =? + (?P[^?]*?) # non-greedy up to the next ? is the charset + \? # literal ? + (?P[qb]) # either a "q" or a "b", case insensitive + \? # literal ? + (?P.*?) # non-greedy up to the next ?= is the encoded string + \?= # literal ?= + ''', re.VERBOSE | re.IGNORECASE | re.MULTILINE) + +# Field name regexp, including trailing colon, but not separating whitespace, +# according to RFC 2822. Character range is from tilde to exclamation mark. +# For use with .match() +fcre = re.compile(r'[\041-\176]+:$') + +# Find a header embedded in a putative header value. Used to check for +# header injection attack. +_embeded_header = re.compile(r'\n[^ \t]+:') + + +def decode_header(header): + """Decode a message header value without converting charset. + + Returns a list of (string, charset) pairs containing each of the decoded + parts of the header. Charset is None for non-encoded parts of the header, + otherwise a lower-case string containing the name of the character set + specified in the encoded string. + + header may be a string that may or may not contain RFC2047 encoded words, + or it may be a Header object. + + An email.errors.HeaderParseError may be raised when certain decoding error + occurs (e.g. a base64 decoding exception). + """ + # If it is a Header object, we can just return the encoded chunks. + if hasattr(header, '_chunks'): + return [(_charset._encode(string, str(charset)), str(charset)) + for string, charset in header._chunks] + # If no encoding, just return the header with no charset. + if not ecre.search(header): + return [(header, None)] + # First step is to parse all the encoded parts into triplets of the form + # (encoded_string, encoding, charset). For unencoded strings, the last + # two parts will be None. + words = [] + for line in header.splitlines(): + parts = ecre.split(line) + first = True + while parts: + unencoded = parts.pop(0) + if first: + unencoded = unencoded.lstrip() + first = False + if unencoded: + words.append((unencoded, None, None)) + if parts: + charset = parts.pop(0).lower() + encoding = parts.pop(0).lower() + encoded = parts.pop(0) + words.append((encoded, encoding, charset)) + # Now loop over words and remove words that consist of whitespace + # between two encoded strings. + import sys + droplist = [] + for n, w in enumerate(words): + if n>1 and w[1] and words[n-2][1] and words[n-1][0].isspace(): + droplist.append(n-1) + for d in reversed(droplist): + del words[d] + + # The next step is to decode each encoded word by applying the reverse + # base64 or quopri transformation. decoded_words is now a list of the + # form (decoded_word, charset). + decoded_words = [] + for encoded_string, encoding, charset in words: + if encoding is None: + # This is an unencoded word. + decoded_words.append((encoded_string, charset)) + elif encoding == 'q': + word = header_decode(encoded_string) + decoded_words.append((word, charset)) + elif encoding == 'b': + paderr = len(encoded_string) % 4 # Postel's law: add missing padding + if paderr: + encoded_string += '==='[:4 - paderr] + try: + word = base64mime.decode(encoded_string) + except binascii.Error: + raise HeaderParseError('Base64 decoding error') + else: + decoded_words.append((word, charset)) + else: + raise AssertionError('Unexpected encoding: ' + encoding) + # Now convert all words to bytes and collapse consecutive runs of + # similarly encoded words. + collapsed = [] + last_word = last_charset = None + for word, charset in decoded_words: + if isinstance(word, str): + word = bytes(word, 'raw-unicode-escape') + if last_word is None: + last_word = word + last_charset = charset + elif charset != last_charset: + collapsed.append((last_word, last_charset)) + last_word = word + last_charset = charset + elif last_charset is None: + last_word += BSPACE + word + else: + last_word += word + collapsed.append((last_word, last_charset)) + return collapsed + + +def make_header(decoded_seq, maxlinelen=None, header_name=None, + continuation_ws=' '): + """Create a Header from a sequence of pairs as returned by decode_header() + + decode_header() takes a header value string and returns a sequence of + pairs of the format (decoded_string, charset) where charset is the string + name of the character set. + + This function takes one of those sequence of pairs and returns a Header + instance. Optional maxlinelen, header_name, and continuation_ws are as in + the Header constructor. + """ + h = Header(maxlinelen=maxlinelen, header_name=header_name, + continuation_ws=continuation_ws) + for s, charset in decoded_seq: + # None means us-ascii but we can simply pass it on to h.append() + if charset is not None and not isinstance(charset, Charset): + charset = Charset(charset) + h.append(s, charset) + return h + + +class Header(object): + def __init__(self, s=None, charset=None, + maxlinelen=None, header_name=None, + continuation_ws=' ', errors='strict'): + """Create a MIME-compliant header that can contain many character sets. + + Optional s is the initial header value. If None, the initial header + value is not set. You can later append to the header with .append() + method calls. s may be a byte string or a Unicode string, but see the + .append() documentation for semantics. + + Optional charset serves two purposes: it has the same meaning as the + charset argument to the .append() method. It also sets the default + character set for all subsequent .append() calls that omit the charset + argument. If charset is not provided in the constructor, the us-ascii + charset is used both as s's initial charset and as the default for + subsequent .append() calls. + + The maximum line length can be specified explicitly via maxlinelen. For + splitting the first line to a shorter value (to account for the field + header which isn't included in s, e.g. `Subject') pass in the name of + the field in header_name. The default maxlinelen is 78 as recommended + by RFC 2822. + + continuation_ws must be RFC 2822 compliant folding whitespace (usually + either a space or a hard tab) which will be prepended to continuation + lines. + + errors is passed through to the .append() call. + """ + if charset is None: + charset = USASCII + elif not isinstance(charset, Charset): + charset = Charset(charset) + self._charset = charset + self._continuation_ws = continuation_ws + self._chunks = [] + if s is not None: + self.append(s, charset, errors) + if maxlinelen is None: + maxlinelen = MAXLINELEN + self._maxlinelen = maxlinelen + if header_name is None: + self._headerlen = 0 + else: + # Take the separating colon and space into account. + self._headerlen = len(header_name) + 2 + + def __str__(self): + """Return the string value of the header.""" + self._normalize() + uchunks = [] + lastcs = None + lastspace = None + for string, charset in self._chunks: + # We must preserve spaces between encoded and non-encoded word + # boundaries, which means for us we need to add a space when we go + # from a charset to None/us-ascii, or from None/us-ascii to a + # charset. Only do this for the second and subsequent chunks. + # Don't add a space if the None/us-ascii string already has + # a space (trailing or leading depending on transition) + nextcs = charset + if nextcs == _charset.UNKNOWN8BIT: + original_bytes = string.encode('ascii', 'surrogateescape') + string = original_bytes.decode('ascii', 'replace') + if uchunks: + hasspace = string and self._nonctext(string[0]) + if lastcs not in (None, 'us-ascii'): + if nextcs in (None, 'us-ascii') and not hasspace: + uchunks.append(SPACE) + nextcs = None + elif nextcs not in (None, 'us-ascii') and not lastspace: + uchunks.append(SPACE) + lastspace = string and self._nonctext(string[-1]) + lastcs = nextcs + uchunks.append(string) + return EMPTYSTRING.join(uchunks) + + # Rich comparison operators for equality only. BAW: does it make sense to + # have or explicitly disable <, <=, >, >= operators? + def __eq__(self, other): + # other may be a Header or a string. Both are fine so coerce + # ourselves to a unicode (of the unencoded header value), swap the + # args and do another comparison. + return other == str(self) + + def __ne__(self, other): + return not self == other + + def append(self, s, charset=None, errors='strict'): + """Append a string to the MIME header. + + Optional charset, if given, should be a Charset instance or the name + of a character set (which will be converted to a Charset instance). A + value of None (the default) means that the charset given in the + constructor is used. + + s may be a byte string or a Unicode string. If it is a byte string + (i.e. isinstance(s, str) is false), then charset is the encoding of + that byte string, and a UnicodeError will be raised if the string + cannot be decoded with that charset. If s is a Unicode string, then + charset is a hint specifying the character set of the characters in + the string. In either case, when producing an RFC 2822 compliant + header using RFC 2047 rules, the string will be encoded using the + output codec of the charset. If the string cannot be encoded to the + output codec, a UnicodeError will be raised. + + Optional `errors' is passed as the errors argument to the decode + call if s is a byte string. + """ + if charset is None: + charset = self._charset + elif not isinstance(charset, Charset): + charset = Charset(charset) + if not isinstance(s, str): + input_charset = charset.input_codec or 'us-ascii' + if input_charset == _charset.UNKNOWN8BIT: + s = s.decode('us-ascii', 'surrogateescape') + else: + s = s.decode(input_charset, errors) + # Ensure that the bytes we're storing can be decoded to the output + # character set, otherwise an early error is raised. + output_charset = charset.output_codec or 'us-ascii' + if output_charset != _charset.UNKNOWN8BIT: + try: + s.encode(output_charset, errors) + except UnicodeEncodeError: + if output_charset!='us-ascii': + raise + charset = UTF8 + self._chunks.append((s, charset)) + + def _nonctext(self, s): + """True if string s is not a ctext character of RFC822. + """ + return s.isspace() or s in ('(', ')', '\\') + + def encode(self, splitchars=';, \t', maxlinelen=None, linesep='\n'): + r"""Encode a message header into an RFC-compliant format. + + There are many issues involved in converting a given string for use in + an email header. Only certain character sets are readable in most + email clients, and as header strings can only contain a subset of + 7-bit ASCII, care must be taken to properly convert and encode (with + Base64 or quoted-printable) header strings. In addition, there is a + 75-character length limit on any given encoded header field, so + line-wrapping must be performed, even with double-byte character sets. + + Optional maxlinelen specifies the maximum length of each generated + line, exclusive of the linesep string. Individual lines may be longer + than maxlinelen if a folding point cannot be found. The first line + will be shorter by the length of the header name plus ": " if a header + name was specified at Header construction time. The default value for + maxlinelen is determined at header construction time. + + Optional splitchars is a string containing characters which should be + given extra weight by the splitting algorithm during normal header + wrapping. This is in very rough support of RFC 2822's `higher level + syntactic breaks': split points preceded by a splitchar are preferred + during line splitting, with the characters preferred in the order in + which they appear in the string. Space and tab may be included in the + string to indicate whether preference should be given to one over the + other as a split point when other split chars do not appear in the line + being split. Splitchars does not affect RFC 2047 encoded lines. + + Optional linesep is a string to be used to separate the lines of + the value. The default value is the most useful for typical + Python applications, but it can be set to \r\n to produce RFC-compliant + line separators when needed. + """ + self._normalize() + if maxlinelen is None: + maxlinelen = self._maxlinelen + # A maxlinelen of 0 means don't wrap. For all practical purposes, + # choosing a huge number here accomplishes that and makes the + # _ValueFormatter algorithm much simpler. + if maxlinelen == 0: + maxlinelen = 1000000 + formatter = _ValueFormatter(self._headerlen, maxlinelen, + self._continuation_ws, splitchars) + lastcs = None + hasspace = lastspace = None + for string, charset in self._chunks: + if hasspace is not None: + hasspace = string and self._nonctext(string[0]) + import sys + if lastcs not in (None, 'us-ascii'): + if not hasspace or charset not in (None, 'us-ascii'): + formatter.add_transition() + elif charset not in (None, 'us-ascii') and not lastspace: + formatter.add_transition() + lastspace = string and self._nonctext(string[-1]) + lastcs = charset + hasspace = False + lines = string.splitlines() + if lines: + formatter.feed('', lines[0], charset) + else: + formatter.feed('', '', charset) + for line in lines[1:]: + formatter.newline() + if charset.header_encoding is not None: + formatter.feed(self._continuation_ws, ' ' + line.lstrip(), + charset) + else: + sline = line.lstrip() + fws = line[:len(line)-len(sline)] + formatter.feed(fws, sline, charset) + if len(lines) > 1: + formatter.newline() + if self._chunks: + formatter.add_transition() + value = formatter._str(linesep) + if _embeded_header.search(value): + raise HeaderParseError("header value appears to contain " + "an embedded header: {!r}".format(value)) + return value + + def _normalize(self): + # Step 1: Normalize the chunks so that all runs of identical charsets + # get collapsed into a single unicode string. + chunks = [] + last_charset = None + last_chunk = [] + for string, charset in self._chunks: + if charset == last_charset: + last_chunk.append(string) + else: + if last_charset is not None: + chunks.append((SPACE.join(last_chunk), last_charset)) + last_chunk = [string] + last_charset = charset + if last_chunk: + chunks.append((SPACE.join(last_chunk), last_charset)) + self._chunks = chunks + + +class _ValueFormatter(object): + def __init__(self, headerlen, maxlen, continuation_ws, splitchars): + self._maxlen = maxlen + self._continuation_ws = continuation_ws + self._continuation_ws_len = len(continuation_ws) + self._splitchars = splitchars + self._lines = [] + self._current_line = _Accumulator(headerlen) + + def _str(self, linesep): + self.newline() + return linesep.join(self._lines) + + def __str__(self): + return self._str(NL) + + def newline(self): + end_of_line = self._current_line.pop() + if end_of_line != (' ', ''): + self._current_line.push(*end_of_line) + if len(self._current_line) > 0: + if self._current_line.is_onlyws(): + self._lines[-1] += str(self._current_line) + else: + self._lines.append(str(self._current_line)) + self._current_line.reset() + + def add_transition(self): + self._current_line.push(' ', '') + + def feed(self, fws, string, charset): + # If the charset has no header encoding (i.e. it is an ASCII encoding) + # then we must split the header at the "highest level syntactic break" + # possible. Note that we don't have a lot of smarts about field + # syntax; we just try to break on semi-colons, then commas, then + # whitespace. Eventually, this should be pluggable. + if charset.header_encoding is None: + self._ascii_split(fws, string, self._splitchars) + return + # Otherwise, we're doing either a Base64 or a quoted-printable + # encoding which means we don't need to split the line on syntactic + # breaks. We can basically just find enough characters to fit on the + # current line, minus the RFC 2047 chrome. What makes this trickier + # though is that we have to split at octet boundaries, not character + # boundaries but it's only safe to split at character boundaries so at + # best we can only get close. + encoded_lines = charset.header_encode_lines(string, self._maxlengths()) + # The first element extends the current line, but if it's None then + # nothing more fit on the current line so start a new line. + try: + first_line = encoded_lines.pop(0) + except IndexError: + # There are no encoded lines, so we're done. + return + if first_line is not None: + self._append_chunk(fws, first_line) + try: + last_line = encoded_lines.pop() + except IndexError: + # There was only one line. + return + self.newline() + self._current_line.push(self._continuation_ws, last_line) + # Everything else are full lines in themselves. + for line in encoded_lines: + self._lines.append(self._continuation_ws + line) + + def _maxlengths(self): + # The first line's length. + yield self._maxlen - len(self._current_line) + while True: + yield self._maxlen - self._continuation_ws_len + + def _ascii_split(self, fws, string, splitchars): + # The RFC 2822 header folding algorithm is simple in principle but + # complex in practice. Lines may be folded any place where "folding + # white space" appears by inserting a linesep character in front of the + # FWS. The complication is that not all spaces or tabs qualify as FWS, + # and we are also supposed to prefer to break at "higher level + # syntactic breaks". We can't do either of these without intimate + # knowledge of the structure of structured headers, which we don't have + # here. So the best we can do here is prefer to break at the specified + # splitchars, and hope that we don't choose any spaces or tabs that + # aren't legal FWS. (This is at least better than the old algorithm, + # where we would sometimes *introduce* FWS after a splitchar, or the + # algorithm before that, where we would turn all white space runs into + # single spaces or tabs.) + parts = re.split("(["+FWS+"]+)", fws+string) + if parts[0]: + parts[:0] = [''] + else: + parts.pop(0) + for fws, part in zip(*[iter(parts)]*2): + self._append_chunk(fws, part) + + def _append_chunk(self, fws, string): + self._current_line.push(fws, string) + if len(self._current_line) > self._maxlen: + # Find the best split point, working backward from the end. + # There might be none, on a long first line. + for ch in self._splitchars: + for i in range(self._current_line.part_count()-1, 0, -1): + if ch.isspace(): + fws = self._current_line[i][0] + if fws and fws[0]==ch: + break + prevpart = self._current_line[i-1][1] + if prevpart and prevpart[-1]==ch: + break + else: + continue + break + else: + fws, part = self._current_line.pop() + if self._current_line._initial_size > 0: + # There will be a header, so leave it on a line by itself. + self.newline() + if not fws: + # We don't use continuation_ws here because the whitespace + # after a header should always be a space. + fws = ' ' + self._current_line.push(fws, part) + return + remainder = self._current_line.pop_from(i) + self._lines.append(str(self._current_line)) + self._current_line.reset(remainder) + + +class _Accumulator(list): + + def __init__(self, initial_size=0): + self._initial_size = initial_size + super().__init__() + + def push(self, fws, string): + self.append((fws, string)) + + def pop_from(self, i=0): + popped = self[i:] + self[i:] = [] + return popped + + def pop(self): + if self.part_count()==0: + return ('', '') + return super().pop() + + def __len__(self): + return sum((len(fws)+len(part) for fws, part in self), + self._initial_size) + + def __str__(self): + return EMPTYSTRING.join((EMPTYSTRING.join((fws, part)) + for fws, part in self)) + + def reset(self, startval=None): + if startval is None: + startval = [] + self[:] = startval + self._initial_size = 0 + + def is_onlyws(self): + return self._initial_size==0 and (not self or str(self).isspace()) + + def part_count(self): + return super().__len__() diff --git a/venv/lib/python3.10/site-packages/future/backports/email/headerregistry.py b/venv/lib/python3.10/site-packages/future/backports/email/headerregistry.py new file mode 100644 index 0000000..9aaad65 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/headerregistry.py @@ -0,0 +1,592 @@ +"""Representing and manipulating email headers via custom objects. + +This module provides an implementation of the HeaderRegistry API. +The implementation is designed to flexibly follow RFC5322 rules. + +Eventually HeaderRegistry will be a public API, but it isn't yet, +and will probably change some before that happens. + +""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +from future.builtins import super +from future.builtins import str +from future.utils import text_to_native_str +from future.backports.email import utils +from future.backports.email import errors +from future.backports.email import _header_value_parser as parser + +class Address(object): + + def __init__(self, display_name='', username='', domain='', addr_spec=None): + """Create an object represeting a full email address. + + An address can have a 'display_name', a 'username', and a 'domain'. In + addition to specifying the username and domain separately, they may be + specified together by using the addr_spec keyword *instead of* the + username and domain keywords. If an addr_spec string is specified it + must be properly quoted according to RFC 5322 rules; an error will be + raised if it is not. + + An Address object has display_name, username, domain, and addr_spec + attributes, all of which are read-only. The addr_spec and the string + value of the object are both quoted according to RFC5322 rules, but + without any Content Transfer Encoding. + + """ + # This clause with its potential 'raise' may only happen when an + # application program creates an Address object using an addr_spec + # keyword. The email library code itself must always supply username + # and domain. + if addr_spec is not None: + if username or domain: + raise TypeError("addrspec specified when username and/or " + "domain also specified") + a_s, rest = parser.get_addr_spec(addr_spec) + if rest: + raise ValueError("Invalid addr_spec; only '{}' " + "could be parsed from '{}'".format( + a_s, addr_spec)) + if a_s.all_defects: + raise a_s.all_defects[0] + username = a_s.local_part + domain = a_s.domain + self._display_name = display_name + self._username = username + self._domain = domain + + @property + def display_name(self): + return self._display_name + + @property + def username(self): + return self._username + + @property + def domain(self): + return self._domain + + @property + def addr_spec(self): + """The addr_spec (username@domain) portion of the address, quoted + according to RFC 5322 rules, but with no Content Transfer Encoding. + """ + nameset = set(self.username) + if len(nameset) > len(nameset-parser.DOT_ATOM_ENDS): + lp = parser.quote_string(self.username) + else: + lp = self.username + if self.domain: + return lp + '@' + self.domain + if not lp: + return '<>' + return lp + + def __repr__(self): + return "Address(display_name={!r}, username={!r}, domain={!r})".format( + self.display_name, self.username, self.domain) + + def __str__(self): + nameset = set(self.display_name) + if len(nameset) > len(nameset-parser.SPECIALS): + disp = parser.quote_string(self.display_name) + else: + disp = self.display_name + if disp: + addr_spec = '' if self.addr_spec=='<>' else self.addr_spec + return "{} <{}>".format(disp, addr_spec) + return self.addr_spec + + def __eq__(self, other): + if type(other) != type(self): + return False + return (self.display_name == other.display_name and + self.username == other.username and + self.domain == other.domain) + + +class Group(object): + + def __init__(self, display_name=None, addresses=None): + """Create an object representing an address group. + + An address group consists of a display_name followed by colon and an + list of addresses (see Address) terminated by a semi-colon. The Group + is created by specifying a display_name and a possibly empty list of + Address objects. A Group can also be used to represent a single + address that is not in a group, which is convenient when manipulating + lists that are a combination of Groups and individual Addresses. In + this case the display_name should be set to None. In particular, the + string representation of a Group whose display_name is None is the same + as the Address object, if there is one and only one Address object in + the addresses list. + + """ + self._display_name = display_name + self._addresses = tuple(addresses) if addresses else tuple() + + @property + def display_name(self): + return self._display_name + + @property + def addresses(self): + return self._addresses + + def __repr__(self): + return "Group(display_name={!r}, addresses={!r}".format( + self.display_name, self.addresses) + + def __str__(self): + if self.display_name is None and len(self.addresses)==1: + return str(self.addresses[0]) + disp = self.display_name + if disp is not None: + nameset = set(disp) + if len(nameset) > len(nameset-parser.SPECIALS): + disp = parser.quote_string(disp) + adrstr = ", ".join(str(x) for x in self.addresses) + adrstr = ' ' + adrstr if adrstr else adrstr + return "{}:{};".format(disp, adrstr) + + def __eq__(self, other): + if type(other) != type(self): + return False + return (self.display_name == other.display_name and + self.addresses == other.addresses) + + +# Header Classes # + +class BaseHeader(str): + + """Base class for message headers. + + Implements generic behavior and provides tools for subclasses. + + A subclass must define a classmethod named 'parse' that takes an unfolded + value string and a dictionary as its arguments. The dictionary will + contain one key, 'defects', initialized to an empty list. After the call + the dictionary must contain two additional keys: parse_tree, set to the + parse tree obtained from parsing the header, and 'decoded', set to the + string value of the idealized representation of the data from the value. + (That is, encoded words are decoded, and values that have canonical + representations are so represented.) + + The defects key is intended to collect parsing defects, which the message + parser will subsequently dispose of as appropriate. The parser should not, + insofar as practical, raise any errors. Defects should be added to the + list instead. The standard header parsers register defects for RFC + compliance issues, for obsolete RFC syntax, and for unrecoverable parsing + errors. + + The parse method may add additional keys to the dictionary. In this case + the subclass must define an 'init' method, which will be passed the + dictionary as its keyword arguments. The method should use (usually by + setting them as the value of similarly named attributes) and remove all the + extra keys added by its parse method, and then use super to call its parent + class with the remaining arguments and keywords. + + The subclass should also make sure that a 'max_count' attribute is defined + that is either None or 1. XXX: need to better define this API. + + """ + + def __new__(cls, name, value): + kwds = {'defects': []} + cls.parse(value, kwds) + if utils._has_surrogates(kwds['decoded']): + kwds['decoded'] = utils._sanitize(kwds['decoded']) + self = str.__new__(cls, kwds['decoded']) + # del kwds['decoded'] + self.init(name, **kwds) + return self + + def init(self, name, **_3to2kwargs): + defects = _3to2kwargs['defects']; del _3to2kwargs['defects'] + parse_tree = _3to2kwargs['parse_tree']; del _3to2kwargs['parse_tree'] + self._name = name + self._parse_tree = parse_tree + self._defects = defects + + @property + def name(self): + return self._name + + @property + def defects(self): + return tuple(self._defects) + + def __reduce__(self): + return ( + _reconstruct_header, + ( + self.__class__.__name__, + self.__class__.__bases__, + str(self), + ), + self.__dict__) + + @classmethod + def _reconstruct(cls, value): + return str.__new__(cls, value) + + def fold(self, **_3to2kwargs): + policy = _3to2kwargs['policy']; del _3to2kwargs['policy'] + """Fold header according to policy. + + The parsed representation of the header is folded according to + RFC5322 rules, as modified by the policy. If the parse tree + contains surrogateescaped bytes, the bytes are CTE encoded using + the charset 'unknown-8bit". + + Any non-ASCII characters in the parse tree are CTE encoded using + charset utf-8. XXX: make this a policy setting. + + The returned value is an ASCII-only string possibly containing linesep + characters, and ending with a linesep character. The string includes + the header name and the ': ' separator. + + """ + # At some point we need to only put fws here if it was in the source. + header = parser.Header([ + parser.HeaderLabel([ + parser.ValueTerminal(self.name, 'header-name'), + parser.ValueTerminal(':', 'header-sep')]), + parser.CFWSList([parser.WhiteSpaceTerminal(' ', 'fws')]), + self._parse_tree]) + return header.fold(policy=policy) + + +def _reconstruct_header(cls_name, bases, value): + return type(text_to_native_str(cls_name), bases, {})._reconstruct(value) + + +class UnstructuredHeader(object): + + max_count = None + value_parser = staticmethod(parser.get_unstructured) + + @classmethod + def parse(cls, value, kwds): + kwds['parse_tree'] = cls.value_parser(value) + kwds['decoded'] = str(kwds['parse_tree']) + + +class UniqueUnstructuredHeader(UnstructuredHeader): + + max_count = 1 + + +class DateHeader(object): + + """Header whose value consists of a single timestamp. + + Provides an additional attribute, datetime, which is either an aware + datetime using a timezone, or a naive datetime if the timezone + in the input string is -0000. Also accepts a datetime as input. + The 'value' attribute is the normalized form of the timestamp, + which means it is the output of format_datetime on the datetime. + """ + + max_count = None + + # This is used only for folding, not for creating 'decoded'. + value_parser = staticmethod(parser.get_unstructured) + + @classmethod + def parse(cls, value, kwds): + if not value: + kwds['defects'].append(errors.HeaderMissingRequiredValue()) + kwds['datetime'] = None + kwds['decoded'] = '' + kwds['parse_tree'] = parser.TokenList() + return + if isinstance(value, str): + value = utils.parsedate_to_datetime(value) + kwds['datetime'] = value + kwds['decoded'] = utils.format_datetime(kwds['datetime']) + kwds['parse_tree'] = cls.value_parser(kwds['decoded']) + + def init(self, *args, **kw): + self._datetime = kw.pop('datetime') + super().init(*args, **kw) + + @property + def datetime(self): + return self._datetime + + +class UniqueDateHeader(DateHeader): + + max_count = 1 + + +class AddressHeader(object): + + max_count = None + + @staticmethod + def value_parser(value): + address_list, value = parser.get_address_list(value) + assert not value, 'this should not happen' + return address_list + + @classmethod + def parse(cls, value, kwds): + if isinstance(value, str): + # We are translating here from the RFC language (address/mailbox) + # to our API language (group/address). + kwds['parse_tree'] = address_list = cls.value_parser(value) + groups = [] + for addr in address_list.addresses: + groups.append(Group(addr.display_name, + [Address(mb.display_name or '', + mb.local_part or '', + mb.domain or '') + for mb in addr.all_mailboxes])) + defects = list(address_list.all_defects) + else: + # Assume it is Address/Group stuff + if not hasattr(value, '__iter__'): + value = [value] + groups = [Group(None, [item]) if not hasattr(item, 'addresses') + else item + for item in value] + defects = [] + kwds['groups'] = groups + kwds['defects'] = defects + kwds['decoded'] = ', '.join([str(item) for item in groups]) + if 'parse_tree' not in kwds: + kwds['parse_tree'] = cls.value_parser(kwds['decoded']) + + def init(self, *args, **kw): + self._groups = tuple(kw.pop('groups')) + self._addresses = None + super().init(*args, **kw) + + @property + def groups(self): + return self._groups + + @property + def addresses(self): + if self._addresses is None: + self._addresses = tuple([address for group in self._groups + for address in group.addresses]) + return self._addresses + + +class UniqueAddressHeader(AddressHeader): + + max_count = 1 + + +class SingleAddressHeader(AddressHeader): + + @property + def address(self): + if len(self.addresses)!=1: + raise ValueError(("value of single address header {} is not " + "a single address").format(self.name)) + return self.addresses[0] + + +class UniqueSingleAddressHeader(SingleAddressHeader): + + max_count = 1 + + +class MIMEVersionHeader(object): + + max_count = 1 + + value_parser = staticmethod(parser.parse_mime_version) + + @classmethod + def parse(cls, value, kwds): + kwds['parse_tree'] = parse_tree = cls.value_parser(value) + kwds['decoded'] = str(parse_tree) + kwds['defects'].extend(parse_tree.all_defects) + kwds['major'] = None if parse_tree.minor is None else parse_tree.major + kwds['minor'] = parse_tree.minor + if parse_tree.minor is not None: + kwds['version'] = '{}.{}'.format(kwds['major'], kwds['minor']) + else: + kwds['version'] = None + + def init(self, *args, **kw): + self._version = kw.pop('version') + self._major = kw.pop('major') + self._minor = kw.pop('minor') + super().init(*args, **kw) + + @property + def major(self): + return self._major + + @property + def minor(self): + return self._minor + + @property + def version(self): + return self._version + + +class ParameterizedMIMEHeader(object): + + # Mixin that handles the params dict. Must be subclassed and + # a property value_parser for the specific header provided. + + max_count = 1 + + @classmethod + def parse(cls, value, kwds): + kwds['parse_tree'] = parse_tree = cls.value_parser(value) + kwds['decoded'] = str(parse_tree) + kwds['defects'].extend(parse_tree.all_defects) + if parse_tree.params is None: + kwds['params'] = {} + else: + # The MIME RFCs specify that parameter ordering is arbitrary. + kwds['params'] = dict((utils._sanitize(name).lower(), + utils._sanitize(value)) + for name, value in parse_tree.params) + + def init(self, *args, **kw): + self._params = kw.pop('params') + super().init(*args, **kw) + + @property + def params(self): + return self._params.copy() + + +class ContentTypeHeader(ParameterizedMIMEHeader): + + value_parser = staticmethod(parser.parse_content_type_header) + + def init(self, *args, **kw): + super().init(*args, **kw) + self._maintype = utils._sanitize(self._parse_tree.maintype) + self._subtype = utils._sanitize(self._parse_tree.subtype) + + @property + def maintype(self): + return self._maintype + + @property + def subtype(self): + return self._subtype + + @property + def content_type(self): + return self.maintype + '/' + self.subtype + + +class ContentDispositionHeader(ParameterizedMIMEHeader): + + value_parser = staticmethod(parser.parse_content_disposition_header) + + def init(self, *args, **kw): + super().init(*args, **kw) + cd = self._parse_tree.content_disposition + self._content_disposition = cd if cd is None else utils._sanitize(cd) + + @property + def content_disposition(self): + return self._content_disposition + + +class ContentTransferEncodingHeader(object): + + max_count = 1 + + value_parser = staticmethod(parser.parse_content_transfer_encoding_header) + + @classmethod + def parse(cls, value, kwds): + kwds['parse_tree'] = parse_tree = cls.value_parser(value) + kwds['decoded'] = str(parse_tree) + kwds['defects'].extend(parse_tree.all_defects) + + def init(self, *args, **kw): + super().init(*args, **kw) + self._cte = utils._sanitize(self._parse_tree.cte) + + @property + def cte(self): + return self._cte + + +# The header factory # + +_default_header_map = { + 'subject': UniqueUnstructuredHeader, + 'date': UniqueDateHeader, + 'resent-date': DateHeader, + 'orig-date': UniqueDateHeader, + 'sender': UniqueSingleAddressHeader, + 'resent-sender': SingleAddressHeader, + 'to': UniqueAddressHeader, + 'resent-to': AddressHeader, + 'cc': UniqueAddressHeader, + 'resent-cc': AddressHeader, + 'bcc': UniqueAddressHeader, + 'resent-bcc': AddressHeader, + 'from': UniqueAddressHeader, + 'resent-from': AddressHeader, + 'reply-to': UniqueAddressHeader, + 'mime-version': MIMEVersionHeader, + 'content-type': ContentTypeHeader, + 'content-disposition': ContentDispositionHeader, + 'content-transfer-encoding': ContentTransferEncodingHeader, + } + +class HeaderRegistry(object): + + """A header_factory and header registry.""" + + def __init__(self, base_class=BaseHeader, default_class=UnstructuredHeader, + use_default_map=True): + """Create a header_factory that works with the Policy API. + + base_class is the class that will be the last class in the created + header class's __bases__ list. default_class is the class that will be + used if "name" (see __call__) does not appear in the registry. + use_default_map controls whether or not the default mapping of names to + specialized classes is copied in to the registry when the factory is + created. The default is True. + + """ + self.registry = {} + self.base_class = base_class + self.default_class = default_class + if use_default_map: + self.registry.update(_default_header_map) + + def map_to_type(self, name, cls): + """Register cls as the specialized class for handling "name" headers. + + """ + self.registry[name.lower()] = cls + + def __getitem__(self, name): + cls = self.registry.get(name.lower(), self.default_class) + return type(text_to_native_str('_'+cls.__name__), (cls, self.base_class), {}) + + def __call__(self, name, value): + """Create a header instance for header 'name' from 'value'. + + Creates a header instance by creating a specialized class for parsing + and representing the specified header by combining the factory + base_class with a specialized class from the registry or the + default_class, and passing the name and value to the constructed + class's constructor. + + """ + return self[name](name, value) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/iterators.py b/venv/lib/python3.10/site-packages/future/backports/email/iterators.py new file mode 100644 index 0000000..82d320f --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/iterators.py @@ -0,0 +1,74 @@ +# Copyright (C) 2001-2006 Python Software Foundation +# Author: Barry Warsaw +# Contact: email-sig@python.org + +"""Various types of useful iterators and generators.""" +from __future__ import print_function +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +__all__ = [ + 'body_line_iterator', + 'typed_subpart_iterator', + 'walk', + # Do not include _structure() since it's part of the debugging API. + ] + +import sys +from io import StringIO + + +# This function will become a method of the Message class +def walk(self): + """Walk over the message tree, yielding each subpart. + + The walk is performed in depth-first order. This method is a + generator. + """ + yield self + if self.is_multipart(): + for subpart in self.get_payload(): + for subsubpart in subpart.walk(): + yield subsubpart + + +# These two functions are imported into the Iterators.py interface module. +def body_line_iterator(msg, decode=False): + """Iterate over the parts, returning string payloads line-by-line. + + Optional decode (default False) is passed through to .get_payload(). + """ + for subpart in msg.walk(): + payload = subpart.get_payload(decode=decode) + if isinstance(payload, str): + for line in StringIO(payload): + yield line + + +def typed_subpart_iterator(msg, maintype='text', subtype=None): + """Iterate over the subparts with a given MIME type. + + Use `maintype' as the main MIME type to match against; this defaults to + "text". Optional `subtype' is the MIME subtype to match against; if + omitted, only the main type is matched. + """ + for subpart in msg.walk(): + if subpart.get_content_maintype() == maintype: + if subtype is None or subpart.get_content_subtype() == subtype: + yield subpart + + +def _structure(msg, fp=None, level=0, include_default=False): + """A handy debugging aid""" + if fp is None: + fp = sys.stdout + tab = ' ' * (level * 4) + print(tab + msg.get_content_type(), end='', file=fp) + if include_default: + print(' [%s]' % msg.get_default_type(), file=fp) + else: + print(file=fp) + if msg.is_multipart(): + for subpart in msg.get_payload(): + _structure(subpart, fp, level+1, include_default) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/message.py b/venv/lib/python3.10/site-packages/future/backports/email/message.py new file mode 100644 index 0000000..d8d9615 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/message.py @@ -0,0 +1,882 @@ +# -*- coding: utf-8 -*- +# Copyright (C) 2001-2007 Python Software Foundation +# Author: Barry Warsaw +# Contact: email-sig@python.org + +"""Basic message object for the email package object model.""" +from __future__ import absolute_import, division, unicode_literals +from future.builtins import list, range, str, zip + +__all__ = ['Message'] + +import re +import uu +import base64 +import binascii +from io import BytesIO, StringIO + +# Intrapackage imports +from future.utils import as_native_str +from future.backports.email import utils +from future.backports.email import errors +from future.backports.email._policybase import compat32 +from future.backports.email import charset as _charset +from future.backports.email._encoded_words import decode_b +Charset = _charset.Charset + +SEMISPACE = '; ' + +# Regular expression that matches `special' characters in parameters, the +# existence of which force quoting of the parameter value. +tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') + + +def _splitparam(param): + # Split header parameters. BAW: this may be too simple. It isn't + # strictly RFC 2045 (section 5.1) compliant, but it catches most headers + # found in the wild. We may eventually need a full fledged parser. + # RDM: we might have a Header here; for now just stringify it. + a, sep, b = str(param).partition(';') + if not sep: + return a.strip(), None + return a.strip(), b.strip() + +def _formatparam(param, value=None, quote=True): + """Convenience function to format and return a key=value pair. + + This will quote the value if needed or if quote is true. If value is a + three tuple (charset, language, value), it will be encoded according + to RFC2231 rules. If it contains non-ascii characters it will likewise + be encoded according to RFC2231 rules, using the utf-8 charset and + a null language. + """ + if value is not None and len(value) > 0: + # A tuple is used for RFC 2231 encoded parameter values where items + # are (charset, language, value). charset is a string, not a Charset + # instance. RFC 2231 encoded values are never quoted, per RFC. + if isinstance(value, tuple): + # Encode as per RFC 2231 + param += '*' + value = utils.encode_rfc2231(value[2], value[0], value[1]) + return '%s=%s' % (param, value) + else: + try: + value.encode('ascii') + except UnicodeEncodeError: + param += '*' + value = utils.encode_rfc2231(value, 'utf-8', '') + return '%s=%s' % (param, value) + # BAW: Please check this. I think that if quote is set it should + # force quoting even if not necessary. + if quote or tspecials.search(value): + return '%s="%s"' % (param, utils.quote(value)) + else: + return '%s=%s' % (param, value) + else: + return param + +def _parseparam(s): + # RDM This might be a Header, so for now stringify it. + s = ';' + str(s) + plist = [] + while s[:1] == ';': + s = s[1:] + end = s.find(';') + while end > 0 and (s.count('"', 0, end) - s.count('\\"', 0, end)) % 2: + end = s.find(';', end + 1) + if end < 0: + end = len(s) + f = s[:end] + if '=' in f: + i = f.index('=') + f = f[:i].strip().lower() + '=' + f[i+1:].strip() + plist.append(f.strip()) + s = s[end:] + return plist + + +def _unquotevalue(value): + # This is different than utils.collapse_rfc2231_value() because it doesn't + # try to convert the value to a unicode. Message.get_param() and + # Message.get_params() are both currently defined to return the tuple in + # the face of RFC 2231 parameters. + if isinstance(value, tuple): + return value[0], value[1], utils.unquote(value[2]) + else: + return utils.unquote(value) + + +class Message(object): + """Basic message object. + + A message object is defined as something that has a bunch of RFC 2822 + headers and a payload. It may optionally have an envelope header + (a.k.a. Unix-From or From_ header). If the message is a container (i.e. a + multipart or a message/rfc822), then the payload is a list of Message + objects, otherwise it is a string. + + Message objects implement part of the `mapping' interface, which assumes + there is exactly one occurrence of the header per message. Some headers + do in fact appear multiple times (e.g. Received) and for those headers, + you must use the explicit API to set or get all the headers. Not all of + the mapping methods are implemented. + """ + def __init__(self, policy=compat32): + self.policy = policy + self._headers = list() + self._unixfrom = None + self._payload = None + self._charset = None + # Defaults for multipart messages + self.preamble = self.epilogue = None + self.defects = [] + # Default content type + self._default_type = 'text/plain' + + @as_native_str(encoding='utf-8') + def __str__(self): + """Return the entire formatted message as a string. + This includes the headers, body, and envelope header. + """ + return self.as_string() + + def as_string(self, unixfrom=False, maxheaderlen=0): + """Return the entire formatted message as a (unicode) string. + Optional `unixfrom' when True, means include the Unix From_ envelope + header. + + This is a convenience method and may not generate the message exactly + as you intend. For more flexibility, use the flatten() method of a + Generator instance. + """ + from future.backports.email.generator import Generator + fp = StringIO() + g = Generator(fp, mangle_from_=False, maxheaderlen=maxheaderlen) + g.flatten(self, unixfrom=unixfrom) + return fp.getvalue() + + def is_multipart(self): + """Return True if the message consists of multiple parts.""" + return isinstance(self._payload, list) + + # + # Unix From_ line + # + def set_unixfrom(self, unixfrom): + self._unixfrom = unixfrom + + def get_unixfrom(self): + return self._unixfrom + + # + # Payload manipulation. + # + def attach(self, payload): + """Add the given payload to the current payload. + + The current payload will always be a list of objects after this method + is called. If you want to set the payload to a scalar object, use + set_payload() instead. + """ + if self._payload is None: + self._payload = [payload] + else: + self._payload.append(payload) + + def get_payload(self, i=None, decode=False): + """Return a reference to the payload. + + The payload will either be a list object or a string. If you mutate + the list object, you modify the message's payload in place. Optional + i returns that index into the payload. + + Optional decode is a flag indicating whether the payload should be + decoded or not, according to the Content-Transfer-Encoding header + (default is False). + + When True and the message is not a multipart, the payload will be + decoded if this header's value is `quoted-printable' or `base64'. If + some other encoding is used, or the header is missing, or if the + payload has bogus data (i.e. bogus base64 or uuencoded data), the + payload is returned as-is. + + If the message is a multipart and the decode flag is True, then None + is returned. + """ + # Here is the logic table for this code, based on the email5.0.0 code: + # i decode is_multipart result + # ------ ------ ------------ ------------------------------ + # None True True None + # i True True None + # None False True _payload (a list) + # i False True _payload element i (a Message) + # i False False error (not a list) + # i True False error (not a list) + # None False False _payload + # None True False _payload decoded (bytes) + # Note that Barry planned to factor out the 'decode' case, but that + # isn't so easy now that we handle the 8 bit data, which needs to be + # converted in both the decode and non-decode path. + if self.is_multipart(): + if decode: + return None + if i is None: + return self._payload + else: + return self._payload[i] + # For backward compatibility, Use isinstance and this error message + # instead of the more logical is_multipart test. + if i is not None and not isinstance(self._payload, list): + raise TypeError('Expected list, got %s' % type(self._payload)) + payload = self._payload + # cte might be a Header, so for now stringify it. + cte = str(self.get('content-transfer-encoding', '')).lower() + # payload may be bytes here. + if isinstance(payload, str): + payload = str(payload) # for Python-Future, so surrogateescape works + if utils._has_surrogates(payload): + bpayload = payload.encode('ascii', 'surrogateescape') + if not decode: + try: + payload = bpayload.decode(self.get_param('charset', 'ascii'), 'replace') + except LookupError: + payload = bpayload.decode('ascii', 'replace') + elif decode: + try: + bpayload = payload.encode('ascii') + except UnicodeError: + # This won't happen for RFC compliant messages (messages + # containing only ASCII codepoints in the unicode input). + # If it does happen, turn the string into bytes in a way + # guaranteed not to fail. + bpayload = payload.encode('raw-unicode-escape') + if not decode: + return payload + if cte == 'quoted-printable': + return utils._qdecode(bpayload) + elif cte == 'base64': + # XXX: this is a bit of a hack; decode_b should probably be factored + # out somewhere, but I haven't figured out where yet. + value, defects = decode_b(b''.join(bpayload.splitlines())) + for defect in defects: + self.policy.handle_defect(self, defect) + return value + elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): + in_file = BytesIO(bpayload) + out_file = BytesIO() + try: + uu.decode(in_file, out_file, quiet=True) + return out_file.getvalue() + except uu.Error: + # Some decoding problem + return bpayload + if isinstance(payload, str): + return bpayload + return payload + + def set_payload(self, payload, charset=None): + """Set the payload to the given value. + + Optional charset sets the message's default character set. See + set_charset() for details. + """ + self._payload = payload + if charset is not None: + self.set_charset(charset) + + def set_charset(self, charset): + """Set the charset of the payload to a given character set. + + charset can be a Charset instance, a string naming a character set, or + None. If it is a string it will be converted to a Charset instance. + If charset is None, the charset parameter will be removed from the + Content-Type field. Anything else will generate a TypeError. + + The message will be assumed to be of type text/* encoded with + charset.input_charset. It will be converted to charset.output_charset + and encoded properly, if needed, when generating the plain text + representation of the message. MIME headers (MIME-Version, + Content-Type, Content-Transfer-Encoding) will be added as needed. + """ + if charset is None: + self.del_param('charset') + self._charset = None + return + if not isinstance(charset, Charset): + charset = Charset(charset) + self._charset = charset + if 'MIME-Version' not in self: + self.add_header('MIME-Version', '1.0') + if 'Content-Type' not in self: + self.add_header('Content-Type', 'text/plain', + charset=charset.get_output_charset()) + else: + self.set_param('charset', charset.get_output_charset()) + if charset != charset.get_output_charset(): + self._payload = charset.body_encode(self._payload) + if 'Content-Transfer-Encoding' not in self: + cte = charset.get_body_encoding() + try: + cte(self) + except TypeError: + self._payload = charset.body_encode(self._payload) + self.add_header('Content-Transfer-Encoding', cte) + + def get_charset(self): + """Return the Charset instance associated with the message's payload. + """ + return self._charset + + # + # MAPPING INTERFACE (partial) + # + def __len__(self): + """Return the total number of headers, including duplicates.""" + return len(self._headers) + + def __getitem__(self, name): + """Get a header value. + + Return None if the header is missing instead of raising an exception. + + Note that if the header appeared multiple times, exactly which + occurrence gets returned is undefined. Use get_all() to get all + the values matching a header field name. + """ + return self.get(name) + + def __setitem__(self, name, val): + """Set the value of a header. + + Note: this does not overwrite an existing header with the same field + name. Use __delitem__() first to delete any existing headers. + """ + max_count = self.policy.header_max_count(name) + if max_count: + lname = name.lower() + found = 0 + for k, v in self._headers: + if k.lower() == lname: + found += 1 + if found >= max_count: + raise ValueError("There may be at most {} {} headers " + "in a message".format(max_count, name)) + self._headers.append(self.policy.header_store_parse(name, val)) + + def __delitem__(self, name): + """Delete all occurrences of a header, if present. + + Does not raise an exception if the header is missing. + """ + name = name.lower() + newheaders = list() + for k, v in self._headers: + if k.lower() != name: + newheaders.append((k, v)) + self._headers = newheaders + + def __contains__(self, name): + return name.lower() in [k.lower() for k, v in self._headers] + + def __iter__(self): + for field, value in self._headers: + yield field + + def keys(self): + """Return a list of all the message's header field names. + + These will be sorted in the order they appeared in the original + message, or were added to the message, and may contain duplicates. + Any fields deleted and re-inserted are always appended to the header + list. + """ + return [k for k, v in self._headers] + + def values(self): + """Return a list of all the message's header values. + + These will be sorted in the order they appeared in the original + message, or were added to the message, and may contain duplicates. + Any fields deleted and re-inserted are always appended to the header + list. + """ + return [self.policy.header_fetch_parse(k, v) + for k, v in self._headers] + + def items(self): + """Get all the message's header fields and values. + + These will be sorted in the order they appeared in the original + message, or were added to the message, and may contain duplicates. + Any fields deleted and re-inserted are always appended to the header + list. + """ + return [(k, self.policy.header_fetch_parse(k, v)) + for k, v in self._headers] + + def get(self, name, failobj=None): + """Get a header value. + + Like __getitem__() but return failobj instead of None when the field + is missing. + """ + name = name.lower() + for k, v in self._headers: + if k.lower() == name: + return self.policy.header_fetch_parse(k, v) + return failobj + + # + # "Internal" methods (public API, but only intended for use by a parser + # or generator, not normal application code. + # + + def set_raw(self, name, value): + """Store name and value in the model without modification. + + This is an "internal" API, intended only for use by a parser. + """ + self._headers.append((name, value)) + + def raw_items(self): + """Return the (name, value) header pairs without modification. + + This is an "internal" API, intended only for use by a generator. + """ + return iter(self._headers.copy()) + + # + # Additional useful stuff + # + + def get_all(self, name, failobj=None): + """Return a list of all the values for the named field. + + These will be sorted in the order they appeared in the original + message, and may contain duplicates. Any fields deleted and + re-inserted are always appended to the header list. + + If no such fields exist, failobj is returned (defaults to None). + """ + values = [] + name = name.lower() + for k, v in self._headers: + if k.lower() == name: + values.append(self.policy.header_fetch_parse(k, v)) + if not values: + return failobj + return values + + def add_header(self, _name, _value, **_params): + """Extended header setting. + + name is the header field to add. keyword arguments can be used to set + additional parameters for the header field, with underscores converted + to dashes. Normally the parameter will be added as key="value" unless + value is None, in which case only the key will be added. If a + parameter value contains non-ASCII characters it can be specified as a + three-tuple of (charset, language, value), in which case it will be + encoded according to RFC2231 rules. Otherwise it will be encoded using + the utf-8 charset and a language of ''. + + Examples: + + msg.add_header('content-disposition', 'attachment', filename='bud.gif') + msg.add_header('content-disposition', 'attachment', + filename=('utf-8', '', 'Fußballer.ppt')) + msg.add_header('content-disposition', 'attachment', + filename='Fußballer.ppt')) + """ + parts = [] + for k, v in _params.items(): + if v is None: + parts.append(k.replace('_', '-')) + else: + parts.append(_formatparam(k.replace('_', '-'), v)) + if _value is not None: + parts.insert(0, _value) + self[_name] = SEMISPACE.join(parts) + + def replace_header(self, _name, _value): + """Replace a header. + + Replace the first matching header found in the message, retaining + header order and case. If no matching header was found, a KeyError is + raised. + """ + _name = _name.lower() + for i, (k, v) in zip(range(len(self._headers)), self._headers): + if k.lower() == _name: + self._headers[i] = self.policy.header_store_parse(k, _value) + break + else: + raise KeyError(_name) + + # + # Use these three methods instead of the three above. + # + + def get_content_type(self): + """Return the message's content type. + + The returned string is coerced to lower case of the form + `maintype/subtype'. If there was no Content-Type header in the + message, the default type as given by get_default_type() will be + returned. Since according to RFC 2045, messages always have a default + type this will always return a value. + + RFC 2045 defines a message's default type to be text/plain unless it + appears inside a multipart/digest container, in which case it would be + message/rfc822. + """ + missing = object() + value = self.get('content-type', missing) + if value is missing: + # This should have no parameters + return self.get_default_type() + ctype = _splitparam(value)[0].lower() + # RFC 2045, section 5.2 says if its invalid, use text/plain + if ctype.count('/') != 1: + return 'text/plain' + return ctype + + def get_content_maintype(self): + """Return the message's main content type. + + This is the `maintype' part of the string returned by + get_content_type(). + """ + ctype = self.get_content_type() + return ctype.split('/')[0] + + def get_content_subtype(self): + """Returns the message's sub-content type. + + This is the `subtype' part of the string returned by + get_content_type(). + """ + ctype = self.get_content_type() + return ctype.split('/')[1] + + def get_default_type(self): + """Return the `default' content type. + + Most messages have a default content type of text/plain, except for + messages that are subparts of multipart/digest containers. Such + subparts have a default content type of message/rfc822. + """ + return self._default_type + + def set_default_type(self, ctype): + """Set the `default' content type. + + ctype should be either "text/plain" or "message/rfc822", although this + is not enforced. The default content type is not stored in the + Content-Type header. + """ + self._default_type = ctype + + def _get_params_preserve(self, failobj, header): + # Like get_params() but preserves the quoting of values. BAW: + # should this be part of the public interface? + missing = object() + value = self.get(header, missing) + if value is missing: + return failobj + params = [] + for p in _parseparam(value): + try: + name, val = p.split('=', 1) + name = name.strip() + val = val.strip() + except ValueError: + # Must have been a bare attribute + name = p.strip() + val = '' + params.append((name, val)) + params = utils.decode_params(params) + return params + + def get_params(self, failobj=None, header='content-type', unquote=True): + """Return the message's Content-Type parameters, as a list. + + The elements of the returned list are 2-tuples of key/value pairs, as + split on the `=' sign. The left hand side of the `=' is the key, + while the right hand side is the value. If there is no `=' sign in + the parameter the value is the empty string. The value is as + described in the get_param() method. + + Optional failobj is the object to return if there is no Content-Type + header. Optional header is the header to search instead of + Content-Type. If unquote is True, the value is unquoted. + """ + missing = object() + params = self._get_params_preserve(missing, header) + if params is missing: + return failobj + if unquote: + return [(k, _unquotevalue(v)) for k, v in params] + else: + return params + + def get_param(self, param, failobj=None, header='content-type', + unquote=True): + """Return the parameter value if found in the Content-Type header. + + Optional failobj is the object to return if there is no Content-Type + header, or the Content-Type header has no such parameter. Optional + header is the header to search instead of Content-Type. + + Parameter keys are always compared case insensitively. The return + value can either be a string, or a 3-tuple if the parameter was RFC + 2231 encoded. When it's a 3-tuple, the elements of the value are of + the form (CHARSET, LANGUAGE, VALUE). Note that both CHARSET and + LANGUAGE can be None, in which case you should consider VALUE to be + encoded in the us-ascii charset. You can usually ignore LANGUAGE. + The parameter value (either the returned string, or the VALUE item in + the 3-tuple) is always unquoted, unless unquote is set to False. + + If your application doesn't care whether the parameter was RFC 2231 + encoded, it can turn the return value into a string as follows: + + param = msg.get_param('foo') + param = email.utils.collapse_rfc2231_value(rawparam) + + """ + if header not in self: + return failobj + for k, v in self._get_params_preserve(failobj, header): + if k.lower() == param.lower(): + if unquote: + return _unquotevalue(v) + else: + return v + return failobj + + def set_param(self, param, value, header='Content-Type', requote=True, + charset=None, language=''): + """Set a parameter in the Content-Type header. + + If the parameter already exists in the header, its value will be + replaced with the new value. + + If header is Content-Type and has not yet been defined for this + message, it will be set to "text/plain" and the new parameter and + value will be appended as per RFC 2045. + + An alternate header can specified in the header argument, and all + parameters will be quoted as necessary unless requote is False. + + If charset is specified, the parameter will be encoded according to RFC + 2231. Optional language specifies the RFC 2231 language, defaulting + to the empty string. Both charset and language should be strings. + """ + if not isinstance(value, tuple) and charset: + value = (charset, language, value) + + if header not in self and header.lower() == 'content-type': + ctype = 'text/plain' + else: + ctype = self.get(header) + if not self.get_param(param, header=header): + if not ctype: + ctype = _formatparam(param, value, requote) + else: + ctype = SEMISPACE.join( + [ctype, _formatparam(param, value, requote)]) + else: + ctype = '' + for old_param, old_value in self.get_params(header=header, + unquote=requote): + append_param = '' + if old_param.lower() == param.lower(): + append_param = _formatparam(param, value, requote) + else: + append_param = _formatparam(old_param, old_value, requote) + if not ctype: + ctype = append_param + else: + ctype = SEMISPACE.join([ctype, append_param]) + if ctype != self.get(header): + del self[header] + self[header] = ctype + + def del_param(self, param, header='content-type', requote=True): + """Remove the given parameter completely from the Content-Type header. + + The header will be re-written in place without the parameter or its + value. All values will be quoted as necessary unless requote is + False. Optional header specifies an alternative to the Content-Type + header. + """ + if header not in self: + return + new_ctype = '' + for p, v in self.get_params(header=header, unquote=requote): + if p.lower() != param.lower(): + if not new_ctype: + new_ctype = _formatparam(p, v, requote) + else: + new_ctype = SEMISPACE.join([new_ctype, + _formatparam(p, v, requote)]) + if new_ctype != self.get(header): + del self[header] + self[header] = new_ctype + + def set_type(self, type, header='Content-Type', requote=True): + """Set the main type and subtype for the Content-Type header. + + type must be a string in the form "maintype/subtype", otherwise a + ValueError is raised. + + This method replaces the Content-Type header, keeping all the + parameters in place. If requote is False, this leaves the existing + header's quoting as is. Otherwise, the parameters will be quoted (the + default). + + An alternative header can be specified in the header argument. When + the Content-Type header is set, we'll always also add a MIME-Version + header. + """ + # BAW: should we be strict? + if not type.count('/') == 1: + raise ValueError + # Set the Content-Type, you get a MIME-Version + if header.lower() == 'content-type': + del self['mime-version'] + self['MIME-Version'] = '1.0' + if header not in self: + self[header] = type + return + params = self.get_params(header=header, unquote=requote) + del self[header] + self[header] = type + # Skip the first param; it's the old type. + for p, v in params[1:]: + self.set_param(p, v, header, requote) + + def get_filename(self, failobj=None): + """Return the filename associated with the payload if present. + + The filename is extracted from the Content-Disposition header's + `filename' parameter, and it is unquoted. If that header is missing + the `filename' parameter, this method falls back to looking for the + `name' parameter. + """ + missing = object() + filename = self.get_param('filename', missing, 'content-disposition') + if filename is missing: + filename = self.get_param('name', missing, 'content-type') + if filename is missing: + return failobj + return utils.collapse_rfc2231_value(filename).strip() + + def get_boundary(self, failobj=None): + """Return the boundary associated with the payload if present. + + The boundary is extracted from the Content-Type header's `boundary' + parameter, and it is unquoted. + """ + missing = object() + boundary = self.get_param('boundary', missing) + if boundary is missing: + return failobj + # RFC 2046 says that boundaries may begin but not end in w/s + return utils.collapse_rfc2231_value(boundary).rstrip() + + def set_boundary(self, boundary): + """Set the boundary parameter in Content-Type to 'boundary'. + + This is subtly different than deleting the Content-Type header and + adding a new one with a new boundary parameter via add_header(). The + main difference is that using the set_boundary() method preserves the + order of the Content-Type header in the original message. + + HeaderParseError is raised if the message has no Content-Type header. + """ + missing = object() + params = self._get_params_preserve(missing, 'content-type') + if params is missing: + # There was no Content-Type header, and we don't know what type + # to set it to, so raise an exception. + raise errors.HeaderParseError('No Content-Type header found') + newparams = list() + foundp = False + for pk, pv in params: + if pk.lower() == 'boundary': + newparams.append(('boundary', '"%s"' % boundary)) + foundp = True + else: + newparams.append((pk, pv)) + if not foundp: + # The original Content-Type header had no boundary attribute. + # Tack one on the end. BAW: should we raise an exception + # instead??? + newparams.append(('boundary', '"%s"' % boundary)) + # Replace the existing Content-Type header with the new value + newheaders = list() + for h, v in self._headers: + if h.lower() == 'content-type': + parts = list() + for k, v in newparams: + if v == '': + parts.append(k) + else: + parts.append('%s=%s' % (k, v)) + val = SEMISPACE.join(parts) + newheaders.append(self.policy.header_store_parse(h, val)) + + else: + newheaders.append((h, v)) + self._headers = newheaders + + def get_content_charset(self, failobj=None): + """Return the charset parameter of the Content-Type header. + + The returned string is always coerced to lower case. If there is no + Content-Type header, or if that header has no charset parameter, + failobj is returned. + """ + missing = object() + charset = self.get_param('charset', missing) + if charset is missing: + return failobj + if isinstance(charset, tuple): + # RFC 2231 encoded, so decode it, and it better end up as ascii. + pcharset = charset[0] or 'us-ascii' + try: + # LookupError will be raised if the charset isn't known to + # Python. UnicodeError will be raised if the encoded text + # contains a character not in the charset. + as_bytes = charset[2].encode('raw-unicode-escape') + charset = str(as_bytes, pcharset) + except (LookupError, UnicodeError): + charset = charset[2] + # charset characters must be in us-ascii range + try: + charset.encode('us-ascii') + except UnicodeError: + return failobj + # RFC 2046, $4.1.2 says charsets are not case sensitive + return charset.lower() + + def get_charsets(self, failobj=None): + """Return a list containing the charset(s) used in this message. + + The returned list of items describes the Content-Type headers' + charset parameter for this message and all the subparts in its + payload. + + Each item will either be a string (the value of the charset parameter + in the Content-Type header of that part) or the value of the + 'failobj' parameter (defaults to None), if the part does not have a + main MIME type of "text", or the charset is not defined. + + The list will contain one string for each part of the message, plus + one for the container message (i.e. self), so that a non-multipart + message will still return a list of length 1. + """ + return [part.get_content_charset(failobj) for part in self.walk()] + + # I.e. def walk(self): ... + from future.backports.email.iterators import walk diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__init__.py b/venv/lib/python3.10/site-packages/future/backports/email/mime/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..b21b7fd Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/application.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/application.cpython-310.pyc new file mode 100644 index 0000000..5b0f7e9 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/application.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/audio.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/audio.cpython-310.pyc new file mode 100644 index 0000000..1370199 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/audio.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/base.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000..32b0694 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/base.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/image.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/image.cpython-310.pyc new file mode 100644 index 0000000..6c0c5b3 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/image.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/message.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/message.cpython-310.pyc new file mode 100644 index 0000000..18d0f0b Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/message.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/multipart.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/multipart.cpython-310.pyc new file mode 100644 index 0000000..70e565d Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/multipart.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/nonmultipart.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/nonmultipart.cpython-310.pyc new file mode 100644 index 0000000..09302b4 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/nonmultipart.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/text.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/text.cpython-310.pyc new file mode 100644 index 0000000..cc77ff8 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/email/mime/__pycache__/text.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/application.py b/venv/lib/python3.10/site-packages/future/backports/email/mime/application.py new file mode 100644 index 0000000..5cbfb17 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/mime/application.py @@ -0,0 +1,39 @@ +# Copyright (C) 2001-2006 Python Software Foundation +# Author: Keith Dart +# Contact: email-sig@python.org + +"""Class representing application/* type MIME documents.""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +from future.backports.email import encoders +from future.backports.email.mime.nonmultipart import MIMENonMultipart + +__all__ = ["MIMEApplication"] + + +class MIMEApplication(MIMENonMultipart): + """Class for generating application/* MIME documents.""" + + def __init__(self, _data, _subtype='octet-stream', + _encoder=encoders.encode_base64, **_params): + """Create an application/* type MIME document. + + _data is a string containing the raw application data. + + _subtype is the MIME content type subtype, defaulting to + 'octet-stream'. + + _encoder is a function which will perform the actual encoding for + transport of the application data, defaulting to base64 encoding. + + Any additional keyword arguments are passed to the base class + constructor, which turns them into parameters on the Content-Type + header. + """ + if _subtype is None: + raise TypeError('Invalid application MIME subtype') + MIMENonMultipart.__init__(self, 'application', _subtype, **_params) + self.set_payload(_data) + _encoder(self) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/audio.py b/venv/lib/python3.10/site-packages/future/backports/email/mime/audio.py new file mode 100644 index 0000000..4989c11 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/mime/audio.py @@ -0,0 +1,74 @@ +# Copyright (C) 2001-2007 Python Software Foundation +# Author: Anthony Baxter +# Contact: email-sig@python.org + +"""Class representing audio/* type MIME documents.""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +__all__ = ['MIMEAudio'] + +import sndhdr + +from io import BytesIO +from future.backports.email import encoders +from future.backports.email.mime.nonmultipart import MIMENonMultipart + + +_sndhdr_MIMEmap = {'au' : 'basic', + 'wav' :'x-wav', + 'aiff':'x-aiff', + 'aifc':'x-aiff', + } + +# There are others in sndhdr that don't have MIME types. :( +# Additional ones to be added to sndhdr? midi, mp3, realaudio, wma?? +def _whatsnd(data): + """Try to identify a sound file type. + + sndhdr.what() has a pretty cruddy interface, unfortunately. This is why + we re-do it here. It would be easier to reverse engineer the Unix 'file' + command and use the standard 'magic' file, as shipped with a modern Unix. + """ + hdr = data[:512] + fakefile = BytesIO(hdr) + for testfn in sndhdr.tests: + res = testfn(hdr, fakefile) + if res is not None: + return _sndhdr_MIMEmap.get(res[0]) + return None + + +class MIMEAudio(MIMENonMultipart): + """Class for generating audio/* MIME documents.""" + + def __init__(self, _audiodata, _subtype=None, + _encoder=encoders.encode_base64, **_params): + """Create an audio/* type MIME document. + + _audiodata is a string containing the raw audio data. If this data + can be decoded by the standard Python `sndhdr' module, then the + subtype will be automatically included in the Content-Type header. + Otherwise, you can specify the specific audio subtype via the + _subtype parameter. If _subtype is not given, and no subtype can be + guessed, a TypeError is raised. + + _encoder is a function which will perform the actual encoding for + transport of the image data. It takes one argument, which is this + Image instance. It should use get_payload() and set_payload() to + change the payload to the encoded form. It should also add any + Content-Transfer-Encoding or other headers to the message as + necessary. The default encoding is Base64. + + Any additional keyword arguments are passed to the base class + constructor, which turns them into parameters on the Content-Type + header. + """ + if _subtype is None: + _subtype = _whatsnd(_audiodata) + if _subtype is None: + raise TypeError('Could not find audio MIME subtype') + MIMENonMultipart.__init__(self, 'audio', _subtype, **_params) + self.set_payload(_audiodata) + _encoder(self) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/base.py b/venv/lib/python3.10/site-packages/future/backports/email/mime/base.py new file mode 100644 index 0000000..e77f3ca --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/mime/base.py @@ -0,0 +1,25 @@ +# Copyright (C) 2001-2006 Python Software Foundation +# Author: Barry Warsaw +# Contact: email-sig@python.org + +"""Base class for MIME specializations.""" +from __future__ import absolute_import, division, unicode_literals +from future.backports.email import message + +__all__ = ['MIMEBase'] + + +class MIMEBase(message.Message): + """Base class for MIME specializations.""" + + def __init__(self, _maintype, _subtype, **_params): + """This constructor adds a Content-Type: and a MIME-Version: header. + + The Content-Type: header is taken from the _maintype and _subtype + arguments. Additional parameters for this header are taken from the + keyword arguments. + """ + message.Message.__init__(self) + ctype = '%s/%s' % (_maintype, _subtype) + self.add_header('Content-Type', ctype, **_params) + self['MIME-Version'] = '1.0' diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/image.py b/venv/lib/python3.10/site-packages/future/backports/email/mime/image.py new file mode 100644 index 0000000..a036024 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/mime/image.py @@ -0,0 +1,48 @@ +# Copyright (C) 2001-2006 Python Software Foundation +# Author: Barry Warsaw +# Contact: email-sig@python.org + +"""Class representing image/* type MIME documents.""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +__all__ = ['MIMEImage'] + +import imghdr + +from future.backports.email import encoders +from future.backports.email.mime.nonmultipart import MIMENonMultipart + + +class MIMEImage(MIMENonMultipart): + """Class for generating image/* type MIME documents.""" + + def __init__(self, _imagedata, _subtype=None, + _encoder=encoders.encode_base64, **_params): + """Create an image/* type MIME document. + + _imagedata is a string containing the raw image data. If this data + can be decoded by the standard Python `imghdr' module, then the + subtype will be automatically included in the Content-Type header. + Otherwise, you can specify the specific image subtype via the _subtype + parameter. + + _encoder is a function which will perform the actual encoding for + transport of the image data. It takes one argument, which is this + Image instance. It should use get_payload() and set_payload() to + change the payload to the encoded form. It should also add any + Content-Transfer-Encoding or other headers to the message as + necessary. The default encoding is Base64. + + Any additional keyword arguments are passed to the base class + constructor, which turns them into parameters on the Content-Type + header. + """ + if _subtype is None: + _subtype = imghdr.what(None, _imagedata) + if _subtype is None: + raise TypeError('Could not guess image MIME subtype') + MIMENonMultipart.__init__(self, 'image', _subtype, **_params) + self.set_payload(_imagedata) + _encoder(self) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/message.py b/venv/lib/python3.10/site-packages/future/backports/email/mime/message.py new file mode 100644 index 0000000..7f92075 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/mime/message.py @@ -0,0 +1,36 @@ +# Copyright (C) 2001-2006 Python Software Foundation +# Author: Barry Warsaw +# Contact: email-sig@python.org + +"""Class representing message/* MIME documents.""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +__all__ = ['MIMEMessage'] + +from future.backports.email import message +from future.backports.email.mime.nonmultipart import MIMENonMultipart + + +class MIMEMessage(MIMENonMultipart): + """Class representing message/* MIME documents.""" + + def __init__(self, _msg, _subtype='rfc822'): + """Create a message/* type MIME document. + + _msg is a message object and must be an instance of Message, or a + derived class of Message, otherwise a TypeError is raised. + + Optional _subtype defines the subtype of the contained message. The + default is "rfc822" (this is defined by the MIME standard, even though + the term "rfc822" is technically outdated by RFC 2822). + """ + MIMENonMultipart.__init__(self, 'message', _subtype) + if not isinstance(_msg, message.Message): + raise TypeError('Argument is not an instance of Message') + # It's convenient to use this base class method. We need to do it + # this way or we'll get an exception + message.Message.attach(self, _msg) + # And be sure our default type is set correctly + self.set_default_type('message/rfc822') diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/multipart.py b/venv/lib/python3.10/site-packages/future/backports/email/mime/multipart.py new file mode 100644 index 0000000..6d7ed3d --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/mime/multipart.py @@ -0,0 +1,49 @@ +# Copyright (C) 2002-2006 Python Software Foundation +# Author: Barry Warsaw +# Contact: email-sig@python.org + +"""Base class for MIME multipart/* type messages.""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +__all__ = ['MIMEMultipart'] + +from future.backports.email.mime.base import MIMEBase + + +class MIMEMultipart(MIMEBase): + """Base class for MIME multipart/* type messages.""" + + def __init__(self, _subtype='mixed', boundary=None, _subparts=None, + **_params): + """Creates a multipart/* type message. + + By default, creates a multipart/mixed message, with proper + Content-Type and MIME-Version headers. + + _subtype is the subtype of the multipart content type, defaulting to + `mixed'. + + boundary is the multipart boundary string. By default it is + calculated as needed. + + _subparts is a sequence of initial subparts for the payload. It + must be an iterable object, such as a list. You can always + attach new subparts to the message by using the attach() method. + + Additional parameters for the Content-Type header are taken from the + keyword arguments (or passed into the _params argument). + """ + MIMEBase.__init__(self, 'multipart', _subtype, **_params) + + # Initialise _payload to an empty list as the Message superclass's + # implementation of is_multipart assumes that _payload is a list for + # multipart messages. + self._payload = [] + + if _subparts: + for p in _subparts: + self.attach(p) + if boundary: + self.set_boundary(boundary) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/nonmultipart.py b/venv/lib/python3.10/site-packages/future/backports/email/mime/nonmultipart.py new file mode 100644 index 0000000..08c37c3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/mime/nonmultipart.py @@ -0,0 +1,24 @@ +# Copyright (C) 2002-2006 Python Software Foundation +# Author: Barry Warsaw +# Contact: email-sig@python.org + +"""Base class for MIME type messages that are not multipart.""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +__all__ = ['MIMENonMultipart'] + +from future.backports.email import errors +from future.backports.email.mime.base import MIMEBase + + +class MIMENonMultipart(MIMEBase): + """Base class for MIME multipart/* type messages.""" + + def attach(self, payload): + # The public API prohibits attaching multiple subparts to MIMEBase + # derived subtypes since none of them are, by definition, of content + # type multipart/* + raise errors.MultipartConversionError( + 'Cannot attach additional subparts to non-multipart/*') diff --git a/venv/lib/python3.10/site-packages/future/backports/email/mime/text.py b/venv/lib/python3.10/site-packages/future/backports/email/mime/text.py new file mode 100644 index 0000000..6269f4a --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/mime/text.py @@ -0,0 +1,44 @@ +# Copyright (C) 2001-2006 Python Software Foundation +# Author: Barry Warsaw +# Contact: email-sig@python.org + +"""Class representing text/* type MIME documents.""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +__all__ = ['MIMEText'] + +from future.backports.email.encoders import encode_7or8bit +from future.backports.email.mime.nonmultipart import MIMENonMultipart + + +class MIMEText(MIMENonMultipart): + """Class for generating text/* type MIME documents.""" + + def __init__(self, _text, _subtype='plain', _charset=None): + """Create a text/* type MIME document. + + _text is the string for this message object. + + _subtype is the MIME sub content type, defaulting to "plain". + + _charset is the character set parameter added to the Content-Type + header. This defaults to "us-ascii". Note that as a side-effect, the + Content-Transfer-Encoding header will also be set. + """ + + # If no _charset was specified, check to see if there are non-ascii + # characters present. If not, use 'us-ascii', otherwise use utf-8. + # XXX: This can be removed once #7304 is fixed. + if _charset is None: + try: + _text.encode('us-ascii') + _charset = 'us-ascii' + except UnicodeEncodeError: + _charset = 'utf-8' + + MIMENonMultipart.__init__(self, 'text', _subtype, + **{'charset': _charset}) + + self.set_payload(_text, _charset) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/parser.py b/venv/lib/python3.10/site-packages/future/backports/email/parser.py new file mode 100644 index 0000000..79f0e5a --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/parser.py @@ -0,0 +1,135 @@ +# Copyright (C) 2001-2007 Python Software Foundation +# Author: Barry Warsaw, Thomas Wouters, Anthony Baxter +# Contact: email-sig@python.org + +"""A parser of RFC 2822 and MIME email messages.""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import + +__all__ = ['Parser', 'HeaderParser', 'BytesParser', 'BytesHeaderParser'] + +import warnings +from io import StringIO, TextIOWrapper + +from future.backports.email.feedparser import FeedParser, BytesFeedParser +from future.backports.email.message import Message +from future.backports.email._policybase import compat32 + + +class Parser(object): + def __init__(self, _class=Message, **_3to2kwargs): + """Parser of RFC 2822 and MIME email messages. + + Creates an in-memory object tree representing the email message, which + can then be manipulated and turned over to a Generator to return the + textual representation of the message. + + The string must be formatted as a block of RFC 2822 headers and header + continuation lines, optionally preceded by a `Unix-from' header. The + header block is terminated either by the end of the string or by a + blank line. + + _class is the class to instantiate for new message objects when they + must be created. This class must have a constructor that can take + zero arguments. Default is Message.Message. + + The policy keyword specifies a policy object that controls a number of + aspects of the parser's operation. The default policy maintains + backward compatibility. + + """ + if 'policy' in _3to2kwargs: policy = _3to2kwargs['policy']; del _3to2kwargs['policy'] + else: policy = compat32 + self._class = _class + self.policy = policy + + def parse(self, fp, headersonly=False): + """Create a message structure from the data in a file. + + Reads all the data from the file and returns the root of the message + structure. Optional headersonly is a flag specifying whether to stop + parsing after reading the headers or not. The default is False, + meaning it parses the entire contents of the file. + """ + feedparser = FeedParser(self._class, policy=self.policy) + if headersonly: + feedparser._set_headersonly() + while True: + data = fp.read(8192) + if not data: + break + feedparser.feed(data) + return feedparser.close() + + def parsestr(self, text, headersonly=False): + """Create a message structure from a string. + + Returns the root of the message structure. Optional headersonly is a + flag specifying whether to stop parsing after reading the headers or + not. The default is False, meaning it parses the entire contents of + the file. + """ + return self.parse(StringIO(text), headersonly=headersonly) + + + +class HeaderParser(Parser): + def parse(self, fp, headersonly=True): + return Parser.parse(self, fp, True) + + def parsestr(self, text, headersonly=True): + return Parser.parsestr(self, text, True) + + +class BytesParser(object): + + def __init__(self, *args, **kw): + """Parser of binary RFC 2822 and MIME email messages. + + Creates an in-memory object tree representing the email message, which + can then be manipulated and turned over to a Generator to return the + textual representation of the message. + + The input must be formatted as a block of RFC 2822 headers and header + continuation lines, optionally preceded by a `Unix-from' header. The + header block is terminated either by the end of the input or by a + blank line. + + _class is the class to instantiate for new message objects when they + must be created. This class must have a constructor that can take + zero arguments. Default is Message.Message. + """ + self.parser = Parser(*args, **kw) + + def parse(self, fp, headersonly=False): + """Create a message structure from the data in a binary file. + + Reads all the data from the file and returns the root of the message + structure. Optional headersonly is a flag specifying whether to stop + parsing after reading the headers or not. The default is False, + meaning it parses the entire contents of the file. + """ + fp = TextIOWrapper(fp, encoding='ascii', errors='surrogateescape') + with fp: + return self.parser.parse(fp, headersonly) + + + def parsebytes(self, text, headersonly=False): + """Create a message structure from a byte string. + + Returns the root of the message structure. Optional headersonly is a + flag specifying whether to stop parsing after reading the headers or + not. The default is False, meaning it parses the entire contents of + the file. + """ + text = text.decode('ASCII', errors='surrogateescape') + return self.parser.parsestr(text, headersonly) + + +class BytesHeaderParser(BytesParser): + def parse(self, fp, headersonly=True): + return BytesParser.parse(self, fp, headersonly=True) + + def parsebytes(self, text, headersonly=True): + return BytesParser.parsebytes(self, text, headersonly=True) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/policy.py b/venv/lib/python3.10/site-packages/future/backports/email/policy.py new file mode 100644 index 0000000..2f609a2 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/policy.py @@ -0,0 +1,193 @@ +"""This will be the home for the policy that hooks in the new +code that adds all the email6 features. +""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import +from future.builtins import super + +from future.standard_library.email._policybase import (Policy, Compat32, + compat32, _extend_docstrings) +from future.standard_library.email.utils import _has_surrogates +from future.standard_library.email.headerregistry import HeaderRegistry as HeaderRegistry + +__all__ = [ + 'Compat32', + 'compat32', + 'Policy', + 'EmailPolicy', + 'default', + 'strict', + 'SMTP', + 'HTTP', + ] + +@_extend_docstrings +class EmailPolicy(Policy): + + """+ + PROVISIONAL + + The API extensions enabled by this policy are currently provisional. + Refer to the documentation for details. + + This policy adds new header parsing and folding algorithms. Instead of + simple strings, headers are custom objects with custom attributes + depending on the type of the field. The folding algorithm fully + implements RFCs 2047 and 5322. + + In addition to the settable attributes listed above that apply to + all Policies, this policy adds the following additional attributes: + + refold_source -- if the value for a header in the Message object + came from the parsing of some source, this attribute + indicates whether or not a generator should refold + that value when transforming the message back into + stream form. The possible values are: + + none -- all source values use original folding + long -- source values that have any line that is + longer than max_line_length will be + refolded + all -- all values are refolded. + + The default is 'long'. + + header_factory -- a callable that takes two arguments, 'name' and + 'value', where 'name' is a header field name and + 'value' is an unfolded header field value, and + returns a string-like object that represents that + header. A default header_factory is provided that + understands some of the RFC5322 header field types. + (Currently address fields and date fields have + special treatment, while all other fields are + treated as unstructured. This list will be + completed before the extension is marked stable.) + """ + + refold_source = 'long' + header_factory = HeaderRegistry() + + def __init__(self, **kw): + # Ensure that each new instance gets a unique header factory + # (as opposed to clones, which share the factory). + if 'header_factory' not in kw: + object.__setattr__(self, 'header_factory', HeaderRegistry()) + super().__init__(**kw) + + def header_max_count(self, name): + """+ + The implementation for this class returns the max_count attribute from + the specialized header class that would be used to construct a header + of type 'name'. + """ + return self.header_factory[name].max_count + + # The logic of the next three methods is chosen such that it is possible to + # switch a Message object between a Compat32 policy and a policy derived + # from this class and have the results stay consistent. This allows a + # Message object constructed with this policy to be passed to a library + # that only handles Compat32 objects, or to receive such an object and + # convert it to use the newer style by just changing its policy. It is + # also chosen because it postpones the relatively expensive full rfc5322 + # parse until as late as possible when parsing from source, since in many + # applications only a few headers will actually be inspected. + + def header_source_parse(self, sourcelines): + """+ + The name is parsed as everything up to the ':' and returned unmodified. + The value is determined by stripping leading whitespace off the + remainder of the first line, joining all subsequent lines together, and + stripping any trailing carriage return or linefeed characters. (This + is the same as Compat32). + + """ + name, value = sourcelines[0].split(':', 1) + value = value.lstrip(' \t') + ''.join(sourcelines[1:]) + return (name, value.rstrip('\r\n')) + + def header_store_parse(self, name, value): + """+ + The name is returned unchanged. If the input value has a 'name' + attribute and it matches the name ignoring case, the value is returned + unchanged. Otherwise the name and value are passed to header_factory + method, and the resulting custom header object is returned as the + value. In this case a ValueError is raised if the input value contains + CR or LF characters. + + """ + if hasattr(value, 'name') and value.name.lower() == name.lower(): + return (name, value) + if isinstance(value, str) and len(value.splitlines())>1: + raise ValueError("Header values may not contain linefeed " + "or carriage return characters") + return (name, self.header_factory(name, value)) + + def header_fetch_parse(self, name, value): + """+ + If the value has a 'name' attribute, it is returned to unmodified. + Otherwise the name and the value with any linesep characters removed + are passed to the header_factory method, and the resulting custom + header object is returned. Any surrogateescaped bytes get turned + into the unicode unknown-character glyph. + + """ + if hasattr(value, 'name'): + return value + return self.header_factory(name, ''.join(value.splitlines())) + + def fold(self, name, value): + """+ + Header folding is controlled by the refold_source policy setting. A + value is considered to be a 'source value' if and only if it does not + have a 'name' attribute (having a 'name' attribute means it is a header + object of some sort). If a source value needs to be refolded according + to the policy, it is converted into a custom header object by passing + the name and the value with any linesep characters removed to the + header_factory method. Folding of a custom header object is done by + calling its fold method with the current policy. + + Source values are split into lines using splitlines. If the value is + not to be refolded, the lines are rejoined using the linesep from the + policy and returned. The exception is lines containing non-ascii + binary data. In that case the value is refolded regardless of the + refold_source setting, which causes the binary data to be CTE encoded + using the unknown-8bit charset. + + """ + return self._fold(name, value, refold_binary=True) + + def fold_binary(self, name, value): + """+ + The same as fold if cte_type is 7bit, except that the returned value is + bytes. + + If cte_type is 8bit, non-ASCII binary data is converted back into + bytes. Headers with binary data are not refolded, regardless of the + refold_header setting, since there is no way to know whether the binary + data consists of single byte characters or multibyte characters. + + """ + folded = self._fold(name, value, refold_binary=self.cte_type=='7bit') + return folded.encode('ascii', 'surrogateescape') + + def _fold(self, name, value, refold_binary=False): + if hasattr(value, 'name'): + return value.fold(policy=self) + maxlen = self.max_line_length if self.max_line_length else float('inf') + lines = value.splitlines() + refold = (self.refold_source == 'all' or + self.refold_source == 'long' and + (lines and len(lines[0])+len(name)+2 > maxlen or + any(len(x) > maxlen for x in lines[1:]))) + if refold or refold_binary and _has_surrogates(value): + return self.header_factory(name, ''.join(lines)).fold(policy=self) + return name + ': ' + self.linesep.join(lines) + self.linesep + + +default = EmailPolicy() +# Make the default policy use the class default header_factory +del default.header_factory +strict = default.clone(raise_on_defect=True) +SMTP = default.clone(linesep='\r\n') +HTTP = default.clone(linesep='\r\n', max_line_length=None) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/quoprimime.py b/venv/lib/python3.10/site-packages/future/backports/email/quoprimime.py new file mode 100644 index 0000000..b69d158 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/quoprimime.py @@ -0,0 +1,326 @@ +# Copyright (C) 2001-2006 Python Software Foundation +# Author: Ben Gertzfield +# Contact: email-sig@python.org + +"""Quoted-printable content transfer encoding per RFCs 2045-2047. + +This module handles the content transfer encoding method defined in RFC 2045 +to encode US ASCII-like 8-bit data called `quoted-printable'. It is used to +safely encode text that is in a character set similar to the 7-bit US ASCII +character set, but that includes some 8-bit characters that are normally not +allowed in email bodies or headers. + +Quoted-printable is very space-inefficient for encoding binary files; use the +email.base64mime module for that instead. + +This module provides an interface to encode and decode both headers and bodies +with quoted-printable encoding. + +RFC 2045 defines a method for including character set information in an +`encoded-word' in a header. This method is commonly used for 8-bit real names +in To:/From:/Cc: etc. fields, as well as Subject: lines. + +This module does not do the line wrapping or end-of-line character +conversion necessary for proper internationalized headers; it only +does dumb encoding and decoding. To deal with the various line +wrapping issues, use the email.header module. +""" +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import +from future.builtins import bytes, chr, dict, int, range, super + +__all__ = [ + 'body_decode', + 'body_encode', + 'body_length', + 'decode', + 'decodestring', + 'header_decode', + 'header_encode', + 'header_length', + 'quote', + 'unquote', + ] + +import re +import io + +from string import ascii_letters, digits, hexdigits + +CRLF = '\r\n' +NL = '\n' +EMPTYSTRING = '' + +# Build a mapping of octets to the expansion of that octet. Since we're only +# going to have 256 of these things, this isn't terribly inefficient +# space-wise. Remember that headers and bodies have different sets of safe +# characters. Initialize both maps with the full expansion, and then override +# the safe bytes with the more compact form. +_QUOPRI_HEADER_MAP = dict((c, '=%02X' % c) for c in range(256)) +_QUOPRI_BODY_MAP = _QUOPRI_HEADER_MAP.copy() + +# Safe header bytes which need no encoding. +for c in bytes(b'-!*+/' + ascii_letters.encode('ascii') + digits.encode('ascii')): + _QUOPRI_HEADER_MAP[c] = chr(c) +# Headers have one other special encoding; spaces become underscores. +_QUOPRI_HEADER_MAP[ord(' ')] = '_' + +# Safe body bytes which need no encoding. +for c in bytes(b' !"#$%&\'()*+,-./0123456789:;<>' + b'?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`' + b'abcdefghijklmnopqrstuvwxyz{|}~\t'): + _QUOPRI_BODY_MAP[c] = chr(c) + + + +# Helpers +def header_check(octet): + """Return True if the octet should be escaped with header quopri.""" + return chr(octet) != _QUOPRI_HEADER_MAP[octet] + + +def body_check(octet): + """Return True if the octet should be escaped with body quopri.""" + return chr(octet) != _QUOPRI_BODY_MAP[octet] + + +def header_length(bytearray): + """Return a header quoted-printable encoding length. + + Note that this does not include any RFC 2047 chrome added by + `header_encode()`. + + :param bytearray: An array of bytes (a.k.a. octets). + :return: The length in bytes of the byte array when it is encoded with + quoted-printable for headers. + """ + return sum(len(_QUOPRI_HEADER_MAP[octet]) for octet in bytearray) + + +def body_length(bytearray): + """Return a body quoted-printable encoding length. + + :param bytearray: An array of bytes (a.k.a. octets). + :return: The length in bytes of the byte array when it is encoded with + quoted-printable for bodies. + """ + return sum(len(_QUOPRI_BODY_MAP[octet]) for octet in bytearray) + + +def _max_append(L, s, maxlen, extra=''): + if not isinstance(s, str): + s = chr(s) + if not L: + L.append(s.lstrip()) + elif len(L[-1]) + len(s) <= maxlen: + L[-1] += extra + s + else: + L.append(s.lstrip()) + + +def unquote(s): + """Turn a string in the form =AB to the ASCII character with value 0xab""" + return chr(int(s[1:3], 16)) + + +def quote(c): + return '=%02X' % ord(c) + + + +def header_encode(header_bytes, charset='iso-8859-1'): + """Encode a single header line with quoted-printable (like) encoding. + + Defined in RFC 2045, this `Q' encoding is similar to quoted-printable, but + used specifically for email header fields to allow charsets with mostly 7 + bit characters (and some 8 bit) to remain more or less readable in non-RFC + 2045 aware mail clients. + + charset names the character set to use in the RFC 2046 header. It + defaults to iso-8859-1. + """ + # Return empty headers as an empty string. + if not header_bytes: + return '' + # Iterate over every byte, encoding if necessary. + encoded = [] + for octet in header_bytes: + encoded.append(_QUOPRI_HEADER_MAP[octet]) + # Now add the RFC chrome to each encoded chunk and glue the chunks + # together. + return '=?%s?q?%s?=' % (charset, EMPTYSTRING.join(encoded)) + + +class _body_accumulator(io.StringIO): + + def __init__(self, maxlinelen, eol, *args, **kw): + super().__init__(*args, **kw) + self.eol = eol + self.maxlinelen = self.room = maxlinelen + + def write_str(self, s): + """Add string s to the accumulated body.""" + self.write(s) + self.room -= len(s) + + def newline(self): + """Write eol, then start new line.""" + self.write_str(self.eol) + self.room = self.maxlinelen + + def write_soft_break(self): + """Write a soft break, then start a new line.""" + self.write_str('=') + self.newline() + + def write_wrapped(self, s, extra_room=0): + """Add a soft line break if needed, then write s.""" + if self.room < len(s) + extra_room: + self.write_soft_break() + self.write_str(s) + + def write_char(self, c, is_last_char): + if not is_last_char: + # Another character follows on this line, so we must leave + # extra room, either for it or a soft break, and whitespace + # need not be quoted. + self.write_wrapped(c, extra_room=1) + elif c not in ' \t': + # For this and remaining cases, no more characters follow, + # so there is no need to reserve extra room (since a hard + # break will immediately follow). + self.write_wrapped(c) + elif self.room >= 3: + # It's a whitespace character at end-of-line, and we have room + # for the three-character quoted encoding. + self.write(quote(c)) + elif self.room == 2: + # There's room for the whitespace character and a soft break. + self.write(c) + self.write_soft_break() + else: + # There's room only for a soft break. The quoted whitespace + # will be the only content on the subsequent line. + self.write_soft_break() + self.write(quote(c)) + + +def body_encode(body, maxlinelen=76, eol=NL): + """Encode with quoted-printable, wrapping at maxlinelen characters. + + Each line of encoded text will end with eol, which defaults to "\\n". Set + this to "\\r\\n" if you will be using the result of this function directly + in an email. + + Each line will be wrapped at, at most, maxlinelen characters before the + eol string (maxlinelen defaults to 76 characters, the maximum value + permitted by RFC 2045). Long lines will have the 'soft line break' + quoted-printable character "=" appended to them, so the decoded text will + be identical to the original text. + + The minimum maxlinelen is 4 to have room for a quoted character ("=XX") + followed by a soft line break. Smaller values will generate a + ValueError. + + """ + + if maxlinelen < 4: + raise ValueError("maxlinelen must be at least 4") + if not body: + return body + + # The last line may or may not end in eol, but all other lines do. + last_has_eol = (body[-1] in '\r\n') + + # This accumulator will make it easier to build the encoded body. + encoded_body = _body_accumulator(maxlinelen, eol) + + lines = body.splitlines() + last_line_no = len(lines) - 1 + for line_no, line in enumerate(lines): + last_char_index = len(line) - 1 + for i, c in enumerate(line): + if body_check(ord(c)): + c = quote(c) + encoded_body.write_char(c, i==last_char_index) + # Add an eol if input line had eol. All input lines have eol except + # possibly the last one. + if line_no < last_line_no or last_has_eol: + encoded_body.newline() + + return encoded_body.getvalue() + + + +# BAW: I'm not sure if the intent was for the signature of this function to be +# the same as base64MIME.decode() or not... +def decode(encoded, eol=NL): + """Decode a quoted-printable string. + + Lines are separated with eol, which defaults to \\n. + """ + if not encoded: + return encoded + # BAW: see comment in encode() above. Again, we're building up the + # decoded string with string concatenation, which could be done much more + # efficiently. + decoded = '' + + for line in encoded.splitlines(): + line = line.rstrip() + if not line: + decoded += eol + continue + + i = 0 + n = len(line) + while i < n: + c = line[i] + if c != '=': + decoded += c + i += 1 + # Otherwise, c == "=". Are we at the end of the line? If so, add + # a soft line break. + elif i+1 == n: + i += 1 + continue + # Decode if in form =AB + elif i+2 < n and line[i+1] in hexdigits and line[i+2] in hexdigits: + decoded += unquote(line[i:i+3]) + i += 3 + # Otherwise, not in form =AB, pass literally + else: + decoded += c + i += 1 + + if i == n: + decoded += eol + # Special case if original string did not end with eol + if encoded[-1] not in '\r\n' and decoded.endswith(eol): + decoded = decoded[:-1] + return decoded + + +# For convenience and backwards compatibility w/ standard base64 module +body_decode = decode +decodestring = decode + + + +def _unquote_match(match): + """Turn a match in the form =AB to the ASCII character with value 0xab""" + s = match.group(0) + return unquote(s) + + +# Header decoding is done a bit differently +def header_decode(s): + """Decode a string encoded with RFC 2045 MIME header `Q' encoding. + + This function does not parse a full MIME header value encoded with + quoted-printable (like =?iso-8895-1?q?Hello_World?=) -- please use + the high level email.header class for that functionality. + """ + s = s.replace('_', ' ') + return re.sub(r'=[a-fA-F0-9]{2}', _unquote_match, s, re.ASCII) diff --git a/venv/lib/python3.10/site-packages/future/backports/email/utils.py b/venv/lib/python3.10/site-packages/future/backports/email/utils.py new file mode 100644 index 0000000..4abebf7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/email/utils.py @@ -0,0 +1,400 @@ +# Copyright (C) 2001-2010 Python Software Foundation +# Author: Barry Warsaw +# Contact: email-sig@python.org + +"""Miscellaneous utilities.""" + +from __future__ import unicode_literals +from __future__ import division +from __future__ import absolute_import +from future import utils +from future.builtins import bytes, int, str + +__all__ = [ + 'collapse_rfc2231_value', + 'decode_params', + 'decode_rfc2231', + 'encode_rfc2231', + 'formataddr', + 'formatdate', + 'format_datetime', + 'getaddresses', + 'make_msgid', + 'mktime_tz', + 'parseaddr', + 'parsedate', + 'parsedate_tz', + 'parsedate_to_datetime', + 'unquote', + ] + +import os +import re +if utils.PY2: + re.ASCII = 0 +import time +import base64 +import random +import socket +from future.backports import datetime +from future.backports.urllib.parse import quote as url_quote, unquote as url_unquote +import warnings +from io import StringIO + +from future.backports.email._parseaddr import quote +from future.backports.email._parseaddr import AddressList as _AddressList +from future.backports.email._parseaddr import mktime_tz + +from future.backports.email._parseaddr import parsedate, parsedate_tz, _parsedate_tz + +from quopri import decodestring as _qdecode + +# Intrapackage imports +from future.backports.email.encoders import _bencode, _qencode +from future.backports.email.charset import Charset + +COMMASPACE = ', ' +EMPTYSTRING = '' +UEMPTYSTRING = '' +CRLF = '\r\n' +TICK = "'" + +specialsre = re.compile(r'[][\\()<>@,:;".]') +escapesre = re.compile(r'[\\"]') + +# How to figure out if we are processing strings that come from a byte +# source with undecodable characters. +_has_surrogates = re.compile( + '([^\ud800-\udbff]|\A)[\udc00-\udfff]([^\udc00-\udfff]|\Z)').search + +# How to deal with a string containing bytes before handing it to the +# application through the 'normal' interface. +def _sanitize(string): + # Turn any escaped bytes into unicode 'unknown' char. + original_bytes = string.encode('ascii', 'surrogateescape') + return original_bytes.decode('ascii', 'replace') + + +# Helpers + +def formataddr(pair, charset='utf-8'): + """The inverse of parseaddr(), this takes a 2-tuple of the form + (realname, email_address) and returns the string value suitable + for an RFC 2822 From, To or Cc header. + + If the first element of pair is false, then the second element is + returned unmodified. + + Optional charset if given is the character set that is used to encode + realname in case realname is not ASCII safe. Can be an instance of str or + a Charset-like object which has a header_encode method. Default is + 'utf-8'. + """ + name, address = pair + # The address MUST (per RFC) be ascii, so raise an UnicodeError if it isn't. + address.encode('ascii') + if name: + try: + name.encode('ascii') + except UnicodeEncodeError: + if isinstance(charset, str): + charset = Charset(charset) + encoded_name = charset.header_encode(name) + return "%s <%s>" % (encoded_name, address) + else: + quotes = '' + if specialsre.search(name): + quotes = '"' + name = escapesre.sub(r'\\\g<0>', name) + return '%s%s%s <%s>' % (quotes, name, quotes, address) + return address + + + +def getaddresses(fieldvalues): + """Return a list of (REALNAME, EMAIL) for each fieldvalue.""" + all = COMMASPACE.join(fieldvalues) + a = _AddressList(all) + return a.addresslist + + + +ecre = re.compile(r''' + =\? # literal =? + (?P[^?]*?) # non-greedy up to the next ? is the charset + \? # literal ? + (?P[qb]) # either a "q" or a "b", case insensitive + \? # literal ? + (?P.*?) # non-greedy up to the next ?= is the atom + \?= # literal ?= + ''', re.VERBOSE | re.IGNORECASE) + + +def _format_timetuple_and_zone(timetuple, zone): + return '%s, %02d %s %04d %02d:%02d:%02d %s' % ( + ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][timetuple[6]], + timetuple[2], + ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][timetuple[1] - 1], + timetuple[0], timetuple[3], timetuple[4], timetuple[5], + zone) + +def formatdate(timeval=None, localtime=False, usegmt=False): + """Returns a date string as specified by RFC 2822, e.g.: + + Fri, 09 Nov 2001 01:08:47 -0000 + + Optional timeval if given is a floating point time value as accepted by + gmtime() and localtime(), otherwise the current time is used. + + Optional localtime is a flag that when True, interprets timeval, and + returns a date relative to the local timezone instead of UTC, properly + taking daylight savings time into account. + + Optional argument usegmt means that the timezone is written out as + an ascii string, not numeric one (so "GMT" instead of "+0000"). This + is needed for HTTP, and is only used when localtime==False. + """ + # Note: we cannot use strftime() because that honors the locale and RFC + # 2822 requires that day and month names be the English abbreviations. + if timeval is None: + timeval = time.time() + if localtime: + now = time.localtime(timeval) + # Calculate timezone offset, based on whether the local zone has + # daylight savings time, and whether DST is in effect. + if time.daylight and now[-1]: + offset = time.altzone + else: + offset = time.timezone + hours, minutes = divmod(abs(offset), 3600) + # Remember offset is in seconds west of UTC, but the timezone is in + # minutes east of UTC, so the signs differ. + if offset > 0: + sign = '-' + else: + sign = '+' + zone = '%s%02d%02d' % (sign, hours, minutes // 60) + else: + now = time.gmtime(timeval) + # Timezone offset is always -0000 + if usegmt: + zone = 'GMT' + else: + zone = '-0000' + return _format_timetuple_and_zone(now, zone) + +def format_datetime(dt, usegmt=False): + """Turn a datetime into a date string as specified in RFC 2822. + + If usegmt is True, dt must be an aware datetime with an offset of zero. In + this case 'GMT' will be rendered instead of the normal +0000 required by + RFC2822. This is to support HTTP headers involving date stamps. + """ + now = dt.timetuple() + if usegmt: + if dt.tzinfo is None or dt.tzinfo != datetime.timezone.utc: + raise ValueError("usegmt option requires a UTC datetime") + zone = 'GMT' + elif dt.tzinfo is None: + zone = '-0000' + else: + zone = dt.strftime("%z") + return _format_timetuple_and_zone(now, zone) + + +def make_msgid(idstring=None, domain=None): + """Returns a string suitable for RFC 2822 compliant Message-ID, e.g: + + <20020201195627.33539.96671@nightshade.la.mastaler.com> + + Optional idstring if given is a string used to strengthen the + uniqueness of the message id. Optional domain if given provides the + portion of the message id after the '@'. It defaults to the locally + defined hostname. + """ + timeval = time.time() + utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval)) + pid = os.getpid() + randint = random.randrange(100000) + if idstring is None: + idstring = '' + else: + idstring = '.' + idstring + if domain is None: + domain = socket.getfqdn() + msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, domain) + return msgid + + +def parsedate_to_datetime(data): + _3to2list = list(_parsedate_tz(data)) + dtuple, tz, = [_3to2list[:-1]] + _3to2list[-1:] + if tz is None: + return datetime.datetime(*dtuple[:6]) + return datetime.datetime(*dtuple[:6], + tzinfo=datetime.timezone(datetime.timedelta(seconds=tz))) + + +def parseaddr(addr): + addrs = _AddressList(addr).addresslist + if not addrs: + return '', '' + return addrs[0] + + +# rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3. +def unquote(str): + """Remove quotes from a string.""" + if len(str) > 1: + if str.startswith('"') and str.endswith('"'): + return str[1:-1].replace('\\\\', '\\').replace('\\"', '"') + if str.startswith('<') and str.endswith('>'): + return str[1:-1] + return str + + + +# RFC2231-related functions - parameter encoding and decoding +def decode_rfc2231(s): + """Decode string according to RFC 2231""" + parts = s.split(TICK, 2) + if len(parts) <= 2: + return None, None, s + return parts + + +def encode_rfc2231(s, charset=None, language=None): + """Encode string according to RFC 2231. + + If neither charset nor language is given, then s is returned as-is. If + charset is given but not language, the string is encoded using the empty + string for language. + """ + s = url_quote(s, safe='', encoding=charset or 'ascii') + if charset is None and language is None: + return s + if language is None: + language = '' + return "%s'%s'%s" % (charset, language, s) + + +rfc2231_continuation = re.compile(r'^(?P\w+)\*((?P[0-9]+)\*?)?$', + re.ASCII) + +def decode_params(params): + """Decode parameters list according to RFC 2231. + + params is a sequence of 2-tuples containing (param name, string value). + """ + # Copy params so we don't mess with the original + params = params[:] + new_params = [] + # Map parameter's name to a list of continuations. The values are a + # 3-tuple of the continuation number, the string value, and a flag + # specifying whether a particular segment is %-encoded. + rfc2231_params = {} + name, value = params.pop(0) + new_params.append((name, value)) + while params: + name, value = params.pop(0) + if name.endswith('*'): + encoded = True + else: + encoded = False + value = unquote(value) + mo = rfc2231_continuation.match(name) + if mo: + name, num = mo.group('name', 'num') + if num is not None: + num = int(num) + rfc2231_params.setdefault(name, []).append((num, value, encoded)) + else: + new_params.append((name, '"%s"' % quote(value))) + if rfc2231_params: + for name, continuations in rfc2231_params.items(): + value = [] + extended = False + # Sort by number + continuations.sort() + # And now append all values in numerical order, converting + # %-encodings for the encoded segments. If any of the + # continuation names ends in a *, then the entire string, after + # decoding segments and concatenating, must have the charset and + # language specifiers at the beginning of the string. + for num, s, encoded in continuations: + if encoded: + # Decode as "latin-1", so the characters in s directly + # represent the percent-encoded octet values. + # collapse_rfc2231_value treats this as an octet sequence. + s = url_unquote(s, encoding="latin-1") + extended = True + value.append(s) + value = quote(EMPTYSTRING.join(value)) + if extended: + charset, language, value = decode_rfc2231(value) + new_params.append((name, (charset, language, '"%s"' % value))) + else: + new_params.append((name, '"%s"' % value)) + return new_params + +def collapse_rfc2231_value(value, errors='replace', + fallback_charset='us-ascii'): + if not isinstance(value, tuple) or len(value) != 3: + return unquote(value) + # While value comes to us as a unicode string, we need it to be a bytes + # object. We do not want bytes() normal utf-8 decoder, we want a straight + # interpretation of the string as character bytes. + charset, language, text = value + rawbytes = bytes(text, 'raw-unicode-escape') + try: + return str(rawbytes, charset, errors) + except LookupError: + # charset is not a known codec. + return unquote(text) + + +# +# datetime doesn't provide a localtime function yet, so provide one. Code +# adapted from the patch in issue 9527. This may not be perfect, but it is +# better than not having it. +# + +def localtime(dt=None, isdst=-1): + """Return local time as an aware datetime object. + + If called without arguments, return current time. Otherwise *dt* + argument should be a datetime instance, and it is converted to the + local time zone according to the system time zone database. If *dt* is + naive (that is, dt.tzinfo is None), it is assumed to be in local time. + In this case, a positive or zero value for *isdst* causes localtime to + presume initially that summer time (for example, Daylight Saving Time) + is or is not (respectively) in effect for the specified time. A + negative value for *isdst* causes the localtime() function to attempt + to divine whether summer time is in effect for the specified time. + + """ + if dt is None: + return datetime.datetime.now(datetime.timezone.utc).astimezone() + if dt.tzinfo is not None: + return dt.astimezone() + # We have a naive datetime. Convert to a (localtime) timetuple and pass to + # system mktime together with the isdst hint. System mktime will return + # seconds since epoch. + tm = dt.timetuple()[:-1] + (isdst,) + seconds = time.mktime(tm) + localtm = time.localtime(seconds) + try: + delta = datetime.timedelta(seconds=localtm.tm_gmtoff) + tz = datetime.timezone(delta, localtm.tm_zone) + except AttributeError: + # Compute UTC offset and compare with the value implied by tm_isdst. + # If the values match, use the zone name implied by tm_isdst. + delta = dt - datetime.datetime(*time.gmtime(seconds)[:6]) + dst = time.daylight and localtm.tm_isdst > 0 + gmtoff = -(time.altzone if dst else time.timezone) + if delta == datetime.timedelta(seconds=gmtoff): + tz = datetime.timezone(delta, time.tzname[dst]) + else: + tz = datetime.timezone(delta) + return dt.replace(tzinfo=tz) diff --git a/venv/lib/python3.10/site-packages/future/backports/html/__init__.py b/venv/lib/python3.10/site-packages/future/backports/html/__init__.py new file mode 100644 index 0000000..58e133f --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/html/__init__.py @@ -0,0 +1,27 @@ +""" +General functions for HTML manipulation, backported from Py3. + +Note that this uses Python 2.7 code with the corresponding Python 3 +module names and locations. +""" + +from __future__ import unicode_literals + + +_escape_map = {ord('&'): '&', ord('<'): '<', ord('>'): '>'} +_escape_map_full = {ord('&'): '&', ord('<'): '<', ord('>'): '>', + ord('"'): '"', ord('\''): '''} + +# NB: this is a candidate for a bytes/string polymorphic interface + +def escape(s, quote=True): + """ + Replace special characters "&", "<" and ">" to HTML-safe sequences. + If the optional flag quote is true (the default), the quotation mark + characters, both double quote (") and single quote (') characters are also + translated. + """ + assert not isinstance(s, bytes), 'Pass a unicode string' + if quote: + return s.translate(_escape_map_full) + return s.translate(_escape_map) diff --git a/venv/lib/python3.10/site-packages/future/backports/html/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/html/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..cc73032 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/html/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/html/__pycache__/entities.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/html/__pycache__/entities.cpython-310.pyc new file mode 100644 index 0000000..0857526 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/html/__pycache__/entities.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/html/__pycache__/parser.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/html/__pycache__/parser.cpython-310.pyc new file mode 100644 index 0000000..885a605 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/html/__pycache__/parser.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/html/entities.py b/venv/lib/python3.10/site-packages/future/backports/html/entities.py new file mode 100644 index 0000000..5c73f69 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/html/entities.py @@ -0,0 +1,2514 @@ +"""HTML character entity references. + +Backported for python-future from Python 3.3 +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) +from future.builtins import * + + +# maps the HTML entity name to the Unicode codepoint +name2codepoint = { + 'AElig': 0x00c6, # latin capital letter AE = latin capital ligature AE, U+00C6 ISOlat1 + 'Aacute': 0x00c1, # latin capital letter A with acute, U+00C1 ISOlat1 + 'Acirc': 0x00c2, # latin capital letter A with circumflex, U+00C2 ISOlat1 + 'Agrave': 0x00c0, # latin capital letter A with grave = latin capital letter A grave, U+00C0 ISOlat1 + 'Alpha': 0x0391, # greek capital letter alpha, U+0391 + 'Aring': 0x00c5, # latin capital letter A with ring above = latin capital letter A ring, U+00C5 ISOlat1 + 'Atilde': 0x00c3, # latin capital letter A with tilde, U+00C3 ISOlat1 + 'Auml': 0x00c4, # latin capital letter A with diaeresis, U+00C4 ISOlat1 + 'Beta': 0x0392, # greek capital letter beta, U+0392 + 'Ccedil': 0x00c7, # latin capital letter C with cedilla, U+00C7 ISOlat1 + 'Chi': 0x03a7, # greek capital letter chi, U+03A7 + 'Dagger': 0x2021, # double dagger, U+2021 ISOpub + 'Delta': 0x0394, # greek capital letter delta, U+0394 ISOgrk3 + 'ETH': 0x00d0, # latin capital letter ETH, U+00D0 ISOlat1 + 'Eacute': 0x00c9, # latin capital letter E with acute, U+00C9 ISOlat1 + 'Ecirc': 0x00ca, # latin capital letter E with circumflex, U+00CA ISOlat1 + 'Egrave': 0x00c8, # latin capital letter E with grave, U+00C8 ISOlat1 + 'Epsilon': 0x0395, # greek capital letter epsilon, U+0395 + 'Eta': 0x0397, # greek capital letter eta, U+0397 + 'Euml': 0x00cb, # latin capital letter E with diaeresis, U+00CB ISOlat1 + 'Gamma': 0x0393, # greek capital letter gamma, U+0393 ISOgrk3 + 'Iacute': 0x00cd, # latin capital letter I with acute, U+00CD ISOlat1 + 'Icirc': 0x00ce, # latin capital letter I with circumflex, U+00CE ISOlat1 + 'Igrave': 0x00cc, # latin capital letter I with grave, U+00CC ISOlat1 + 'Iota': 0x0399, # greek capital letter iota, U+0399 + 'Iuml': 0x00cf, # latin capital letter I with diaeresis, U+00CF ISOlat1 + 'Kappa': 0x039a, # greek capital letter kappa, U+039A + 'Lambda': 0x039b, # greek capital letter lambda, U+039B ISOgrk3 + 'Mu': 0x039c, # greek capital letter mu, U+039C + 'Ntilde': 0x00d1, # latin capital letter N with tilde, U+00D1 ISOlat1 + 'Nu': 0x039d, # greek capital letter nu, U+039D + 'OElig': 0x0152, # latin capital ligature OE, U+0152 ISOlat2 + 'Oacute': 0x00d3, # latin capital letter O with acute, U+00D3 ISOlat1 + 'Ocirc': 0x00d4, # latin capital letter O with circumflex, U+00D4 ISOlat1 + 'Ograve': 0x00d2, # latin capital letter O with grave, U+00D2 ISOlat1 + 'Omega': 0x03a9, # greek capital letter omega, U+03A9 ISOgrk3 + 'Omicron': 0x039f, # greek capital letter omicron, U+039F + 'Oslash': 0x00d8, # latin capital letter O with stroke = latin capital letter O slash, U+00D8 ISOlat1 + 'Otilde': 0x00d5, # latin capital letter O with tilde, U+00D5 ISOlat1 + 'Ouml': 0x00d6, # latin capital letter O with diaeresis, U+00D6 ISOlat1 + 'Phi': 0x03a6, # greek capital letter phi, U+03A6 ISOgrk3 + 'Pi': 0x03a0, # greek capital letter pi, U+03A0 ISOgrk3 + 'Prime': 0x2033, # double prime = seconds = inches, U+2033 ISOtech + 'Psi': 0x03a8, # greek capital letter psi, U+03A8 ISOgrk3 + 'Rho': 0x03a1, # greek capital letter rho, U+03A1 + 'Scaron': 0x0160, # latin capital letter S with caron, U+0160 ISOlat2 + 'Sigma': 0x03a3, # greek capital letter sigma, U+03A3 ISOgrk3 + 'THORN': 0x00de, # latin capital letter THORN, U+00DE ISOlat1 + 'Tau': 0x03a4, # greek capital letter tau, U+03A4 + 'Theta': 0x0398, # greek capital letter theta, U+0398 ISOgrk3 + 'Uacute': 0x00da, # latin capital letter U with acute, U+00DA ISOlat1 + 'Ucirc': 0x00db, # latin capital letter U with circumflex, U+00DB ISOlat1 + 'Ugrave': 0x00d9, # latin capital letter U with grave, U+00D9 ISOlat1 + 'Upsilon': 0x03a5, # greek capital letter upsilon, U+03A5 ISOgrk3 + 'Uuml': 0x00dc, # latin capital letter U with diaeresis, U+00DC ISOlat1 + 'Xi': 0x039e, # greek capital letter xi, U+039E ISOgrk3 + 'Yacute': 0x00dd, # latin capital letter Y with acute, U+00DD ISOlat1 + 'Yuml': 0x0178, # latin capital letter Y with diaeresis, U+0178 ISOlat2 + 'Zeta': 0x0396, # greek capital letter zeta, U+0396 + 'aacute': 0x00e1, # latin small letter a with acute, U+00E1 ISOlat1 + 'acirc': 0x00e2, # latin small letter a with circumflex, U+00E2 ISOlat1 + 'acute': 0x00b4, # acute accent = spacing acute, U+00B4 ISOdia + 'aelig': 0x00e6, # latin small letter ae = latin small ligature ae, U+00E6 ISOlat1 + 'agrave': 0x00e0, # latin small letter a with grave = latin small letter a grave, U+00E0 ISOlat1 + 'alefsym': 0x2135, # alef symbol = first transfinite cardinal, U+2135 NEW + 'alpha': 0x03b1, # greek small letter alpha, U+03B1 ISOgrk3 + 'amp': 0x0026, # ampersand, U+0026 ISOnum + 'and': 0x2227, # logical and = wedge, U+2227 ISOtech + 'ang': 0x2220, # angle, U+2220 ISOamso + 'aring': 0x00e5, # latin small letter a with ring above = latin small letter a ring, U+00E5 ISOlat1 + 'asymp': 0x2248, # almost equal to = asymptotic to, U+2248 ISOamsr + 'atilde': 0x00e3, # latin small letter a with tilde, U+00E3 ISOlat1 + 'auml': 0x00e4, # latin small letter a with diaeresis, U+00E4 ISOlat1 + 'bdquo': 0x201e, # double low-9 quotation mark, U+201E NEW + 'beta': 0x03b2, # greek small letter beta, U+03B2 ISOgrk3 + 'brvbar': 0x00a6, # broken bar = broken vertical bar, U+00A6 ISOnum + 'bull': 0x2022, # bullet = black small circle, U+2022 ISOpub + 'cap': 0x2229, # intersection = cap, U+2229 ISOtech + 'ccedil': 0x00e7, # latin small letter c with cedilla, U+00E7 ISOlat1 + 'cedil': 0x00b8, # cedilla = spacing cedilla, U+00B8 ISOdia + 'cent': 0x00a2, # cent sign, U+00A2 ISOnum + 'chi': 0x03c7, # greek small letter chi, U+03C7 ISOgrk3 + 'circ': 0x02c6, # modifier letter circumflex accent, U+02C6 ISOpub + 'clubs': 0x2663, # black club suit = shamrock, U+2663 ISOpub + 'cong': 0x2245, # approximately equal to, U+2245 ISOtech + 'copy': 0x00a9, # copyright sign, U+00A9 ISOnum + 'crarr': 0x21b5, # downwards arrow with corner leftwards = carriage return, U+21B5 NEW + 'cup': 0x222a, # union = cup, U+222A ISOtech + 'curren': 0x00a4, # currency sign, U+00A4 ISOnum + 'dArr': 0x21d3, # downwards double arrow, U+21D3 ISOamsa + 'dagger': 0x2020, # dagger, U+2020 ISOpub + 'darr': 0x2193, # downwards arrow, U+2193 ISOnum + 'deg': 0x00b0, # degree sign, U+00B0 ISOnum + 'delta': 0x03b4, # greek small letter delta, U+03B4 ISOgrk3 + 'diams': 0x2666, # black diamond suit, U+2666 ISOpub + 'divide': 0x00f7, # division sign, U+00F7 ISOnum + 'eacute': 0x00e9, # latin small letter e with acute, U+00E9 ISOlat1 + 'ecirc': 0x00ea, # latin small letter e with circumflex, U+00EA ISOlat1 + 'egrave': 0x00e8, # latin small letter e with grave, U+00E8 ISOlat1 + 'empty': 0x2205, # empty set = null set = diameter, U+2205 ISOamso + 'emsp': 0x2003, # em space, U+2003 ISOpub + 'ensp': 0x2002, # en space, U+2002 ISOpub + 'epsilon': 0x03b5, # greek small letter epsilon, U+03B5 ISOgrk3 + 'equiv': 0x2261, # identical to, U+2261 ISOtech + 'eta': 0x03b7, # greek small letter eta, U+03B7 ISOgrk3 + 'eth': 0x00f0, # latin small letter eth, U+00F0 ISOlat1 + 'euml': 0x00eb, # latin small letter e with diaeresis, U+00EB ISOlat1 + 'euro': 0x20ac, # euro sign, U+20AC NEW + 'exist': 0x2203, # there exists, U+2203 ISOtech + 'fnof': 0x0192, # latin small f with hook = function = florin, U+0192 ISOtech + 'forall': 0x2200, # for all, U+2200 ISOtech + 'frac12': 0x00bd, # vulgar fraction one half = fraction one half, U+00BD ISOnum + 'frac14': 0x00bc, # vulgar fraction one quarter = fraction one quarter, U+00BC ISOnum + 'frac34': 0x00be, # vulgar fraction three quarters = fraction three quarters, U+00BE ISOnum + 'frasl': 0x2044, # fraction slash, U+2044 NEW + 'gamma': 0x03b3, # greek small letter gamma, U+03B3 ISOgrk3 + 'ge': 0x2265, # greater-than or equal to, U+2265 ISOtech + 'gt': 0x003e, # greater-than sign, U+003E ISOnum + 'hArr': 0x21d4, # left right double arrow, U+21D4 ISOamsa + 'harr': 0x2194, # left right arrow, U+2194 ISOamsa + 'hearts': 0x2665, # black heart suit = valentine, U+2665 ISOpub + 'hellip': 0x2026, # horizontal ellipsis = three dot leader, U+2026 ISOpub + 'iacute': 0x00ed, # latin small letter i with acute, U+00ED ISOlat1 + 'icirc': 0x00ee, # latin small letter i with circumflex, U+00EE ISOlat1 + 'iexcl': 0x00a1, # inverted exclamation mark, U+00A1 ISOnum + 'igrave': 0x00ec, # latin small letter i with grave, U+00EC ISOlat1 + 'image': 0x2111, # blackletter capital I = imaginary part, U+2111 ISOamso + 'infin': 0x221e, # infinity, U+221E ISOtech + 'int': 0x222b, # integral, U+222B ISOtech + 'iota': 0x03b9, # greek small letter iota, U+03B9 ISOgrk3 + 'iquest': 0x00bf, # inverted question mark = turned question mark, U+00BF ISOnum + 'isin': 0x2208, # element of, U+2208 ISOtech + 'iuml': 0x00ef, # latin small letter i with diaeresis, U+00EF ISOlat1 + 'kappa': 0x03ba, # greek small letter kappa, U+03BA ISOgrk3 + 'lArr': 0x21d0, # leftwards double arrow, U+21D0 ISOtech + 'lambda': 0x03bb, # greek small letter lambda, U+03BB ISOgrk3 + 'lang': 0x2329, # left-pointing angle bracket = bra, U+2329 ISOtech + 'laquo': 0x00ab, # left-pointing double angle quotation mark = left pointing guillemet, U+00AB ISOnum + 'larr': 0x2190, # leftwards arrow, U+2190 ISOnum + 'lceil': 0x2308, # left ceiling = apl upstile, U+2308 ISOamsc + 'ldquo': 0x201c, # left double quotation mark, U+201C ISOnum + 'le': 0x2264, # less-than or equal to, U+2264 ISOtech + 'lfloor': 0x230a, # left floor = apl downstile, U+230A ISOamsc + 'lowast': 0x2217, # asterisk operator, U+2217 ISOtech + 'loz': 0x25ca, # lozenge, U+25CA ISOpub + 'lrm': 0x200e, # left-to-right mark, U+200E NEW RFC 2070 + 'lsaquo': 0x2039, # single left-pointing angle quotation mark, U+2039 ISO proposed + 'lsquo': 0x2018, # left single quotation mark, U+2018 ISOnum + 'lt': 0x003c, # less-than sign, U+003C ISOnum + 'macr': 0x00af, # macron = spacing macron = overline = APL overbar, U+00AF ISOdia + 'mdash': 0x2014, # em dash, U+2014 ISOpub + 'micro': 0x00b5, # micro sign, U+00B5 ISOnum + 'middot': 0x00b7, # middle dot = Georgian comma = Greek middle dot, U+00B7 ISOnum + 'minus': 0x2212, # minus sign, U+2212 ISOtech + 'mu': 0x03bc, # greek small letter mu, U+03BC ISOgrk3 + 'nabla': 0x2207, # nabla = backward difference, U+2207 ISOtech + 'nbsp': 0x00a0, # no-break space = non-breaking space, U+00A0 ISOnum + 'ndash': 0x2013, # en dash, U+2013 ISOpub + 'ne': 0x2260, # not equal to, U+2260 ISOtech + 'ni': 0x220b, # contains as member, U+220B ISOtech + 'not': 0x00ac, # not sign, U+00AC ISOnum + 'notin': 0x2209, # not an element of, U+2209 ISOtech + 'nsub': 0x2284, # not a subset of, U+2284 ISOamsn + 'ntilde': 0x00f1, # latin small letter n with tilde, U+00F1 ISOlat1 + 'nu': 0x03bd, # greek small letter nu, U+03BD ISOgrk3 + 'oacute': 0x00f3, # latin small letter o with acute, U+00F3 ISOlat1 + 'ocirc': 0x00f4, # latin small letter o with circumflex, U+00F4 ISOlat1 + 'oelig': 0x0153, # latin small ligature oe, U+0153 ISOlat2 + 'ograve': 0x00f2, # latin small letter o with grave, U+00F2 ISOlat1 + 'oline': 0x203e, # overline = spacing overscore, U+203E NEW + 'omega': 0x03c9, # greek small letter omega, U+03C9 ISOgrk3 + 'omicron': 0x03bf, # greek small letter omicron, U+03BF NEW + 'oplus': 0x2295, # circled plus = direct sum, U+2295 ISOamsb + 'or': 0x2228, # logical or = vee, U+2228 ISOtech + 'ordf': 0x00aa, # feminine ordinal indicator, U+00AA ISOnum + 'ordm': 0x00ba, # masculine ordinal indicator, U+00BA ISOnum + 'oslash': 0x00f8, # latin small letter o with stroke, = latin small letter o slash, U+00F8 ISOlat1 + 'otilde': 0x00f5, # latin small letter o with tilde, U+00F5 ISOlat1 + 'otimes': 0x2297, # circled times = vector product, U+2297 ISOamsb + 'ouml': 0x00f6, # latin small letter o with diaeresis, U+00F6 ISOlat1 + 'para': 0x00b6, # pilcrow sign = paragraph sign, U+00B6 ISOnum + 'part': 0x2202, # partial differential, U+2202 ISOtech + 'permil': 0x2030, # per mille sign, U+2030 ISOtech + 'perp': 0x22a5, # up tack = orthogonal to = perpendicular, U+22A5 ISOtech + 'phi': 0x03c6, # greek small letter phi, U+03C6 ISOgrk3 + 'pi': 0x03c0, # greek small letter pi, U+03C0 ISOgrk3 + 'piv': 0x03d6, # greek pi symbol, U+03D6 ISOgrk3 + 'plusmn': 0x00b1, # plus-minus sign = plus-or-minus sign, U+00B1 ISOnum + 'pound': 0x00a3, # pound sign, U+00A3 ISOnum + 'prime': 0x2032, # prime = minutes = feet, U+2032 ISOtech + 'prod': 0x220f, # n-ary product = product sign, U+220F ISOamsb + 'prop': 0x221d, # proportional to, U+221D ISOtech + 'psi': 0x03c8, # greek small letter psi, U+03C8 ISOgrk3 + 'quot': 0x0022, # quotation mark = APL quote, U+0022 ISOnum + 'rArr': 0x21d2, # rightwards double arrow, U+21D2 ISOtech + 'radic': 0x221a, # square root = radical sign, U+221A ISOtech + 'rang': 0x232a, # right-pointing angle bracket = ket, U+232A ISOtech + 'raquo': 0x00bb, # right-pointing double angle quotation mark = right pointing guillemet, U+00BB ISOnum + 'rarr': 0x2192, # rightwards arrow, U+2192 ISOnum + 'rceil': 0x2309, # right ceiling, U+2309 ISOamsc + 'rdquo': 0x201d, # right double quotation mark, U+201D ISOnum + 'real': 0x211c, # blackletter capital R = real part symbol, U+211C ISOamso + 'reg': 0x00ae, # registered sign = registered trade mark sign, U+00AE ISOnum + 'rfloor': 0x230b, # right floor, U+230B ISOamsc + 'rho': 0x03c1, # greek small letter rho, U+03C1 ISOgrk3 + 'rlm': 0x200f, # right-to-left mark, U+200F NEW RFC 2070 + 'rsaquo': 0x203a, # single right-pointing angle quotation mark, U+203A ISO proposed + 'rsquo': 0x2019, # right single quotation mark, U+2019 ISOnum + 'sbquo': 0x201a, # single low-9 quotation mark, U+201A NEW + 'scaron': 0x0161, # latin small letter s with caron, U+0161 ISOlat2 + 'sdot': 0x22c5, # dot operator, U+22C5 ISOamsb + 'sect': 0x00a7, # section sign, U+00A7 ISOnum + 'shy': 0x00ad, # soft hyphen = discretionary hyphen, U+00AD ISOnum + 'sigma': 0x03c3, # greek small letter sigma, U+03C3 ISOgrk3 + 'sigmaf': 0x03c2, # greek small letter final sigma, U+03C2 ISOgrk3 + 'sim': 0x223c, # tilde operator = varies with = similar to, U+223C ISOtech + 'spades': 0x2660, # black spade suit, U+2660 ISOpub + 'sub': 0x2282, # subset of, U+2282 ISOtech + 'sube': 0x2286, # subset of or equal to, U+2286 ISOtech + 'sum': 0x2211, # n-ary sumation, U+2211 ISOamsb + 'sup': 0x2283, # superset of, U+2283 ISOtech + 'sup1': 0x00b9, # superscript one = superscript digit one, U+00B9 ISOnum + 'sup2': 0x00b2, # superscript two = superscript digit two = squared, U+00B2 ISOnum + 'sup3': 0x00b3, # superscript three = superscript digit three = cubed, U+00B3 ISOnum + 'supe': 0x2287, # superset of or equal to, U+2287 ISOtech + 'szlig': 0x00df, # latin small letter sharp s = ess-zed, U+00DF ISOlat1 + 'tau': 0x03c4, # greek small letter tau, U+03C4 ISOgrk3 + 'there4': 0x2234, # therefore, U+2234 ISOtech + 'theta': 0x03b8, # greek small letter theta, U+03B8 ISOgrk3 + 'thetasym': 0x03d1, # greek small letter theta symbol, U+03D1 NEW + 'thinsp': 0x2009, # thin space, U+2009 ISOpub + 'thorn': 0x00fe, # latin small letter thorn with, U+00FE ISOlat1 + 'tilde': 0x02dc, # small tilde, U+02DC ISOdia + 'times': 0x00d7, # multiplication sign, U+00D7 ISOnum + 'trade': 0x2122, # trade mark sign, U+2122 ISOnum + 'uArr': 0x21d1, # upwards double arrow, U+21D1 ISOamsa + 'uacute': 0x00fa, # latin small letter u with acute, U+00FA ISOlat1 + 'uarr': 0x2191, # upwards arrow, U+2191 ISOnum + 'ucirc': 0x00fb, # latin small letter u with circumflex, U+00FB ISOlat1 + 'ugrave': 0x00f9, # latin small letter u with grave, U+00F9 ISOlat1 + 'uml': 0x00a8, # diaeresis = spacing diaeresis, U+00A8 ISOdia + 'upsih': 0x03d2, # greek upsilon with hook symbol, U+03D2 NEW + 'upsilon': 0x03c5, # greek small letter upsilon, U+03C5 ISOgrk3 + 'uuml': 0x00fc, # latin small letter u with diaeresis, U+00FC ISOlat1 + 'weierp': 0x2118, # script capital P = power set = Weierstrass p, U+2118 ISOamso + 'xi': 0x03be, # greek small letter xi, U+03BE ISOgrk3 + 'yacute': 0x00fd, # latin small letter y with acute, U+00FD ISOlat1 + 'yen': 0x00a5, # yen sign = yuan sign, U+00A5 ISOnum + 'yuml': 0x00ff, # latin small letter y with diaeresis, U+00FF ISOlat1 + 'zeta': 0x03b6, # greek small letter zeta, U+03B6 ISOgrk3 + 'zwj': 0x200d, # zero width joiner, U+200D NEW RFC 2070 + 'zwnj': 0x200c, # zero width non-joiner, U+200C NEW RFC 2070 +} + + +# maps the HTML5 named character references to the equivalent Unicode character(s) +html5 = { + 'Aacute': '\xc1', + 'aacute': '\xe1', + 'Aacute;': '\xc1', + 'aacute;': '\xe1', + 'Abreve;': '\u0102', + 'abreve;': '\u0103', + 'ac;': '\u223e', + 'acd;': '\u223f', + 'acE;': '\u223e\u0333', + 'Acirc': '\xc2', + 'acirc': '\xe2', + 'Acirc;': '\xc2', + 'acirc;': '\xe2', + 'acute': '\xb4', + 'acute;': '\xb4', + 'Acy;': '\u0410', + 'acy;': '\u0430', + 'AElig': '\xc6', + 'aelig': '\xe6', + 'AElig;': '\xc6', + 'aelig;': '\xe6', + 'af;': '\u2061', + 'Afr;': '\U0001d504', + 'afr;': '\U0001d51e', + 'Agrave': '\xc0', + 'agrave': '\xe0', + 'Agrave;': '\xc0', + 'agrave;': '\xe0', + 'alefsym;': '\u2135', + 'aleph;': '\u2135', + 'Alpha;': '\u0391', + 'alpha;': '\u03b1', + 'Amacr;': '\u0100', + 'amacr;': '\u0101', + 'amalg;': '\u2a3f', + 'AMP': '&', + 'amp': '&', + 'AMP;': '&', + 'amp;': '&', + 'And;': '\u2a53', + 'and;': '\u2227', + 'andand;': '\u2a55', + 'andd;': '\u2a5c', + 'andslope;': '\u2a58', + 'andv;': '\u2a5a', + 'ang;': '\u2220', + 'ange;': '\u29a4', + 'angle;': '\u2220', + 'angmsd;': '\u2221', + 'angmsdaa;': '\u29a8', + 'angmsdab;': '\u29a9', + 'angmsdac;': '\u29aa', + 'angmsdad;': '\u29ab', + 'angmsdae;': '\u29ac', + 'angmsdaf;': '\u29ad', + 'angmsdag;': '\u29ae', + 'angmsdah;': '\u29af', + 'angrt;': '\u221f', + 'angrtvb;': '\u22be', + 'angrtvbd;': '\u299d', + 'angsph;': '\u2222', + 'angst;': '\xc5', + 'angzarr;': '\u237c', + 'Aogon;': '\u0104', + 'aogon;': '\u0105', + 'Aopf;': '\U0001d538', + 'aopf;': '\U0001d552', + 'ap;': '\u2248', + 'apacir;': '\u2a6f', + 'apE;': '\u2a70', + 'ape;': '\u224a', + 'apid;': '\u224b', + 'apos;': "'", + 'ApplyFunction;': '\u2061', + 'approx;': '\u2248', + 'approxeq;': '\u224a', + 'Aring': '\xc5', + 'aring': '\xe5', + 'Aring;': '\xc5', + 'aring;': '\xe5', + 'Ascr;': '\U0001d49c', + 'ascr;': '\U0001d4b6', + 'Assign;': '\u2254', + 'ast;': '*', + 'asymp;': '\u2248', + 'asympeq;': '\u224d', + 'Atilde': '\xc3', + 'atilde': '\xe3', + 'Atilde;': '\xc3', + 'atilde;': '\xe3', + 'Auml': '\xc4', + 'auml': '\xe4', + 'Auml;': '\xc4', + 'auml;': '\xe4', + 'awconint;': '\u2233', + 'awint;': '\u2a11', + 'backcong;': '\u224c', + 'backepsilon;': '\u03f6', + 'backprime;': '\u2035', + 'backsim;': '\u223d', + 'backsimeq;': '\u22cd', + 'Backslash;': '\u2216', + 'Barv;': '\u2ae7', + 'barvee;': '\u22bd', + 'Barwed;': '\u2306', + 'barwed;': '\u2305', + 'barwedge;': '\u2305', + 'bbrk;': '\u23b5', + 'bbrktbrk;': '\u23b6', + 'bcong;': '\u224c', + 'Bcy;': '\u0411', + 'bcy;': '\u0431', + 'bdquo;': '\u201e', + 'becaus;': '\u2235', + 'Because;': '\u2235', + 'because;': '\u2235', + 'bemptyv;': '\u29b0', + 'bepsi;': '\u03f6', + 'bernou;': '\u212c', + 'Bernoullis;': '\u212c', + 'Beta;': '\u0392', + 'beta;': '\u03b2', + 'beth;': '\u2136', + 'between;': '\u226c', + 'Bfr;': '\U0001d505', + 'bfr;': '\U0001d51f', + 'bigcap;': '\u22c2', + 'bigcirc;': '\u25ef', + 'bigcup;': '\u22c3', + 'bigodot;': '\u2a00', + 'bigoplus;': '\u2a01', + 'bigotimes;': '\u2a02', + 'bigsqcup;': '\u2a06', + 'bigstar;': '\u2605', + 'bigtriangledown;': '\u25bd', + 'bigtriangleup;': '\u25b3', + 'biguplus;': '\u2a04', + 'bigvee;': '\u22c1', + 'bigwedge;': '\u22c0', + 'bkarow;': '\u290d', + 'blacklozenge;': '\u29eb', + 'blacksquare;': '\u25aa', + 'blacktriangle;': '\u25b4', + 'blacktriangledown;': '\u25be', + 'blacktriangleleft;': '\u25c2', + 'blacktriangleright;': '\u25b8', + 'blank;': '\u2423', + 'blk12;': '\u2592', + 'blk14;': '\u2591', + 'blk34;': '\u2593', + 'block;': '\u2588', + 'bne;': '=\u20e5', + 'bnequiv;': '\u2261\u20e5', + 'bNot;': '\u2aed', + 'bnot;': '\u2310', + 'Bopf;': '\U0001d539', + 'bopf;': '\U0001d553', + 'bot;': '\u22a5', + 'bottom;': '\u22a5', + 'bowtie;': '\u22c8', + 'boxbox;': '\u29c9', + 'boxDL;': '\u2557', + 'boxDl;': '\u2556', + 'boxdL;': '\u2555', + 'boxdl;': '\u2510', + 'boxDR;': '\u2554', + 'boxDr;': '\u2553', + 'boxdR;': '\u2552', + 'boxdr;': '\u250c', + 'boxH;': '\u2550', + 'boxh;': '\u2500', + 'boxHD;': '\u2566', + 'boxHd;': '\u2564', + 'boxhD;': '\u2565', + 'boxhd;': '\u252c', + 'boxHU;': '\u2569', + 'boxHu;': '\u2567', + 'boxhU;': '\u2568', + 'boxhu;': '\u2534', + 'boxminus;': '\u229f', + 'boxplus;': '\u229e', + 'boxtimes;': '\u22a0', + 'boxUL;': '\u255d', + 'boxUl;': '\u255c', + 'boxuL;': '\u255b', + 'boxul;': '\u2518', + 'boxUR;': '\u255a', + 'boxUr;': '\u2559', + 'boxuR;': '\u2558', + 'boxur;': '\u2514', + 'boxV;': '\u2551', + 'boxv;': '\u2502', + 'boxVH;': '\u256c', + 'boxVh;': '\u256b', + 'boxvH;': '\u256a', + 'boxvh;': '\u253c', + 'boxVL;': '\u2563', + 'boxVl;': '\u2562', + 'boxvL;': '\u2561', + 'boxvl;': '\u2524', + 'boxVR;': '\u2560', + 'boxVr;': '\u255f', + 'boxvR;': '\u255e', + 'boxvr;': '\u251c', + 'bprime;': '\u2035', + 'Breve;': '\u02d8', + 'breve;': '\u02d8', + 'brvbar': '\xa6', + 'brvbar;': '\xa6', + 'Bscr;': '\u212c', + 'bscr;': '\U0001d4b7', + 'bsemi;': '\u204f', + 'bsim;': '\u223d', + 'bsime;': '\u22cd', + 'bsol;': '\\', + 'bsolb;': '\u29c5', + 'bsolhsub;': '\u27c8', + 'bull;': '\u2022', + 'bullet;': '\u2022', + 'bump;': '\u224e', + 'bumpE;': '\u2aae', + 'bumpe;': '\u224f', + 'Bumpeq;': '\u224e', + 'bumpeq;': '\u224f', + 'Cacute;': '\u0106', + 'cacute;': '\u0107', + 'Cap;': '\u22d2', + 'cap;': '\u2229', + 'capand;': '\u2a44', + 'capbrcup;': '\u2a49', + 'capcap;': '\u2a4b', + 'capcup;': '\u2a47', + 'capdot;': '\u2a40', + 'CapitalDifferentialD;': '\u2145', + 'caps;': '\u2229\ufe00', + 'caret;': '\u2041', + 'caron;': '\u02c7', + 'Cayleys;': '\u212d', + 'ccaps;': '\u2a4d', + 'Ccaron;': '\u010c', + 'ccaron;': '\u010d', + 'Ccedil': '\xc7', + 'ccedil': '\xe7', + 'Ccedil;': '\xc7', + 'ccedil;': '\xe7', + 'Ccirc;': '\u0108', + 'ccirc;': '\u0109', + 'Cconint;': '\u2230', + 'ccups;': '\u2a4c', + 'ccupssm;': '\u2a50', + 'Cdot;': '\u010a', + 'cdot;': '\u010b', + 'cedil': '\xb8', + 'cedil;': '\xb8', + 'Cedilla;': '\xb8', + 'cemptyv;': '\u29b2', + 'cent': '\xa2', + 'cent;': '\xa2', + 'CenterDot;': '\xb7', + 'centerdot;': '\xb7', + 'Cfr;': '\u212d', + 'cfr;': '\U0001d520', + 'CHcy;': '\u0427', + 'chcy;': '\u0447', + 'check;': '\u2713', + 'checkmark;': '\u2713', + 'Chi;': '\u03a7', + 'chi;': '\u03c7', + 'cir;': '\u25cb', + 'circ;': '\u02c6', + 'circeq;': '\u2257', + 'circlearrowleft;': '\u21ba', + 'circlearrowright;': '\u21bb', + 'circledast;': '\u229b', + 'circledcirc;': '\u229a', + 'circleddash;': '\u229d', + 'CircleDot;': '\u2299', + 'circledR;': '\xae', + 'circledS;': '\u24c8', + 'CircleMinus;': '\u2296', + 'CirclePlus;': '\u2295', + 'CircleTimes;': '\u2297', + 'cirE;': '\u29c3', + 'cire;': '\u2257', + 'cirfnint;': '\u2a10', + 'cirmid;': '\u2aef', + 'cirscir;': '\u29c2', + 'ClockwiseContourIntegral;': '\u2232', + 'CloseCurlyDoubleQuote;': '\u201d', + 'CloseCurlyQuote;': '\u2019', + 'clubs;': '\u2663', + 'clubsuit;': '\u2663', + 'Colon;': '\u2237', + 'colon;': ':', + 'Colone;': '\u2a74', + 'colone;': '\u2254', + 'coloneq;': '\u2254', + 'comma;': ',', + 'commat;': '@', + 'comp;': '\u2201', + 'compfn;': '\u2218', + 'complement;': '\u2201', + 'complexes;': '\u2102', + 'cong;': '\u2245', + 'congdot;': '\u2a6d', + 'Congruent;': '\u2261', + 'Conint;': '\u222f', + 'conint;': '\u222e', + 'ContourIntegral;': '\u222e', + 'Copf;': '\u2102', + 'copf;': '\U0001d554', + 'coprod;': '\u2210', + 'Coproduct;': '\u2210', + 'COPY': '\xa9', + 'copy': '\xa9', + 'COPY;': '\xa9', + 'copy;': '\xa9', + 'copysr;': '\u2117', + 'CounterClockwiseContourIntegral;': '\u2233', + 'crarr;': '\u21b5', + 'Cross;': '\u2a2f', + 'cross;': '\u2717', + 'Cscr;': '\U0001d49e', + 'cscr;': '\U0001d4b8', + 'csub;': '\u2acf', + 'csube;': '\u2ad1', + 'csup;': '\u2ad0', + 'csupe;': '\u2ad2', + 'ctdot;': '\u22ef', + 'cudarrl;': '\u2938', + 'cudarrr;': '\u2935', + 'cuepr;': '\u22de', + 'cuesc;': '\u22df', + 'cularr;': '\u21b6', + 'cularrp;': '\u293d', + 'Cup;': '\u22d3', + 'cup;': '\u222a', + 'cupbrcap;': '\u2a48', + 'CupCap;': '\u224d', + 'cupcap;': '\u2a46', + 'cupcup;': '\u2a4a', + 'cupdot;': '\u228d', + 'cupor;': '\u2a45', + 'cups;': '\u222a\ufe00', + 'curarr;': '\u21b7', + 'curarrm;': '\u293c', + 'curlyeqprec;': '\u22de', + 'curlyeqsucc;': '\u22df', + 'curlyvee;': '\u22ce', + 'curlywedge;': '\u22cf', + 'curren': '\xa4', + 'curren;': '\xa4', + 'curvearrowleft;': '\u21b6', + 'curvearrowright;': '\u21b7', + 'cuvee;': '\u22ce', + 'cuwed;': '\u22cf', + 'cwconint;': '\u2232', + 'cwint;': '\u2231', + 'cylcty;': '\u232d', + 'Dagger;': '\u2021', + 'dagger;': '\u2020', + 'daleth;': '\u2138', + 'Darr;': '\u21a1', + 'dArr;': '\u21d3', + 'darr;': '\u2193', + 'dash;': '\u2010', + 'Dashv;': '\u2ae4', + 'dashv;': '\u22a3', + 'dbkarow;': '\u290f', + 'dblac;': '\u02dd', + 'Dcaron;': '\u010e', + 'dcaron;': '\u010f', + 'Dcy;': '\u0414', + 'dcy;': '\u0434', + 'DD;': '\u2145', + 'dd;': '\u2146', + 'ddagger;': '\u2021', + 'ddarr;': '\u21ca', + 'DDotrahd;': '\u2911', + 'ddotseq;': '\u2a77', + 'deg': '\xb0', + 'deg;': '\xb0', + 'Del;': '\u2207', + 'Delta;': '\u0394', + 'delta;': '\u03b4', + 'demptyv;': '\u29b1', + 'dfisht;': '\u297f', + 'Dfr;': '\U0001d507', + 'dfr;': '\U0001d521', + 'dHar;': '\u2965', + 'dharl;': '\u21c3', + 'dharr;': '\u21c2', + 'DiacriticalAcute;': '\xb4', + 'DiacriticalDot;': '\u02d9', + 'DiacriticalDoubleAcute;': '\u02dd', + 'DiacriticalGrave;': '`', + 'DiacriticalTilde;': '\u02dc', + 'diam;': '\u22c4', + 'Diamond;': '\u22c4', + 'diamond;': '\u22c4', + 'diamondsuit;': '\u2666', + 'diams;': '\u2666', + 'die;': '\xa8', + 'DifferentialD;': '\u2146', + 'digamma;': '\u03dd', + 'disin;': '\u22f2', + 'div;': '\xf7', + 'divide': '\xf7', + 'divide;': '\xf7', + 'divideontimes;': '\u22c7', + 'divonx;': '\u22c7', + 'DJcy;': '\u0402', + 'djcy;': '\u0452', + 'dlcorn;': '\u231e', + 'dlcrop;': '\u230d', + 'dollar;': '$', + 'Dopf;': '\U0001d53b', + 'dopf;': '\U0001d555', + 'Dot;': '\xa8', + 'dot;': '\u02d9', + 'DotDot;': '\u20dc', + 'doteq;': '\u2250', + 'doteqdot;': '\u2251', + 'DotEqual;': '\u2250', + 'dotminus;': '\u2238', + 'dotplus;': '\u2214', + 'dotsquare;': '\u22a1', + 'doublebarwedge;': '\u2306', + 'DoubleContourIntegral;': '\u222f', + 'DoubleDot;': '\xa8', + 'DoubleDownArrow;': '\u21d3', + 'DoubleLeftArrow;': '\u21d0', + 'DoubleLeftRightArrow;': '\u21d4', + 'DoubleLeftTee;': '\u2ae4', + 'DoubleLongLeftArrow;': '\u27f8', + 'DoubleLongLeftRightArrow;': '\u27fa', + 'DoubleLongRightArrow;': '\u27f9', + 'DoubleRightArrow;': '\u21d2', + 'DoubleRightTee;': '\u22a8', + 'DoubleUpArrow;': '\u21d1', + 'DoubleUpDownArrow;': '\u21d5', + 'DoubleVerticalBar;': '\u2225', + 'DownArrow;': '\u2193', + 'Downarrow;': '\u21d3', + 'downarrow;': '\u2193', + 'DownArrowBar;': '\u2913', + 'DownArrowUpArrow;': '\u21f5', + 'DownBreve;': '\u0311', + 'downdownarrows;': '\u21ca', + 'downharpoonleft;': '\u21c3', + 'downharpoonright;': '\u21c2', + 'DownLeftRightVector;': '\u2950', + 'DownLeftTeeVector;': '\u295e', + 'DownLeftVector;': '\u21bd', + 'DownLeftVectorBar;': '\u2956', + 'DownRightTeeVector;': '\u295f', + 'DownRightVector;': '\u21c1', + 'DownRightVectorBar;': '\u2957', + 'DownTee;': '\u22a4', + 'DownTeeArrow;': '\u21a7', + 'drbkarow;': '\u2910', + 'drcorn;': '\u231f', + 'drcrop;': '\u230c', + 'Dscr;': '\U0001d49f', + 'dscr;': '\U0001d4b9', + 'DScy;': '\u0405', + 'dscy;': '\u0455', + 'dsol;': '\u29f6', + 'Dstrok;': '\u0110', + 'dstrok;': '\u0111', + 'dtdot;': '\u22f1', + 'dtri;': '\u25bf', + 'dtrif;': '\u25be', + 'duarr;': '\u21f5', + 'duhar;': '\u296f', + 'dwangle;': '\u29a6', + 'DZcy;': '\u040f', + 'dzcy;': '\u045f', + 'dzigrarr;': '\u27ff', + 'Eacute': '\xc9', + 'eacute': '\xe9', + 'Eacute;': '\xc9', + 'eacute;': '\xe9', + 'easter;': '\u2a6e', + 'Ecaron;': '\u011a', + 'ecaron;': '\u011b', + 'ecir;': '\u2256', + 'Ecirc': '\xca', + 'ecirc': '\xea', + 'Ecirc;': '\xca', + 'ecirc;': '\xea', + 'ecolon;': '\u2255', + 'Ecy;': '\u042d', + 'ecy;': '\u044d', + 'eDDot;': '\u2a77', + 'Edot;': '\u0116', + 'eDot;': '\u2251', + 'edot;': '\u0117', + 'ee;': '\u2147', + 'efDot;': '\u2252', + 'Efr;': '\U0001d508', + 'efr;': '\U0001d522', + 'eg;': '\u2a9a', + 'Egrave': '\xc8', + 'egrave': '\xe8', + 'Egrave;': '\xc8', + 'egrave;': '\xe8', + 'egs;': '\u2a96', + 'egsdot;': '\u2a98', + 'el;': '\u2a99', + 'Element;': '\u2208', + 'elinters;': '\u23e7', + 'ell;': '\u2113', + 'els;': '\u2a95', + 'elsdot;': '\u2a97', + 'Emacr;': '\u0112', + 'emacr;': '\u0113', + 'empty;': '\u2205', + 'emptyset;': '\u2205', + 'EmptySmallSquare;': '\u25fb', + 'emptyv;': '\u2205', + 'EmptyVerySmallSquare;': '\u25ab', + 'emsp13;': '\u2004', + 'emsp14;': '\u2005', + 'emsp;': '\u2003', + 'ENG;': '\u014a', + 'eng;': '\u014b', + 'ensp;': '\u2002', + 'Eogon;': '\u0118', + 'eogon;': '\u0119', + 'Eopf;': '\U0001d53c', + 'eopf;': '\U0001d556', + 'epar;': '\u22d5', + 'eparsl;': '\u29e3', + 'eplus;': '\u2a71', + 'epsi;': '\u03b5', + 'Epsilon;': '\u0395', + 'epsilon;': '\u03b5', + 'epsiv;': '\u03f5', + 'eqcirc;': '\u2256', + 'eqcolon;': '\u2255', + 'eqsim;': '\u2242', + 'eqslantgtr;': '\u2a96', + 'eqslantless;': '\u2a95', + 'Equal;': '\u2a75', + 'equals;': '=', + 'EqualTilde;': '\u2242', + 'equest;': '\u225f', + 'Equilibrium;': '\u21cc', + 'equiv;': '\u2261', + 'equivDD;': '\u2a78', + 'eqvparsl;': '\u29e5', + 'erarr;': '\u2971', + 'erDot;': '\u2253', + 'Escr;': '\u2130', + 'escr;': '\u212f', + 'esdot;': '\u2250', + 'Esim;': '\u2a73', + 'esim;': '\u2242', + 'Eta;': '\u0397', + 'eta;': '\u03b7', + 'ETH': '\xd0', + 'eth': '\xf0', + 'ETH;': '\xd0', + 'eth;': '\xf0', + 'Euml': '\xcb', + 'euml': '\xeb', + 'Euml;': '\xcb', + 'euml;': '\xeb', + 'euro;': '\u20ac', + 'excl;': '!', + 'exist;': '\u2203', + 'Exists;': '\u2203', + 'expectation;': '\u2130', + 'ExponentialE;': '\u2147', + 'exponentiale;': '\u2147', + 'fallingdotseq;': '\u2252', + 'Fcy;': '\u0424', + 'fcy;': '\u0444', + 'female;': '\u2640', + 'ffilig;': '\ufb03', + 'fflig;': '\ufb00', + 'ffllig;': '\ufb04', + 'Ffr;': '\U0001d509', + 'ffr;': '\U0001d523', + 'filig;': '\ufb01', + 'FilledSmallSquare;': '\u25fc', + 'FilledVerySmallSquare;': '\u25aa', + 'fjlig;': 'fj', + 'flat;': '\u266d', + 'fllig;': '\ufb02', + 'fltns;': '\u25b1', + 'fnof;': '\u0192', + 'Fopf;': '\U0001d53d', + 'fopf;': '\U0001d557', + 'ForAll;': '\u2200', + 'forall;': '\u2200', + 'fork;': '\u22d4', + 'forkv;': '\u2ad9', + 'Fouriertrf;': '\u2131', + 'fpartint;': '\u2a0d', + 'frac12': '\xbd', + 'frac12;': '\xbd', + 'frac13;': '\u2153', + 'frac14': '\xbc', + 'frac14;': '\xbc', + 'frac15;': '\u2155', + 'frac16;': '\u2159', + 'frac18;': '\u215b', + 'frac23;': '\u2154', + 'frac25;': '\u2156', + 'frac34': '\xbe', + 'frac34;': '\xbe', + 'frac35;': '\u2157', + 'frac38;': '\u215c', + 'frac45;': '\u2158', + 'frac56;': '\u215a', + 'frac58;': '\u215d', + 'frac78;': '\u215e', + 'frasl;': '\u2044', + 'frown;': '\u2322', + 'Fscr;': '\u2131', + 'fscr;': '\U0001d4bb', + 'gacute;': '\u01f5', + 'Gamma;': '\u0393', + 'gamma;': '\u03b3', + 'Gammad;': '\u03dc', + 'gammad;': '\u03dd', + 'gap;': '\u2a86', + 'Gbreve;': '\u011e', + 'gbreve;': '\u011f', + 'Gcedil;': '\u0122', + 'Gcirc;': '\u011c', + 'gcirc;': '\u011d', + 'Gcy;': '\u0413', + 'gcy;': '\u0433', + 'Gdot;': '\u0120', + 'gdot;': '\u0121', + 'gE;': '\u2267', + 'ge;': '\u2265', + 'gEl;': '\u2a8c', + 'gel;': '\u22db', + 'geq;': '\u2265', + 'geqq;': '\u2267', + 'geqslant;': '\u2a7e', + 'ges;': '\u2a7e', + 'gescc;': '\u2aa9', + 'gesdot;': '\u2a80', + 'gesdoto;': '\u2a82', + 'gesdotol;': '\u2a84', + 'gesl;': '\u22db\ufe00', + 'gesles;': '\u2a94', + 'Gfr;': '\U0001d50a', + 'gfr;': '\U0001d524', + 'Gg;': '\u22d9', + 'gg;': '\u226b', + 'ggg;': '\u22d9', + 'gimel;': '\u2137', + 'GJcy;': '\u0403', + 'gjcy;': '\u0453', + 'gl;': '\u2277', + 'gla;': '\u2aa5', + 'glE;': '\u2a92', + 'glj;': '\u2aa4', + 'gnap;': '\u2a8a', + 'gnapprox;': '\u2a8a', + 'gnE;': '\u2269', + 'gne;': '\u2a88', + 'gneq;': '\u2a88', + 'gneqq;': '\u2269', + 'gnsim;': '\u22e7', + 'Gopf;': '\U0001d53e', + 'gopf;': '\U0001d558', + 'grave;': '`', + 'GreaterEqual;': '\u2265', + 'GreaterEqualLess;': '\u22db', + 'GreaterFullEqual;': '\u2267', + 'GreaterGreater;': '\u2aa2', + 'GreaterLess;': '\u2277', + 'GreaterSlantEqual;': '\u2a7e', + 'GreaterTilde;': '\u2273', + 'Gscr;': '\U0001d4a2', + 'gscr;': '\u210a', + 'gsim;': '\u2273', + 'gsime;': '\u2a8e', + 'gsiml;': '\u2a90', + 'GT': '>', + 'gt': '>', + 'GT;': '>', + 'Gt;': '\u226b', + 'gt;': '>', + 'gtcc;': '\u2aa7', + 'gtcir;': '\u2a7a', + 'gtdot;': '\u22d7', + 'gtlPar;': '\u2995', + 'gtquest;': '\u2a7c', + 'gtrapprox;': '\u2a86', + 'gtrarr;': '\u2978', + 'gtrdot;': '\u22d7', + 'gtreqless;': '\u22db', + 'gtreqqless;': '\u2a8c', + 'gtrless;': '\u2277', + 'gtrsim;': '\u2273', + 'gvertneqq;': '\u2269\ufe00', + 'gvnE;': '\u2269\ufe00', + 'Hacek;': '\u02c7', + 'hairsp;': '\u200a', + 'half;': '\xbd', + 'hamilt;': '\u210b', + 'HARDcy;': '\u042a', + 'hardcy;': '\u044a', + 'hArr;': '\u21d4', + 'harr;': '\u2194', + 'harrcir;': '\u2948', + 'harrw;': '\u21ad', + 'Hat;': '^', + 'hbar;': '\u210f', + 'Hcirc;': '\u0124', + 'hcirc;': '\u0125', + 'hearts;': '\u2665', + 'heartsuit;': '\u2665', + 'hellip;': '\u2026', + 'hercon;': '\u22b9', + 'Hfr;': '\u210c', + 'hfr;': '\U0001d525', + 'HilbertSpace;': '\u210b', + 'hksearow;': '\u2925', + 'hkswarow;': '\u2926', + 'hoarr;': '\u21ff', + 'homtht;': '\u223b', + 'hookleftarrow;': '\u21a9', + 'hookrightarrow;': '\u21aa', + 'Hopf;': '\u210d', + 'hopf;': '\U0001d559', + 'horbar;': '\u2015', + 'HorizontalLine;': '\u2500', + 'Hscr;': '\u210b', + 'hscr;': '\U0001d4bd', + 'hslash;': '\u210f', + 'Hstrok;': '\u0126', + 'hstrok;': '\u0127', + 'HumpDownHump;': '\u224e', + 'HumpEqual;': '\u224f', + 'hybull;': '\u2043', + 'hyphen;': '\u2010', + 'Iacute': '\xcd', + 'iacute': '\xed', + 'Iacute;': '\xcd', + 'iacute;': '\xed', + 'ic;': '\u2063', + 'Icirc': '\xce', + 'icirc': '\xee', + 'Icirc;': '\xce', + 'icirc;': '\xee', + 'Icy;': '\u0418', + 'icy;': '\u0438', + 'Idot;': '\u0130', + 'IEcy;': '\u0415', + 'iecy;': '\u0435', + 'iexcl': '\xa1', + 'iexcl;': '\xa1', + 'iff;': '\u21d4', + 'Ifr;': '\u2111', + 'ifr;': '\U0001d526', + 'Igrave': '\xcc', + 'igrave': '\xec', + 'Igrave;': '\xcc', + 'igrave;': '\xec', + 'ii;': '\u2148', + 'iiiint;': '\u2a0c', + 'iiint;': '\u222d', + 'iinfin;': '\u29dc', + 'iiota;': '\u2129', + 'IJlig;': '\u0132', + 'ijlig;': '\u0133', + 'Im;': '\u2111', + 'Imacr;': '\u012a', + 'imacr;': '\u012b', + 'image;': '\u2111', + 'ImaginaryI;': '\u2148', + 'imagline;': '\u2110', + 'imagpart;': '\u2111', + 'imath;': '\u0131', + 'imof;': '\u22b7', + 'imped;': '\u01b5', + 'Implies;': '\u21d2', + 'in;': '\u2208', + 'incare;': '\u2105', + 'infin;': '\u221e', + 'infintie;': '\u29dd', + 'inodot;': '\u0131', + 'Int;': '\u222c', + 'int;': '\u222b', + 'intcal;': '\u22ba', + 'integers;': '\u2124', + 'Integral;': '\u222b', + 'intercal;': '\u22ba', + 'Intersection;': '\u22c2', + 'intlarhk;': '\u2a17', + 'intprod;': '\u2a3c', + 'InvisibleComma;': '\u2063', + 'InvisibleTimes;': '\u2062', + 'IOcy;': '\u0401', + 'iocy;': '\u0451', + 'Iogon;': '\u012e', + 'iogon;': '\u012f', + 'Iopf;': '\U0001d540', + 'iopf;': '\U0001d55a', + 'Iota;': '\u0399', + 'iota;': '\u03b9', + 'iprod;': '\u2a3c', + 'iquest': '\xbf', + 'iquest;': '\xbf', + 'Iscr;': '\u2110', + 'iscr;': '\U0001d4be', + 'isin;': '\u2208', + 'isindot;': '\u22f5', + 'isinE;': '\u22f9', + 'isins;': '\u22f4', + 'isinsv;': '\u22f3', + 'isinv;': '\u2208', + 'it;': '\u2062', + 'Itilde;': '\u0128', + 'itilde;': '\u0129', + 'Iukcy;': '\u0406', + 'iukcy;': '\u0456', + 'Iuml': '\xcf', + 'iuml': '\xef', + 'Iuml;': '\xcf', + 'iuml;': '\xef', + 'Jcirc;': '\u0134', + 'jcirc;': '\u0135', + 'Jcy;': '\u0419', + 'jcy;': '\u0439', + 'Jfr;': '\U0001d50d', + 'jfr;': '\U0001d527', + 'jmath;': '\u0237', + 'Jopf;': '\U0001d541', + 'jopf;': '\U0001d55b', + 'Jscr;': '\U0001d4a5', + 'jscr;': '\U0001d4bf', + 'Jsercy;': '\u0408', + 'jsercy;': '\u0458', + 'Jukcy;': '\u0404', + 'jukcy;': '\u0454', + 'Kappa;': '\u039a', + 'kappa;': '\u03ba', + 'kappav;': '\u03f0', + 'Kcedil;': '\u0136', + 'kcedil;': '\u0137', + 'Kcy;': '\u041a', + 'kcy;': '\u043a', + 'Kfr;': '\U0001d50e', + 'kfr;': '\U0001d528', + 'kgreen;': '\u0138', + 'KHcy;': '\u0425', + 'khcy;': '\u0445', + 'KJcy;': '\u040c', + 'kjcy;': '\u045c', + 'Kopf;': '\U0001d542', + 'kopf;': '\U0001d55c', + 'Kscr;': '\U0001d4a6', + 'kscr;': '\U0001d4c0', + 'lAarr;': '\u21da', + 'Lacute;': '\u0139', + 'lacute;': '\u013a', + 'laemptyv;': '\u29b4', + 'lagran;': '\u2112', + 'Lambda;': '\u039b', + 'lambda;': '\u03bb', + 'Lang;': '\u27ea', + 'lang;': '\u27e8', + 'langd;': '\u2991', + 'langle;': '\u27e8', + 'lap;': '\u2a85', + 'Laplacetrf;': '\u2112', + 'laquo': '\xab', + 'laquo;': '\xab', + 'Larr;': '\u219e', + 'lArr;': '\u21d0', + 'larr;': '\u2190', + 'larrb;': '\u21e4', + 'larrbfs;': '\u291f', + 'larrfs;': '\u291d', + 'larrhk;': '\u21a9', + 'larrlp;': '\u21ab', + 'larrpl;': '\u2939', + 'larrsim;': '\u2973', + 'larrtl;': '\u21a2', + 'lat;': '\u2aab', + 'lAtail;': '\u291b', + 'latail;': '\u2919', + 'late;': '\u2aad', + 'lates;': '\u2aad\ufe00', + 'lBarr;': '\u290e', + 'lbarr;': '\u290c', + 'lbbrk;': '\u2772', + 'lbrace;': '{', + 'lbrack;': '[', + 'lbrke;': '\u298b', + 'lbrksld;': '\u298f', + 'lbrkslu;': '\u298d', + 'Lcaron;': '\u013d', + 'lcaron;': '\u013e', + 'Lcedil;': '\u013b', + 'lcedil;': '\u013c', + 'lceil;': '\u2308', + 'lcub;': '{', + 'Lcy;': '\u041b', + 'lcy;': '\u043b', + 'ldca;': '\u2936', + 'ldquo;': '\u201c', + 'ldquor;': '\u201e', + 'ldrdhar;': '\u2967', + 'ldrushar;': '\u294b', + 'ldsh;': '\u21b2', + 'lE;': '\u2266', + 'le;': '\u2264', + 'LeftAngleBracket;': '\u27e8', + 'LeftArrow;': '\u2190', + 'Leftarrow;': '\u21d0', + 'leftarrow;': '\u2190', + 'LeftArrowBar;': '\u21e4', + 'LeftArrowRightArrow;': '\u21c6', + 'leftarrowtail;': '\u21a2', + 'LeftCeiling;': '\u2308', + 'LeftDoubleBracket;': '\u27e6', + 'LeftDownTeeVector;': '\u2961', + 'LeftDownVector;': '\u21c3', + 'LeftDownVectorBar;': '\u2959', + 'LeftFloor;': '\u230a', + 'leftharpoondown;': '\u21bd', + 'leftharpoonup;': '\u21bc', + 'leftleftarrows;': '\u21c7', + 'LeftRightArrow;': '\u2194', + 'Leftrightarrow;': '\u21d4', + 'leftrightarrow;': '\u2194', + 'leftrightarrows;': '\u21c6', + 'leftrightharpoons;': '\u21cb', + 'leftrightsquigarrow;': '\u21ad', + 'LeftRightVector;': '\u294e', + 'LeftTee;': '\u22a3', + 'LeftTeeArrow;': '\u21a4', + 'LeftTeeVector;': '\u295a', + 'leftthreetimes;': '\u22cb', + 'LeftTriangle;': '\u22b2', + 'LeftTriangleBar;': '\u29cf', + 'LeftTriangleEqual;': '\u22b4', + 'LeftUpDownVector;': '\u2951', + 'LeftUpTeeVector;': '\u2960', + 'LeftUpVector;': '\u21bf', + 'LeftUpVectorBar;': '\u2958', + 'LeftVector;': '\u21bc', + 'LeftVectorBar;': '\u2952', + 'lEg;': '\u2a8b', + 'leg;': '\u22da', + 'leq;': '\u2264', + 'leqq;': '\u2266', + 'leqslant;': '\u2a7d', + 'les;': '\u2a7d', + 'lescc;': '\u2aa8', + 'lesdot;': '\u2a7f', + 'lesdoto;': '\u2a81', + 'lesdotor;': '\u2a83', + 'lesg;': '\u22da\ufe00', + 'lesges;': '\u2a93', + 'lessapprox;': '\u2a85', + 'lessdot;': '\u22d6', + 'lesseqgtr;': '\u22da', + 'lesseqqgtr;': '\u2a8b', + 'LessEqualGreater;': '\u22da', + 'LessFullEqual;': '\u2266', + 'LessGreater;': '\u2276', + 'lessgtr;': '\u2276', + 'LessLess;': '\u2aa1', + 'lesssim;': '\u2272', + 'LessSlantEqual;': '\u2a7d', + 'LessTilde;': '\u2272', + 'lfisht;': '\u297c', + 'lfloor;': '\u230a', + 'Lfr;': '\U0001d50f', + 'lfr;': '\U0001d529', + 'lg;': '\u2276', + 'lgE;': '\u2a91', + 'lHar;': '\u2962', + 'lhard;': '\u21bd', + 'lharu;': '\u21bc', + 'lharul;': '\u296a', + 'lhblk;': '\u2584', + 'LJcy;': '\u0409', + 'ljcy;': '\u0459', + 'Ll;': '\u22d8', + 'll;': '\u226a', + 'llarr;': '\u21c7', + 'llcorner;': '\u231e', + 'Lleftarrow;': '\u21da', + 'llhard;': '\u296b', + 'lltri;': '\u25fa', + 'Lmidot;': '\u013f', + 'lmidot;': '\u0140', + 'lmoust;': '\u23b0', + 'lmoustache;': '\u23b0', + 'lnap;': '\u2a89', + 'lnapprox;': '\u2a89', + 'lnE;': '\u2268', + 'lne;': '\u2a87', + 'lneq;': '\u2a87', + 'lneqq;': '\u2268', + 'lnsim;': '\u22e6', + 'loang;': '\u27ec', + 'loarr;': '\u21fd', + 'lobrk;': '\u27e6', + 'LongLeftArrow;': '\u27f5', + 'Longleftarrow;': '\u27f8', + 'longleftarrow;': '\u27f5', + 'LongLeftRightArrow;': '\u27f7', + 'Longleftrightarrow;': '\u27fa', + 'longleftrightarrow;': '\u27f7', + 'longmapsto;': '\u27fc', + 'LongRightArrow;': '\u27f6', + 'Longrightarrow;': '\u27f9', + 'longrightarrow;': '\u27f6', + 'looparrowleft;': '\u21ab', + 'looparrowright;': '\u21ac', + 'lopar;': '\u2985', + 'Lopf;': '\U0001d543', + 'lopf;': '\U0001d55d', + 'loplus;': '\u2a2d', + 'lotimes;': '\u2a34', + 'lowast;': '\u2217', + 'lowbar;': '_', + 'LowerLeftArrow;': '\u2199', + 'LowerRightArrow;': '\u2198', + 'loz;': '\u25ca', + 'lozenge;': '\u25ca', + 'lozf;': '\u29eb', + 'lpar;': '(', + 'lparlt;': '\u2993', + 'lrarr;': '\u21c6', + 'lrcorner;': '\u231f', + 'lrhar;': '\u21cb', + 'lrhard;': '\u296d', + 'lrm;': '\u200e', + 'lrtri;': '\u22bf', + 'lsaquo;': '\u2039', + 'Lscr;': '\u2112', + 'lscr;': '\U0001d4c1', + 'Lsh;': '\u21b0', + 'lsh;': '\u21b0', + 'lsim;': '\u2272', + 'lsime;': '\u2a8d', + 'lsimg;': '\u2a8f', + 'lsqb;': '[', + 'lsquo;': '\u2018', + 'lsquor;': '\u201a', + 'Lstrok;': '\u0141', + 'lstrok;': '\u0142', + 'LT': '<', + 'lt': '<', + 'LT;': '<', + 'Lt;': '\u226a', + 'lt;': '<', + 'ltcc;': '\u2aa6', + 'ltcir;': '\u2a79', + 'ltdot;': '\u22d6', + 'lthree;': '\u22cb', + 'ltimes;': '\u22c9', + 'ltlarr;': '\u2976', + 'ltquest;': '\u2a7b', + 'ltri;': '\u25c3', + 'ltrie;': '\u22b4', + 'ltrif;': '\u25c2', + 'ltrPar;': '\u2996', + 'lurdshar;': '\u294a', + 'luruhar;': '\u2966', + 'lvertneqq;': '\u2268\ufe00', + 'lvnE;': '\u2268\ufe00', + 'macr': '\xaf', + 'macr;': '\xaf', + 'male;': '\u2642', + 'malt;': '\u2720', + 'maltese;': '\u2720', + 'Map;': '\u2905', + 'map;': '\u21a6', + 'mapsto;': '\u21a6', + 'mapstodown;': '\u21a7', + 'mapstoleft;': '\u21a4', + 'mapstoup;': '\u21a5', + 'marker;': '\u25ae', + 'mcomma;': '\u2a29', + 'Mcy;': '\u041c', + 'mcy;': '\u043c', + 'mdash;': '\u2014', + 'mDDot;': '\u223a', + 'measuredangle;': '\u2221', + 'MediumSpace;': '\u205f', + 'Mellintrf;': '\u2133', + 'Mfr;': '\U0001d510', + 'mfr;': '\U0001d52a', + 'mho;': '\u2127', + 'micro': '\xb5', + 'micro;': '\xb5', + 'mid;': '\u2223', + 'midast;': '*', + 'midcir;': '\u2af0', + 'middot': '\xb7', + 'middot;': '\xb7', + 'minus;': '\u2212', + 'minusb;': '\u229f', + 'minusd;': '\u2238', + 'minusdu;': '\u2a2a', + 'MinusPlus;': '\u2213', + 'mlcp;': '\u2adb', + 'mldr;': '\u2026', + 'mnplus;': '\u2213', + 'models;': '\u22a7', + 'Mopf;': '\U0001d544', + 'mopf;': '\U0001d55e', + 'mp;': '\u2213', + 'Mscr;': '\u2133', + 'mscr;': '\U0001d4c2', + 'mstpos;': '\u223e', + 'Mu;': '\u039c', + 'mu;': '\u03bc', + 'multimap;': '\u22b8', + 'mumap;': '\u22b8', + 'nabla;': '\u2207', + 'Nacute;': '\u0143', + 'nacute;': '\u0144', + 'nang;': '\u2220\u20d2', + 'nap;': '\u2249', + 'napE;': '\u2a70\u0338', + 'napid;': '\u224b\u0338', + 'napos;': '\u0149', + 'napprox;': '\u2249', + 'natur;': '\u266e', + 'natural;': '\u266e', + 'naturals;': '\u2115', + 'nbsp': '\xa0', + 'nbsp;': '\xa0', + 'nbump;': '\u224e\u0338', + 'nbumpe;': '\u224f\u0338', + 'ncap;': '\u2a43', + 'Ncaron;': '\u0147', + 'ncaron;': '\u0148', + 'Ncedil;': '\u0145', + 'ncedil;': '\u0146', + 'ncong;': '\u2247', + 'ncongdot;': '\u2a6d\u0338', + 'ncup;': '\u2a42', + 'Ncy;': '\u041d', + 'ncy;': '\u043d', + 'ndash;': '\u2013', + 'ne;': '\u2260', + 'nearhk;': '\u2924', + 'neArr;': '\u21d7', + 'nearr;': '\u2197', + 'nearrow;': '\u2197', + 'nedot;': '\u2250\u0338', + 'NegativeMediumSpace;': '\u200b', + 'NegativeThickSpace;': '\u200b', + 'NegativeThinSpace;': '\u200b', + 'NegativeVeryThinSpace;': '\u200b', + 'nequiv;': '\u2262', + 'nesear;': '\u2928', + 'nesim;': '\u2242\u0338', + 'NestedGreaterGreater;': '\u226b', + 'NestedLessLess;': '\u226a', + 'NewLine;': '\n', + 'nexist;': '\u2204', + 'nexists;': '\u2204', + 'Nfr;': '\U0001d511', + 'nfr;': '\U0001d52b', + 'ngE;': '\u2267\u0338', + 'nge;': '\u2271', + 'ngeq;': '\u2271', + 'ngeqq;': '\u2267\u0338', + 'ngeqslant;': '\u2a7e\u0338', + 'nges;': '\u2a7e\u0338', + 'nGg;': '\u22d9\u0338', + 'ngsim;': '\u2275', + 'nGt;': '\u226b\u20d2', + 'ngt;': '\u226f', + 'ngtr;': '\u226f', + 'nGtv;': '\u226b\u0338', + 'nhArr;': '\u21ce', + 'nharr;': '\u21ae', + 'nhpar;': '\u2af2', + 'ni;': '\u220b', + 'nis;': '\u22fc', + 'nisd;': '\u22fa', + 'niv;': '\u220b', + 'NJcy;': '\u040a', + 'njcy;': '\u045a', + 'nlArr;': '\u21cd', + 'nlarr;': '\u219a', + 'nldr;': '\u2025', + 'nlE;': '\u2266\u0338', + 'nle;': '\u2270', + 'nLeftarrow;': '\u21cd', + 'nleftarrow;': '\u219a', + 'nLeftrightarrow;': '\u21ce', + 'nleftrightarrow;': '\u21ae', + 'nleq;': '\u2270', + 'nleqq;': '\u2266\u0338', + 'nleqslant;': '\u2a7d\u0338', + 'nles;': '\u2a7d\u0338', + 'nless;': '\u226e', + 'nLl;': '\u22d8\u0338', + 'nlsim;': '\u2274', + 'nLt;': '\u226a\u20d2', + 'nlt;': '\u226e', + 'nltri;': '\u22ea', + 'nltrie;': '\u22ec', + 'nLtv;': '\u226a\u0338', + 'nmid;': '\u2224', + 'NoBreak;': '\u2060', + 'NonBreakingSpace;': '\xa0', + 'Nopf;': '\u2115', + 'nopf;': '\U0001d55f', + 'not': '\xac', + 'Not;': '\u2aec', + 'not;': '\xac', + 'NotCongruent;': '\u2262', + 'NotCupCap;': '\u226d', + 'NotDoubleVerticalBar;': '\u2226', + 'NotElement;': '\u2209', + 'NotEqual;': '\u2260', + 'NotEqualTilde;': '\u2242\u0338', + 'NotExists;': '\u2204', + 'NotGreater;': '\u226f', + 'NotGreaterEqual;': '\u2271', + 'NotGreaterFullEqual;': '\u2267\u0338', + 'NotGreaterGreater;': '\u226b\u0338', + 'NotGreaterLess;': '\u2279', + 'NotGreaterSlantEqual;': '\u2a7e\u0338', + 'NotGreaterTilde;': '\u2275', + 'NotHumpDownHump;': '\u224e\u0338', + 'NotHumpEqual;': '\u224f\u0338', + 'notin;': '\u2209', + 'notindot;': '\u22f5\u0338', + 'notinE;': '\u22f9\u0338', + 'notinva;': '\u2209', + 'notinvb;': '\u22f7', + 'notinvc;': '\u22f6', + 'NotLeftTriangle;': '\u22ea', + 'NotLeftTriangleBar;': '\u29cf\u0338', + 'NotLeftTriangleEqual;': '\u22ec', + 'NotLess;': '\u226e', + 'NotLessEqual;': '\u2270', + 'NotLessGreater;': '\u2278', + 'NotLessLess;': '\u226a\u0338', + 'NotLessSlantEqual;': '\u2a7d\u0338', + 'NotLessTilde;': '\u2274', + 'NotNestedGreaterGreater;': '\u2aa2\u0338', + 'NotNestedLessLess;': '\u2aa1\u0338', + 'notni;': '\u220c', + 'notniva;': '\u220c', + 'notnivb;': '\u22fe', + 'notnivc;': '\u22fd', + 'NotPrecedes;': '\u2280', + 'NotPrecedesEqual;': '\u2aaf\u0338', + 'NotPrecedesSlantEqual;': '\u22e0', + 'NotReverseElement;': '\u220c', + 'NotRightTriangle;': '\u22eb', + 'NotRightTriangleBar;': '\u29d0\u0338', + 'NotRightTriangleEqual;': '\u22ed', + 'NotSquareSubset;': '\u228f\u0338', + 'NotSquareSubsetEqual;': '\u22e2', + 'NotSquareSuperset;': '\u2290\u0338', + 'NotSquareSupersetEqual;': '\u22e3', + 'NotSubset;': '\u2282\u20d2', + 'NotSubsetEqual;': '\u2288', + 'NotSucceeds;': '\u2281', + 'NotSucceedsEqual;': '\u2ab0\u0338', + 'NotSucceedsSlantEqual;': '\u22e1', + 'NotSucceedsTilde;': '\u227f\u0338', + 'NotSuperset;': '\u2283\u20d2', + 'NotSupersetEqual;': '\u2289', + 'NotTilde;': '\u2241', + 'NotTildeEqual;': '\u2244', + 'NotTildeFullEqual;': '\u2247', + 'NotTildeTilde;': '\u2249', + 'NotVerticalBar;': '\u2224', + 'npar;': '\u2226', + 'nparallel;': '\u2226', + 'nparsl;': '\u2afd\u20e5', + 'npart;': '\u2202\u0338', + 'npolint;': '\u2a14', + 'npr;': '\u2280', + 'nprcue;': '\u22e0', + 'npre;': '\u2aaf\u0338', + 'nprec;': '\u2280', + 'npreceq;': '\u2aaf\u0338', + 'nrArr;': '\u21cf', + 'nrarr;': '\u219b', + 'nrarrc;': '\u2933\u0338', + 'nrarrw;': '\u219d\u0338', + 'nRightarrow;': '\u21cf', + 'nrightarrow;': '\u219b', + 'nrtri;': '\u22eb', + 'nrtrie;': '\u22ed', + 'nsc;': '\u2281', + 'nsccue;': '\u22e1', + 'nsce;': '\u2ab0\u0338', + 'Nscr;': '\U0001d4a9', + 'nscr;': '\U0001d4c3', + 'nshortmid;': '\u2224', + 'nshortparallel;': '\u2226', + 'nsim;': '\u2241', + 'nsime;': '\u2244', + 'nsimeq;': '\u2244', + 'nsmid;': '\u2224', + 'nspar;': '\u2226', + 'nsqsube;': '\u22e2', + 'nsqsupe;': '\u22e3', + 'nsub;': '\u2284', + 'nsubE;': '\u2ac5\u0338', + 'nsube;': '\u2288', + 'nsubset;': '\u2282\u20d2', + 'nsubseteq;': '\u2288', + 'nsubseteqq;': '\u2ac5\u0338', + 'nsucc;': '\u2281', + 'nsucceq;': '\u2ab0\u0338', + 'nsup;': '\u2285', + 'nsupE;': '\u2ac6\u0338', + 'nsupe;': '\u2289', + 'nsupset;': '\u2283\u20d2', + 'nsupseteq;': '\u2289', + 'nsupseteqq;': '\u2ac6\u0338', + 'ntgl;': '\u2279', + 'Ntilde': '\xd1', + 'ntilde': '\xf1', + 'Ntilde;': '\xd1', + 'ntilde;': '\xf1', + 'ntlg;': '\u2278', + 'ntriangleleft;': '\u22ea', + 'ntrianglelefteq;': '\u22ec', + 'ntriangleright;': '\u22eb', + 'ntrianglerighteq;': '\u22ed', + 'Nu;': '\u039d', + 'nu;': '\u03bd', + 'num;': '#', + 'numero;': '\u2116', + 'numsp;': '\u2007', + 'nvap;': '\u224d\u20d2', + 'nVDash;': '\u22af', + 'nVdash;': '\u22ae', + 'nvDash;': '\u22ad', + 'nvdash;': '\u22ac', + 'nvge;': '\u2265\u20d2', + 'nvgt;': '>\u20d2', + 'nvHarr;': '\u2904', + 'nvinfin;': '\u29de', + 'nvlArr;': '\u2902', + 'nvle;': '\u2264\u20d2', + 'nvlt;': '<\u20d2', + 'nvltrie;': '\u22b4\u20d2', + 'nvrArr;': '\u2903', + 'nvrtrie;': '\u22b5\u20d2', + 'nvsim;': '\u223c\u20d2', + 'nwarhk;': '\u2923', + 'nwArr;': '\u21d6', + 'nwarr;': '\u2196', + 'nwarrow;': '\u2196', + 'nwnear;': '\u2927', + 'Oacute': '\xd3', + 'oacute': '\xf3', + 'Oacute;': '\xd3', + 'oacute;': '\xf3', + 'oast;': '\u229b', + 'ocir;': '\u229a', + 'Ocirc': '\xd4', + 'ocirc': '\xf4', + 'Ocirc;': '\xd4', + 'ocirc;': '\xf4', + 'Ocy;': '\u041e', + 'ocy;': '\u043e', + 'odash;': '\u229d', + 'Odblac;': '\u0150', + 'odblac;': '\u0151', + 'odiv;': '\u2a38', + 'odot;': '\u2299', + 'odsold;': '\u29bc', + 'OElig;': '\u0152', + 'oelig;': '\u0153', + 'ofcir;': '\u29bf', + 'Ofr;': '\U0001d512', + 'ofr;': '\U0001d52c', + 'ogon;': '\u02db', + 'Ograve': '\xd2', + 'ograve': '\xf2', + 'Ograve;': '\xd2', + 'ograve;': '\xf2', + 'ogt;': '\u29c1', + 'ohbar;': '\u29b5', + 'ohm;': '\u03a9', + 'oint;': '\u222e', + 'olarr;': '\u21ba', + 'olcir;': '\u29be', + 'olcross;': '\u29bb', + 'oline;': '\u203e', + 'olt;': '\u29c0', + 'Omacr;': '\u014c', + 'omacr;': '\u014d', + 'Omega;': '\u03a9', + 'omega;': '\u03c9', + 'Omicron;': '\u039f', + 'omicron;': '\u03bf', + 'omid;': '\u29b6', + 'ominus;': '\u2296', + 'Oopf;': '\U0001d546', + 'oopf;': '\U0001d560', + 'opar;': '\u29b7', + 'OpenCurlyDoubleQuote;': '\u201c', + 'OpenCurlyQuote;': '\u2018', + 'operp;': '\u29b9', + 'oplus;': '\u2295', + 'Or;': '\u2a54', + 'or;': '\u2228', + 'orarr;': '\u21bb', + 'ord;': '\u2a5d', + 'order;': '\u2134', + 'orderof;': '\u2134', + 'ordf': '\xaa', + 'ordf;': '\xaa', + 'ordm': '\xba', + 'ordm;': '\xba', + 'origof;': '\u22b6', + 'oror;': '\u2a56', + 'orslope;': '\u2a57', + 'orv;': '\u2a5b', + 'oS;': '\u24c8', + 'Oscr;': '\U0001d4aa', + 'oscr;': '\u2134', + 'Oslash': '\xd8', + 'oslash': '\xf8', + 'Oslash;': '\xd8', + 'oslash;': '\xf8', + 'osol;': '\u2298', + 'Otilde': '\xd5', + 'otilde': '\xf5', + 'Otilde;': '\xd5', + 'otilde;': '\xf5', + 'Otimes;': '\u2a37', + 'otimes;': '\u2297', + 'otimesas;': '\u2a36', + 'Ouml': '\xd6', + 'ouml': '\xf6', + 'Ouml;': '\xd6', + 'ouml;': '\xf6', + 'ovbar;': '\u233d', + 'OverBar;': '\u203e', + 'OverBrace;': '\u23de', + 'OverBracket;': '\u23b4', + 'OverParenthesis;': '\u23dc', + 'par;': '\u2225', + 'para': '\xb6', + 'para;': '\xb6', + 'parallel;': '\u2225', + 'parsim;': '\u2af3', + 'parsl;': '\u2afd', + 'part;': '\u2202', + 'PartialD;': '\u2202', + 'Pcy;': '\u041f', + 'pcy;': '\u043f', + 'percnt;': '%', + 'period;': '.', + 'permil;': '\u2030', + 'perp;': '\u22a5', + 'pertenk;': '\u2031', + 'Pfr;': '\U0001d513', + 'pfr;': '\U0001d52d', + 'Phi;': '\u03a6', + 'phi;': '\u03c6', + 'phiv;': '\u03d5', + 'phmmat;': '\u2133', + 'phone;': '\u260e', + 'Pi;': '\u03a0', + 'pi;': '\u03c0', + 'pitchfork;': '\u22d4', + 'piv;': '\u03d6', + 'planck;': '\u210f', + 'planckh;': '\u210e', + 'plankv;': '\u210f', + 'plus;': '+', + 'plusacir;': '\u2a23', + 'plusb;': '\u229e', + 'pluscir;': '\u2a22', + 'plusdo;': '\u2214', + 'plusdu;': '\u2a25', + 'pluse;': '\u2a72', + 'PlusMinus;': '\xb1', + 'plusmn': '\xb1', + 'plusmn;': '\xb1', + 'plussim;': '\u2a26', + 'plustwo;': '\u2a27', + 'pm;': '\xb1', + 'Poincareplane;': '\u210c', + 'pointint;': '\u2a15', + 'Popf;': '\u2119', + 'popf;': '\U0001d561', + 'pound': '\xa3', + 'pound;': '\xa3', + 'Pr;': '\u2abb', + 'pr;': '\u227a', + 'prap;': '\u2ab7', + 'prcue;': '\u227c', + 'prE;': '\u2ab3', + 'pre;': '\u2aaf', + 'prec;': '\u227a', + 'precapprox;': '\u2ab7', + 'preccurlyeq;': '\u227c', + 'Precedes;': '\u227a', + 'PrecedesEqual;': '\u2aaf', + 'PrecedesSlantEqual;': '\u227c', + 'PrecedesTilde;': '\u227e', + 'preceq;': '\u2aaf', + 'precnapprox;': '\u2ab9', + 'precneqq;': '\u2ab5', + 'precnsim;': '\u22e8', + 'precsim;': '\u227e', + 'Prime;': '\u2033', + 'prime;': '\u2032', + 'primes;': '\u2119', + 'prnap;': '\u2ab9', + 'prnE;': '\u2ab5', + 'prnsim;': '\u22e8', + 'prod;': '\u220f', + 'Product;': '\u220f', + 'profalar;': '\u232e', + 'profline;': '\u2312', + 'profsurf;': '\u2313', + 'prop;': '\u221d', + 'Proportion;': '\u2237', + 'Proportional;': '\u221d', + 'propto;': '\u221d', + 'prsim;': '\u227e', + 'prurel;': '\u22b0', + 'Pscr;': '\U0001d4ab', + 'pscr;': '\U0001d4c5', + 'Psi;': '\u03a8', + 'psi;': '\u03c8', + 'puncsp;': '\u2008', + 'Qfr;': '\U0001d514', + 'qfr;': '\U0001d52e', + 'qint;': '\u2a0c', + 'Qopf;': '\u211a', + 'qopf;': '\U0001d562', + 'qprime;': '\u2057', + 'Qscr;': '\U0001d4ac', + 'qscr;': '\U0001d4c6', + 'quaternions;': '\u210d', + 'quatint;': '\u2a16', + 'quest;': '?', + 'questeq;': '\u225f', + 'QUOT': '"', + 'quot': '"', + 'QUOT;': '"', + 'quot;': '"', + 'rAarr;': '\u21db', + 'race;': '\u223d\u0331', + 'Racute;': '\u0154', + 'racute;': '\u0155', + 'radic;': '\u221a', + 'raemptyv;': '\u29b3', + 'Rang;': '\u27eb', + 'rang;': '\u27e9', + 'rangd;': '\u2992', + 'range;': '\u29a5', + 'rangle;': '\u27e9', + 'raquo': '\xbb', + 'raquo;': '\xbb', + 'Rarr;': '\u21a0', + 'rArr;': '\u21d2', + 'rarr;': '\u2192', + 'rarrap;': '\u2975', + 'rarrb;': '\u21e5', + 'rarrbfs;': '\u2920', + 'rarrc;': '\u2933', + 'rarrfs;': '\u291e', + 'rarrhk;': '\u21aa', + 'rarrlp;': '\u21ac', + 'rarrpl;': '\u2945', + 'rarrsim;': '\u2974', + 'Rarrtl;': '\u2916', + 'rarrtl;': '\u21a3', + 'rarrw;': '\u219d', + 'rAtail;': '\u291c', + 'ratail;': '\u291a', + 'ratio;': '\u2236', + 'rationals;': '\u211a', + 'RBarr;': '\u2910', + 'rBarr;': '\u290f', + 'rbarr;': '\u290d', + 'rbbrk;': '\u2773', + 'rbrace;': '}', + 'rbrack;': ']', + 'rbrke;': '\u298c', + 'rbrksld;': '\u298e', + 'rbrkslu;': '\u2990', + 'Rcaron;': '\u0158', + 'rcaron;': '\u0159', + 'Rcedil;': '\u0156', + 'rcedil;': '\u0157', + 'rceil;': '\u2309', + 'rcub;': '}', + 'Rcy;': '\u0420', + 'rcy;': '\u0440', + 'rdca;': '\u2937', + 'rdldhar;': '\u2969', + 'rdquo;': '\u201d', + 'rdquor;': '\u201d', + 'rdsh;': '\u21b3', + 'Re;': '\u211c', + 'real;': '\u211c', + 'realine;': '\u211b', + 'realpart;': '\u211c', + 'reals;': '\u211d', + 'rect;': '\u25ad', + 'REG': '\xae', + 'reg': '\xae', + 'REG;': '\xae', + 'reg;': '\xae', + 'ReverseElement;': '\u220b', + 'ReverseEquilibrium;': '\u21cb', + 'ReverseUpEquilibrium;': '\u296f', + 'rfisht;': '\u297d', + 'rfloor;': '\u230b', + 'Rfr;': '\u211c', + 'rfr;': '\U0001d52f', + 'rHar;': '\u2964', + 'rhard;': '\u21c1', + 'rharu;': '\u21c0', + 'rharul;': '\u296c', + 'Rho;': '\u03a1', + 'rho;': '\u03c1', + 'rhov;': '\u03f1', + 'RightAngleBracket;': '\u27e9', + 'RightArrow;': '\u2192', + 'Rightarrow;': '\u21d2', + 'rightarrow;': '\u2192', + 'RightArrowBar;': '\u21e5', + 'RightArrowLeftArrow;': '\u21c4', + 'rightarrowtail;': '\u21a3', + 'RightCeiling;': '\u2309', + 'RightDoubleBracket;': '\u27e7', + 'RightDownTeeVector;': '\u295d', + 'RightDownVector;': '\u21c2', + 'RightDownVectorBar;': '\u2955', + 'RightFloor;': '\u230b', + 'rightharpoondown;': '\u21c1', + 'rightharpoonup;': '\u21c0', + 'rightleftarrows;': '\u21c4', + 'rightleftharpoons;': '\u21cc', + 'rightrightarrows;': '\u21c9', + 'rightsquigarrow;': '\u219d', + 'RightTee;': '\u22a2', + 'RightTeeArrow;': '\u21a6', + 'RightTeeVector;': '\u295b', + 'rightthreetimes;': '\u22cc', + 'RightTriangle;': '\u22b3', + 'RightTriangleBar;': '\u29d0', + 'RightTriangleEqual;': '\u22b5', + 'RightUpDownVector;': '\u294f', + 'RightUpTeeVector;': '\u295c', + 'RightUpVector;': '\u21be', + 'RightUpVectorBar;': '\u2954', + 'RightVector;': '\u21c0', + 'RightVectorBar;': '\u2953', + 'ring;': '\u02da', + 'risingdotseq;': '\u2253', + 'rlarr;': '\u21c4', + 'rlhar;': '\u21cc', + 'rlm;': '\u200f', + 'rmoust;': '\u23b1', + 'rmoustache;': '\u23b1', + 'rnmid;': '\u2aee', + 'roang;': '\u27ed', + 'roarr;': '\u21fe', + 'robrk;': '\u27e7', + 'ropar;': '\u2986', + 'Ropf;': '\u211d', + 'ropf;': '\U0001d563', + 'roplus;': '\u2a2e', + 'rotimes;': '\u2a35', + 'RoundImplies;': '\u2970', + 'rpar;': ')', + 'rpargt;': '\u2994', + 'rppolint;': '\u2a12', + 'rrarr;': '\u21c9', + 'Rrightarrow;': '\u21db', + 'rsaquo;': '\u203a', + 'Rscr;': '\u211b', + 'rscr;': '\U0001d4c7', + 'Rsh;': '\u21b1', + 'rsh;': '\u21b1', + 'rsqb;': ']', + 'rsquo;': '\u2019', + 'rsquor;': '\u2019', + 'rthree;': '\u22cc', + 'rtimes;': '\u22ca', + 'rtri;': '\u25b9', + 'rtrie;': '\u22b5', + 'rtrif;': '\u25b8', + 'rtriltri;': '\u29ce', + 'RuleDelayed;': '\u29f4', + 'ruluhar;': '\u2968', + 'rx;': '\u211e', + 'Sacute;': '\u015a', + 'sacute;': '\u015b', + 'sbquo;': '\u201a', + 'Sc;': '\u2abc', + 'sc;': '\u227b', + 'scap;': '\u2ab8', + 'Scaron;': '\u0160', + 'scaron;': '\u0161', + 'sccue;': '\u227d', + 'scE;': '\u2ab4', + 'sce;': '\u2ab0', + 'Scedil;': '\u015e', + 'scedil;': '\u015f', + 'Scirc;': '\u015c', + 'scirc;': '\u015d', + 'scnap;': '\u2aba', + 'scnE;': '\u2ab6', + 'scnsim;': '\u22e9', + 'scpolint;': '\u2a13', + 'scsim;': '\u227f', + 'Scy;': '\u0421', + 'scy;': '\u0441', + 'sdot;': '\u22c5', + 'sdotb;': '\u22a1', + 'sdote;': '\u2a66', + 'searhk;': '\u2925', + 'seArr;': '\u21d8', + 'searr;': '\u2198', + 'searrow;': '\u2198', + 'sect': '\xa7', + 'sect;': '\xa7', + 'semi;': ';', + 'seswar;': '\u2929', + 'setminus;': '\u2216', + 'setmn;': '\u2216', + 'sext;': '\u2736', + 'Sfr;': '\U0001d516', + 'sfr;': '\U0001d530', + 'sfrown;': '\u2322', + 'sharp;': '\u266f', + 'SHCHcy;': '\u0429', + 'shchcy;': '\u0449', + 'SHcy;': '\u0428', + 'shcy;': '\u0448', + 'ShortDownArrow;': '\u2193', + 'ShortLeftArrow;': '\u2190', + 'shortmid;': '\u2223', + 'shortparallel;': '\u2225', + 'ShortRightArrow;': '\u2192', + 'ShortUpArrow;': '\u2191', + 'shy': '\xad', + 'shy;': '\xad', + 'Sigma;': '\u03a3', + 'sigma;': '\u03c3', + 'sigmaf;': '\u03c2', + 'sigmav;': '\u03c2', + 'sim;': '\u223c', + 'simdot;': '\u2a6a', + 'sime;': '\u2243', + 'simeq;': '\u2243', + 'simg;': '\u2a9e', + 'simgE;': '\u2aa0', + 'siml;': '\u2a9d', + 'simlE;': '\u2a9f', + 'simne;': '\u2246', + 'simplus;': '\u2a24', + 'simrarr;': '\u2972', + 'slarr;': '\u2190', + 'SmallCircle;': '\u2218', + 'smallsetminus;': '\u2216', + 'smashp;': '\u2a33', + 'smeparsl;': '\u29e4', + 'smid;': '\u2223', + 'smile;': '\u2323', + 'smt;': '\u2aaa', + 'smte;': '\u2aac', + 'smtes;': '\u2aac\ufe00', + 'SOFTcy;': '\u042c', + 'softcy;': '\u044c', + 'sol;': '/', + 'solb;': '\u29c4', + 'solbar;': '\u233f', + 'Sopf;': '\U0001d54a', + 'sopf;': '\U0001d564', + 'spades;': '\u2660', + 'spadesuit;': '\u2660', + 'spar;': '\u2225', + 'sqcap;': '\u2293', + 'sqcaps;': '\u2293\ufe00', + 'sqcup;': '\u2294', + 'sqcups;': '\u2294\ufe00', + 'Sqrt;': '\u221a', + 'sqsub;': '\u228f', + 'sqsube;': '\u2291', + 'sqsubset;': '\u228f', + 'sqsubseteq;': '\u2291', + 'sqsup;': '\u2290', + 'sqsupe;': '\u2292', + 'sqsupset;': '\u2290', + 'sqsupseteq;': '\u2292', + 'squ;': '\u25a1', + 'Square;': '\u25a1', + 'square;': '\u25a1', + 'SquareIntersection;': '\u2293', + 'SquareSubset;': '\u228f', + 'SquareSubsetEqual;': '\u2291', + 'SquareSuperset;': '\u2290', + 'SquareSupersetEqual;': '\u2292', + 'SquareUnion;': '\u2294', + 'squarf;': '\u25aa', + 'squf;': '\u25aa', + 'srarr;': '\u2192', + 'Sscr;': '\U0001d4ae', + 'sscr;': '\U0001d4c8', + 'ssetmn;': '\u2216', + 'ssmile;': '\u2323', + 'sstarf;': '\u22c6', + 'Star;': '\u22c6', + 'star;': '\u2606', + 'starf;': '\u2605', + 'straightepsilon;': '\u03f5', + 'straightphi;': '\u03d5', + 'strns;': '\xaf', + 'Sub;': '\u22d0', + 'sub;': '\u2282', + 'subdot;': '\u2abd', + 'subE;': '\u2ac5', + 'sube;': '\u2286', + 'subedot;': '\u2ac3', + 'submult;': '\u2ac1', + 'subnE;': '\u2acb', + 'subne;': '\u228a', + 'subplus;': '\u2abf', + 'subrarr;': '\u2979', + 'Subset;': '\u22d0', + 'subset;': '\u2282', + 'subseteq;': '\u2286', + 'subseteqq;': '\u2ac5', + 'SubsetEqual;': '\u2286', + 'subsetneq;': '\u228a', + 'subsetneqq;': '\u2acb', + 'subsim;': '\u2ac7', + 'subsub;': '\u2ad5', + 'subsup;': '\u2ad3', + 'succ;': '\u227b', + 'succapprox;': '\u2ab8', + 'succcurlyeq;': '\u227d', + 'Succeeds;': '\u227b', + 'SucceedsEqual;': '\u2ab0', + 'SucceedsSlantEqual;': '\u227d', + 'SucceedsTilde;': '\u227f', + 'succeq;': '\u2ab0', + 'succnapprox;': '\u2aba', + 'succneqq;': '\u2ab6', + 'succnsim;': '\u22e9', + 'succsim;': '\u227f', + 'SuchThat;': '\u220b', + 'Sum;': '\u2211', + 'sum;': '\u2211', + 'sung;': '\u266a', + 'sup1': '\xb9', + 'sup1;': '\xb9', + 'sup2': '\xb2', + 'sup2;': '\xb2', + 'sup3': '\xb3', + 'sup3;': '\xb3', + 'Sup;': '\u22d1', + 'sup;': '\u2283', + 'supdot;': '\u2abe', + 'supdsub;': '\u2ad8', + 'supE;': '\u2ac6', + 'supe;': '\u2287', + 'supedot;': '\u2ac4', + 'Superset;': '\u2283', + 'SupersetEqual;': '\u2287', + 'suphsol;': '\u27c9', + 'suphsub;': '\u2ad7', + 'suplarr;': '\u297b', + 'supmult;': '\u2ac2', + 'supnE;': '\u2acc', + 'supne;': '\u228b', + 'supplus;': '\u2ac0', + 'Supset;': '\u22d1', + 'supset;': '\u2283', + 'supseteq;': '\u2287', + 'supseteqq;': '\u2ac6', + 'supsetneq;': '\u228b', + 'supsetneqq;': '\u2acc', + 'supsim;': '\u2ac8', + 'supsub;': '\u2ad4', + 'supsup;': '\u2ad6', + 'swarhk;': '\u2926', + 'swArr;': '\u21d9', + 'swarr;': '\u2199', + 'swarrow;': '\u2199', + 'swnwar;': '\u292a', + 'szlig': '\xdf', + 'szlig;': '\xdf', + 'Tab;': '\t', + 'target;': '\u2316', + 'Tau;': '\u03a4', + 'tau;': '\u03c4', + 'tbrk;': '\u23b4', + 'Tcaron;': '\u0164', + 'tcaron;': '\u0165', + 'Tcedil;': '\u0162', + 'tcedil;': '\u0163', + 'Tcy;': '\u0422', + 'tcy;': '\u0442', + 'tdot;': '\u20db', + 'telrec;': '\u2315', + 'Tfr;': '\U0001d517', + 'tfr;': '\U0001d531', + 'there4;': '\u2234', + 'Therefore;': '\u2234', + 'therefore;': '\u2234', + 'Theta;': '\u0398', + 'theta;': '\u03b8', + 'thetasym;': '\u03d1', + 'thetav;': '\u03d1', + 'thickapprox;': '\u2248', + 'thicksim;': '\u223c', + 'ThickSpace;': '\u205f\u200a', + 'thinsp;': '\u2009', + 'ThinSpace;': '\u2009', + 'thkap;': '\u2248', + 'thksim;': '\u223c', + 'THORN': '\xde', + 'thorn': '\xfe', + 'THORN;': '\xde', + 'thorn;': '\xfe', + 'Tilde;': '\u223c', + 'tilde;': '\u02dc', + 'TildeEqual;': '\u2243', + 'TildeFullEqual;': '\u2245', + 'TildeTilde;': '\u2248', + 'times': '\xd7', + 'times;': '\xd7', + 'timesb;': '\u22a0', + 'timesbar;': '\u2a31', + 'timesd;': '\u2a30', + 'tint;': '\u222d', + 'toea;': '\u2928', + 'top;': '\u22a4', + 'topbot;': '\u2336', + 'topcir;': '\u2af1', + 'Topf;': '\U0001d54b', + 'topf;': '\U0001d565', + 'topfork;': '\u2ada', + 'tosa;': '\u2929', + 'tprime;': '\u2034', + 'TRADE;': '\u2122', + 'trade;': '\u2122', + 'triangle;': '\u25b5', + 'triangledown;': '\u25bf', + 'triangleleft;': '\u25c3', + 'trianglelefteq;': '\u22b4', + 'triangleq;': '\u225c', + 'triangleright;': '\u25b9', + 'trianglerighteq;': '\u22b5', + 'tridot;': '\u25ec', + 'trie;': '\u225c', + 'triminus;': '\u2a3a', + 'TripleDot;': '\u20db', + 'triplus;': '\u2a39', + 'trisb;': '\u29cd', + 'tritime;': '\u2a3b', + 'trpezium;': '\u23e2', + 'Tscr;': '\U0001d4af', + 'tscr;': '\U0001d4c9', + 'TScy;': '\u0426', + 'tscy;': '\u0446', + 'TSHcy;': '\u040b', + 'tshcy;': '\u045b', + 'Tstrok;': '\u0166', + 'tstrok;': '\u0167', + 'twixt;': '\u226c', + 'twoheadleftarrow;': '\u219e', + 'twoheadrightarrow;': '\u21a0', + 'Uacute': '\xda', + 'uacute': '\xfa', + 'Uacute;': '\xda', + 'uacute;': '\xfa', + 'Uarr;': '\u219f', + 'uArr;': '\u21d1', + 'uarr;': '\u2191', + 'Uarrocir;': '\u2949', + 'Ubrcy;': '\u040e', + 'ubrcy;': '\u045e', + 'Ubreve;': '\u016c', + 'ubreve;': '\u016d', + 'Ucirc': '\xdb', + 'ucirc': '\xfb', + 'Ucirc;': '\xdb', + 'ucirc;': '\xfb', + 'Ucy;': '\u0423', + 'ucy;': '\u0443', + 'udarr;': '\u21c5', + 'Udblac;': '\u0170', + 'udblac;': '\u0171', + 'udhar;': '\u296e', + 'ufisht;': '\u297e', + 'Ufr;': '\U0001d518', + 'ufr;': '\U0001d532', + 'Ugrave': '\xd9', + 'ugrave': '\xf9', + 'Ugrave;': '\xd9', + 'ugrave;': '\xf9', + 'uHar;': '\u2963', + 'uharl;': '\u21bf', + 'uharr;': '\u21be', + 'uhblk;': '\u2580', + 'ulcorn;': '\u231c', + 'ulcorner;': '\u231c', + 'ulcrop;': '\u230f', + 'ultri;': '\u25f8', + 'Umacr;': '\u016a', + 'umacr;': '\u016b', + 'uml': '\xa8', + 'uml;': '\xa8', + 'UnderBar;': '_', + 'UnderBrace;': '\u23df', + 'UnderBracket;': '\u23b5', + 'UnderParenthesis;': '\u23dd', + 'Union;': '\u22c3', + 'UnionPlus;': '\u228e', + 'Uogon;': '\u0172', + 'uogon;': '\u0173', + 'Uopf;': '\U0001d54c', + 'uopf;': '\U0001d566', + 'UpArrow;': '\u2191', + 'Uparrow;': '\u21d1', + 'uparrow;': '\u2191', + 'UpArrowBar;': '\u2912', + 'UpArrowDownArrow;': '\u21c5', + 'UpDownArrow;': '\u2195', + 'Updownarrow;': '\u21d5', + 'updownarrow;': '\u2195', + 'UpEquilibrium;': '\u296e', + 'upharpoonleft;': '\u21bf', + 'upharpoonright;': '\u21be', + 'uplus;': '\u228e', + 'UpperLeftArrow;': '\u2196', + 'UpperRightArrow;': '\u2197', + 'Upsi;': '\u03d2', + 'upsi;': '\u03c5', + 'upsih;': '\u03d2', + 'Upsilon;': '\u03a5', + 'upsilon;': '\u03c5', + 'UpTee;': '\u22a5', + 'UpTeeArrow;': '\u21a5', + 'upuparrows;': '\u21c8', + 'urcorn;': '\u231d', + 'urcorner;': '\u231d', + 'urcrop;': '\u230e', + 'Uring;': '\u016e', + 'uring;': '\u016f', + 'urtri;': '\u25f9', + 'Uscr;': '\U0001d4b0', + 'uscr;': '\U0001d4ca', + 'utdot;': '\u22f0', + 'Utilde;': '\u0168', + 'utilde;': '\u0169', + 'utri;': '\u25b5', + 'utrif;': '\u25b4', + 'uuarr;': '\u21c8', + 'Uuml': '\xdc', + 'uuml': '\xfc', + 'Uuml;': '\xdc', + 'uuml;': '\xfc', + 'uwangle;': '\u29a7', + 'vangrt;': '\u299c', + 'varepsilon;': '\u03f5', + 'varkappa;': '\u03f0', + 'varnothing;': '\u2205', + 'varphi;': '\u03d5', + 'varpi;': '\u03d6', + 'varpropto;': '\u221d', + 'vArr;': '\u21d5', + 'varr;': '\u2195', + 'varrho;': '\u03f1', + 'varsigma;': '\u03c2', + 'varsubsetneq;': '\u228a\ufe00', + 'varsubsetneqq;': '\u2acb\ufe00', + 'varsupsetneq;': '\u228b\ufe00', + 'varsupsetneqq;': '\u2acc\ufe00', + 'vartheta;': '\u03d1', + 'vartriangleleft;': '\u22b2', + 'vartriangleright;': '\u22b3', + 'Vbar;': '\u2aeb', + 'vBar;': '\u2ae8', + 'vBarv;': '\u2ae9', + 'Vcy;': '\u0412', + 'vcy;': '\u0432', + 'VDash;': '\u22ab', + 'Vdash;': '\u22a9', + 'vDash;': '\u22a8', + 'vdash;': '\u22a2', + 'Vdashl;': '\u2ae6', + 'Vee;': '\u22c1', + 'vee;': '\u2228', + 'veebar;': '\u22bb', + 'veeeq;': '\u225a', + 'vellip;': '\u22ee', + 'Verbar;': '\u2016', + 'verbar;': '|', + 'Vert;': '\u2016', + 'vert;': '|', + 'VerticalBar;': '\u2223', + 'VerticalLine;': '|', + 'VerticalSeparator;': '\u2758', + 'VerticalTilde;': '\u2240', + 'VeryThinSpace;': '\u200a', + 'Vfr;': '\U0001d519', + 'vfr;': '\U0001d533', + 'vltri;': '\u22b2', + 'vnsub;': '\u2282\u20d2', + 'vnsup;': '\u2283\u20d2', + 'Vopf;': '\U0001d54d', + 'vopf;': '\U0001d567', + 'vprop;': '\u221d', + 'vrtri;': '\u22b3', + 'Vscr;': '\U0001d4b1', + 'vscr;': '\U0001d4cb', + 'vsubnE;': '\u2acb\ufe00', + 'vsubne;': '\u228a\ufe00', + 'vsupnE;': '\u2acc\ufe00', + 'vsupne;': '\u228b\ufe00', + 'Vvdash;': '\u22aa', + 'vzigzag;': '\u299a', + 'Wcirc;': '\u0174', + 'wcirc;': '\u0175', + 'wedbar;': '\u2a5f', + 'Wedge;': '\u22c0', + 'wedge;': '\u2227', + 'wedgeq;': '\u2259', + 'weierp;': '\u2118', + 'Wfr;': '\U0001d51a', + 'wfr;': '\U0001d534', + 'Wopf;': '\U0001d54e', + 'wopf;': '\U0001d568', + 'wp;': '\u2118', + 'wr;': '\u2240', + 'wreath;': '\u2240', + 'Wscr;': '\U0001d4b2', + 'wscr;': '\U0001d4cc', + 'xcap;': '\u22c2', + 'xcirc;': '\u25ef', + 'xcup;': '\u22c3', + 'xdtri;': '\u25bd', + 'Xfr;': '\U0001d51b', + 'xfr;': '\U0001d535', + 'xhArr;': '\u27fa', + 'xharr;': '\u27f7', + 'Xi;': '\u039e', + 'xi;': '\u03be', + 'xlArr;': '\u27f8', + 'xlarr;': '\u27f5', + 'xmap;': '\u27fc', + 'xnis;': '\u22fb', + 'xodot;': '\u2a00', + 'Xopf;': '\U0001d54f', + 'xopf;': '\U0001d569', + 'xoplus;': '\u2a01', + 'xotime;': '\u2a02', + 'xrArr;': '\u27f9', + 'xrarr;': '\u27f6', + 'Xscr;': '\U0001d4b3', + 'xscr;': '\U0001d4cd', + 'xsqcup;': '\u2a06', + 'xuplus;': '\u2a04', + 'xutri;': '\u25b3', + 'xvee;': '\u22c1', + 'xwedge;': '\u22c0', + 'Yacute': '\xdd', + 'yacute': '\xfd', + 'Yacute;': '\xdd', + 'yacute;': '\xfd', + 'YAcy;': '\u042f', + 'yacy;': '\u044f', + 'Ycirc;': '\u0176', + 'ycirc;': '\u0177', + 'Ycy;': '\u042b', + 'ycy;': '\u044b', + 'yen': '\xa5', + 'yen;': '\xa5', + 'Yfr;': '\U0001d51c', + 'yfr;': '\U0001d536', + 'YIcy;': '\u0407', + 'yicy;': '\u0457', + 'Yopf;': '\U0001d550', + 'yopf;': '\U0001d56a', + 'Yscr;': '\U0001d4b4', + 'yscr;': '\U0001d4ce', + 'YUcy;': '\u042e', + 'yucy;': '\u044e', + 'yuml': '\xff', + 'Yuml;': '\u0178', + 'yuml;': '\xff', + 'Zacute;': '\u0179', + 'zacute;': '\u017a', + 'Zcaron;': '\u017d', + 'zcaron;': '\u017e', + 'Zcy;': '\u0417', + 'zcy;': '\u0437', + 'Zdot;': '\u017b', + 'zdot;': '\u017c', + 'zeetrf;': '\u2128', + 'ZeroWidthSpace;': '\u200b', + 'Zeta;': '\u0396', + 'zeta;': '\u03b6', + 'Zfr;': '\u2128', + 'zfr;': '\U0001d537', + 'ZHcy;': '\u0416', + 'zhcy;': '\u0436', + 'zigrarr;': '\u21dd', + 'Zopf;': '\u2124', + 'zopf;': '\U0001d56b', + 'Zscr;': '\U0001d4b5', + 'zscr;': '\U0001d4cf', + 'zwj;': '\u200d', + 'zwnj;': '\u200c', +} + +# maps the Unicode codepoint to the HTML entity name +codepoint2name = {} + +# maps the HTML entity name to the character +# (or a character reference if the character is outside the Latin-1 range) +entitydefs = {} + +for (name, codepoint) in name2codepoint.items(): + codepoint2name[codepoint] = name + entitydefs[name] = chr(codepoint) + +del name, codepoint diff --git a/venv/lib/python3.10/site-packages/future/backports/html/parser.py b/venv/lib/python3.10/site-packages/future/backports/html/parser.py new file mode 100644 index 0000000..fb65263 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/html/parser.py @@ -0,0 +1,536 @@ +"""A parser for HTML and XHTML. + +Backported for python-future from Python 3.3. +""" + +# This file is based on sgmllib.py, but the API is slightly different. + +# XXX There should be a way to distinguish between PCDATA (parsed +# character data -- the normal case), RCDATA (replaceable character +# data -- only char and entity references and end tags are special) +# and CDATA (character data -- only end tags are special). + +from __future__ import (absolute_import, division, + print_function, unicode_literals) +from future.builtins import * +from future.backports import _markupbase +import re +import warnings + +# Regular expressions used for parsing + +interesting_normal = re.compile('[&<]') +incomplete = re.compile('&[a-zA-Z#]') + +entityref = re.compile('&([a-zA-Z][-.a-zA-Z0-9]*)[^a-zA-Z0-9]') +charref = re.compile('&#(?:[0-9]+|[xX][0-9a-fA-F]+)[^0-9a-fA-F]') + +starttagopen = re.compile('<[a-zA-Z]') +piclose = re.compile('>') +commentclose = re.compile(r'--\s*>') +tagfind = re.compile('([a-zA-Z][-.a-zA-Z0-9:_]*)(?:\s|/(?!>))*') +# see http://www.w3.org/TR/html5/tokenization.html#tag-open-state +# and http://www.w3.org/TR/html5/tokenization.html#tag-name-state +tagfind_tolerant = re.compile('[a-zA-Z][^\t\n\r\f />\x00]*') +# Note: +# 1) the strict attrfind isn't really strict, but we can't make it +# correctly strict without breaking backward compatibility; +# 2) if you change attrfind remember to update locatestarttagend too; +# 3) if you change attrfind and/or locatestarttagend the parser will +# explode, so don't do it. +attrfind = re.compile( + r'\s*([a-zA-Z_][-.:a-zA-Z_0-9]*)(\s*=\s*' + r'(\'[^\']*\'|"[^"]*"|[^\s"\'=<>`]*))?') +attrfind_tolerant = re.compile( + r'((?<=[\'"\s/])[^\s/>][^\s/=>]*)(\s*=+\s*' + r'(\'[^\']*\'|"[^"]*"|(?![\'"])[^>\s]*))?(?:\s|/(?!>))*') +locatestarttagend = re.compile(r""" + <[a-zA-Z][-.a-zA-Z0-9:_]* # tag name + (?:\s+ # whitespace before attribute name + (?:[a-zA-Z_][-.:a-zA-Z0-9_]* # attribute name + (?:\s*=\s* # value indicator + (?:'[^']*' # LITA-enclosed value + |\"[^\"]*\" # LIT-enclosed value + |[^'\">\s]+ # bare value + ) + )? + ) + )* + \s* # trailing whitespace +""", re.VERBOSE) +locatestarttagend_tolerant = re.compile(r""" + <[a-zA-Z][-.a-zA-Z0-9:_]* # tag name + (?:[\s/]* # optional whitespace before attribute name + (?:(?<=['"\s/])[^\s/>][^\s/=>]* # attribute name + (?:\s*=+\s* # value indicator + (?:'[^']*' # LITA-enclosed value + |"[^"]*" # LIT-enclosed value + |(?!['"])[^>\s]* # bare value + ) + (?:\s*,)* # possibly followed by a comma + )?(?:\s|/(?!>))* + )* + )? + \s* # trailing whitespace +""", re.VERBOSE) +endendtag = re.compile('>') +# the HTML 5 spec, section 8.1.2.2, doesn't allow spaces between +# ') + + +class HTMLParseError(Exception): + """Exception raised for all parse errors.""" + + def __init__(self, msg, position=(None, None)): + assert msg + self.msg = msg + self.lineno = position[0] + self.offset = position[1] + + def __str__(self): + result = self.msg + if self.lineno is not None: + result = result + ", at line %d" % self.lineno + if self.offset is not None: + result = result + ", column %d" % (self.offset + 1) + return result + + +class HTMLParser(_markupbase.ParserBase): + """Find tags and other markup and call handler functions. + + Usage: + p = HTMLParser() + p.feed(data) + ... + p.close() + + Start tags are handled by calling self.handle_starttag() or + self.handle_startendtag(); end tags by self.handle_endtag(). The + data between tags is passed from the parser to the derived class + by calling self.handle_data() with the data as argument (the data + may be split up in arbitrary chunks). Entity references are + passed by calling self.handle_entityref() with the entity + reference as the argument. Numeric character references are + passed to self.handle_charref() with the string containing the + reference as the argument. + """ + + CDATA_CONTENT_ELEMENTS = ("script", "style") + + def __init__(self, strict=False): + """Initialize and reset this instance. + + If strict is set to False (the default) the parser will parse invalid + markup, otherwise it will raise an error. Note that the strict mode + is deprecated. + """ + if strict: + warnings.warn("The strict mode is deprecated.", + DeprecationWarning, stacklevel=2) + self.strict = strict + self.reset() + + def reset(self): + """Reset this instance. Loses all unprocessed data.""" + self.rawdata = '' + self.lasttag = '???' + self.interesting = interesting_normal + self.cdata_elem = None + _markupbase.ParserBase.reset(self) + + def feed(self, data): + r"""Feed data to the parser. + + Call this as often as you want, with as little or as much text + as you want (may include '\n'). + """ + self.rawdata = self.rawdata + data + self.goahead(0) + + def close(self): + """Handle any buffered data.""" + self.goahead(1) + + def error(self, message): + raise HTMLParseError(message, self.getpos()) + + __starttag_text = None + + def get_starttag_text(self): + """Return full source of start tag: '<...>'.""" + return self.__starttag_text + + def set_cdata_mode(self, elem): + self.cdata_elem = elem.lower() + self.interesting = re.compile(r'' % self.cdata_elem, re.I) + + def clear_cdata_mode(self): + self.interesting = interesting_normal + self.cdata_elem = None + + # Internal -- handle data as far as reasonable. May leave state + # and data to be processed by a subsequent call. If 'end' is + # true, force handling all data as if followed by EOF marker. + def goahead(self, end): + rawdata = self.rawdata + i = 0 + n = len(rawdata) + while i < n: + match = self.interesting.search(rawdata, i) # < or & + if match: + j = match.start() + else: + if self.cdata_elem: + break + j = n + if i < j: self.handle_data(rawdata[i:j]) + i = self.updatepos(i, j) + if i == n: break + startswith = rawdata.startswith + if startswith('<', i): + if starttagopen.match(rawdata, i): # < + letter + k = self.parse_starttag(i) + elif startswith("', i + 1) + if k < 0: + k = rawdata.find('<', i + 1) + if k < 0: + k = i + 1 + else: + k += 1 + self.handle_data(rawdata[i:k]) + i = self.updatepos(i, k) + elif startswith("&#", i): + match = charref.match(rawdata, i) + if match: + name = match.group()[2:-1] + self.handle_charref(name) + k = match.end() + if not startswith(';', k-1): + k = k - 1 + i = self.updatepos(i, k) + continue + else: + if ";" in rawdata[i:]: #bail by consuming &# + self.handle_data(rawdata[0:2]) + i = self.updatepos(i, 2) + break + elif startswith('&', i): + match = entityref.match(rawdata, i) + if match: + name = match.group(1) + self.handle_entityref(name) + k = match.end() + if not startswith(';', k-1): + k = k - 1 + i = self.updatepos(i, k) + continue + match = incomplete.match(rawdata, i) + if match: + # match.group() will contain at least 2 chars + if end and match.group() == rawdata[i:]: + if self.strict: + self.error("EOF in middle of entity or char ref") + else: + if k <= i: + k = n + i = self.updatepos(i, i + 1) + # incomplete + break + elif (i + 1) < n: + # not the end of the buffer, and can't be confused + # with some other construct + self.handle_data("&") + i = self.updatepos(i, i + 1) + else: + break + else: + assert 0, "interesting.search() lied" + # end while + if end and i < n and not self.cdata_elem: + self.handle_data(rawdata[i:n]) + i = self.updatepos(i, n) + self.rawdata = rawdata[i:] + + # Internal -- parse html declarations, return length or -1 if not terminated + # See w3.org/TR/html5/tokenization.html#markup-declaration-open-state + # See also parse_declaration in _markupbase + def parse_html_declaration(self, i): + rawdata = self.rawdata + assert rawdata[i:i+2] == ' + gtpos = rawdata.find('>', i+9) + if gtpos == -1: + return -1 + self.handle_decl(rawdata[i+2:gtpos]) + return gtpos+1 + else: + return self.parse_bogus_comment(i) + + # Internal -- parse bogus comment, return length or -1 if not terminated + # see http://www.w3.org/TR/html5/tokenization.html#bogus-comment-state + def parse_bogus_comment(self, i, report=1): + rawdata = self.rawdata + assert rawdata[i:i+2] in ('', i+2) + if pos == -1: + return -1 + if report: + self.handle_comment(rawdata[i+2:pos]) + return pos + 1 + + # Internal -- parse processing instr, return end or -1 if not terminated + def parse_pi(self, i): + rawdata = self.rawdata + assert rawdata[i:i+2] == ' + if not match: + return -1 + j = match.start() + self.handle_pi(rawdata[i+2: j]) + j = match.end() + return j + + # Internal -- handle starttag, return end or -1 if not terminated + def parse_starttag(self, i): + self.__starttag_text = None + endpos = self.check_for_whole_start_tag(i) + if endpos < 0: + return endpos + rawdata = self.rawdata + self.__starttag_text = rawdata[i:endpos] + + # Now parse the data between i+1 and j into a tag and attrs + attrs = [] + match = tagfind.match(rawdata, i+1) + assert match, 'unexpected call to parse_starttag()' + k = match.end() + self.lasttag = tag = match.group(1).lower() + while k < endpos: + if self.strict: + m = attrfind.match(rawdata, k) + else: + m = attrfind_tolerant.match(rawdata, k) + if not m: + break + attrname, rest, attrvalue = m.group(1, 2, 3) + if not rest: + attrvalue = None + elif attrvalue[:1] == '\'' == attrvalue[-1:] or \ + attrvalue[:1] == '"' == attrvalue[-1:]: + attrvalue = attrvalue[1:-1] + if attrvalue: + attrvalue = self.unescape(attrvalue) + attrs.append((attrname.lower(), attrvalue)) + k = m.end() + + end = rawdata[k:endpos].strip() + if end not in (">", "/>"): + lineno, offset = self.getpos() + if "\n" in self.__starttag_text: + lineno = lineno + self.__starttag_text.count("\n") + offset = len(self.__starttag_text) \ + - self.__starttag_text.rfind("\n") + else: + offset = offset + len(self.__starttag_text) + if self.strict: + self.error("junk characters in start tag: %r" + % (rawdata[k:endpos][:20],)) + self.handle_data(rawdata[i:endpos]) + return endpos + if end.endswith('/>'): + # XHTML-style empty tag: + self.handle_startendtag(tag, attrs) + else: + self.handle_starttag(tag, attrs) + if tag in self.CDATA_CONTENT_ELEMENTS: + self.set_cdata_mode(tag) + return endpos + + # Internal -- check to see if we have a complete starttag; return end + # or -1 if incomplete. + def check_for_whole_start_tag(self, i): + rawdata = self.rawdata + if self.strict: + m = locatestarttagend.match(rawdata, i) + else: + m = locatestarttagend_tolerant.match(rawdata, i) + if m: + j = m.end() + next = rawdata[j:j+1] + if next == ">": + return j + 1 + if next == "/": + if rawdata.startswith("/>", j): + return j + 2 + if rawdata.startswith("/", j): + # buffer boundary + return -1 + # else bogus input + if self.strict: + self.updatepos(i, j + 1) + self.error("malformed empty start tag") + if j > i: + return j + else: + return i + 1 + if next == "": + # end of input + return -1 + if next in ("abcdefghijklmnopqrstuvwxyz=/" + "ABCDEFGHIJKLMNOPQRSTUVWXYZ"): + # end of input in or before attribute value, or we have the + # '/' from a '/>' ending + return -1 + if self.strict: + self.updatepos(i, j) + self.error("malformed start tag") + if j > i: + return j + else: + return i + 1 + raise AssertionError("we should not get here!") + + # Internal -- parse endtag, return end or -1 if incomplete + def parse_endtag(self, i): + rawdata = self.rawdata + assert rawdata[i:i+2] == " + if not match: + return -1 + gtpos = match.end() + match = endtagfind.match(rawdata, i) # + if not match: + if self.cdata_elem is not None: + self.handle_data(rawdata[i:gtpos]) + return gtpos + if self.strict: + self.error("bad end tag: %r" % (rawdata[i:gtpos],)) + # find the name: w3.org/TR/html5/tokenization.html#tag-name-state + namematch = tagfind_tolerant.match(rawdata, i+2) + if not namematch: + # w3.org/TR/html5/tokenization.html#end-tag-open-state + if rawdata[i:i+3] == '': + return i+3 + else: + return self.parse_bogus_comment(i) + tagname = namematch.group().lower() + # consume and ignore other stuff between the name and the > + # Note: this is not 100% correct, since we might have things like + # , but looking for > after tha name should cover + # most of the cases and is much simpler + gtpos = rawdata.find('>', namematch.end()) + self.handle_endtag(tagname) + return gtpos+1 + + elem = match.group(1).lower() # script or style + if self.cdata_elem is not None: + if elem != self.cdata_elem: + self.handle_data(rawdata[i:gtpos]) + return gtpos + + self.handle_endtag(elem.lower()) + self.clear_cdata_mode() + return gtpos + + # Overridable -- finish processing of start+end tag: + def handle_startendtag(self, tag, attrs): + self.handle_starttag(tag, attrs) + self.handle_endtag(tag) + + # Overridable -- handle start tag + def handle_starttag(self, tag, attrs): + pass + + # Overridable -- handle end tag + def handle_endtag(self, tag): + pass + + # Overridable -- handle character reference + def handle_charref(self, name): + pass + + # Overridable -- handle entity reference + def handle_entityref(self, name): + pass + + # Overridable -- handle data + def handle_data(self, data): + pass + + # Overridable -- handle comment + def handle_comment(self, data): + pass + + # Overridable -- handle declaration + def handle_decl(self, decl): + pass + + # Overridable -- handle processing instruction + def handle_pi(self, data): + pass + + def unknown_decl(self, data): + if self.strict: + self.error("unknown declaration: %r" % (data,)) + + # Internal -- helper to remove special character quoting + def unescape(self, s): + if '&' not in s: + return s + def replaceEntities(s): + s = s.groups()[0] + try: + if s[0] == "#": + s = s[1:] + if s[0] in ['x','X']: + c = int(s[1:].rstrip(';'), 16) + else: + c = int(s.rstrip(';')) + return chr(c) + except ValueError: + return '&#' + s + else: + from future.backports.html.entities import html5 + if s in html5: + return html5[s] + elif s.endswith(';'): + return '&' + s + for x in range(2, len(s)): + if s[:x] in html5: + return html5[s[:x]] + s[x:] + else: + return '&' + s + + return re.sub(r"&(#?[xX]?(?:[0-9a-fA-F]+;|\w{1,32};?))", + replaceEntities, s) diff --git a/venv/lib/python3.10/site-packages/future/backports/http/__init__.py b/venv/lib/python3.10/site-packages/future/backports/http/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..032aec3 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/client.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/client.cpython-310.pyc new file mode 100644 index 0000000..c6ec100 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/client.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/cookiejar.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/cookiejar.cpython-310.pyc new file mode 100644 index 0000000..804475a Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/cookiejar.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/cookies.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/cookies.cpython-310.pyc new file mode 100644 index 0000000..610fd8b Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/cookies.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/server.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/server.cpython-310.pyc new file mode 100644 index 0000000..9ba3469 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/http/__pycache__/server.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/http/client.py b/venv/lib/python3.10/site-packages/future/backports/http/client.py new file mode 100644 index 0000000..e663d12 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/http/client.py @@ -0,0 +1,1346 @@ +"""HTTP/1.1 client library + +A backport of the Python 3.3 http/client.py module for python-future. + + + + +HTTPConnection goes through a number of "states", which define when a client +may legally make another request or fetch the response for a particular +request. This diagram details these state transitions: + + (null) + | + | HTTPConnection() + v + Idle + | + | putrequest() + v + Request-started + | + | ( putheader() )* endheaders() + v + Request-sent + | + | response = getresponse() + v + Unread-response [Response-headers-read] + |\____________________ + | | + | response.read() | putrequest() + v v + Idle Req-started-unread-response + ______/| + / | + response.read() | | ( putheader() )* endheaders() + v v + Request-started Req-sent-unread-response + | + | response.read() + v + Request-sent + +This diagram presents the following rules: + -- a second request may not be started until {response-headers-read} + -- a response [object] cannot be retrieved until {request-sent} + -- there is no differentiation between an unread response body and a + partially read response body + +Note: this enforcement is applied by the HTTPConnection class. The + HTTPResponse class does not enforce this state machine, which + implies sophisticated clients may accelerate the request/response + pipeline. Caution should be taken, though: accelerating the states + beyond the above pattern may imply knowledge of the server's + connection-close behavior for certain requests. For example, it + is impossible to tell whether the server will close the connection + UNTIL the response headers have been read; this means that further + requests cannot be placed into the pipeline until it is known that + the server will NOT be closing the connection. + +Logical State __state __response +------------- ------- ---------- +Idle _CS_IDLE None +Request-started _CS_REQ_STARTED None +Request-sent _CS_REQ_SENT None +Unread-response _CS_IDLE +Req-started-unread-response _CS_REQ_STARTED +Req-sent-unread-response _CS_REQ_SENT +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) +from future.builtins import bytes, int, str, super +from future.utils import PY2 + +from future.backports.email import parser as email_parser +from future.backports.email import message as email_message +from future.backports.misc import create_connection as socket_create_connection +import io +import os +import socket +from future.backports.urllib.parse import urlsplit +import warnings +from array import array + +if PY2: + from collections import Iterable +else: + from collections.abc import Iterable + +__all__ = ["HTTPResponse", "HTTPConnection", + "HTTPException", "NotConnected", "UnknownProtocol", + "UnknownTransferEncoding", "UnimplementedFileMode", + "IncompleteRead", "InvalidURL", "ImproperConnectionState", + "CannotSendRequest", "CannotSendHeader", "ResponseNotReady", + "BadStatusLine", "error", "responses"] + +HTTP_PORT = 80 +HTTPS_PORT = 443 + +_UNKNOWN = 'UNKNOWN' + +# connection states +_CS_IDLE = 'Idle' +_CS_REQ_STARTED = 'Request-started' +_CS_REQ_SENT = 'Request-sent' + +# status codes +# informational +CONTINUE = 100 +SWITCHING_PROTOCOLS = 101 +PROCESSING = 102 + +# successful +OK = 200 +CREATED = 201 +ACCEPTED = 202 +NON_AUTHORITATIVE_INFORMATION = 203 +NO_CONTENT = 204 +RESET_CONTENT = 205 +PARTIAL_CONTENT = 206 +MULTI_STATUS = 207 +IM_USED = 226 + +# redirection +MULTIPLE_CHOICES = 300 +MOVED_PERMANENTLY = 301 +FOUND = 302 +SEE_OTHER = 303 +NOT_MODIFIED = 304 +USE_PROXY = 305 +TEMPORARY_REDIRECT = 307 + +# client error +BAD_REQUEST = 400 +UNAUTHORIZED = 401 +PAYMENT_REQUIRED = 402 +FORBIDDEN = 403 +NOT_FOUND = 404 +METHOD_NOT_ALLOWED = 405 +NOT_ACCEPTABLE = 406 +PROXY_AUTHENTICATION_REQUIRED = 407 +REQUEST_TIMEOUT = 408 +CONFLICT = 409 +GONE = 410 +LENGTH_REQUIRED = 411 +PRECONDITION_FAILED = 412 +REQUEST_ENTITY_TOO_LARGE = 413 +REQUEST_URI_TOO_LONG = 414 +UNSUPPORTED_MEDIA_TYPE = 415 +REQUESTED_RANGE_NOT_SATISFIABLE = 416 +EXPECTATION_FAILED = 417 +UNPROCESSABLE_ENTITY = 422 +LOCKED = 423 +FAILED_DEPENDENCY = 424 +UPGRADE_REQUIRED = 426 +PRECONDITION_REQUIRED = 428 +TOO_MANY_REQUESTS = 429 +REQUEST_HEADER_FIELDS_TOO_LARGE = 431 + +# server error +INTERNAL_SERVER_ERROR = 500 +NOT_IMPLEMENTED = 501 +BAD_GATEWAY = 502 +SERVICE_UNAVAILABLE = 503 +GATEWAY_TIMEOUT = 504 +HTTP_VERSION_NOT_SUPPORTED = 505 +INSUFFICIENT_STORAGE = 507 +NOT_EXTENDED = 510 +NETWORK_AUTHENTICATION_REQUIRED = 511 + +# Mapping status codes to official W3C names +responses = { + 100: 'Continue', + 101: 'Switching Protocols', + + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + + 300: 'Multiple Choices', + 301: 'Moved Permanently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 306: '(Unused)', + 307: 'Temporary Redirect', + + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Request Entity Too Large', + 414: 'Request-URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Requested Range Not Satisfiable', + 417: 'Expectation Failed', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 511: 'Network Authentication Required', +} + +# maximal amount of data to read at one time in _safe_read +MAXAMOUNT = 1048576 + +# maximal line length when calling readline(). +_MAXLINE = 65536 +_MAXHEADERS = 100 + + +class HTTPMessage(email_message.Message): + # XXX The only usage of this method is in + # http.server.CGIHTTPRequestHandler. Maybe move the code there so + # that it doesn't need to be part of the public API. The API has + # never been defined so this could cause backwards compatibility + # issues. + + def getallmatchingheaders(self, name): + """Find all header lines matching a given header name. + + Look through the list of headers and find all lines matching a given + header name (and their continuation lines). A list of the lines is + returned, without interpretation. If the header does not occur, an + empty list is returned. If the header occurs multiple times, all + occurrences are returned. Case is not important in the header name. + + """ + name = name.lower() + ':' + n = len(name) + lst = [] + hit = 0 + for line in self.keys(): + if line[:n].lower() == name: + hit = 1 + elif not line[:1].isspace(): + hit = 0 + if hit: + lst.append(line) + return lst + +def parse_headers(fp, _class=HTTPMessage): + """Parses only RFC2822 headers from a file pointer. + + email Parser wants to see strings rather than bytes. + But a TextIOWrapper around self.rfile would buffer too many bytes + from the stream, bytes which we later need to read as bytes. + So we read the correct bytes here, as bytes, for email Parser + to parse. + + """ + headers = [] + while True: + line = fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise LineTooLong("header line") + headers.append(line) + if len(headers) > _MAXHEADERS: + raise HTTPException("got more than %d headers" % _MAXHEADERS) + if line in (b'\r\n', b'\n', b''): + break + hstring = bytes(b'').join(headers).decode('iso-8859-1') + return email_parser.Parser(_class=_class).parsestr(hstring) + + +_strict_sentinel = object() + +class HTTPResponse(io.RawIOBase): + + # See RFC 2616 sec 19.6 and RFC 1945 sec 6 for details. + + # The bytes from the socket object are iso-8859-1 strings. + # See RFC 2616 sec 2.2 which notes an exception for MIME-encoded + # text following RFC 2047. The basic status line parsing only + # accepts iso-8859-1. + + def __init__(self, sock, debuglevel=0, strict=_strict_sentinel, method=None, url=None): + # If the response includes a content-length header, we need to + # make sure that the client doesn't read more than the + # specified number of bytes. If it does, it will block until + # the server times out and closes the connection. This will + # happen if a self.fp.read() is done (without a size) whether + # self.fp is buffered or not. So, no self.fp.read() by + # clients unless they know what they are doing. + self.fp = sock.makefile("rb") + self.debuglevel = debuglevel + if strict is not _strict_sentinel: + warnings.warn("the 'strict' argument isn't supported anymore; " + "http.client now always assumes HTTP/1.x compliant servers.", + DeprecationWarning, 2) + self._method = method + + # The HTTPResponse object is returned via urllib. The clients + # of http and urllib expect different attributes for the + # headers. headers is used here and supports urllib. msg is + # provided as a backwards compatibility layer for http + # clients. + + self.headers = self.msg = None + + # from the Status-Line of the response + self.version = _UNKNOWN # HTTP-Version + self.status = _UNKNOWN # Status-Code + self.reason = _UNKNOWN # Reason-Phrase + + self.chunked = _UNKNOWN # is "chunked" being used? + self.chunk_left = _UNKNOWN # bytes left to read in current chunk + self.length = _UNKNOWN # number of bytes left in response + self.will_close = _UNKNOWN # conn will close at end of response + + def _read_status(self): + line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1") + if len(line) > _MAXLINE: + raise LineTooLong("status line") + if self.debuglevel > 0: + print("reply:", repr(line)) + if not line: + # Presumably, the server closed the connection before + # sending a valid response. + raise BadStatusLine(line) + try: + version, status, reason = line.split(None, 2) + except ValueError: + try: + version, status = line.split(None, 1) + reason = "" + except ValueError: + # empty version will cause next test to fail. + version = "" + if not version.startswith("HTTP/"): + self._close_conn() + raise BadStatusLine(line) + + # The status code is a three-digit number + try: + status = int(status) + if status < 100 or status > 999: + raise BadStatusLine(line) + except ValueError: + raise BadStatusLine(line) + return version, status, reason + + def begin(self): + if self.headers is not None: + # we've already started reading the response + return + + # read until we get a non-100 response + while True: + version, status, reason = self._read_status() + if status != CONTINUE: + break + # skip the header from the 100 response + while True: + skip = self.fp.readline(_MAXLINE + 1) + if len(skip) > _MAXLINE: + raise LineTooLong("header line") + skip = skip.strip() + if not skip: + break + if self.debuglevel > 0: + print("header:", skip) + + self.code = self.status = status + self.reason = reason.strip() + if version in ("HTTP/1.0", "HTTP/0.9"): + # Some servers might still return "0.9", treat it as 1.0 anyway + self.version = 10 + elif version.startswith("HTTP/1."): + self.version = 11 # use HTTP/1.1 code for HTTP/1.x where x>=1 + else: + raise UnknownProtocol(version) + + self.headers = self.msg = parse_headers(self.fp) + + if self.debuglevel > 0: + for hdr in self.headers: + print("header:", hdr, end=" ") + + # are we using the chunked-style of transfer encoding? + tr_enc = self.headers.get("transfer-encoding") + if tr_enc and tr_enc.lower() == "chunked": + self.chunked = True + self.chunk_left = None + else: + self.chunked = False + + # will the connection close at the end of the response? + self.will_close = self._check_close() + + # do we have a Content-Length? + # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked" + self.length = None + length = self.headers.get("content-length") + + # are we using the chunked-style of transfer encoding? + tr_enc = self.headers.get("transfer-encoding") + if length and not self.chunked: + try: + self.length = int(length) + except ValueError: + self.length = None + else: + if self.length < 0: # ignore nonsensical negative lengths + self.length = None + else: + self.length = None + + # does the body have a fixed length? (of zero) + if (status == NO_CONTENT or status == NOT_MODIFIED or + 100 <= status < 200 or # 1xx codes + self._method == "HEAD"): + self.length = 0 + + # if the connection remains open, and we aren't using chunked, and + # a content-length was not provided, then assume that the connection + # WILL close. + if (not self.will_close and + not self.chunked and + self.length is None): + self.will_close = True + + def _check_close(self): + conn = self.headers.get("connection") + if self.version == 11: + # An HTTP/1.1 proxy is assumed to stay open unless + # explicitly closed. + conn = self.headers.get("connection") + if conn and "close" in conn.lower(): + return True + return False + + # Some HTTP/1.0 implementations have support for persistent + # connections, using rules different than HTTP/1.1. + + # For older HTTP, Keep-Alive indicates persistent connection. + if self.headers.get("keep-alive"): + return False + + # At least Akamai returns a "Connection: Keep-Alive" header, + # which was supposed to be sent by the client. + if conn and "keep-alive" in conn.lower(): + return False + + # Proxy-Connection is a netscape hack. + pconn = self.headers.get("proxy-connection") + if pconn and "keep-alive" in pconn.lower(): + return False + + # otherwise, assume it will close + return True + + def _close_conn(self): + fp = self.fp + self.fp = None + fp.close() + + def close(self): + super().close() # set "closed" flag + if self.fp: + self._close_conn() + + # These implementations are for the benefit of io.BufferedReader. + + # XXX This class should probably be revised to act more like + # the "raw stream" that BufferedReader expects. + + def flush(self): + super().flush() + if self.fp: + self.fp.flush() + + def readable(self): + return True + + # End of "raw stream" methods + + def isclosed(self): + """True if the connection is closed.""" + # NOTE: it is possible that we will not ever call self.close(). This + # case occurs when will_close is TRUE, length is None, and we + # read up to the last byte, but NOT past it. + # + # IMPLIES: if will_close is FALSE, then self.close() will ALWAYS be + # called, meaning self.isclosed() is meaningful. + return self.fp is None + + def read(self, amt=None): + if self.fp is None: + return bytes(b"") + + if self._method == "HEAD": + self._close_conn() + return bytes(b"") + + if amt is not None: + # Amount is given, so call base class version + # (which is implemented in terms of self.readinto) + return bytes(super(HTTPResponse, self).read(amt)) + else: + # Amount is not given (unbounded read) so we must check self.length + # and self.chunked + + if self.chunked: + return self._readall_chunked() + + if self.length is None: + s = self.fp.read() + else: + try: + s = self._safe_read(self.length) + except IncompleteRead: + self._close_conn() + raise + self.length = 0 + self._close_conn() # we read everything + return bytes(s) + + def readinto(self, b): + if self.fp is None: + return 0 + + if self._method == "HEAD": + self._close_conn() + return 0 + + if self.chunked: + return self._readinto_chunked(b) + + if self.length is not None: + if len(b) > self.length: + # clip the read to the "end of response" + b = memoryview(b)[0:self.length] + + # we do not use _safe_read() here because this may be a .will_close + # connection, and the user is reading more bytes than will be provided + # (for example, reading in 1k chunks) + + if PY2: + data = self.fp.read(len(b)) + n = len(data) + b[:n] = data + else: + n = self.fp.readinto(b) + + if not n and b: + # Ideally, we would raise IncompleteRead if the content-length + # wasn't satisfied, but it might break compatibility. + self._close_conn() + elif self.length is not None: + self.length -= n + if not self.length: + self._close_conn() + return n + + def _read_next_chunk_size(self): + # Read the next chunk size from the file + line = self.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise LineTooLong("chunk size") + i = line.find(b";") + if i >= 0: + line = line[:i] # strip chunk-extensions + try: + return int(line, 16) + except ValueError: + # close the connection as protocol synchronisation is + # probably lost + self._close_conn() + raise + + def _read_and_discard_trailer(self): + # read and discard trailer up to the CRLF terminator + ### note: we shouldn't have any trailers! + while True: + line = self.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise LineTooLong("trailer line") + if not line: + # a vanishingly small number of sites EOF without + # sending the trailer + break + if line in (b'\r\n', b'\n', b''): + break + + def _readall_chunked(self): + assert self.chunked != _UNKNOWN + chunk_left = self.chunk_left + value = [] + while True: + if chunk_left is None: + try: + chunk_left = self._read_next_chunk_size() + if chunk_left == 0: + break + except ValueError: + raise IncompleteRead(bytes(b'').join(value)) + value.append(self._safe_read(chunk_left)) + + # we read the whole chunk, get another + self._safe_read(2) # toss the CRLF at the end of the chunk + chunk_left = None + + self._read_and_discard_trailer() + + # we read everything; close the "file" + self._close_conn() + + return bytes(b'').join(value) + + def _readinto_chunked(self, b): + assert self.chunked != _UNKNOWN + chunk_left = self.chunk_left + + total_bytes = 0 + mvb = memoryview(b) + while True: + if chunk_left is None: + try: + chunk_left = self._read_next_chunk_size() + if chunk_left == 0: + break + except ValueError: + raise IncompleteRead(bytes(b[0:total_bytes])) + + if len(mvb) < chunk_left: + n = self._safe_readinto(mvb) + self.chunk_left = chunk_left - n + return total_bytes + n + elif len(mvb) == chunk_left: + n = self._safe_readinto(mvb) + self._safe_read(2) # toss the CRLF at the end of the chunk + self.chunk_left = None + return total_bytes + n + else: + temp_mvb = mvb[0:chunk_left] + n = self._safe_readinto(temp_mvb) + mvb = mvb[n:] + total_bytes += n + + # we read the whole chunk, get another + self._safe_read(2) # toss the CRLF at the end of the chunk + chunk_left = None + + self._read_and_discard_trailer() + + # we read everything; close the "file" + self._close_conn() + + return total_bytes + + def _safe_read(self, amt): + """Read the number of bytes requested, compensating for partial reads. + + Normally, we have a blocking socket, but a read() can be interrupted + by a signal (resulting in a partial read). + + Note that we cannot distinguish between EOF and an interrupt when zero + bytes have been read. IncompleteRead() will be raised in this + situation. + + This function should be used when bytes "should" be present for + reading. If the bytes are truly not available (due to EOF), then the + IncompleteRead exception can be used to detect the problem. + """ + s = [] + while amt > 0: + chunk = self.fp.read(min(amt, MAXAMOUNT)) + if not chunk: + raise IncompleteRead(bytes(b'').join(s), amt) + s.append(chunk) + amt -= len(chunk) + return bytes(b"").join(s) + + def _safe_readinto(self, b): + """Same as _safe_read, but for reading into a buffer.""" + total_bytes = 0 + mvb = memoryview(b) + while total_bytes < len(b): + if MAXAMOUNT < len(mvb): + temp_mvb = mvb[0:MAXAMOUNT] + if PY2: + data = self.fp.read(len(temp_mvb)) + n = len(data) + temp_mvb[:n] = data + else: + n = self.fp.readinto(temp_mvb) + else: + if PY2: + data = self.fp.read(len(mvb)) + n = len(data) + mvb[:n] = data + else: + n = self.fp.readinto(mvb) + if not n: + raise IncompleteRead(bytes(mvb[0:total_bytes]), len(b)) + mvb = mvb[n:] + total_bytes += n + return total_bytes + + def fileno(self): + return self.fp.fileno() + + def getheader(self, name, default=None): + if self.headers is None: + raise ResponseNotReady() + headers = self.headers.get_all(name) or default + if isinstance(headers, str) or not hasattr(headers, '__iter__'): + return headers + else: + return ', '.join(headers) + + def getheaders(self): + """Return list of (header, value) tuples.""" + if self.headers is None: + raise ResponseNotReady() + return list(self.headers.items()) + + # We override IOBase.__iter__ so that it doesn't check for closed-ness + + def __iter__(self): + return self + + # For compatibility with old-style urllib responses. + + def info(self): + return self.headers + + def geturl(self): + return self.url + + def getcode(self): + return self.status + +class HTTPConnection(object): + + _http_vsn = 11 + _http_vsn_str = 'HTTP/1.1' + + response_class = HTTPResponse + default_port = HTTP_PORT + auto_open = 1 + debuglevel = 0 + + def __init__(self, host, port=None, strict=_strict_sentinel, + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, source_address=None): + if strict is not _strict_sentinel: + warnings.warn("the 'strict' argument isn't supported anymore; " + "http.client now always assumes HTTP/1.x compliant servers.", + DeprecationWarning, 2) + self.timeout = timeout + self.source_address = source_address + self.sock = None + self._buffer = [] + self.__response = None + self.__state = _CS_IDLE + self._method = None + self._tunnel_host = None + self._tunnel_port = None + self._tunnel_headers = {} + + self._set_hostport(host, port) + + def set_tunnel(self, host, port=None, headers=None): + """ Sets up the host and the port for the HTTP CONNECT Tunnelling. + + The headers argument should be a mapping of extra HTTP headers + to send with the CONNECT request. + """ + self._tunnel_host = host + self._tunnel_port = port + if headers: + self._tunnel_headers = headers + else: + self._tunnel_headers.clear() + + def _set_hostport(self, host, port): + if port is None: + i = host.rfind(':') + j = host.rfind(']') # ipv6 addresses have [...] + if i > j: + try: + port = int(host[i+1:]) + except ValueError: + if host[i+1:] == "": # http://foo.com:/ == http://foo.com/ + port = self.default_port + else: + raise InvalidURL("nonnumeric port: '%s'" % host[i+1:]) + host = host[:i] + else: + port = self.default_port + if host and host[0] == '[' and host[-1] == ']': + host = host[1:-1] + self.host = host + self.port = port + + def set_debuglevel(self, level): + self.debuglevel = level + + def _tunnel(self): + self._set_hostport(self._tunnel_host, self._tunnel_port) + connect_str = "CONNECT %s:%d HTTP/1.0\r\n" % (self.host, self.port) + connect_bytes = connect_str.encode("ascii") + self.send(connect_bytes) + for header, value in self._tunnel_headers.items(): + header_str = "%s: %s\r\n" % (header, value) + header_bytes = header_str.encode("latin-1") + self.send(header_bytes) + self.send(bytes(b'\r\n')) + + response = self.response_class(self.sock, method=self._method) + (version, code, message) = response._read_status() + + if code != 200: + self.close() + raise socket.error("Tunnel connection failed: %d %s" % (code, + message.strip())) + while True: + line = response.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise LineTooLong("header line") + if not line: + # for sites which EOF without sending a trailer + break + if line in (b'\r\n', b'\n', b''): + break + + def connect(self): + """Connect to the host and port specified in __init__.""" + self.sock = socket_create_connection((self.host,self.port), + self.timeout, self.source_address) + if self._tunnel_host: + self._tunnel() + + def close(self): + """Close the connection to the HTTP server.""" + if self.sock: + self.sock.close() # close it manually... there may be other refs + self.sock = None + if self.__response: + self.__response.close() + self.__response = None + self.__state = _CS_IDLE + + def send(self, data): + """Send `data' to the server. + ``data`` can be a string object, a bytes object, an array object, a + file-like object that supports a .read() method, or an iterable object. + """ + + if self.sock is None: + if self.auto_open: + self.connect() + else: + raise NotConnected() + + if self.debuglevel > 0: + print("send:", repr(data)) + blocksize = 8192 + # Python 2.7 array objects have a read method which is incompatible + # with the 2-arg calling syntax below. + if hasattr(data, "read") and not isinstance(data, array): + if self.debuglevel > 0: + print("sendIng a read()able") + encode = False + try: + mode = data.mode + except AttributeError: + # io.BytesIO and other file-like objects don't have a `mode` + # attribute. + pass + else: + if "b" not in mode: + encode = True + if self.debuglevel > 0: + print("encoding file using iso-8859-1") + while 1: + datablock = data.read(blocksize) + if not datablock: + break + if encode: + datablock = datablock.encode("iso-8859-1") + self.sock.sendall(datablock) + return + try: + self.sock.sendall(data) + except TypeError: + if isinstance(data, Iterable): + for d in data: + self.sock.sendall(d) + else: + raise TypeError("data should be a bytes-like object " + "or an iterable, got %r" % type(data)) + + def _output(self, s): + """Add a line of output to the current request buffer. + + Assumes that the line does *not* end with \\r\\n. + """ + self._buffer.append(s) + + def _send_output(self, message_body=None): + """Send the currently buffered request and clear the buffer. + + Appends an extra \\r\\n to the buffer. + A message_body may be specified, to be appended to the request. + """ + self._buffer.extend((bytes(b""), bytes(b""))) + msg = bytes(b"\r\n").join(self._buffer) + del self._buffer[:] + # If msg and message_body are sent in a single send() call, + # it will avoid performance problems caused by the interaction + # between delayed ack and the Nagle algorithm. + if isinstance(message_body, bytes): + msg += message_body + message_body = None + self.send(msg) + if message_body is not None: + # message_body was not a string (i.e. it is a file), and + # we must run the risk of Nagle. + self.send(message_body) + + def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0): + """Send a request to the server. + + `method' specifies an HTTP request method, e.g. 'GET'. + `url' specifies the object being requested, e.g. '/index.html'. + `skip_host' if True does not add automatically a 'Host:' header + `skip_accept_encoding' if True does not add automatically an + 'Accept-Encoding:' header + """ + + # if a prior response has been completed, then forget about it. + if self.__response and self.__response.isclosed(): + self.__response = None + + + # in certain cases, we cannot issue another request on this connection. + # this occurs when: + # 1) we are in the process of sending a request. (_CS_REQ_STARTED) + # 2) a response to a previous request has signalled that it is going + # to close the connection upon completion. + # 3) the headers for the previous response have not been read, thus + # we cannot determine whether point (2) is true. (_CS_REQ_SENT) + # + # if there is no prior response, then we can request at will. + # + # if point (2) is true, then we will have passed the socket to the + # response (effectively meaning, "there is no prior response"), and + # will open a new one when a new request is made. + # + # Note: if a prior response exists, then we *can* start a new request. + # We are not allowed to begin fetching the response to this new + # request, however, until that prior response is complete. + # + if self.__state == _CS_IDLE: + self.__state = _CS_REQ_STARTED + else: + raise CannotSendRequest(self.__state) + + # Save the method we use, we need it later in the response phase + self._method = method + if not url: + url = '/' + request = '%s %s %s' % (method, url, self._http_vsn_str) + + # Non-ASCII characters should have been eliminated earlier + self._output(request.encode('ascii')) + + if self._http_vsn == 11: + # Issue some standard headers for better HTTP/1.1 compliance + + if not skip_host: + # this header is issued *only* for HTTP/1.1 + # connections. more specifically, this means it is + # only issued when the client uses the new + # HTTPConnection() class. backwards-compat clients + # will be using HTTP/1.0 and those clients may be + # issuing this header themselves. we should NOT issue + # it twice; some web servers (such as Apache) barf + # when they see two Host: headers + + # If we need a non-standard port,include it in the + # header. If the request is going through a proxy, + # but the host of the actual URL, not the host of the + # proxy. + + netloc = '' + if url.startswith('http'): + nil, netloc, nil, nil, nil = urlsplit(url) + + if netloc: + try: + netloc_enc = netloc.encode("ascii") + except UnicodeEncodeError: + netloc_enc = netloc.encode("idna") + self.putheader('Host', netloc_enc) + else: + try: + host_enc = self.host.encode("ascii") + except UnicodeEncodeError: + host_enc = self.host.encode("idna") + + # As per RFC 273, IPv6 address should be wrapped with [] + # when used as Host header + + if self.host.find(':') >= 0: + host_enc = bytes(b'[' + host_enc + b']') + + if self.port == self.default_port: + self.putheader('Host', host_enc) + else: + host_enc = host_enc.decode("ascii") + self.putheader('Host', "%s:%s" % (host_enc, self.port)) + + # note: we are assuming that clients will not attempt to set these + # headers since *this* library must deal with the + # consequences. this also means that when the supporting + # libraries are updated to recognize other forms, then this + # code should be changed (removed or updated). + + # we only want a Content-Encoding of "identity" since we don't + # support encodings such as x-gzip or x-deflate. + if not skip_accept_encoding: + self.putheader('Accept-Encoding', 'identity') + + # we can accept "chunked" Transfer-Encodings, but no others + # NOTE: no TE header implies *only* "chunked" + #self.putheader('TE', 'chunked') + + # if TE is supplied in the header, then it must appear in a + # Connection header. + #self.putheader('Connection', 'TE') + + else: + # For HTTP/1.0, the server will assume "not chunked" + pass + + def putheader(self, header, *values): + """Send a request header line to the server. + + For example: h.putheader('Accept', 'text/html') + """ + if self.__state != _CS_REQ_STARTED: + raise CannotSendHeader() + + if hasattr(header, 'encode'): + header = header.encode('ascii') + values = list(values) + for i, one_value in enumerate(values): + if hasattr(one_value, 'encode'): + values[i] = one_value.encode('latin-1') + elif isinstance(one_value, int): + values[i] = str(one_value).encode('ascii') + value = bytes(b'\r\n\t').join(values) + header = header + bytes(b': ') + value + self._output(header) + + def endheaders(self, message_body=None): + """Indicate that the last header line has been sent to the server. + + This method sends the request to the server. The optional message_body + argument can be used to pass a message body associated with the + request. The message body will be sent in the same packet as the + message headers if it is a string, otherwise it is sent as a separate + packet. + """ + if self.__state == _CS_REQ_STARTED: + self.__state = _CS_REQ_SENT + else: + raise CannotSendHeader() + self._send_output(message_body) + + def request(self, method, url, body=None, headers={}): + """Send a complete request to the server.""" + self._send_request(method, url, body, headers) + + def _set_content_length(self, body): + # Set the content-length based on the body. + thelen = None + try: + thelen = str(len(body)) + except TypeError as te: + # If this is a file-like object, try to + # fstat its file descriptor + try: + thelen = str(os.fstat(body.fileno()).st_size) + except (AttributeError, OSError): + # Don't send a length if this failed + if self.debuglevel > 0: print("Cannot stat!!") + + if thelen is not None: + self.putheader('Content-Length', thelen) + + def _send_request(self, method, url, body, headers): + # Honor explicitly requested Host: and Accept-Encoding: headers. + header_names = dict.fromkeys([k.lower() for k in headers]) + skips = {} + if 'host' in header_names: + skips['skip_host'] = 1 + if 'accept-encoding' in header_names: + skips['skip_accept_encoding'] = 1 + + self.putrequest(method, url, **skips) + + if body is not None and ('content-length' not in header_names): + self._set_content_length(body) + for hdr, value in headers.items(): + self.putheader(hdr, value) + if isinstance(body, str): + # RFC 2616 Section 3.7.1 says that text default has a + # default charset of iso-8859-1. + body = body.encode('iso-8859-1') + self.endheaders(body) + + def getresponse(self): + """Get the response from the server. + + If the HTTPConnection is in the correct state, returns an + instance of HTTPResponse or of whatever object is returned by + class the response_class variable. + + If a request has not been sent or if a previous response has + not be handled, ResponseNotReady is raised. If the HTTP + response indicates that the connection should be closed, then + it will be closed before the response is returned. When the + connection is closed, the underlying socket is closed. + """ + + # if a prior response has been completed, then forget about it. + if self.__response and self.__response.isclosed(): + self.__response = None + + # if a prior response exists, then it must be completed (otherwise, we + # cannot read this response's header to determine the connection-close + # behavior) + # + # note: if a prior response existed, but was connection-close, then the + # socket and response were made independent of this HTTPConnection + # object since a new request requires that we open a whole new + # connection + # + # this means the prior response had one of two states: + # 1) will_close: this connection was reset and the prior socket and + # response operate independently + # 2) persistent: the response was retained and we await its + # isclosed() status to become true. + # + if self.__state != _CS_REQ_SENT or self.__response: + raise ResponseNotReady(self.__state) + + if self.debuglevel > 0: + response = self.response_class(self.sock, self.debuglevel, + method=self._method) + else: + response = self.response_class(self.sock, method=self._method) + + response.begin() + assert response.will_close != _UNKNOWN + self.__state = _CS_IDLE + + if response.will_close: + # this effectively passes the connection to the response + self.close() + else: + # remember this, so we can tell when it is complete + self.__response = response + + return response + +try: + import ssl + from ssl import SSLContext +except ImportError: + pass +else: + class HTTPSConnection(HTTPConnection): + "This class allows communication via SSL." + + default_port = HTTPS_PORT + + # XXX Should key_file and cert_file be deprecated in favour of context? + + def __init__(self, host, port=None, key_file=None, cert_file=None, + strict=_strict_sentinel, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, **_3to2kwargs): + if 'check_hostname' in _3to2kwargs: check_hostname = _3to2kwargs['check_hostname']; del _3to2kwargs['check_hostname'] + else: check_hostname = None + if 'context' in _3to2kwargs: context = _3to2kwargs['context']; del _3to2kwargs['context'] + else: context = None + super(HTTPSConnection, self).__init__(host, port, strict, timeout, + source_address) + self.key_file = key_file + self.cert_file = cert_file + if context is None: + # Some reasonable defaults + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_SSLv2 + will_verify = context.verify_mode != ssl.CERT_NONE + if check_hostname is None: + check_hostname = will_verify + elif check_hostname and not will_verify: + raise ValueError("check_hostname needs a SSL context with " + "either CERT_OPTIONAL or CERT_REQUIRED") + if key_file or cert_file: + context.load_cert_chain(cert_file, key_file) + self._context = context + self._check_hostname = check_hostname + + def connect(self): + "Connect to a host on a given (SSL) port." + + sock = socket_create_connection((self.host, self.port), + self.timeout, self.source_address) + + if self._tunnel_host: + self.sock = sock + self._tunnel() + + server_hostname = self.host if ssl.HAS_SNI else None + self.sock = self._context.wrap_socket(sock, + server_hostname=server_hostname) + try: + if self._check_hostname: + ssl.match_hostname(self.sock.getpeercert(), self.host) + except Exception: + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() + raise + + __all__.append("HTTPSConnection") + + + # ###################################### + # # We use the old HTTPSConnection class from Py2.7, because ssl.SSLContext + # # doesn't exist in the Py2.7 stdlib + # class HTTPSConnection(HTTPConnection): + # "This class allows communication via SSL." + + # default_port = HTTPS_PORT + + # def __init__(self, host, port=None, key_file=None, cert_file=None, + # strict=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + # source_address=None): + # HTTPConnection.__init__(self, host, port, strict, timeout, + # source_address) + # self.key_file = key_file + # self.cert_file = cert_file + + # def connect(self): + # "Connect to a host on a given (SSL) port." + + # sock = socket_create_connection((self.host, self.port), + # self.timeout, self.source_address) + # if self._tunnel_host: + # self.sock = sock + # self._tunnel() + # self.sock = ssl.wrap_socket(sock, self.key_file, self.cert_file) + + # __all__.append("HTTPSConnection") + # ###################################### + + +class HTTPException(Exception): + # Subclasses that define an __init__ must call Exception.__init__ + # or define self.args. Otherwise, str() will fail. + pass + +class NotConnected(HTTPException): + pass + +class InvalidURL(HTTPException): + pass + +class UnknownProtocol(HTTPException): + def __init__(self, version): + self.args = version, + self.version = version + +class UnknownTransferEncoding(HTTPException): + pass + +class UnimplementedFileMode(HTTPException): + pass + +class IncompleteRead(HTTPException): + def __init__(self, partial, expected=None): + self.args = partial, + self.partial = partial + self.expected = expected + def __repr__(self): + if self.expected is not None: + e = ', %i more expected' % self.expected + else: + e = '' + return 'IncompleteRead(%i bytes read%s)' % (len(self.partial), e) + def __str__(self): + return repr(self) + +class ImproperConnectionState(HTTPException): + pass + +class CannotSendRequest(ImproperConnectionState): + pass + +class CannotSendHeader(ImproperConnectionState): + pass + +class ResponseNotReady(ImproperConnectionState): + pass + +class BadStatusLine(HTTPException): + def __init__(self, line): + if not line: + line = repr(line) + self.args = line, + self.line = line + +class LineTooLong(HTTPException): + def __init__(self, line_type): + HTTPException.__init__(self, "got more than %d bytes when reading %s" + % (_MAXLINE, line_type)) + +# for backwards compatibility +error = HTTPException diff --git a/venv/lib/python3.10/site-packages/future/backports/http/cookiejar.py b/venv/lib/python3.10/site-packages/future/backports/http/cookiejar.py new file mode 100644 index 0000000..a39242c --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/http/cookiejar.py @@ -0,0 +1,2116 @@ +r"""HTTP cookie handling for web clients. + +This is a backport of the Py3.3 ``http.cookiejar`` module for +python-future. + +This module has (now fairly distant) origins in Gisle Aas' Perl module +HTTP::Cookies, from the libwww-perl library. + +Docstrings, comments and debug strings in this code refer to the +attributes of the HTTP cookie system as cookie-attributes, to distinguish +them clearly from Python attributes. + +Class diagram (note that BSDDBCookieJar and the MSIE* classes are not +distributed with the Python standard library, but are available from +http://wwwsearch.sf.net/): + + CookieJar____ + / \ \ + FileCookieJar \ \ + / | \ \ \ + MozillaCookieJar | LWPCookieJar \ \ + | | \ + | ---MSIEBase | \ + | / | | \ + | / MSIEDBCookieJar BSDDBCookieJar + |/ + MSIECookieJar + +""" + +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from future.builtins import filter, int, map, open, str +from future.utils import as_native_str, PY2 + +__all__ = ['Cookie', 'CookieJar', 'CookiePolicy', 'DefaultCookiePolicy', + 'FileCookieJar', 'LWPCookieJar', 'LoadError', 'MozillaCookieJar'] + +import copy +import datetime +import re +if PY2: + re.ASCII = 0 +import time +from future.backports.urllib.parse import urlparse, urlsplit, quote +from future.backports.http.client import HTTP_PORT +try: + import threading as _threading +except ImportError: + import dummy_threading as _threading +from calendar import timegm + +debug = False # set to True to enable debugging via the logging module +logger = None + +def _debug(*args): + if not debug: + return + global logger + if not logger: + import logging + logger = logging.getLogger("http.cookiejar") + return logger.debug(*args) + + +DEFAULT_HTTP_PORT = str(HTTP_PORT) +MISSING_FILENAME_TEXT = ("a filename was not supplied (nor was the CookieJar " + "instance initialised with one)") + +def _warn_unhandled_exception(): + # There are a few catch-all except: statements in this module, for + # catching input that's bad in unexpected ways. Warn if any + # exceptions are caught there. + import io, warnings, traceback + f = io.StringIO() + traceback.print_exc(None, f) + msg = f.getvalue() + warnings.warn("http.cookiejar bug!\n%s" % msg, stacklevel=2) + + +# Date/time conversion +# ----------------------------------------------------------------------------- + +EPOCH_YEAR = 1970 +def _timegm(tt): + year, month, mday, hour, min, sec = tt[:6] + if ((year >= EPOCH_YEAR) and (1 <= month <= 12) and (1 <= mday <= 31) and + (0 <= hour <= 24) and (0 <= min <= 59) and (0 <= sec <= 61)): + return timegm(tt) + else: + return None + +DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] +MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +MONTHS_LOWER = [] +for month in MONTHS: MONTHS_LOWER.append(month.lower()) + +def time2isoz(t=None): + """Return a string representing time in seconds since epoch, t. + + If the function is called without an argument, it will use the current + time. + + The format of the returned string is like "YYYY-MM-DD hh:mm:ssZ", + representing Universal Time (UTC, aka GMT). An example of this format is: + + 1994-11-24 08:49:37Z + + """ + if t is None: + dt = datetime.datetime.utcnow() + else: + dt = datetime.datetime.utcfromtimestamp(t) + return "%04d-%02d-%02d %02d:%02d:%02dZ" % ( + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) + +def time2netscape(t=None): + """Return a string representing time in seconds since epoch, t. + + If the function is called without an argument, it will use the current + time. + + The format of the returned string is like this: + + Wed, DD-Mon-YYYY HH:MM:SS GMT + + """ + if t is None: + dt = datetime.datetime.utcnow() + else: + dt = datetime.datetime.utcfromtimestamp(t) + return "%s %02d-%s-%04d %02d:%02d:%02d GMT" % ( + DAYS[dt.weekday()], dt.day, MONTHS[dt.month-1], + dt.year, dt.hour, dt.minute, dt.second) + + +UTC_ZONES = {"GMT": None, "UTC": None, "UT": None, "Z": None} + +TIMEZONE_RE = re.compile(r"^([-+])?(\d\d?):?(\d\d)?$", re.ASCII) +def offset_from_tz_string(tz): + offset = None + if tz in UTC_ZONES: + offset = 0 + else: + m = TIMEZONE_RE.search(tz) + if m: + offset = 3600 * int(m.group(2)) + if m.group(3): + offset = offset + 60 * int(m.group(3)) + if m.group(1) == '-': + offset = -offset + return offset + +def _str2time(day, mon, yr, hr, min, sec, tz): + # translate month name to number + # month numbers start with 1 (January) + try: + mon = MONTHS_LOWER.index(mon.lower())+1 + except ValueError: + # maybe it's already a number + try: + imon = int(mon) + except ValueError: + return None + if 1 <= imon <= 12: + mon = imon + else: + return None + + # make sure clock elements are defined + if hr is None: hr = 0 + if min is None: min = 0 + if sec is None: sec = 0 + + yr = int(yr) + day = int(day) + hr = int(hr) + min = int(min) + sec = int(sec) + + if yr < 1000: + # find "obvious" year + cur_yr = time.localtime(time.time())[0] + m = cur_yr % 100 + tmp = yr + yr = yr + cur_yr - m + m = m - tmp + if abs(m) > 50: + if m > 0: yr = yr + 100 + else: yr = yr - 100 + + # convert UTC time tuple to seconds since epoch (not timezone-adjusted) + t = _timegm((yr, mon, day, hr, min, sec, tz)) + + if t is not None: + # adjust time using timezone string, to get absolute time since epoch + if tz is None: + tz = "UTC" + tz = tz.upper() + offset = offset_from_tz_string(tz) + if offset is None: + return None + t = t - offset + + return t + +STRICT_DATE_RE = re.compile( + r"^[SMTWF][a-z][a-z], (\d\d) ([JFMASOND][a-z][a-z]) " + "(\d\d\d\d) (\d\d):(\d\d):(\d\d) GMT$", re.ASCII) +WEEKDAY_RE = re.compile( + r"^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)[a-z]*,?\s*", re.I | re.ASCII) +LOOSE_HTTP_DATE_RE = re.compile( + r"""^ + (\d\d?) # day + (?:\s+|[-\/]) + (\w+) # month + (?:\s+|[-\/]) + (\d+) # year + (?: + (?:\s+|:) # separator before clock + (\d\d?):(\d\d) # hour:min + (?::(\d\d))? # optional seconds + )? # optional clock + \s* + (?: + ([-+]?\d{2,4}|(?![APap][Mm]\b)[A-Za-z]+) # timezone + \s* + )? + (?: + \(\w+\) # ASCII representation of timezone in parens. + \s* + )?$""", re.X | re.ASCII) +def http2time(text): + """Returns time in seconds since epoch of time represented by a string. + + Return value is an integer. + + None is returned if the format of str is unrecognized, the time is outside + the representable range, or the timezone string is not recognized. If the + string contains no timezone, UTC is assumed. + + The timezone in the string may be numerical (like "-0800" or "+0100") or a + string timezone (like "UTC", "GMT", "BST" or "EST"). Currently, only the + timezone strings equivalent to UTC (zero offset) are known to the function. + + The function loosely parses the following formats: + + Wed, 09 Feb 1994 22:23:32 GMT -- HTTP format + Tuesday, 08-Feb-94 14:15:29 GMT -- old rfc850 HTTP format + Tuesday, 08-Feb-1994 14:15:29 GMT -- broken rfc850 HTTP format + 09 Feb 1994 22:23:32 GMT -- HTTP format (no weekday) + 08-Feb-94 14:15:29 GMT -- rfc850 format (no weekday) + 08-Feb-1994 14:15:29 GMT -- broken rfc850 format (no weekday) + + The parser ignores leading and trailing whitespace. The time may be + absent. + + If the year is given with only 2 digits, the function will select the + century that makes the year closest to the current date. + + """ + # fast exit for strictly conforming string + m = STRICT_DATE_RE.search(text) + if m: + g = m.groups() + mon = MONTHS_LOWER.index(g[1].lower()) + 1 + tt = (int(g[2]), mon, int(g[0]), + int(g[3]), int(g[4]), float(g[5])) + return _timegm(tt) + + # No, we need some messy parsing... + + # clean up + text = text.lstrip() + text = WEEKDAY_RE.sub("", text, 1) # Useless weekday + + # tz is time zone specifier string + day, mon, yr, hr, min, sec, tz = [None]*7 + + # loose regexp parse + m = LOOSE_HTTP_DATE_RE.search(text) + if m is not None: + day, mon, yr, hr, min, sec, tz = m.groups() + else: + return None # bad format + + return _str2time(day, mon, yr, hr, min, sec, tz) + +ISO_DATE_RE = re.compile( + """^ + (\d{4}) # year + [-\/]? + (\d\d?) # numerical month + [-\/]? + (\d\d?) # day + (?: + (?:\s+|[-:Tt]) # separator before clock + (\d\d?):?(\d\d) # hour:min + (?::?(\d\d(?:\.\d*)?))? # optional seconds (and fractional) + )? # optional clock + \s* + (?: + ([-+]?\d\d?:?(:?\d\d)? + |Z|z) # timezone (Z is "zero meridian", i.e. GMT) + \s* + )?$""", re.X | re. ASCII) +def iso2time(text): + """ + As for http2time, but parses the ISO 8601 formats: + + 1994-02-03 14:15:29 -0100 -- ISO 8601 format + 1994-02-03 14:15:29 -- zone is optional + 1994-02-03 -- only date + 1994-02-03T14:15:29 -- Use T as separator + 19940203T141529Z -- ISO 8601 compact format + 19940203 -- only date + + """ + # clean up + text = text.lstrip() + + # tz is time zone specifier string + day, mon, yr, hr, min, sec, tz = [None]*7 + + # loose regexp parse + m = ISO_DATE_RE.search(text) + if m is not None: + # XXX there's an extra bit of the timezone I'm ignoring here: is + # this the right thing to do? + yr, mon, day, hr, min, sec, tz, _ = m.groups() + else: + return None # bad format + + return _str2time(day, mon, yr, hr, min, sec, tz) + + +# Header parsing +# ----------------------------------------------------------------------------- + +def unmatched(match): + """Return unmatched part of re.Match object.""" + start, end = match.span(0) + return match.string[:start]+match.string[end:] + +HEADER_TOKEN_RE = re.compile(r"^\s*([^=\s;,]+)") +HEADER_QUOTED_VALUE_RE = re.compile(r"^\s*=\s*\"([^\"\\]*(?:\\.[^\"\\]*)*)\"") +HEADER_VALUE_RE = re.compile(r"^\s*=\s*([^\s;,]*)") +HEADER_ESCAPE_RE = re.compile(r"\\(.)") +def split_header_words(header_values): + r"""Parse header values into a list of lists containing key,value pairs. + + The function knows how to deal with ",", ";" and "=" as well as quoted + values after "=". A list of space separated tokens are parsed as if they + were separated by ";". + + If the header_values passed as argument contains multiple values, then they + are treated as if they were a single value separated by comma ",". + + This means that this function is useful for parsing header fields that + follow this syntax (BNF as from the HTTP/1.1 specification, but we relax + the requirement for tokens). + + headers = #header + header = (token | parameter) *( [";"] (token | parameter)) + + token = 1* + separators = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT + + quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + qdtext = > + quoted-pair = "\" CHAR + + parameter = attribute "=" value + attribute = token + value = token | quoted-string + + Each header is represented by a list of key/value pairs. The value for a + simple token (not part of a parameter) is None. Syntactically incorrect + headers will not necessarily be parsed as you would want. + + This is easier to describe with some examples: + + >>> split_header_words(['foo="bar"; port="80,81"; discard, bar=baz']) + [[('foo', 'bar'), ('port', '80,81'), ('discard', None)], [('bar', 'baz')]] + >>> split_header_words(['text/html; charset="iso-8859-1"']) + [[('text/html', None), ('charset', 'iso-8859-1')]] + >>> split_header_words([r'Basic realm="\"foo\bar\""']) + [[('Basic', None), ('realm', '"foobar"')]] + + """ + assert not isinstance(header_values, str) + result = [] + for text in header_values: + orig_text = text + pairs = [] + while text: + m = HEADER_TOKEN_RE.search(text) + if m: + text = unmatched(m) + name = m.group(1) + m = HEADER_QUOTED_VALUE_RE.search(text) + if m: # quoted value + text = unmatched(m) + value = m.group(1) + value = HEADER_ESCAPE_RE.sub(r"\1", value) + else: + m = HEADER_VALUE_RE.search(text) + if m: # unquoted value + text = unmatched(m) + value = m.group(1) + value = value.rstrip() + else: + # no value, a lone token + value = None + pairs.append((name, value)) + elif text.lstrip().startswith(","): + # concatenated headers, as per RFC 2616 section 4.2 + text = text.lstrip()[1:] + if pairs: result.append(pairs) + pairs = [] + else: + # skip junk + non_junk, nr_junk_chars = re.subn("^[=\s;]*", "", text) + assert nr_junk_chars > 0, ( + "split_header_words bug: '%s', '%s', %s" % + (orig_text, text, pairs)) + text = non_junk + if pairs: result.append(pairs) + return result + +HEADER_JOIN_ESCAPE_RE = re.compile(r"([\"\\])") +def join_header_words(lists): + """Do the inverse (almost) of the conversion done by split_header_words. + + Takes a list of lists of (key, value) pairs and produces a single header + value. Attribute values are quoted if needed. + + >>> join_header_words([[("text/plain", None), ("charset", "iso-8859/1")]]) + 'text/plain; charset="iso-8859/1"' + >>> join_header_words([[("text/plain", None)], [("charset", "iso-8859/1")]]) + 'text/plain, charset="iso-8859/1"' + + """ + headers = [] + for pairs in lists: + attr = [] + for k, v in pairs: + if v is not None: + if not re.search(r"^\w+$", v): + v = HEADER_JOIN_ESCAPE_RE.sub(r"\\\1", v) # escape " and \ + v = '"%s"' % v + k = "%s=%s" % (k, v) + attr.append(k) + if attr: headers.append("; ".join(attr)) + return ", ".join(headers) + +def strip_quotes(text): + if text.startswith('"'): + text = text[1:] + if text.endswith('"'): + text = text[:-1] + return text + +def parse_ns_headers(ns_headers): + """Ad-hoc parser for Netscape protocol cookie-attributes. + + The old Netscape cookie format for Set-Cookie can for instance contain + an unquoted "," in the expires field, so we have to use this ad-hoc + parser instead of split_header_words. + + XXX This may not make the best possible effort to parse all the crap + that Netscape Cookie headers contain. Ronald Tschalar's HTTPClient + parser is probably better, so could do worse than following that if + this ever gives any trouble. + + Currently, this is also used for parsing RFC 2109 cookies. + + """ + known_attrs = ("expires", "domain", "path", "secure", + # RFC 2109 attrs (may turn up in Netscape cookies, too) + "version", "port", "max-age") + + result = [] + for ns_header in ns_headers: + pairs = [] + version_set = False + for ii, param in enumerate(re.split(r";\s*", ns_header)): + param = param.rstrip() + if param == "": continue + if "=" not in param: + k, v = param, None + else: + k, v = re.split(r"\s*=\s*", param, 1) + k = k.lstrip() + if ii != 0: + lc = k.lower() + if lc in known_attrs: + k = lc + if k == "version": + # This is an RFC 2109 cookie. + v = strip_quotes(v) + version_set = True + if k == "expires": + # convert expires date to seconds since epoch + v = http2time(strip_quotes(v)) # None if invalid + pairs.append((k, v)) + + if pairs: + if not version_set: + pairs.append(("version", "0")) + result.append(pairs) + + return result + + +IPV4_RE = re.compile(r"\.\d+$", re.ASCII) +def is_HDN(text): + """Return True if text is a host domain name.""" + # XXX + # This may well be wrong. Which RFC is HDN defined in, if any (for + # the purposes of RFC 2965)? + # For the current implementation, what about IPv6? Remember to look + # at other uses of IPV4_RE also, if change this. + if IPV4_RE.search(text): + return False + if text == "": + return False + if text[0] == "." or text[-1] == ".": + return False + return True + +def domain_match(A, B): + """Return True if domain A domain-matches domain B, according to RFC 2965. + + A and B may be host domain names or IP addresses. + + RFC 2965, section 1: + + Host names can be specified either as an IP address or a HDN string. + Sometimes we compare one host name with another. (Such comparisons SHALL + be case-insensitive.) Host A's name domain-matches host B's if + + * their host name strings string-compare equal; or + + * A is a HDN string and has the form NB, where N is a non-empty + name string, B has the form .B', and B' is a HDN string. (So, + x.y.com domain-matches .Y.com but not Y.com.) + + Note that domain-match is not a commutative operation: a.b.c.com + domain-matches .c.com, but not the reverse. + + """ + # Note that, if A or B are IP addresses, the only relevant part of the + # definition of the domain-match algorithm is the direct string-compare. + A = A.lower() + B = B.lower() + if A == B: + return True + if not is_HDN(A): + return False + i = A.rfind(B) + if i == -1 or i == 0: + # A does not have form NB, or N is the empty string + return False + if not B.startswith("."): + return False + if not is_HDN(B[1:]): + return False + return True + +def liberal_is_HDN(text): + """Return True if text is a sort-of-like a host domain name. + + For accepting/blocking domains. + + """ + if IPV4_RE.search(text): + return False + return True + +def user_domain_match(A, B): + """For blocking/accepting domains. + + A and B may be host domain names or IP addresses. + + """ + A = A.lower() + B = B.lower() + if not (liberal_is_HDN(A) and liberal_is_HDN(B)): + if A == B: + # equal IP addresses + return True + return False + initial_dot = B.startswith(".") + if initial_dot and A.endswith(B): + return True + if not initial_dot and A == B: + return True + return False + +cut_port_re = re.compile(r":\d+$", re.ASCII) +def request_host(request): + """Return request-host, as defined by RFC 2965. + + Variation from RFC: returned value is lowercased, for convenient + comparison. + + """ + url = request.get_full_url() + host = urlparse(url)[1] + if host == "": + host = request.get_header("Host", "") + + # remove port, if present + host = cut_port_re.sub("", host, 1) + return host.lower() + +def eff_request_host(request): + """Return a tuple (request-host, effective request-host name). + + As defined by RFC 2965, except both are lowercased. + + """ + erhn = req_host = request_host(request) + if req_host.find(".") == -1 and not IPV4_RE.search(req_host): + erhn = req_host + ".local" + return req_host, erhn + +def request_path(request): + """Path component of request-URI, as defined by RFC 2965.""" + url = request.get_full_url() + parts = urlsplit(url) + path = escape_path(parts.path) + if not path.startswith("/"): + # fix bad RFC 2396 absoluteURI + path = "/" + path + return path + +def request_port(request): + host = request.host + i = host.find(':') + if i >= 0: + port = host[i+1:] + try: + int(port) + except ValueError: + _debug("nonnumeric port: '%s'", port) + return None + else: + port = DEFAULT_HTTP_PORT + return port + +# Characters in addition to A-Z, a-z, 0-9, '_', '.', and '-' that don't +# need to be escaped to form a valid HTTP URL (RFCs 2396 and 1738). +HTTP_PATH_SAFE = "%/;:@&=+$,!~*'()" +ESCAPED_CHAR_RE = re.compile(r"%([0-9a-fA-F][0-9a-fA-F])") +def uppercase_escaped_char(match): + return "%%%s" % match.group(1).upper() +def escape_path(path): + """Escape any invalid characters in HTTP URL, and uppercase all escapes.""" + # There's no knowing what character encoding was used to create URLs + # containing %-escapes, but since we have to pick one to escape invalid + # path characters, we pick UTF-8, as recommended in the HTML 4.0 + # specification: + # http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.2.1 + # And here, kind of: draft-fielding-uri-rfc2396bis-03 + # (And in draft IRI specification: draft-duerst-iri-05) + # (And here, for new URI schemes: RFC 2718) + path = quote(path, HTTP_PATH_SAFE) + path = ESCAPED_CHAR_RE.sub(uppercase_escaped_char, path) + return path + +def reach(h): + """Return reach of host h, as defined by RFC 2965, section 1. + + The reach R of a host name H is defined as follows: + + * If + + - H is the host domain name of a host; and, + + - H has the form A.B; and + + - A has no embedded (that is, interior) dots; and + + - B has at least one embedded dot, or B is the string "local". + then the reach of H is .B. + + * Otherwise, the reach of H is H. + + >>> reach("www.acme.com") + '.acme.com' + >>> reach("acme.com") + 'acme.com' + >>> reach("acme.local") + '.local' + + """ + i = h.find(".") + if i >= 0: + #a = h[:i] # this line is only here to show what a is + b = h[i+1:] + i = b.find(".") + if is_HDN(h) and (i >= 0 or b == "local"): + return "."+b + return h + +def is_third_party(request): + """ + + RFC 2965, section 3.3.6: + + An unverifiable transaction is to a third-party host if its request- + host U does not domain-match the reach R of the request-host O in the + origin transaction. + + """ + req_host = request_host(request) + if not domain_match(req_host, reach(request.get_origin_req_host())): + return True + else: + return False + + +class Cookie(object): + """HTTP Cookie. + + This class represents both Netscape and RFC 2965 cookies. + + This is deliberately a very simple class. It just holds attributes. It's + possible to construct Cookie instances that don't comply with the cookie + standards. CookieJar.make_cookies is the factory function for Cookie + objects -- it deals with cookie parsing, supplying defaults, and + normalising to the representation used in this class. CookiePolicy is + responsible for checking them to see whether they should be accepted from + and returned to the server. + + Note that the port may be present in the headers, but unspecified ("Port" + rather than"Port=80", for example); if this is the case, port is None. + + """ + + def __init__(self, version, name, value, + port, port_specified, + domain, domain_specified, domain_initial_dot, + path, path_specified, + secure, + expires, + discard, + comment, + comment_url, + rest, + rfc2109=False, + ): + + if version is not None: version = int(version) + if expires is not None: expires = int(expires) + if port is None and port_specified is True: + raise ValueError("if port is None, port_specified must be false") + + self.version = version + self.name = name + self.value = value + self.port = port + self.port_specified = port_specified + # normalise case, as per RFC 2965 section 3.3.3 + self.domain = domain.lower() + self.domain_specified = domain_specified + # Sigh. We need to know whether the domain given in the + # cookie-attribute had an initial dot, in order to follow RFC 2965 + # (as clarified in draft errata). Needed for the returned $Domain + # value. + self.domain_initial_dot = domain_initial_dot + self.path = path + self.path_specified = path_specified + self.secure = secure + self.expires = expires + self.discard = discard + self.comment = comment + self.comment_url = comment_url + self.rfc2109 = rfc2109 + + self._rest = copy.copy(rest) + + def has_nonstandard_attr(self, name): + return name in self._rest + def get_nonstandard_attr(self, name, default=None): + return self._rest.get(name, default) + def set_nonstandard_attr(self, name, value): + self._rest[name] = value + + def is_expired(self, now=None): + if now is None: now = time.time() + if (self.expires is not None) and (self.expires <= now): + return True + return False + + def __str__(self): + if self.port is None: p = "" + else: p = ":"+self.port + limit = self.domain + p + self.path + if self.value is not None: + namevalue = "%s=%s" % (self.name, self.value) + else: + namevalue = self.name + return "" % (namevalue, limit) + + @as_native_str() + def __repr__(self): + args = [] + for name in ("version", "name", "value", + "port", "port_specified", + "domain", "domain_specified", "domain_initial_dot", + "path", "path_specified", + "secure", "expires", "discard", "comment", "comment_url", + ): + attr = getattr(self, name) + ### Python-Future: + # Avoid u'...' prefixes for unicode strings: + if isinstance(attr, str): + attr = str(attr) + ### + args.append(str("%s=%s") % (name, repr(attr))) + args.append("rest=%s" % repr(self._rest)) + args.append("rfc2109=%s" % repr(self.rfc2109)) + return "Cookie(%s)" % ", ".join(args) + + +class CookiePolicy(object): + """Defines which cookies get accepted from and returned to server. + + May also modify cookies, though this is probably a bad idea. + + The subclass DefaultCookiePolicy defines the standard rules for Netscape + and RFC 2965 cookies -- override that if you want a customised policy. + + """ + def set_ok(self, cookie, request): + """Return true if (and only if) cookie should be accepted from server. + + Currently, pre-expired cookies never get this far -- the CookieJar + class deletes such cookies itself. + + """ + raise NotImplementedError() + + def return_ok(self, cookie, request): + """Return true if (and only if) cookie should be returned to server.""" + raise NotImplementedError() + + def domain_return_ok(self, domain, request): + """Return false if cookies should not be returned, given cookie domain. + """ + return True + + def path_return_ok(self, path, request): + """Return false if cookies should not be returned, given cookie path. + """ + return True + + +class DefaultCookiePolicy(CookiePolicy): + """Implements the standard rules for accepting and returning cookies.""" + + DomainStrictNoDots = 1 + DomainStrictNonDomain = 2 + DomainRFC2965Match = 4 + + DomainLiberal = 0 + DomainStrict = DomainStrictNoDots|DomainStrictNonDomain + + def __init__(self, + blocked_domains=None, allowed_domains=None, + netscape=True, rfc2965=False, + rfc2109_as_netscape=None, + hide_cookie2=False, + strict_domain=False, + strict_rfc2965_unverifiable=True, + strict_ns_unverifiable=False, + strict_ns_domain=DomainLiberal, + strict_ns_set_initial_dollar=False, + strict_ns_set_path=False, + ): + """Constructor arguments should be passed as keyword arguments only.""" + self.netscape = netscape + self.rfc2965 = rfc2965 + self.rfc2109_as_netscape = rfc2109_as_netscape + self.hide_cookie2 = hide_cookie2 + self.strict_domain = strict_domain + self.strict_rfc2965_unverifiable = strict_rfc2965_unverifiable + self.strict_ns_unverifiable = strict_ns_unverifiable + self.strict_ns_domain = strict_ns_domain + self.strict_ns_set_initial_dollar = strict_ns_set_initial_dollar + self.strict_ns_set_path = strict_ns_set_path + + if blocked_domains is not None: + self._blocked_domains = tuple(blocked_domains) + else: + self._blocked_domains = () + + if allowed_domains is not None: + allowed_domains = tuple(allowed_domains) + self._allowed_domains = allowed_domains + + def blocked_domains(self): + """Return the sequence of blocked domains (as a tuple).""" + return self._blocked_domains + def set_blocked_domains(self, blocked_domains): + """Set the sequence of blocked domains.""" + self._blocked_domains = tuple(blocked_domains) + + def is_blocked(self, domain): + for blocked_domain in self._blocked_domains: + if user_domain_match(domain, blocked_domain): + return True + return False + + def allowed_domains(self): + """Return None, or the sequence of allowed domains (as a tuple).""" + return self._allowed_domains + def set_allowed_domains(self, allowed_domains): + """Set the sequence of allowed domains, or None.""" + if allowed_domains is not None: + allowed_domains = tuple(allowed_domains) + self._allowed_domains = allowed_domains + + def is_not_allowed(self, domain): + if self._allowed_domains is None: + return False + for allowed_domain in self._allowed_domains: + if user_domain_match(domain, allowed_domain): + return False + return True + + def set_ok(self, cookie, request): + """ + If you override .set_ok(), be sure to call this method. If it returns + false, so should your subclass (assuming your subclass wants to be more + strict about which cookies to accept). + + """ + _debug(" - checking cookie %s=%s", cookie.name, cookie.value) + + assert cookie.name is not None + + for n in "version", "verifiability", "name", "path", "domain", "port": + fn_name = "set_ok_"+n + fn = getattr(self, fn_name) + if not fn(cookie, request): + return False + + return True + + def set_ok_version(self, cookie, request): + if cookie.version is None: + # Version is always set to 0 by parse_ns_headers if it's a Netscape + # cookie, so this must be an invalid RFC 2965 cookie. + _debug(" Set-Cookie2 without version attribute (%s=%s)", + cookie.name, cookie.value) + return False + if cookie.version > 0 and not self.rfc2965: + _debug(" RFC 2965 cookies are switched off") + return False + elif cookie.version == 0 and not self.netscape: + _debug(" Netscape cookies are switched off") + return False + return True + + def set_ok_verifiability(self, cookie, request): + if request.unverifiable and is_third_party(request): + if cookie.version > 0 and self.strict_rfc2965_unverifiable: + _debug(" third-party RFC 2965 cookie during " + "unverifiable transaction") + return False + elif cookie.version == 0 and self.strict_ns_unverifiable: + _debug(" third-party Netscape cookie during " + "unverifiable transaction") + return False + return True + + def set_ok_name(self, cookie, request): + # Try and stop servers setting V0 cookies designed to hack other + # servers that know both V0 and V1 protocols. + if (cookie.version == 0 and self.strict_ns_set_initial_dollar and + cookie.name.startswith("$")): + _debug(" illegal name (starts with '$'): '%s'", cookie.name) + return False + return True + + def set_ok_path(self, cookie, request): + if cookie.path_specified: + req_path = request_path(request) + if ((cookie.version > 0 or + (cookie.version == 0 and self.strict_ns_set_path)) and + not req_path.startswith(cookie.path)): + _debug(" path attribute %s is not a prefix of request " + "path %s", cookie.path, req_path) + return False + return True + + def set_ok_domain(self, cookie, request): + if self.is_blocked(cookie.domain): + _debug(" domain %s is in user block-list", cookie.domain) + return False + if self.is_not_allowed(cookie.domain): + _debug(" domain %s is not in user allow-list", cookie.domain) + return False + if cookie.domain_specified: + req_host, erhn = eff_request_host(request) + domain = cookie.domain + if self.strict_domain and (domain.count(".") >= 2): + # XXX This should probably be compared with the Konqueror + # (kcookiejar.cpp) and Mozilla implementations, but it's a + # losing battle. + i = domain.rfind(".") + j = domain.rfind(".", 0, i) + if j == 0: # domain like .foo.bar + tld = domain[i+1:] + sld = domain[j+1:i] + if sld.lower() in ("co", "ac", "com", "edu", "org", "net", + "gov", "mil", "int", "aero", "biz", "cat", "coop", + "info", "jobs", "mobi", "museum", "name", "pro", + "travel", "eu") and len(tld) == 2: + # domain like .co.uk + _debug(" country-code second level domain %s", domain) + return False + if domain.startswith("."): + undotted_domain = domain[1:] + else: + undotted_domain = domain + embedded_dots = (undotted_domain.find(".") >= 0) + if not embedded_dots and domain != ".local": + _debug(" non-local domain %s contains no embedded dot", + domain) + return False + if cookie.version == 0: + if (not erhn.endswith(domain) and + (not erhn.startswith(".") and + not ("."+erhn).endswith(domain))): + _debug(" effective request-host %s (even with added " + "initial dot) does not end with %s", + erhn, domain) + return False + if (cookie.version > 0 or + (self.strict_ns_domain & self.DomainRFC2965Match)): + if not domain_match(erhn, domain): + _debug(" effective request-host %s does not domain-match " + "%s", erhn, domain) + return False + if (cookie.version > 0 or + (self.strict_ns_domain & self.DomainStrictNoDots)): + host_prefix = req_host[:-len(domain)] + if (host_prefix.find(".") >= 0 and + not IPV4_RE.search(req_host)): + _debug(" host prefix %s for domain %s contains a dot", + host_prefix, domain) + return False + return True + + def set_ok_port(self, cookie, request): + if cookie.port_specified: + req_port = request_port(request) + if req_port is None: + req_port = "80" + else: + req_port = str(req_port) + for p in cookie.port.split(","): + try: + int(p) + except ValueError: + _debug(" bad port %s (not numeric)", p) + return False + if p == req_port: + break + else: + _debug(" request port (%s) not found in %s", + req_port, cookie.port) + return False + return True + + def return_ok(self, cookie, request): + """ + If you override .return_ok(), be sure to call this method. If it + returns false, so should your subclass (assuming your subclass wants to + be more strict about which cookies to return). + + """ + # Path has already been checked by .path_return_ok(), and domain + # blocking done by .domain_return_ok(). + _debug(" - checking cookie %s=%s", cookie.name, cookie.value) + + for n in "version", "verifiability", "secure", "expires", "port", "domain": + fn_name = "return_ok_"+n + fn = getattr(self, fn_name) + if not fn(cookie, request): + return False + return True + + def return_ok_version(self, cookie, request): + if cookie.version > 0 and not self.rfc2965: + _debug(" RFC 2965 cookies are switched off") + return False + elif cookie.version == 0 and not self.netscape: + _debug(" Netscape cookies are switched off") + return False + return True + + def return_ok_verifiability(self, cookie, request): + if request.unverifiable and is_third_party(request): + if cookie.version > 0 and self.strict_rfc2965_unverifiable: + _debug(" third-party RFC 2965 cookie during unverifiable " + "transaction") + return False + elif cookie.version == 0 and self.strict_ns_unverifiable: + _debug(" third-party Netscape cookie during unverifiable " + "transaction") + return False + return True + + def return_ok_secure(self, cookie, request): + if cookie.secure and request.type != "https": + _debug(" secure cookie with non-secure request") + return False + return True + + def return_ok_expires(self, cookie, request): + if cookie.is_expired(self._now): + _debug(" cookie expired") + return False + return True + + def return_ok_port(self, cookie, request): + if cookie.port: + req_port = request_port(request) + if req_port is None: + req_port = "80" + for p in cookie.port.split(","): + if p == req_port: + break + else: + _debug(" request port %s does not match cookie port %s", + req_port, cookie.port) + return False + return True + + def return_ok_domain(self, cookie, request): + req_host, erhn = eff_request_host(request) + domain = cookie.domain + + # strict check of non-domain cookies: Mozilla does this, MSIE5 doesn't + if (cookie.version == 0 and + (self.strict_ns_domain & self.DomainStrictNonDomain) and + not cookie.domain_specified and domain != erhn): + _debug(" cookie with unspecified domain does not string-compare " + "equal to request domain") + return False + + if cookie.version > 0 and not domain_match(erhn, domain): + _debug(" effective request-host name %s does not domain-match " + "RFC 2965 cookie domain %s", erhn, domain) + return False + if cookie.version == 0 and not ("."+erhn).endswith(domain): + _debug(" request-host %s does not match Netscape cookie domain " + "%s", req_host, domain) + return False + return True + + def domain_return_ok(self, domain, request): + # Liberal check of. This is here as an optimization to avoid + # having to load lots of MSIE cookie files unless necessary. + req_host, erhn = eff_request_host(request) + if not req_host.startswith("."): + req_host = "."+req_host + if not erhn.startswith("."): + erhn = "."+erhn + if not (req_host.endswith(domain) or erhn.endswith(domain)): + #_debug(" request domain %s does not match cookie domain %s", + # req_host, domain) + return False + + if self.is_blocked(domain): + _debug(" domain %s is in user block-list", domain) + return False + if self.is_not_allowed(domain): + _debug(" domain %s is not in user allow-list", domain) + return False + + return True + + def path_return_ok(self, path, request): + _debug("- checking cookie path=%s", path) + req_path = request_path(request) + if not req_path.startswith(path): + _debug(" %s does not path-match %s", req_path, path) + return False + return True + + +def vals_sorted_by_key(adict): + keys = sorted(adict.keys()) + return map(adict.get, keys) + +def deepvalues(mapping): + """Iterates over nested mapping, depth-first, in sorted order by key.""" + values = vals_sorted_by_key(mapping) + for obj in values: + mapping = False + try: + obj.items + except AttributeError: + pass + else: + mapping = True + for subobj in deepvalues(obj): + yield subobj + if not mapping: + yield obj + + +# Used as second parameter to dict.get() method, to distinguish absent +# dict key from one with a None value. +class Absent(object): pass + +class CookieJar(object): + """Collection of HTTP cookies. + + You may not need to know about this class: try + urllib.request.build_opener(HTTPCookieProcessor).open(url). + """ + + non_word_re = re.compile(r"\W") + quote_re = re.compile(r"([\"\\])") + strict_domain_re = re.compile(r"\.?[^.]*") + domain_re = re.compile(r"[^.]*") + dots_re = re.compile(r"^\.+") + + magic_re = re.compile(r"^\#LWP-Cookies-(\d+\.\d+)", re.ASCII) + + def __init__(self, policy=None): + if policy is None: + policy = DefaultCookiePolicy() + self._policy = policy + + self._cookies_lock = _threading.RLock() + self._cookies = {} + + def set_policy(self, policy): + self._policy = policy + + def _cookies_for_domain(self, domain, request): + cookies = [] + if not self._policy.domain_return_ok(domain, request): + return [] + _debug("Checking %s for cookies to return", domain) + cookies_by_path = self._cookies[domain] + for path in cookies_by_path.keys(): + if not self._policy.path_return_ok(path, request): + continue + cookies_by_name = cookies_by_path[path] + for cookie in cookies_by_name.values(): + if not self._policy.return_ok(cookie, request): + _debug(" not returning cookie") + continue + _debug(" it's a match") + cookies.append(cookie) + return cookies + + def _cookies_for_request(self, request): + """Return a list of cookies to be returned to server.""" + cookies = [] + for domain in self._cookies.keys(): + cookies.extend(self._cookies_for_domain(domain, request)) + return cookies + + def _cookie_attrs(self, cookies): + """Return a list of cookie-attributes to be returned to server. + + like ['foo="bar"; $Path="/"', ...] + + The $Version attribute is also added when appropriate (currently only + once per request). + + """ + # add cookies in order of most specific (ie. longest) path first + cookies.sort(key=lambda a: len(a.path), reverse=True) + + version_set = False + + attrs = [] + for cookie in cookies: + # set version of Cookie header + # XXX + # What should it be if multiple matching Set-Cookie headers have + # different versions themselves? + # Answer: there is no answer; was supposed to be settled by + # RFC 2965 errata, but that may never appear... + version = cookie.version + if not version_set: + version_set = True + if version > 0: + attrs.append("$Version=%s" % version) + + # quote cookie value if necessary + # (not for Netscape protocol, which already has any quotes + # intact, due to the poorly-specified Netscape Cookie: syntax) + if ((cookie.value is not None) and + self.non_word_re.search(cookie.value) and version > 0): + value = self.quote_re.sub(r"\\\1", cookie.value) + else: + value = cookie.value + + # add cookie-attributes to be returned in Cookie header + if cookie.value is None: + attrs.append(cookie.name) + else: + attrs.append("%s=%s" % (cookie.name, value)) + if version > 0: + if cookie.path_specified: + attrs.append('$Path="%s"' % cookie.path) + if cookie.domain.startswith("."): + domain = cookie.domain + if (not cookie.domain_initial_dot and + domain.startswith(".")): + domain = domain[1:] + attrs.append('$Domain="%s"' % domain) + if cookie.port is not None: + p = "$Port" + if cookie.port_specified: + p = p + ('="%s"' % cookie.port) + attrs.append(p) + + return attrs + + def add_cookie_header(self, request): + """Add correct Cookie: header to request (urllib.request.Request object). + + The Cookie2 header is also added unless policy.hide_cookie2 is true. + + """ + _debug("add_cookie_header") + self._cookies_lock.acquire() + try: + + self._policy._now = self._now = int(time.time()) + + cookies = self._cookies_for_request(request) + + attrs = self._cookie_attrs(cookies) + if attrs: + if not request.has_header("Cookie"): + request.add_unredirected_header( + "Cookie", "; ".join(attrs)) + + # if necessary, advertise that we know RFC 2965 + if (self._policy.rfc2965 and not self._policy.hide_cookie2 and + not request.has_header("Cookie2")): + for cookie in cookies: + if cookie.version != 1: + request.add_unredirected_header("Cookie2", '$Version="1"') + break + + finally: + self._cookies_lock.release() + + self.clear_expired_cookies() + + def _normalized_cookie_tuples(self, attrs_set): + """Return list of tuples containing normalised cookie information. + + attrs_set is the list of lists of key,value pairs extracted from + the Set-Cookie or Set-Cookie2 headers. + + Tuples are name, value, standard, rest, where name and value are the + cookie name and value, standard is a dictionary containing the standard + cookie-attributes (discard, secure, version, expires or max-age, + domain, path and port) and rest is a dictionary containing the rest of + the cookie-attributes. + + """ + cookie_tuples = [] + + boolean_attrs = "discard", "secure" + value_attrs = ("version", + "expires", "max-age", + "domain", "path", "port", + "comment", "commenturl") + + for cookie_attrs in attrs_set: + name, value = cookie_attrs[0] + + # Build dictionary of standard cookie-attributes (standard) and + # dictionary of other cookie-attributes (rest). + + # Note: expiry time is normalised to seconds since epoch. V0 + # cookies should have the Expires cookie-attribute, and V1 cookies + # should have Max-Age, but since V1 includes RFC 2109 cookies (and + # since V0 cookies may be a mish-mash of Netscape and RFC 2109), we + # accept either (but prefer Max-Age). + max_age_set = False + + bad_cookie = False + + standard = {} + rest = {} + for k, v in cookie_attrs[1:]: + lc = k.lower() + # don't lose case distinction for unknown fields + if lc in value_attrs or lc in boolean_attrs: + k = lc + if k in boolean_attrs and v is None: + # boolean cookie-attribute is present, but has no value + # (like "discard", rather than "port=80") + v = True + if k in standard: + # only first value is significant + continue + if k == "domain": + if v is None: + _debug(" missing value for domain attribute") + bad_cookie = True + break + # RFC 2965 section 3.3.3 + v = v.lower() + if k == "expires": + if max_age_set: + # Prefer max-age to expires (like Mozilla) + continue + if v is None: + _debug(" missing or invalid value for expires " + "attribute: treating as session cookie") + continue + if k == "max-age": + max_age_set = True + try: + v = int(v) + except ValueError: + _debug(" missing or invalid (non-numeric) value for " + "max-age attribute") + bad_cookie = True + break + # convert RFC 2965 Max-Age to seconds since epoch + # XXX Strictly you're supposed to follow RFC 2616 + # age-calculation rules. Remember that zero Max-Age is a + # is a request to discard (old and new) cookie, though. + k = "expires" + v = self._now + v + if (k in value_attrs) or (k in boolean_attrs): + if (v is None and + k not in ("port", "comment", "commenturl")): + _debug(" missing value for %s attribute" % k) + bad_cookie = True + break + standard[k] = v + else: + rest[k] = v + + if bad_cookie: + continue + + cookie_tuples.append((name, value, standard, rest)) + + return cookie_tuples + + def _cookie_from_cookie_tuple(self, tup, request): + # standard is dict of standard cookie-attributes, rest is dict of the + # rest of them + name, value, standard, rest = tup + + domain = standard.get("domain", Absent) + path = standard.get("path", Absent) + port = standard.get("port", Absent) + expires = standard.get("expires", Absent) + + # set the easy defaults + version = standard.get("version", None) + if version is not None: + try: + version = int(version) + except ValueError: + return None # invalid version, ignore cookie + secure = standard.get("secure", False) + # (discard is also set if expires is Absent) + discard = standard.get("discard", False) + comment = standard.get("comment", None) + comment_url = standard.get("commenturl", None) + + # set default path + if path is not Absent and path != "": + path_specified = True + path = escape_path(path) + else: + path_specified = False + path = request_path(request) + i = path.rfind("/") + if i != -1: + if version == 0: + # Netscape spec parts company from reality here + path = path[:i] + else: + path = path[:i+1] + if len(path) == 0: path = "/" + + # set default domain + domain_specified = domain is not Absent + # but first we have to remember whether it starts with a dot + domain_initial_dot = False + if domain_specified: + domain_initial_dot = bool(domain.startswith(".")) + if domain is Absent: + req_host, erhn = eff_request_host(request) + domain = erhn + elif not domain.startswith("."): + domain = "."+domain + + # set default port + port_specified = False + if port is not Absent: + if port is None: + # Port attr present, but has no value: default to request port. + # Cookie should then only be sent back on that port. + port = request_port(request) + else: + port_specified = True + port = re.sub(r"\s+", "", port) + else: + # No port attr present. Cookie can be sent back on any port. + port = None + + # set default expires and discard + if expires is Absent: + expires = None + discard = True + elif expires <= self._now: + # Expiry date in past is request to delete cookie. This can't be + # in DefaultCookiePolicy, because can't delete cookies there. + try: + self.clear(domain, path, name) + except KeyError: + pass + _debug("Expiring cookie, domain='%s', path='%s', name='%s'", + domain, path, name) + return None + + return Cookie(version, + name, value, + port, port_specified, + domain, domain_specified, domain_initial_dot, + path, path_specified, + secure, + expires, + discard, + comment, + comment_url, + rest) + + def _cookies_from_attrs_set(self, attrs_set, request): + cookie_tuples = self._normalized_cookie_tuples(attrs_set) + + cookies = [] + for tup in cookie_tuples: + cookie = self._cookie_from_cookie_tuple(tup, request) + if cookie: cookies.append(cookie) + return cookies + + def _process_rfc2109_cookies(self, cookies): + rfc2109_as_ns = getattr(self._policy, 'rfc2109_as_netscape', None) + if rfc2109_as_ns is None: + rfc2109_as_ns = not self._policy.rfc2965 + for cookie in cookies: + if cookie.version == 1: + cookie.rfc2109 = True + if rfc2109_as_ns: + # treat 2109 cookies as Netscape cookies rather than + # as RFC2965 cookies + cookie.version = 0 + + def make_cookies(self, response, request): + """Return sequence of Cookie objects extracted from response object.""" + # get cookie-attributes for RFC 2965 and Netscape protocols + headers = response.info() + rfc2965_hdrs = headers.get_all("Set-Cookie2", []) + ns_hdrs = headers.get_all("Set-Cookie", []) + + rfc2965 = self._policy.rfc2965 + netscape = self._policy.netscape + + if ((not rfc2965_hdrs and not ns_hdrs) or + (not ns_hdrs and not rfc2965) or + (not rfc2965_hdrs and not netscape) or + (not netscape and not rfc2965)): + return [] # no relevant cookie headers: quick exit + + try: + cookies = self._cookies_from_attrs_set( + split_header_words(rfc2965_hdrs), request) + except Exception: + _warn_unhandled_exception() + cookies = [] + + if ns_hdrs and netscape: + try: + # RFC 2109 and Netscape cookies + ns_cookies = self._cookies_from_attrs_set( + parse_ns_headers(ns_hdrs), request) + except Exception: + _warn_unhandled_exception() + ns_cookies = [] + self._process_rfc2109_cookies(ns_cookies) + + # Look for Netscape cookies (from Set-Cookie headers) that match + # corresponding RFC 2965 cookies (from Set-Cookie2 headers). + # For each match, keep the RFC 2965 cookie and ignore the Netscape + # cookie (RFC 2965 section 9.1). Actually, RFC 2109 cookies are + # bundled in with the Netscape cookies for this purpose, which is + # reasonable behaviour. + if rfc2965: + lookup = {} + for cookie in cookies: + lookup[(cookie.domain, cookie.path, cookie.name)] = None + + def no_matching_rfc2965(ns_cookie, lookup=lookup): + key = ns_cookie.domain, ns_cookie.path, ns_cookie.name + return key not in lookup + ns_cookies = filter(no_matching_rfc2965, ns_cookies) + + if ns_cookies: + cookies.extend(ns_cookies) + + return cookies + + def set_cookie_if_ok(self, cookie, request): + """Set a cookie if policy says it's OK to do so.""" + self._cookies_lock.acquire() + try: + self._policy._now = self._now = int(time.time()) + + if self._policy.set_ok(cookie, request): + self.set_cookie(cookie) + + + finally: + self._cookies_lock.release() + + def set_cookie(self, cookie): + """Set a cookie, without checking whether or not it should be set.""" + c = self._cookies + self._cookies_lock.acquire() + try: + if cookie.domain not in c: c[cookie.domain] = {} + c2 = c[cookie.domain] + if cookie.path not in c2: c2[cookie.path] = {} + c3 = c2[cookie.path] + c3[cookie.name] = cookie + finally: + self._cookies_lock.release() + + def extract_cookies(self, response, request): + """Extract cookies from response, where allowable given the request.""" + _debug("extract_cookies: %s", response.info()) + self._cookies_lock.acquire() + try: + self._policy._now = self._now = int(time.time()) + + for cookie in self.make_cookies(response, request): + if self._policy.set_ok(cookie, request): + _debug(" setting cookie: %s", cookie) + self.set_cookie(cookie) + finally: + self._cookies_lock.release() + + def clear(self, domain=None, path=None, name=None): + """Clear some cookies. + + Invoking this method without arguments will clear all cookies. If + given a single argument, only cookies belonging to that domain will be + removed. If given two arguments, cookies belonging to the specified + path within that domain are removed. If given three arguments, then + the cookie with the specified name, path and domain is removed. + + Raises KeyError if no matching cookie exists. + + """ + if name is not None: + if (domain is None) or (path is None): + raise ValueError( + "domain and path must be given to remove a cookie by name") + del self._cookies[domain][path][name] + elif path is not None: + if domain is None: + raise ValueError( + "domain must be given to remove cookies by path") + del self._cookies[domain][path] + elif domain is not None: + del self._cookies[domain] + else: + self._cookies = {} + + def clear_session_cookies(self): + """Discard all session cookies. + + Note that the .save() method won't save session cookies anyway, unless + you ask otherwise by passing a true ignore_discard argument. + + """ + self._cookies_lock.acquire() + try: + for cookie in self: + if cookie.discard: + self.clear(cookie.domain, cookie.path, cookie.name) + finally: + self._cookies_lock.release() + + def clear_expired_cookies(self): + """Discard all expired cookies. + + You probably don't need to call this method: expired cookies are never + sent back to the server (provided you're using DefaultCookiePolicy), + this method is called by CookieJar itself every so often, and the + .save() method won't save expired cookies anyway (unless you ask + otherwise by passing a true ignore_expires argument). + + """ + self._cookies_lock.acquire() + try: + now = time.time() + for cookie in self: + if cookie.is_expired(now): + self.clear(cookie.domain, cookie.path, cookie.name) + finally: + self._cookies_lock.release() + + def __iter__(self): + return deepvalues(self._cookies) + + def __len__(self): + """Return number of contained cookies.""" + i = 0 + for cookie in self: i = i + 1 + return i + + @as_native_str() + def __repr__(self): + r = [] + for cookie in self: r.append(repr(cookie)) + return "<%s[%s]>" % (self.__class__, ", ".join(r)) + + def __str__(self): + r = [] + for cookie in self: r.append(str(cookie)) + return "<%s[%s]>" % (self.__class__, ", ".join(r)) + + +# derives from IOError for backwards-compatibility with Python 2.4.0 +class LoadError(IOError): pass + +class FileCookieJar(CookieJar): + """CookieJar that can be loaded from and saved to a file.""" + + def __init__(self, filename=None, delayload=False, policy=None): + """ + Cookies are NOT loaded from the named file until either the .load() or + .revert() method is called. + + """ + CookieJar.__init__(self, policy) + if filename is not None: + try: + filename+"" + except: + raise ValueError("filename must be string-like") + self.filename = filename + self.delayload = bool(delayload) + + def save(self, filename=None, ignore_discard=False, ignore_expires=False): + """Save cookies to a file.""" + raise NotImplementedError() + + def load(self, filename=None, ignore_discard=False, ignore_expires=False): + """Load cookies from a file.""" + if filename is None: + if self.filename is not None: filename = self.filename + else: raise ValueError(MISSING_FILENAME_TEXT) + + f = open(filename) + try: + self._really_load(f, filename, ignore_discard, ignore_expires) + finally: + f.close() + + def revert(self, filename=None, + ignore_discard=False, ignore_expires=False): + """Clear all cookies and reload cookies from a saved file. + + Raises LoadError (or IOError) if reversion is not successful; the + object's state will not be altered if this happens. + + """ + if filename is None: + if self.filename is not None: filename = self.filename + else: raise ValueError(MISSING_FILENAME_TEXT) + + self._cookies_lock.acquire() + try: + + old_state = copy.deepcopy(self._cookies) + self._cookies = {} + try: + self.load(filename, ignore_discard, ignore_expires) + except (LoadError, IOError): + self._cookies = old_state + raise + + finally: + self._cookies_lock.release() + + +def lwp_cookie_str(cookie): + """Return string representation of Cookie in an the LWP cookie file format. + + Actually, the format is extended a bit -- see module docstring. + + """ + h = [(cookie.name, cookie.value), + ("path", cookie.path), + ("domain", cookie.domain)] + if cookie.port is not None: h.append(("port", cookie.port)) + if cookie.path_specified: h.append(("path_spec", None)) + if cookie.port_specified: h.append(("port_spec", None)) + if cookie.domain_initial_dot: h.append(("domain_dot", None)) + if cookie.secure: h.append(("secure", None)) + if cookie.expires: h.append(("expires", + time2isoz(float(cookie.expires)))) + if cookie.discard: h.append(("discard", None)) + if cookie.comment: h.append(("comment", cookie.comment)) + if cookie.comment_url: h.append(("commenturl", cookie.comment_url)) + + keys = sorted(cookie._rest.keys()) + for k in keys: + h.append((k, str(cookie._rest[k]))) + + h.append(("version", str(cookie.version))) + + return join_header_words([h]) + +class LWPCookieJar(FileCookieJar): + """ + The LWPCookieJar saves a sequence of "Set-Cookie3" lines. + "Set-Cookie3" is the format used by the libwww-perl library, not known + to be compatible with any browser, but which is easy to read and + doesn't lose information about RFC 2965 cookies. + + Additional methods + + as_lwp_str(ignore_discard=True, ignore_expired=True) + + """ + + def as_lwp_str(self, ignore_discard=True, ignore_expires=True): + """Return cookies as a string of "\\n"-separated "Set-Cookie3" headers. + + ignore_discard and ignore_expires: see docstring for FileCookieJar.save + + """ + now = time.time() + r = [] + for cookie in self: + if not ignore_discard and cookie.discard: + continue + if not ignore_expires and cookie.is_expired(now): + continue + r.append("Set-Cookie3: %s" % lwp_cookie_str(cookie)) + return "\n".join(r+[""]) + + def save(self, filename=None, ignore_discard=False, ignore_expires=False): + if filename is None: + if self.filename is not None: filename = self.filename + else: raise ValueError(MISSING_FILENAME_TEXT) + + f = open(filename, "w") + try: + # There really isn't an LWP Cookies 2.0 format, but this indicates + # that there is extra information in here (domain_dot and + # port_spec) while still being compatible with libwww-perl, I hope. + f.write("#LWP-Cookies-2.0\n") + f.write(self.as_lwp_str(ignore_discard, ignore_expires)) + finally: + f.close() + + def _really_load(self, f, filename, ignore_discard, ignore_expires): + magic = f.readline() + if not self.magic_re.search(magic): + msg = ("%r does not look like a Set-Cookie3 (LWP) format " + "file" % filename) + raise LoadError(msg) + + now = time.time() + + header = "Set-Cookie3:" + boolean_attrs = ("port_spec", "path_spec", "domain_dot", + "secure", "discard") + value_attrs = ("version", + "port", "path", "domain", + "expires", + "comment", "commenturl") + + try: + while 1: + line = f.readline() + if line == "": break + if not line.startswith(header): + continue + line = line[len(header):].strip() + + for data in split_header_words([line]): + name, value = data[0] + standard = {} + rest = {} + for k in boolean_attrs: + standard[k] = False + for k, v in data[1:]: + if k is not None: + lc = k.lower() + else: + lc = None + # don't lose case distinction for unknown fields + if (lc in value_attrs) or (lc in boolean_attrs): + k = lc + if k in boolean_attrs: + if v is None: v = True + standard[k] = v + elif k in value_attrs: + standard[k] = v + else: + rest[k] = v + + h = standard.get + expires = h("expires") + discard = h("discard") + if expires is not None: + expires = iso2time(expires) + if expires is None: + discard = True + domain = h("domain") + domain_specified = domain.startswith(".") + c = Cookie(h("version"), name, value, + h("port"), h("port_spec"), + domain, domain_specified, h("domain_dot"), + h("path"), h("path_spec"), + h("secure"), + expires, + discard, + h("comment"), + h("commenturl"), + rest) + if not ignore_discard and c.discard: + continue + if not ignore_expires and c.is_expired(now): + continue + self.set_cookie(c) + + except IOError: + raise + except Exception: + _warn_unhandled_exception() + raise LoadError("invalid Set-Cookie3 format file %r: %r" % + (filename, line)) + + +class MozillaCookieJar(FileCookieJar): + """ + + WARNING: you may want to backup your browser's cookies file if you use + this class to save cookies. I *think* it works, but there have been + bugs in the past! + + This class differs from CookieJar only in the format it uses to save and + load cookies to and from a file. This class uses the Mozilla/Netscape + `cookies.txt' format. lynx uses this file format, too. + + Don't expect cookies saved while the browser is running to be noticed by + the browser (in fact, Mozilla on unix will overwrite your saved cookies if + you change them on disk while it's running; on Windows, you probably can't + save at all while the browser is running). + + Note that the Mozilla/Netscape format will downgrade RFC2965 cookies to + Netscape cookies on saving. + + In particular, the cookie version and port number information is lost, + together with information about whether or not Path, Port and Discard were + specified by the Set-Cookie2 (or Set-Cookie) header, and whether or not the + domain as set in the HTTP header started with a dot (yes, I'm aware some + domains in Netscape files start with a dot and some don't -- trust me, you + really don't want to know any more about this). + + Note that though Mozilla and Netscape use the same format, they use + slightly different headers. The class saves cookies using the Netscape + header by default (Mozilla can cope with that). + + """ + magic_re = re.compile("#( Netscape)? HTTP Cookie File") + header = """\ +# Netscape HTTP Cookie File +# http://www.netscape.com/newsref/std/cookie_spec.html +# This is a generated file! Do not edit. + +""" + + def _really_load(self, f, filename, ignore_discard, ignore_expires): + now = time.time() + + magic = f.readline() + if not self.magic_re.search(magic): + f.close() + raise LoadError( + "%r does not look like a Netscape format cookies file" % + filename) + + try: + while 1: + line = f.readline() + if line == "": break + + # last field may be absent, so keep any trailing tab + if line.endswith("\n"): line = line[:-1] + + # skip comments and blank lines XXX what is $ for? + if (line.strip().startswith(("#", "$")) or + line.strip() == ""): + continue + + domain, domain_specified, path, secure, expires, name, value = \ + line.split("\t") + secure = (secure == "TRUE") + domain_specified = (domain_specified == "TRUE") + if name == "": + # cookies.txt regards 'Set-Cookie: foo' as a cookie + # with no name, whereas http.cookiejar regards it as a + # cookie with no value. + name = value + value = None + + initial_dot = domain.startswith(".") + assert domain_specified == initial_dot + + discard = False + if expires == "": + expires = None + discard = True + + # assume path_specified is false + c = Cookie(0, name, value, + None, False, + domain, domain_specified, initial_dot, + path, False, + secure, + expires, + discard, + None, + None, + {}) + if not ignore_discard and c.discard: + continue + if not ignore_expires and c.is_expired(now): + continue + self.set_cookie(c) + + except IOError: + raise + except Exception: + _warn_unhandled_exception() + raise LoadError("invalid Netscape format cookies file %r: %r" % + (filename, line)) + + def save(self, filename=None, ignore_discard=False, ignore_expires=False): + if filename is None: + if self.filename is not None: filename = self.filename + else: raise ValueError(MISSING_FILENAME_TEXT) + + f = open(filename, "w") + try: + f.write(self.header) + now = time.time() + for cookie in self: + if not ignore_discard and cookie.discard: + continue + if not ignore_expires and cookie.is_expired(now): + continue + if cookie.secure: secure = "TRUE" + else: secure = "FALSE" + if cookie.domain.startswith("."): initial_dot = "TRUE" + else: initial_dot = "FALSE" + if cookie.expires is not None: + expires = str(cookie.expires) + else: + expires = "" + if cookie.value is None: + # cookies.txt regards 'Set-Cookie: foo' as a cookie + # with no name, whereas http.cookiejar regards it as a + # cookie with no value. + name = "" + value = cookie.name + else: + name = cookie.name + value = cookie.value + f.write( + "\t".join([cookie.domain, initial_dot, cookie.path, + secure, expires, name, value])+ + "\n") + finally: + f.close() diff --git a/venv/lib/python3.10/site-packages/future/backports/http/cookies.py b/venv/lib/python3.10/site-packages/future/backports/http/cookies.py new file mode 100644 index 0000000..8bb61e2 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/http/cookies.py @@ -0,0 +1,598 @@ +#### +# Copyright 2000 by Timothy O'Malley +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software +# and its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Timothy O'Malley not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# Timothy O'Malley DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS, IN NO EVENT SHALL Timothy O'Malley BE LIABLE FOR +# ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# +#### +# +# Id: Cookie.py,v 2.29 2000/08/23 05:28:49 timo Exp +# by Timothy O'Malley +# +# Cookie.py is a Python module for the handling of HTTP +# cookies as a Python dictionary. See RFC 2109 for more +# information on cookies. +# +# The original idea to treat Cookies as a dictionary came from +# Dave Mitchell (davem@magnet.com) in 1995, when he released the +# first version of nscookie.py. +# +#### + +r""" +http.cookies module ported to python-future from Py3.3 + +Here's a sample session to show how to use this module. +At the moment, this is the only documentation. + +The Basics +---------- + +Importing is easy... + + >>> from http import cookies + +Most of the time you start by creating a cookie. + + >>> C = cookies.SimpleCookie() + +Once you've created your Cookie, you can add values just as if it were +a dictionary. + + >>> C = cookies.SimpleCookie() + >>> C["fig"] = "newton" + >>> C["sugar"] = "wafer" + >>> C.output() + 'Set-Cookie: fig=newton\r\nSet-Cookie: sugar=wafer' + +Notice that the printable representation of a Cookie is the +appropriate format for a Set-Cookie: header. This is the +default behavior. You can change the header and printed +attributes by using the .output() function + + >>> C = cookies.SimpleCookie() + >>> C["rocky"] = "road" + >>> C["rocky"]["path"] = "/cookie" + >>> print(C.output(header="Cookie:")) + Cookie: rocky=road; Path=/cookie + >>> print(C.output(attrs=[], header="Cookie:")) + Cookie: rocky=road + +The load() method of a Cookie extracts cookies from a string. In a +CGI script, you would use this method to extract the cookies from the +HTTP_COOKIE environment variable. + + >>> C = cookies.SimpleCookie() + >>> C.load("chips=ahoy; vienna=finger") + >>> C.output() + 'Set-Cookie: chips=ahoy\r\nSet-Cookie: vienna=finger' + +The load() method is darn-tootin smart about identifying cookies +within a string. Escaped quotation marks, nested semicolons, and other +such trickeries do not confuse it. + + >>> C = cookies.SimpleCookie() + >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";') + >>> print(C) + Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;" + +Each element of the Cookie also supports all of the RFC 2109 +Cookie attributes. Here's an example which sets the Path +attribute. + + >>> C = cookies.SimpleCookie() + >>> C["oreo"] = "doublestuff" + >>> C["oreo"]["path"] = "/" + >>> print(C) + Set-Cookie: oreo=doublestuff; Path=/ + +Each dictionary element has a 'value' attribute, which gives you +back the value associated with the key. + + >>> C = cookies.SimpleCookie() + >>> C["twix"] = "none for you" + >>> C["twix"].value + 'none for you' + +The SimpleCookie expects that all values should be standard strings. +Just to be sure, SimpleCookie invokes the str() builtin to convert +the value to a string, when the values are set dictionary-style. + + >>> C = cookies.SimpleCookie() + >>> C["number"] = 7 + >>> C["string"] = "seven" + >>> C["number"].value + '7' + >>> C["string"].value + 'seven' + >>> C.output() + 'Set-Cookie: number=7\r\nSet-Cookie: string=seven' + +Finis. +""" +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from future.builtins import chr, dict, int, str +from future.utils import PY2, as_native_str + +# +# Import our required modules +# +import re +if PY2: + re.ASCII = 0 # for py2 compatibility +import string + +__all__ = ["CookieError", "BaseCookie", "SimpleCookie"] + +_nulljoin = ''.join +_semispacejoin = '; '.join +_spacejoin = ' '.join + +# +# Define an exception visible to External modules +# +class CookieError(Exception): + pass + + +# These quoting routines conform to the RFC2109 specification, which in +# turn references the character definitions from RFC2068. They provide +# a two-way quoting algorithm. Any non-text character is translated +# into a 4 character sequence: a forward-slash followed by the +# three-digit octal equivalent of the character. Any '\' or '"' is +# quoted with a preceeding '\' slash. +# +# These are taken from RFC2068 and RFC2109. +# _LegalChars is the list of chars which don't require "'s +# _Translator hash-table for fast quoting +# +_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" +_Translator = { + '\000' : '\\000', '\001' : '\\001', '\002' : '\\002', + '\003' : '\\003', '\004' : '\\004', '\005' : '\\005', + '\006' : '\\006', '\007' : '\\007', '\010' : '\\010', + '\011' : '\\011', '\012' : '\\012', '\013' : '\\013', + '\014' : '\\014', '\015' : '\\015', '\016' : '\\016', + '\017' : '\\017', '\020' : '\\020', '\021' : '\\021', + '\022' : '\\022', '\023' : '\\023', '\024' : '\\024', + '\025' : '\\025', '\026' : '\\026', '\027' : '\\027', + '\030' : '\\030', '\031' : '\\031', '\032' : '\\032', + '\033' : '\\033', '\034' : '\\034', '\035' : '\\035', + '\036' : '\\036', '\037' : '\\037', + + # Because of the way browsers really handle cookies (as opposed + # to what the RFC says) we also encode , and ; + + ',' : '\\054', ';' : '\\073', + + '"' : '\\"', '\\' : '\\\\', + + '\177' : '\\177', '\200' : '\\200', '\201' : '\\201', + '\202' : '\\202', '\203' : '\\203', '\204' : '\\204', + '\205' : '\\205', '\206' : '\\206', '\207' : '\\207', + '\210' : '\\210', '\211' : '\\211', '\212' : '\\212', + '\213' : '\\213', '\214' : '\\214', '\215' : '\\215', + '\216' : '\\216', '\217' : '\\217', '\220' : '\\220', + '\221' : '\\221', '\222' : '\\222', '\223' : '\\223', + '\224' : '\\224', '\225' : '\\225', '\226' : '\\226', + '\227' : '\\227', '\230' : '\\230', '\231' : '\\231', + '\232' : '\\232', '\233' : '\\233', '\234' : '\\234', + '\235' : '\\235', '\236' : '\\236', '\237' : '\\237', + '\240' : '\\240', '\241' : '\\241', '\242' : '\\242', + '\243' : '\\243', '\244' : '\\244', '\245' : '\\245', + '\246' : '\\246', '\247' : '\\247', '\250' : '\\250', + '\251' : '\\251', '\252' : '\\252', '\253' : '\\253', + '\254' : '\\254', '\255' : '\\255', '\256' : '\\256', + '\257' : '\\257', '\260' : '\\260', '\261' : '\\261', + '\262' : '\\262', '\263' : '\\263', '\264' : '\\264', + '\265' : '\\265', '\266' : '\\266', '\267' : '\\267', + '\270' : '\\270', '\271' : '\\271', '\272' : '\\272', + '\273' : '\\273', '\274' : '\\274', '\275' : '\\275', + '\276' : '\\276', '\277' : '\\277', '\300' : '\\300', + '\301' : '\\301', '\302' : '\\302', '\303' : '\\303', + '\304' : '\\304', '\305' : '\\305', '\306' : '\\306', + '\307' : '\\307', '\310' : '\\310', '\311' : '\\311', + '\312' : '\\312', '\313' : '\\313', '\314' : '\\314', + '\315' : '\\315', '\316' : '\\316', '\317' : '\\317', + '\320' : '\\320', '\321' : '\\321', '\322' : '\\322', + '\323' : '\\323', '\324' : '\\324', '\325' : '\\325', + '\326' : '\\326', '\327' : '\\327', '\330' : '\\330', + '\331' : '\\331', '\332' : '\\332', '\333' : '\\333', + '\334' : '\\334', '\335' : '\\335', '\336' : '\\336', + '\337' : '\\337', '\340' : '\\340', '\341' : '\\341', + '\342' : '\\342', '\343' : '\\343', '\344' : '\\344', + '\345' : '\\345', '\346' : '\\346', '\347' : '\\347', + '\350' : '\\350', '\351' : '\\351', '\352' : '\\352', + '\353' : '\\353', '\354' : '\\354', '\355' : '\\355', + '\356' : '\\356', '\357' : '\\357', '\360' : '\\360', + '\361' : '\\361', '\362' : '\\362', '\363' : '\\363', + '\364' : '\\364', '\365' : '\\365', '\366' : '\\366', + '\367' : '\\367', '\370' : '\\370', '\371' : '\\371', + '\372' : '\\372', '\373' : '\\373', '\374' : '\\374', + '\375' : '\\375', '\376' : '\\376', '\377' : '\\377' + } + +def _quote(str, LegalChars=_LegalChars): + r"""Quote a string for use in a cookie header. + + If the string does not need to be double-quoted, then just return the + string. Otherwise, surround the string in doublequotes and quote + (with a \) special characters. + """ + if all(c in LegalChars for c in str): + return str + else: + return '"' + _nulljoin(_Translator.get(s, s) for s in str) + '"' + + +_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") +_QuotePatt = re.compile(r"[\\].") + +def _unquote(mystr): + # If there aren't any doublequotes, + # then there can't be any special characters. See RFC 2109. + if len(mystr) < 2: + return mystr + if mystr[0] != '"' or mystr[-1] != '"': + return mystr + + # We have to assume that we must decode this string. + # Down to work. + + # Remove the "s + mystr = mystr[1:-1] + + # Check for special sequences. Examples: + # \012 --> \n + # \" --> " + # + i = 0 + n = len(mystr) + res = [] + while 0 <= i < n: + o_match = _OctalPatt.search(mystr, i) + q_match = _QuotePatt.search(mystr, i) + if not o_match and not q_match: # Neither matched + res.append(mystr[i:]) + break + # else: + j = k = -1 + if o_match: + j = o_match.start(0) + if q_match: + k = q_match.start(0) + if q_match and (not o_match or k < j): # QuotePatt matched + res.append(mystr[i:k]) + res.append(mystr[k+1]) + i = k + 2 + else: # OctalPatt matched + res.append(mystr[i:j]) + res.append(chr(int(mystr[j+1:j+4], 8))) + i = j + 4 + return _nulljoin(res) + +# The _getdate() routine is used to set the expiration time in the cookie's HTTP +# header. By default, _getdate() returns the current time in the appropriate +# "expires" format for a Set-Cookie header. The one optional argument is an +# offset from now, in seconds. For example, an offset of -3600 means "one hour +# ago". The offset may be a floating point number. +# + +_weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + +_monthname = [None, + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + +def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname): + from time import gmtime, time + now = time() + year, month, day, hh, mm, ss, wd, y, z = gmtime(now + future) + return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % \ + (weekdayname[wd], day, monthname[month], year, hh, mm, ss) + + +class Morsel(dict): + """A class to hold ONE (key, value) pair. + + In a cookie, each such pair may have several attributes, so this class is + used to keep the attributes associated with the appropriate key,value pair. + This class also includes a coded_value attribute, which is used to hold + the network representation of the value. This is most useful when Python + objects are pickled for network transit. + """ + # RFC 2109 lists these attributes as reserved: + # path comment domain + # max-age secure version + # + # For historical reasons, these attributes are also reserved: + # expires + # + # This is an extension from Microsoft: + # httponly + # + # This dictionary provides a mapping from the lowercase + # variant on the left to the appropriate traditional + # formatting on the right. + _reserved = { + "expires" : "expires", + "path" : "Path", + "comment" : "Comment", + "domain" : "Domain", + "max-age" : "Max-Age", + "secure" : "secure", + "httponly" : "httponly", + "version" : "Version", + } + + _flags = set(['secure', 'httponly']) + + def __init__(self): + # Set defaults + self.key = self.value = self.coded_value = None + + # Set default attributes + for key in self._reserved: + dict.__setitem__(self, key, "") + + def __setitem__(self, K, V): + K = K.lower() + if not K in self._reserved: + raise CookieError("Invalid Attribute %s" % K) + dict.__setitem__(self, K, V) + + def isReservedKey(self, K): + return K.lower() in self._reserved + + def set(self, key, val, coded_val, LegalChars=_LegalChars): + # First we verify that the key isn't a reserved word + # Second we make sure it only contains legal characters + if key.lower() in self._reserved: + raise CookieError("Attempt to set a reserved key: %s" % key) + if any(c not in LegalChars for c in key): + raise CookieError("Illegal key value: %s" % key) + + # It's a good key, so save it. + self.key = key + self.value = val + self.coded_value = coded_val + + def output(self, attrs=None, header="Set-Cookie:"): + return "%s %s" % (header, self.OutputString(attrs)) + + __str__ = output + + @as_native_str() + def __repr__(self): + if PY2 and isinstance(self.value, unicode): + val = str(self.value) # make it a newstr to remove the u prefix + else: + val = self.value + return '<%s: %s=%s>' % (self.__class__.__name__, + str(self.key), repr(val)) + + def js_output(self, attrs=None): + # Print javascript + return """ + + """ % (self.OutputString(attrs).replace('"', r'\"')) + + def OutputString(self, attrs=None): + # Build up our result + # + result = [] + append = result.append + + # First, the key=value pair + append("%s=%s" % (self.key, self.coded_value)) + + # Now add any defined attributes + if attrs is None: + attrs = self._reserved + items = sorted(self.items()) + for key, value in items: + if value == "": + continue + if key not in attrs: + continue + if key == "expires" and isinstance(value, int): + append("%s=%s" % (self._reserved[key], _getdate(value))) + elif key == "max-age" and isinstance(value, int): + append("%s=%d" % (self._reserved[key], value)) + elif key == "secure": + append(str(self._reserved[key])) + elif key == "httponly": + append(str(self._reserved[key])) + else: + append("%s=%s" % (self._reserved[key], value)) + + # Return the result + return _semispacejoin(result) + + +# +# Pattern for finding cookie +# +# This used to be strict parsing based on the RFC2109 and RFC2068 +# specifications. I have since discovered that MSIE 3.0x doesn't +# follow the character rules outlined in those specs. As a +# result, the parsing rules here are less strict. +# + +_LegalCharsPatt = r"[\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=]" +_CookiePattern = re.compile(r""" + (?x) # This is a verbose pattern + (?P # Start of group 'key' + """ + _LegalCharsPatt + r"""+? # Any word of at least one letter + ) # End of group 'key' + ( # Optional group: there may not be a value. + \s*=\s* # Equal Sign + (?P # Start of group 'val' + "(?:[^\\"]|\\.)*" # Any doublequoted string + | # or + \w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT # Special case for "expires" attr + | # or + """ + _LegalCharsPatt + r"""* # Any word or empty string + ) # End of group 'val' + )? # End of optional value group + \s* # Any number of spaces. + (\s+|;|$) # Ending either at space, semicolon, or EOS. + """, re.ASCII) # May be removed if safe. + + +# At long last, here is the cookie class. Using this class is almost just like +# using a dictionary. See this module's docstring for example usage. +# +class BaseCookie(dict): + """A container class for a set of Morsels.""" + + def value_decode(self, val): + """real_value, coded_value = value_decode(STRING) + Called prior to setting a cookie's value from the network + representation. The VALUE is the value read from HTTP + header. + Override this function to modify the behavior of cookies. + """ + return val, val + + def value_encode(self, val): + """real_value, coded_value = value_encode(VALUE) + Called prior to setting a cookie's value from the dictionary + representation. The VALUE is the value being assigned. + Override this function to modify the behavior of cookies. + """ + strval = str(val) + return strval, strval + + def __init__(self, input=None): + if input: + self.load(input) + + def __set(self, key, real_value, coded_value): + """Private method for setting a cookie's value""" + M = self.get(key, Morsel()) + M.set(key, real_value, coded_value) + dict.__setitem__(self, key, M) + + def __setitem__(self, key, value): + """Dictionary style assignment.""" + rval, cval = self.value_encode(value) + self.__set(key, rval, cval) + + def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"): + """Return a string suitable for HTTP.""" + result = [] + items = sorted(self.items()) + for key, value in items: + result.append(value.output(attrs, header)) + return sep.join(result) + + __str__ = output + + @as_native_str() + def __repr__(self): + l = [] + items = sorted(self.items()) + for key, value in items: + if PY2 and isinstance(value.value, unicode): + val = str(value.value) # make it a newstr to remove the u prefix + else: + val = value.value + l.append('%s=%s' % (str(key), repr(val))) + return '<%s: %s>' % (self.__class__.__name__, _spacejoin(l)) + + def js_output(self, attrs=None): + """Return a string suitable for JavaScript.""" + result = [] + items = sorted(self.items()) + for key, value in items: + result.append(value.js_output(attrs)) + return _nulljoin(result) + + def load(self, rawdata): + """Load cookies from a string (presumably HTTP_COOKIE) or + from a dictionary. Loading cookies from a dictionary 'd' + is equivalent to calling: + map(Cookie.__setitem__, d.keys(), d.values()) + """ + if isinstance(rawdata, str): + self.__parse_string(rawdata) + else: + # self.update() wouldn't call our custom __setitem__ + for key, value in rawdata.items(): + self[key] = value + return + + def __parse_string(self, mystr, patt=_CookiePattern): + i = 0 # Our starting point + n = len(mystr) # Length of string + M = None # current morsel + + while 0 <= i < n: + # Start looking for a cookie + match = patt.search(mystr, i) + if not match: + # No more cookies + break + + key, value = match.group("key"), match.group("val") + + i = match.end(0) + + # Parse the key, value in case it's metainfo + if key[0] == "$": + # We ignore attributes which pertain to the cookie + # mechanism as a whole. See RFC 2109. + # (Does anyone care?) + if M: + M[key[1:]] = value + elif key.lower() in Morsel._reserved: + if M: + if value is None: + if key.lower() in Morsel._flags: + M[key] = True + else: + M[key] = _unquote(value) + elif value is not None: + rval, cval = self.value_decode(value) + self.__set(key, rval, cval) + M = self[key] + + +class SimpleCookie(BaseCookie): + """ + SimpleCookie supports strings as cookie values. When setting + the value using the dictionary assignment notation, SimpleCookie + calls the builtin str() to convert the value to a string. Values + received from HTTP are kept as strings. + """ + def value_decode(self, val): + return _unquote(val), val + + def value_encode(self, val): + strval = str(val) + return strval, _quote(strval) diff --git a/venv/lib/python3.10/site-packages/future/backports/http/server.py b/venv/lib/python3.10/site-packages/future/backports/http/server.py new file mode 100644 index 0000000..b1c11e0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/http/server.py @@ -0,0 +1,1226 @@ +"""HTTP server classes. + +From Python 3.3 + +Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see +SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST, +and CGIHTTPRequestHandler for CGI scripts. + +It does, however, optionally implement HTTP/1.1 persistent connections, +as of version 0.3. + +Notes on CGIHTTPRequestHandler +------------------------------ + +This class implements GET and POST requests to cgi-bin scripts. + +If the os.fork() function is not present (e.g. on Windows), +subprocess.Popen() is used as a fallback, with slightly altered semantics. + +In all cases, the implementation is intentionally naive -- all +requests are executed synchronously. + +SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL +-- it may execute arbitrary Python code or external programs. + +Note that status code 200 is sent prior to execution of a CGI script, so +scripts cannot send other status codes such as 302 (redirect). + +XXX To do: + +- log requests even later (to capture byte count) +- log user-agent header and other interesting goodies +- send error log to separate file +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) +from future import utils +from future.builtins import * + + +# See also: +# +# HTTP Working Group T. Berners-Lee +# INTERNET-DRAFT R. T. Fielding +# H. Frystyk Nielsen +# Expires September 8, 1995 March 8, 1995 +# +# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt +# +# and +# +# Network Working Group R. Fielding +# Request for Comments: 2616 et al +# Obsoletes: 2068 June 1999 +# Category: Standards Track +# +# URL: http://www.faqs.org/rfcs/rfc2616.html + +# Log files +# --------- +# +# Here's a quote from the NCSA httpd docs about log file format. +# +# | The logfile format is as follows. Each line consists of: +# | +# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb +# | +# | host: Either the DNS name or the IP number of the remote client +# | rfc931: Any information returned by identd for this person, +# | - otherwise. +# | authuser: If user sent a userid for authentication, the user name, +# | - otherwise. +# | DD: Day +# | Mon: Month (calendar name) +# | YYYY: Year +# | hh: hour (24-hour format, the machine's timezone) +# | mm: minutes +# | ss: seconds +# | request: The first line of the HTTP request as sent by the client. +# | ddd: the status code returned by the server, - if not available. +# | bbbb: the total number of bytes sent, +# | *not including the HTTP/1.0 header*, - if not available +# | +# | You can determine the name of the file accessed through request. +# +# (Actually, the latter is only true if you know the server configuration +# at the time the request was made!) + +__version__ = "0.6" + +__all__ = ["HTTPServer", "BaseHTTPRequestHandler"] + +from future.backports import html +from future.backports.http import client as http_client +from future.backports.urllib import parse as urllib_parse +from future.backports import socketserver + +import io +import mimetypes +import os +import posixpath +import select +import shutil +import socket # For gethostbyaddr() +import sys +import time +import copy +import argparse + + +# Default error message template +DEFAULT_ERROR_MESSAGE = """\ + + + + + Error response + + +

Error response

+

Error code: %(code)d

+

Message: %(message)s.

+

Error code explanation: %(code)s - %(explain)s.

+ + +""" + +DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8" + +def _quote_html(html): + return html.replace("&", "&").replace("<", "<").replace(">", ">") + +class HTTPServer(socketserver.TCPServer): + + allow_reuse_address = 1 # Seems to make sense in testing environment + + def server_bind(self): + """Override server_bind to store the server name.""" + socketserver.TCPServer.server_bind(self) + host, port = self.socket.getsockname()[:2] + self.server_name = socket.getfqdn(host) + self.server_port = port + + +class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): + + """HTTP request handler base class. + + The following explanation of HTTP serves to guide you through the + code as well as to expose any misunderstandings I may have about + HTTP (so you don't need to read the code to figure out I'm wrong + :-). + + HTTP (HyperText Transfer Protocol) is an extensible protocol on + top of a reliable stream transport (e.g. TCP/IP). The protocol + recognizes three parts to a request: + + 1. One line identifying the request type and path + 2. An optional set of RFC-822-style headers + 3. An optional data part + + The headers and data are separated by a blank line. + + The first line of the request has the form + + + + where is a (case-sensitive) keyword such as GET or POST, + is a string containing path information for the request, + and should be the string "HTTP/1.0" or "HTTP/1.1". + is encoded using the URL encoding scheme (using %xx to signify + the ASCII character with hex code xx). + + The specification specifies that lines are separated by CRLF but + for compatibility with the widest range of clients recommends + servers also handle LF. Similarly, whitespace in the request line + is treated sensibly (allowing multiple spaces between components + and allowing trailing whitespace). + + Similarly, for output, lines ought to be separated by CRLF pairs + but most clients grok LF characters just fine. + + If the first line of the request has the form + + + + (i.e. is left out) then this is assumed to be an HTTP + 0.9 request; this form has no optional headers and data part and + the reply consists of just the data. + + The reply form of the HTTP 1.x protocol again has three parts: + + 1. One line giving the response code + 2. An optional set of RFC-822-style headers + 3. The data + + Again, the headers and data are separated by a blank line. + + The response code line has the form + + + + where is the protocol version ("HTTP/1.0" or "HTTP/1.1"), + is a 3-digit response code indicating success or + failure of the request, and is an optional + human-readable string explaining what the response code means. + + This server parses the request and the headers, and then calls a + function specific to the request type (). Specifically, + a request SPAM will be handled by a method do_SPAM(). If no + such method exists the server sends an error response to the + client. If it exists, it is called with no arguments: + + do_SPAM() + + Note that the request name is case sensitive (i.e. SPAM and spam + are different requests). + + The various request details are stored in instance variables: + + - client_address is the client IP address in the form (host, + port); + + - command, path and version are the broken-down request line; + + - headers is an instance of email.message.Message (or a derived + class) containing the header information; + + - rfile is a file object open for reading positioned at the + start of the optional input data part; + + - wfile is a file object open for writing. + + IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING! + + The first thing to be written must be the response line. Then + follow 0 or more header lines, then a blank line, and then the + actual data (if any). The meaning of the header lines depends on + the command executed by the server; in most cases, when data is + returned, there should be at least one header line of the form + + Content-type: / + + where and should be registered MIME types, + e.g. "text/html" or "text/plain". + + """ + + # The Python system version, truncated to its first component. + sys_version = "Python/" + sys.version.split()[0] + + # The server software version. You may want to override this. + # The format is multiple whitespace-separated strings, + # where each string is of the form name[/version]. + server_version = "BaseHTTP/" + __version__ + + error_message_format = DEFAULT_ERROR_MESSAGE + error_content_type = DEFAULT_ERROR_CONTENT_TYPE + + # The default request version. This only affects responses up until + # the point where the request line is parsed, so it mainly decides what + # the client gets back when sending a malformed request line. + # Most web servers default to HTTP 0.9, i.e. don't send a status line. + default_request_version = "HTTP/0.9" + + def parse_request(self): + """Parse a request (internal). + + The request should be stored in self.raw_requestline; the results + are in self.command, self.path, self.request_version and + self.headers. + + Return True for success, False for failure; on failure, an + error is sent back. + + """ + self.command = None # set in case of error on the first line + self.request_version = version = self.default_request_version + self.close_connection = 1 + requestline = str(self.raw_requestline, 'iso-8859-1') + requestline = requestline.rstrip('\r\n') + self.requestline = requestline + words = requestline.split() + if len(words) == 3: + command, path, version = words + if version[:5] != 'HTTP/': + self.send_error(400, "Bad request version (%r)" % version) + return False + try: + base_version_number = version.split('/', 1)[1] + version_number = base_version_number.split(".") + # RFC 2145 section 3.1 says there can be only one "." and + # - major and minor numbers MUST be treated as + # separate integers; + # - HTTP/2.4 is a lower version than HTTP/2.13, which in + # turn is lower than HTTP/12.3; + # - Leading zeros MUST be ignored by recipients. + if len(version_number) != 2: + raise ValueError + version_number = int(version_number[0]), int(version_number[1]) + except (ValueError, IndexError): + self.send_error(400, "Bad request version (%r)" % version) + return False + if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": + self.close_connection = 0 + if version_number >= (2, 0): + self.send_error(505, + "Invalid HTTP Version (%s)" % base_version_number) + return False + elif len(words) == 2: + command, path = words + self.close_connection = 1 + if command != 'GET': + self.send_error(400, + "Bad HTTP/0.9 request type (%r)" % command) + return False + elif not words: + return False + else: + self.send_error(400, "Bad request syntax (%r)" % requestline) + return False + self.command, self.path, self.request_version = command, path, version + + # Examine the headers and look for a Connection directive. + try: + self.headers = http_client.parse_headers(self.rfile, + _class=self.MessageClass) + except http_client.LineTooLong: + self.send_error(400, "Line too long") + return False + + conntype = self.headers.get('Connection', "") + if conntype.lower() == 'close': + self.close_connection = 1 + elif (conntype.lower() == 'keep-alive' and + self.protocol_version >= "HTTP/1.1"): + self.close_connection = 0 + # Examine the headers and look for an Expect directive + expect = self.headers.get('Expect', "") + if (expect.lower() == "100-continue" and + self.protocol_version >= "HTTP/1.1" and + self.request_version >= "HTTP/1.1"): + if not self.handle_expect_100(): + return False + return True + + def handle_expect_100(self): + """Decide what to do with an "Expect: 100-continue" header. + + If the client is expecting a 100 Continue response, we must + respond with either a 100 Continue or a final response before + waiting for the request body. The default is to always respond + with a 100 Continue. You can behave differently (for example, + reject unauthorized requests) by overriding this method. + + This method should either return True (possibly after sending + a 100 Continue response) or send an error response and return + False. + + """ + self.send_response_only(100) + self.flush_headers() + return True + + def handle_one_request(self): + """Handle a single HTTP request. + + You normally don't need to override this method; see the class + __doc__ string for information on how to handle specific HTTP + commands such as GET and POST. + + """ + try: + self.raw_requestline = self.rfile.readline(65537) + if len(self.raw_requestline) > 65536: + self.requestline = '' + self.request_version = '' + self.command = '' + self.send_error(414) + return + if not self.raw_requestline: + self.close_connection = 1 + return + if not self.parse_request(): + # An error code has been sent, just exit + return + mname = 'do_' + self.command + if not hasattr(self, mname): + self.send_error(501, "Unsupported method (%r)" % self.command) + return + method = getattr(self, mname) + method() + self.wfile.flush() #actually send the response if not already done. + except socket.timeout as e: + #a read or a write timed out. Discard this connection + self.log_error("Request timed out: %r", e) + self.close_connection = 1 + return + + def handle(self): + """Handle multiple requests if necessary.""" + self.close_connection = 1 + + self.handle_one_request() + while not self.close_connection: + self.handle_one_request() + + def send_error(self, code, message=None): + """Send and log an error reply. + + Arguments are the error code, and a detailed message. + The detailed message defaults to the short entry matching the + response code. + + This sends an error response (so it must be called before any + output has been generated), logs the error, and finally sends + a piece of HTML explaining the error to the user. + + """ + + try: + shortmsg, longmsg = self.responses[code] + except KeyError: + shortmsg, longmsg = '???', '???' + if message is None: + message = shortmsg + explain = longmsg + self.log_error("code %d, message %s", code, message) + # using _quote_html to prevent Cross Site Scripting attacks (see bug #1100201) + content = (self.error_message_format % + {'code': code, 'message': _quote_html(message), 'explain': explain}) + self.send_response(code, message) + self.send_header("Content-Type", self.error_content_type) + self.send_header('Connection', 'close') + self.end_headers() + if self.command != 'HEAD' and code >= 200 and code not in (204, 304): + self.wfile.write(content.encode('UTF-8', 'replace')) + + def send_response(self, code, message=None): + """Add the response header to the headers buffer and log the + response code. + + Also send two standard headers with the server software + version and the current date. + + """ + self.log_request(code) + self.send_response_only(code, message) + self.send_header('Server', self.version_string()) + self.send_header('Date', self.date_time_string()) + + def send_response_only(self, code, message=None): + """Send the response header only.""" + if message is None: + if code in self.responses: + message = self.responses[code][0] + else: + message = '' + if self.request_version != 'HTTP/0.9': + if not hasattr(self, '_headers_buffer'): + self._headers_buffer = [] + self._headers_buffer.append(("%s %d %s\r\n" % + (self.protocol_version, code, message)).encode( + 'latin-1', 'strict')) + + def send_header(self, keyword, value): + """Send a MIME header to the headers buffer.""" + if self.request_version != 'HTTP/0.9': + if not hasattr(self, '_headers_buffer'): + self._headers_buffer = [] + self._headers_buffer.append( + ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) + + if keyword.lower() == 'connection': + if value.lower() == 'close': + self.close_connection = 1 + elif value.lower() == 'keep-alive': + self.close_connection = 0 + + def end_headers(self): + """Send the blank line ending the MIME headers.""" + if self.request_version != 'HTTP/0.9': + self._headers_buffer.append(b"\r\n") + self.flush_headers() + + def flush_headers(self): + if hasattr(self, '_headers_buffer'): + self.wfile.write(b"".join(self._headers_buffer)) + self._headers_buffer = [] + + def log_request(self, code='-', size='-'): + """Log an accepted request. + + This is called by send_response(). + + """ + + self.log_message('"%s" %s %s', + self.requestline, str(code), str(size)) + + def log_error(self, format, *args): + """Log an error. + + This is called when a request cannot be fulfilled. By + default it passes the message on to log_message(). + + Arguments are the same as for log_message(). + + XXX This should go to the separate error log. + + """ + + self.log_message(format, *args) + + def log_message(self, format, *args): + """Log an arbitrary message. + + This is used by all other logging functions. Override + it if you have specific logging wishes. + + The first argument, FORMAT, is a format string for the + message to be logged. If the format string contains + any % escapes requiring parameters, they should be + specified as subsequent arguments (it's just like + printf!). + + The client ip and current date/time are prefixed to + every message. + + """ + + sys.stderr.write("%s - - [%s] %s\n" % + (self.address_string(), + self.log_date_time_string(), + format%args)) + + def version_string(self): + """Return the server software version string.""" + return self.server_version + ' ' + self.sys_version + + def date_time_string(self, timestamp=None): + """Return the current date and time formatted for a message header.""" + if timestamp is None: + timestamp = time.time() + year, month, day, hh, mm, ss, wd, y, z = time.gmtime(timestamp) + s = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( + self.weekdayname[wd], + day, self.monthname[month], year, + hh, mm, ss) + return s + + def log_date_time_string(self): + """Return the current time formatted for logging.""" + now = time.time() + year, month, day, hh, mm, ss, x, y, z = time.localtime(now) + s = "%02d/%3s/%04d %02d:%02d:%02d" % ( + day, self.monthname[month], year, hh, mm, ss) + return s + + weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + + monthname = [None, + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + + def address_string(self): + """Return the client address.""" + + return self.client_address[0] + + # Essentially static class variables + + # The version of the HTTP protocol we support. + # Set this to HTTP/1.1 to enable automatic keepalive + protocol_version = "HTTP/1.0" + + # MessageClass used to parse headers + MessageClass = http_client.HTTPMessage + + # Table mapping response codes to messages; entries have the + # form {code: (shortmessage, longmessage)}. + # See RFC 2616 and 6585. + responses = { + 100: ('Continue', 'Request received, please continue'), + 101: ('Switching Protocols', + 'Switching to new protocol; obey Upgrade header'), + + 200: ('OK', 'Request fulfilled, document follows'), + 201: ('Created', 'Document created, URL follows'), + 202: ('Accepted', + 'Request accepted, processing continues off-line'), + 203: ('Non-Authoritative Information', 'Request fulfilled from cache'), + 204: ('No Content', 'Request fulfilled, nothing follows'), + 205: ('Reset Content', 'Clear input form for further input.'), + 206: ('Partial Content', 'Partial content follows.'), + + 300: ('Multiple Choices', + 'Object has several resources -- see URI list'), + 301: ('Moved Permanently', 'Object moved permanently -- see URI list'), + 302: ('Found', 'Object moved temporarily -- see URI list'), + 303: ('See Other', 'Object moved -- see Method and URL list'), + 304: ('Not Modified', + 'Document has not changed since given time'), + 305: ('Use Proxy', + 'You must use proxy specified in Location to access this ' + 'resource.'), + 307: ('Temporary Redirect', + 'Object moved temporarily -- see URI list'), + + 400: ('Bad Request', + 'Bad request syntax or unsupported method'), + 401: ('Unauthorized', + 'No permission -- see authorization schemes'), + 402: ('Payment Required', + 'No payment -- see charging schemes'), + 403: ('Forbidden', + 'Request forbidden -- authorization will not help'), + 404: ('Not Found', 'Nothing matches the given URI'), + 405: ('Method Not Allowed', + 'Specified method is invalid for this resource.'), + 406: ('Not Acceptable', 'URI not available in preferred format.'), + 407: ('Proxy Authentication Required', 'You must authenticate with ' + 'this proxy before proceeding.'), + 408: ('Request Timeout', 'Request timed out; try again later.'), + 409: ('Conflict', 'Request conflict.'), + 410: ('Gone', + 'URI no longer exists and has been permanently removed.'), + 411: ('Length Required', 'Client must specify Content-Length.'), + 412: ('Precondition Failed', 'Precondition in headers is false.'), + 413: ('Request Entity Too Large', 'Entity is too large.'), + 414: ('Request-URI Too Long', 'URI is too long.'), + 415: ('Unsupported Media Type', 'Entity body in unsupported format.'), + 416: ('Requested Range Not Satisfiable', + 'Cannot satisfy request range.'), + 417: ('Expectation Failed', + 'Expect condition could not be satisfied.'), + 428: ('Precondition Required', + 'The origin server requires the request to be conditional.'), + 429: ('Too Many Requests', 'The user has sent too many requests ' + 'in a given amount of time ("rate limiting").'), + 431: ('Request Header Fields Too Large', 'The server is unwilling to ' + 'process the request because its header fields are too large.'), + + 500: ('Internal Server Error', 'Server got itself in trouble'), + 501: ('Not Implemented', + 'Server does not support this operation'), + 502: ('Bad Gateway', 'Invalid responses from another server/proxy.'), + 503: ('Service Unavailable', + 'The server cannot process the request due to a high load'), + 504: ('Gateway Timeout', + 'The gateway server did not receive a timely response'), + 505: ('HTTP Version Not Supported', 'Cannot fulfill request.'), + 511: ('Network Authentication Required', + 'The client needs to authenticate to gain network access.'), + } + + +class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + + """Simple HTTP request handler with GET and HEAD commands. + + This serves files from the current directory and any of its + subdirectories. The MIME type for files is determined by + calling the .guess_type() method. + + The GET and HEAD requests are identical except that the HEAD + request omits the actual contents of the file. + + """ + + server_version = "SimpleHTTP/" + __version__ + + def do_GET(self): + """Serve a GET request.""" + f = self.send_head() + if f: + self.copyfile(f, self.wfile) + f.close() + + def do_HEAD(self): + """Serve a HEAD request.""" + f = self.send_head() + if f: + f.close() + + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + """ + path = self.translate_path(self.path) + f = None + if os.path.isdir(path): + if not self.path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(301) + self.send_header("Location", self.path + "/") + self.end_headers() + return None + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + f = open(path, 'rb') + except IOError: + self.send_error(404, "File not found") + return None + self.send_response(200) + self.send_header("Content-type", ctype) + fs = os.fstat(f.fileno()) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + + def list_directory(self, path): + """Helper to produce a directory listing (absent index.html). + + Return value is either a file object, or None (indicating an + error). In either case, the headers are sent, making the + interface the same as for send_head(). + + """ + try: + list = os.listdir(path) + except os.error: + self.send_error(404, "No permission to list directory") + return None + list.sort(key=lambda a: a.lower()) + r = [] + displaypath = html.escape(urllib_parse.unquote(self.path)) + enc = sys.getfilesystemencoding() + title = 'Directory listing for %s' % displaypath + r.append('') + r.append('\n') + r.append('' % enc) + r.append('%s\n' % title) + r.append('\n

%s

' % title) + r.append('
\n
    ') + for name in list: + fullname = os.path.join(path, name) + displayname = linkname = name + # Append / for directories or @ for symbolic links + if os.path.isdir(fullname): + displayname = name + "/" + linkname = name + "/" + if os.path.islink(fullname): + displayname = name + "@" + # Note: a link to a directory displays with @ and links with / + r.append('
  • %s
  • ' + % (urllib_parse.quote(linkname), html.escape(displayname))) + # # Use this instead: + # r.append('
  • %s
  • ' + # % (urllib.quote(linkname), cgi.escape(displayname))) + r.append('
\n
\n\n\n') + encoded = '\n'.join(r).encode(enc) + f = io.BytesIO() + f.write(encoded) + f.seek(0) + self.send_response(200) + self.send_header("Content-type", "text/html; charset=%s" % enc) + self.send_header("Content-Length", str(len(encoded))) + self.end_headers() + return f + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = path.split('?',1)[0] + path = path.split('#',1)[0] + path = posixpath.normpath(urllib_parse.unquote(path)) + words = path.split('/') + words = filter(None, words) + path = os.getcwd() + for word in words: + drive, word = os.path.splitdrive(word) + head, word = os.path.split(word) + if word in (os.curdir, os.pardir): continue + path = os.path.join(path, word) + return path + + def copyfile(self, source, outputfile): + """Copy all data between two file objects. + + The SOURCE argument is a file object open for reading + (or anything with a read() method) and the DESTINATION + argument is a file object open for writing (or + anything with a write() method). + + The only reason for overriding this would be to change + the block size or perhaps to replace newlines by CRLF + -- note however that this the default server uses this + to copy binary data as well. + + """ + shutil.copyfileobj(source, outputfile) + + def guess_type(self, path): + """Guess the type of a file. + + Argument is a PATH (a filename). + + Return value is a string of the form type/subtype, + usable for a MIME Content-type header. + + The default implementation looks the file's extension + up in the table self.extensions_map, using application/octet-stream + as a default; however it would be permissible (if + slow) to look inside the data to make a better guess. + + """ + + base, ext = posixpath.splitext(path) + if ext in self.extensions_map: + return self.extensions_map[ext] + ext = ext.lower() + if ext in self.extensions_map: + return self.extensions_map[ext] + else: + return self.extensions_map[''] + + if not mimetypes.inited: + mimetypes.init() # try to read system mime.types + extensions_map = mimetypes.types_map.copy() + extensions_map.update({ + '': 'application/octet-stream', # Default + '.py': 'text/plain', + '.c': 'text/plain', + '.h': 'text/plain', + }) + + +# Utilities for CGIHTTPRequestHandler + +def _url_collapse_path(path): + """ + Given a URL path, remove extra '/'s and '.' path elements and collapse + any '..' references and returns a colllapsed path. + + Implements something akin to RFC-2396 5.2 step 6 to parse relative paths. + The utility of this function is limited to is_cgi method and helps + preventing some security attacks. + + Returns: A tuple of (head, tail) where tail is everything after the final / + and head is everything before it. Head will always start with a '/' and, + if it contains anything else, never have a trailing '/'. + + Raises: IndexError if too many '..' occur within the path. + + """ + # Similar to os.path.split(os.path.normpath(path)) but specific to URL + # path semantics rather than local operating system semantics. + path_parts = path.split('/') + head_parts = [] + for part in path_parts[:-1]: + if part == '..': + head_parts.pop() # IndexError if more '..' than prior parts + elif part and part != '.': + head_parts.append( part ) + if path_parts: + tail_part = path_parts.pop() + if tail_part: + if tail_part == '..': + head_parts.pop() + tail_part = '' + elif tail_part == '.': + tail_part = '' + else: + tail_part = '' + + splitpath = ('/' + '/'.join(head_parts), tail_part) + collapsed_path = "/".join(splitpath) + + return collapsed_path + + + +nobody = None + +def nobody_uid(): + """Internal routine to get nobody's uid""" + global nobody + if nobody: + return nobody + try: + import pwd + except ImportError: + return -1 + try: + nobody = pwd.getpwnam('nobody')[2] + except KeyError: + nobody = 1 + max(x[2] for x in pwd.getpwall()) + return nobody + + +def executable(path): + """Test for executable file.""" + return os.access(path, os.X_OK) + + +class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): + + """Complete HTTP server with GET, HEAD and POST commands. + + GET and HEAD also support running CGI scripts. + + The POST command is *only* implemented for CGI scripts. + + """ + + # Determine platform specifics + have_fork = hasattr(os, 'fork') + + # Make rfile unbuffered -- we need to read one line and then pass + # the rest to a subprocess, so we can't use buffered input. + rbufsize = 0 + + def do_POST(self): + """Serve a POST request. + + This is only implemented for CGI scripts. + + """ + + if self.is_cgi(): + self.run_cgi() + else: + self.send_error(501, "Can only POST to CGI scripts") + + def send_head(self): + """Version of send_head that support CGI scripts""" + if self.is_cgi(): + return self.run_cgi() + else: + return SimpleHTTPRequestHandler.send_head(self) + + def is_cgi(self): + """Test whether self.path corresponds to a CGI script. + + Returns True and updates the cgi_info attribute to the tuple + (dir, rest) if self.path requires running a CGI script. + Returns False otherwise. + + If any exception is raised, the caller should assume that + self.path was rejected as invalid and act accordingly. + + The default implementation tests whether the normalized url + path begins with one of the strings in self.cgi_directories + (and the next character is a '/' or the end of the string). + + """ + collapsed_path = _url_collapse_path(self.path) + dir_sep = collapsed_path.find('/', 1) + head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:] + if head in self.cgi_directories: + self.cgi_info = head, tail + return True + return False + + + cgi_directories = ['/cgi-bin', '/htbin'] + + def is_executable(self, path): + """Test whether argument path is an executable file.""" + return executable(path) + + def is_python(self, path): + """Test whether argument path is a Python script.""" + head, tail = os.path.splitext(path) + return tail.lower() in (".py", ".pyw") + + def run_cgi(self): + """Execute a CGI script.""" + path = self.path + dir, rest = self.cgi_info + + i = path.find('/', len(dir) + 1) + while i >= 0: + nextdir = path[:i] + nextrest = path[i+1:] + + scriptdir = self.translate_path(nextdir) + if os.path.isdir(scriptdir): + dir, rest = nextdir, nextrest + i = path.find('/', len(dir) + 1) + else: + break + + # find an explicit query string, if present. + i = rest.rfind('?') + if i >= 0: + rest, query = rest[:i], rest[i+1:] + else: + query = '' + + # dissect the part after the directory name into a script name & + # a possible additional path, to be stored in PATH_INFO. + i = rest.find('/') + if i >= 0: + script, rest = rest[:i], rest[i:] + else: + script, rest = rest, '' + + scriptname = dir + '/' + script + scriptfile = self.translate_path(scriptname) + if not os.path.exists(scriptfile): + self.send_error(404, "No such CGI script (%r)" % scriptname) + return + if not os.path.isfile(scriptfile): + self.send_error(403, "CGI script is not a plain file (%r)" % + scriptname) + return + ispy = self.is_python(scriptname) + if self.have_fork or not ispy: + if not self.is_executable(scriptfile): + self.send_error(403, "CGI script is not executable (%r)" % + scriptname) + return + + # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html + # XXX Much of the following could be prepared ahead of time! + env = copy.deepcopy(os.environ) + env['SERVER_SOFTWARE'] = self.version_string() + env['SERVER_NAME'] = self.server.server_name + env['GATEWAY_INTERFACE'] = 'CGI/1.1' + env['SERVER_PROTOCOL'] = self.protocol_version + env['SERVER_PORT'] = str(self.server.server_port) + env['REQUEST_METHOD'] = self.command + uqrest = urllib_parse.unquote(rest) + env['PATH_INFO'] = uqrest + env['PATH_TRANSLATED'] = self.translate_path(uqrest) + env['SCRIPT_NAME'] = scriptname + if query: + env['QUERY_STRING'] = query + env['REMOTE_ADDR'] = self.client_address[0] + authorization = self.headers.get("authorization") + if authorization: + authorization = authorization.split() + if len(authorization) == 2: + import base64, binascii + env['AUTH_TYPE'] = authorization[0] + if authorization[0].lower() == "basic": + try: + authorization = authorization[1].encode('ascii') + if utils.PY3: + # In Py3.3, was: + authorization = base64.decodebytes(authorization).\ + decode('ascii') + else: + # Backport to Py2.7: + authorization = base64.decodestring(authorization).\ + decode('ascii') + except (binascii.Error, UnicodeError): + pass + else: + authorization = authorization.split(':') + if len(authorization) == 2: + env['REMOTE_USER'] = authorization[0] + # XXX REMOTE_IDENT + if self.headers.get('content-type') is None: + env['CONTENT_TYPE'] = self.headers.get_content_type() + else: + env['CONTENT_TYPE'] = self.headers['content-type'] + length = self.headers.get('content-length') + if length: + env['CONTENT_LENGTH'] = length + referer = self.headers.get('referer') + if referer: + env['HTTP_REFERER'] = referer + accept = [] + for line in self.headers.getallmatchingheaders('accept'): + if line[:1] in "\t\n\r ": + accept.append(line.strip()) + else: + accept = accept + line[7:].split(',') + env['HTTP_ACCEPT'] = ','.join(accept) + ua = self.headers.get('user-agent') + if ua: + env['HTTP_USER_AGENT'] = ua + co = filter(None, self.headers.get_all('cookie', [])) + cookie_str = ', '.join(co) + if cookie_str: + env['HTTP_COOKIE'] = cookie_str + # XXX Other HTTP_* headers + # Since we're setting the env in the parent, provide empty + # values to override previously set values + for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', + 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'): + env.setdefault(k, "") + + self.send_response(200, "Script output follows") + self.flush_headers() + + decoded_query = query.replace('+', ' ') + + if self.have_fork: + # Unix -- fork as we should + args = [script] + if '=' not in decoded_query: + args.append(decoded_query) + nobody = nobody_uid() + self.wfile.flush() # Always flush before forking + pid = os.fork() + if pid != 0: + # Parent + pid, sts = os.waitpid(pid, 0) + # throw away additional data [see bug #427345] + while select.select([self.rfile], [], [], 0)[0]: + if not self.rfile.read(1): + break + if sts: + self.log_error("CGI script exit status %#x", sts) + return + # Child + try: + try: + os.setuid(nobody) + except os.error: + pass + os.dup2(self.rfile.fileno(), 0) + os.dup2(self.wfile.fileno(), 1) + os.execve(scriptfile, args, env) + except: + self.server.handle_error(self.request, self.client_address) + os._exit(127) + + else: + # Non-Unix -- use subprocess + import subprocess + cmdline = [scriptfile] + if self.is_python(scriptfile): + interp = sys.executable + if interp.lower().endswith("w.exe"): + # On Windows, use python.exe, not pythonw.exe + interp = interp[:-5] + interp[-4:] + cmdline = [interp, '-u'] + cmdline + if '=' not in query: + cmdline.append(query) + self.log_message("command: %s", subprocess.list2cmdline(cmdline)) + try: + nbytes = int(length) + except (TypeError, ValueError): + nbytes = 0 + p = subprocess.Popen(cmdline, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env = env + ) + if self.command.lower() == "post" and nbytes > 0: + data = self.rfile.read(nbytes) + else: + data = None + # throw away additional data [see bug #427345] + while select.select([self.rfile._sock], [], [], 0)[0]: + if not self.rfile._sock.recv(1): + break + stdout, stderr = p.communicate(data) + self.wfile.write(stdout) + if stderr: + self.log_error('%s', stderr) + p.stderr.close() + p.stdout.close() + status = p.returncode + if status: + self.log_error("CGI script exit status %#x", status) + else: + self.log_message("CGI script exited OK") + + +def test(HandlerClass = BaseHTTPRequestHandler, + ServerClass = HTTPServer, protocol="HTTP/1.0", port=8000): + """Test the HTTP request handler class. + + This runs an HTTP server on port 8000 (or the first command line + argument). + + """ + server_address = ('', port) + + HandlerClass.protocol_version = protocol + httpd = ServerClass(server_address, HandlerClass) + + sa = httpd.socket.getsockname() + print("Serving HTTP on", sa[0], "port", sa[1], "...") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nKeyboard interrupt received, exiting.") + httpd.server_close() + sys.exit(0) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--cgi', action='store_true', + help='Run as CGI Server') + parser.add_argument('port', action='store', + default=8000, type=int, + nargs='?', + help='Specify alternate port [default: 8000]') + args = parser.parse_args() + if args.cgi: + test(HandlerClass=CGIHTTPRequestHandler, port=args.port) + else: + test(HandlerClass=SimpleHTTPRequestHandler, port=args.port) diff --git a/venv/lib/python3.10/site-packages/future/backports/misc.py b/venv/lib/python3.10/site-packages/future/backports/misc.py new file mode 100644 index 0000000..992b978 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/misc.py @@ -0,0 +1,958 @@ +""" +Miscellaneous function (re)definitions from the Py3.4+ standard library +for Python 2.6/2.7. + +- math.ceil (for Python 2.7) +- collections.OrderedDict (for Python 2.6) +- collections.Counter (for Python 2.6) +- collections.ChainMap (for all versions prior to Python 3.3) +- itertools.count (for Python 2.6, with step parameter) +- subprocess.check_output (for Python 2.6) +- reprlib.recursive_repr (for Python 2.6+) +- functools.cmp_to_key (for Python 2.6) +""" + +from __future__ import absolute_import + +import subprocess +from math import ceil as oldceil + +from operator import itemgetter as _itemgetter, eq as _eq +import sys +import heapq as _heapq +from _weakref import proxy as _proxy +from itertools import repeat as _repeat, chain as _chain, starmap as _starmap +from socket import getaddrinfo, SOCK_STREAM, error, socket + +from future.utils import iteritems, itervalues, PY2, PY26, PY3 + +if PY2: + from collections import Mapping, MutableMapping +else: + from collections.abc import Mapping, MutableMapping + + +def ceil(x): + """ + Return the ceiling of x as an int. + This is the smallest integral value >= x. + """ + return int(oldceil(x)) + + +######################################################################## +### reprlib.recursive_repr decorator from Py3.4 +######################################################################## + +from itertools import islice + +if PY26: + # itertools.count in Py 2.6 doesn't accept a step parameter + def count(start=0, step=1): + while True: + yield start + start += step +else: + from itertools import count + + +if PY3: + try: + from _thread import get_ident + except ImportError: + from _dummy_thread import get_ident +else: + try: + from thread import get_ident + except ImportError: + from dummy_thread import get_ident + + +def recursive_repr(fillvalue='...'): + 'Decorator to make a repr function return fillvalue for a recursive call' + + def decorating_function(user_function): + repr_running = set() + + def wrapper(self): + key = id(self), get_ident() + if key in repr_running: + return fillvalue + repr_running.add(key) + try: + result = user_function(self) + finally: + repr_running.discard(key) + return result + + # Can't use functools.wraps() here because of bootstrap issues + wrapper.__module__ = getattr(user_function, '__module__') + wrapper.__doc__ = getattr(user_function, '__doc__') + wrapper.__name__ = getattr(user_function, '__name__') + wrapper.__annotations__ = getattr(user_function, '__annotations__', {}) + return wrapper + + return decorating_function + + +# OrderedDict Shim from Raymond Hettinger, python core dev +# http://code.activestate.com/recipes/576693-ordered-dictionary-for-py24/ +# here to support version 2.6. + +################################################################################ +### OrderedDict +################################################################################ + +class _Link(object): + __slots__ = 'prev', 'next', 'key', '__weakref__' + +class OrderedDict(dict): + 'Dictionary that remembers insertion order' + # An inherited dict maps keys to values. + # The inherited dict provides __getitem__, __len__, __contains__, and get. + # The remaining methods are order-aware. + # Big-O running times for all methods are the same as regular dictionaries. + + # The internal self.__map dict maps keys to links in a doubly linked list. + # The circular doubly linked list starts and ends with a sentinel element. + # The sentinel element never gets deleted (this simplifies the algorithm). + # The sentinel is in self.__hardroot with a weakref proxy in self.__root. + # The prev links are weakref proxies (to prevent circular references). + # Individual links are kept alive by the hard reference in self.__map. + # Those hard references disappear when a key is deleted from an OrderedDict. + + def __init__(*args, **kwds): + '''Initialize an ordered dictionary. The signature is the same as + regular dictionaries, but keyword arguments are not recommended because + their insertion order is arbitrary. + + ''' + if not args: + raise TypeError("descriptor '__init__' of 'OrderedDict' object " + "needs an argument") + self = args[0] + args = args[1:] + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + try: + self.__root + except AttributeError: + self.__hardroot = _Link() + self.__root = root = _proxy(self.__hardroot) + root.prev = root.next = root + self.__map = {} + self.__update(*args, **kwds) + + def __setitem__(self, key, value, + dict_setitem=dict.__setitem__, proxy=_proxy, Link=_Link): + 'od.__setitem__(i, y) <==> od[i]=y' + # Setting a new item creates a new link at the end of the linked list, + # and the inherited dictionary is updated with the new key/value pair. + if key not in self: + self.__map[key] = link = Link() + root = self.__root + last = root.prev + link.prev, link.next, link.key = last, root, key + last.next = link + root.prev = proxy(link) + dict_setitem(self, key, value) + + def __delitem__(self, key, dict_delitem=dict.__delitem__): + 'od.__delitem__(y) <==> del od[y]' + # Deleting an existing item uses self.__map to find the link which gets + # removed by updating the links in the predecessor and successor nodes. + dict_delitem(self, key) + link = self.__map.pop(key) + link_prev = link.prev + link_next = link.next + link_prev.next = link_next + link_next.prev = link_prev + + def __iter__(self): + 'od.__iter__() <==> iter(od)' + # Traverse the linked list in order. + root = self.__root + curr = root.next + while curr is not root: + yield curr.key + curr = curr.next + + def __reversed__(self): + 'od.__reversed__() <==> reversed(od)' + # Traverse the linked list in reverse order. + root = self.__root + curr = root.prev + while curr is not root: + yield curr.key + curr = curr.prev + + def clear(self): + 'od.clear() -> None. Remove all items from od.' + root = self.__root + root.prev = root.next = root + self.__map.clear() + dict.clear(self) + + def popitem(self, last=True): + '''od.popitem() -> (k, v), return and remove a (key, value) pair. + Pairs are returned in LIFO order if last is true or FIFO order if false. + + ''' + if not self: + raise KeyError('dictionary is empty') + root = self.__root + if last: + link = root.prev + link_prev = link.prev + link_prev.next = root + root.prev = link_prev + else: + link = root.next + link_next = link.next + root.next = link_next + link_next.prev = root + key = link.key + del self.__map[key] + value = dict.pop(self, key) + return key, value + + def move_to_end(self, key, last=True): + '''Move an existing element to the end (or beginning if last==False). + + Raises KeyError if the element does not exist. + When last=True, acts like a fast version of self[key]=self.pop(key). + + ''' + link = self.__map[key] + link_prev = link.prev + link_next = link.next + link_prev.next = link_next + link_next.prev = link_prev + root = self.__root + if last: + last = root.prev + link.prev = last + link.next = root + last.next = root.prev = link + else: + first = root.next + link.prev = root + link.next = first + root.next = first.prev = link + + def __sizeof__(self): + sizeof = sys.getsizeof + n = len(self) + 1 # number of links including root + size = sizeof(self.__dict__) # instance dictionary + size += sizeof(self.__map) * 2 # internal dict and inherited dict + size += sizeof(self.__hardroot) * n # link objects + size += sizeof(self.__root) * n # proxy objects + return size + + update = __update = MutableMapping.update + keys = MutableMapping.keys + values = MutableMapping.values + items = MutableMapping.items + __ne__ = MutableMapping.__ne__ + + __marker = object() + + def pop(self, key, default=__marker): + '''od.pop(k[,d]) -> v, remove specified key and return the corresponding + value. If key is not found, d is returned if given, otherwise KeyError + is raised. + + ''' + if key in self: + result = self[key] + del self[key] + return result + if default is self.__marker: + raise KeyError(key) + return default + + def setdefault(self, key, default=None): + 'od.setdefault(k[,d]) -> od.get(k,d), also set od[k]=d if k not in od' + if key in self: + return self[key] + self[key] = default + return default + + @recursive_repr() + def __repr__(self): + 'od.__repr__() <==> repr(od)' + if not self: + return '%s()' % (self.__class__.__name__,) + return '%s(%r)' % (self.__class__.__name__, list(self.items())) + + def __reduce__(self): + 'Return state information for pickling' + inst_dict = vars(self).copy() + for k in vars(OrderedDict()): + inst_dict.pop(k, None) + return self.__class__, (), inst_dict or None, None, iter(self.items()) + + def copy(self): + 'od.copy() -> a shallow copy of od' + return self.__class__(self) + + @classmethod + def fromkeys(cls, iterable, value=None): + '''OD.fromkeys(S[, v]) -> New ordered dictionary with keys from S. + If not specified, the value defaults to None. + + ''' + self = cls() + for key in iterable: + self[key] = value + return self + + def __eq__(self, other): + '''od.__eq__(y) <==> od==y. Comparison to another OD is order-sensitive + while comparison to a regular mapping is order-insensitive. + + ''' + if isinstance(other, OrderedDict): + return dict.__eq__(self, other) and all(map(_eq, self, other)) + return dict.__eq__(self, other) + + +# {{{ http://code.activestate.com/recipes/576611/ (r11) + +try: + from operator import itemgetter + from heapq import nlargest +except ImportError: + pass + +######################################################################## +### Counter +######################################################################## + +def _count_elements(mapping, iterable): + 'Tally elements from the iterable.' + mapping_get = mapping.get + for elem in iterable: + mapping[elem] = mapping_get(elem, 0) + 1 + +class Counter(dict): + '''Dict subclass for counting hashable items. Sometimes called a bag + or multiset. Elements are stored as dictionary keys and their counts + are stored as dictionary values. + + >>> c = Counter('abcdeabcdabcaba') # count elements from a string + + >>> c.most_common(3) # three most common elements + [('a', 5), ('b', 4), ('c', 3)] + >>> sorted(c) # list all unique elements + ['a', 'b', 'c', 'd', 'e'] + >>> ''.join(sorted(c.elements())) # list elements with repetitions + 'aaaaabbbbcccdde' + >>> sum(c.values()) # total of all counts + 15 + + >>> c['a'] # count of letter 'a' + 5 + >>> for elem in 'shazam': # update counts from an iterable + ... c[elem] += 1 # by adding 1 to each element's count + >>> c['a'] # now there are seven 'a' + 7 + >>> del c['b'] # remove all 'b' + >>> c['b'] # now there are zero 'b' + 0 + + >>> d = Counter('simsalabim') # make another counter + >>> c.update(d) # add in the second counter + >>> c['a'] # now there are nine 'a' + 9 + + >>> c.clear() # empty the counter + >>> c + Counter() + + Note: If a count is set to zero or reduced to zero, it will remain + in the counter until the entry is deleted or the counter is cleared: + + >>> c = Counter('aaabbc') + >>> c['b'] -= 2 # reduce the count of 'b' by two + >>> c.most_common() # 'b' is still in, but its count is zero + [('a', 3), ('c', 1), ('b', 0)] + + ''' + # References: + # http://en.wikipedia.org/wiki/Multiset + # http://www.gnu.org/software/smalltalk/manual-base/html_node/Bag.html + # http://www.demo2s.com/Tutorial/Cpp/0380__set-multiset/Catalog0380__set-multiset.htm + # http://code.activestate.com/recipes/259174/ + # Knuth, TAOCP Vol. II section 4.6.3 + + def __init__(*args, **kwds): + '''Create a new, empty Counter object. And if given, count elements + from an input iterable. Or, initialize the count from another mapping + of elements to their counts. + + >>> c = Counter() # a new, empty counter + >>> c = Counter('gallahad') # a new counter from an iterable + >>> c = Counter({'a': 4, 'b': 2}) # a new counter from a mapping + >>> c = Counter(a=4, b=2) # a new counter from keyword args + + ''' + if not args: + raise TypeError("descriptor '__init__' of 'Counter' object " + "needs an argument") + self = args[0] + args = args[1:] + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + super(Counter, self).__init__() + self.update(*args, **kwds) + + def __missing__(self, key): + 'The count of elements not in the Counter is zero.' + # Needed so that self[missing_item] does not raise KeyError + return 0 + + def most_common(self, n=None): + '''List the n most common elements and their counts from the most + common to the least. If n is None, then list all element counts. + + >>> Counter('abcdeabcdabcaba').most_common(3) + [('a', 5), ('b', 4), ('c', 3)] + + ''' + # Emulate Bag.sortedByCount from Smalltalk + if n is None: + return sorted(self.items(), key=_itemgetter(1), reverse=True) + return _heapq.nlargest(n, self.items(), key=_itemgetter(1)) + + def elements(self): + '''Iterator over elements repeating each as many times as its count. + + >>> c = Counter('ABCABC') + >>> sorted(c.elements()) + ['A', 'A', 'B', 'B', 'C', 'C'] + + # Knuth's example for prime factors of 1836: 2**2 * 3**3 * 17**1 + >>> prime_factors = Counter({2: 2, 3: 3, 17: 1}) + >>> product = 1 + >>> for factor in prime_factors.elements(): # loop over factors + ... product *= factor # and multiply them + >>> product + 1836 + + Note, if an element's count has been set to zero or is a negative + number, elements() will ignore it. + + ''' + # Emulate Bag.do from Smalltalk and Multiset.begin from C++. + return _chain.from_iterable(_starmap(_repeat, self.items())) + + # Override dict methods where necessary + + @classmethod + def fromkeys(cls, iterable, v=None): + # There is no equivalent method for counters because setting v=1 + # means that no element can have a count greater than one. + raise NotImplementedError( + 'Counter.fromkeys() is undefined. Use Counter(iterable) instead.') + + def update(*args, **kwds): + '''Like dict.update() but add counts instead of replacing them. + + Source can be an iterable, a dictionary, or another Counter instance. + + >>> c = Counter('which') + >>> c.update('witch') # add elements from another iterable + >>> d = Counter('watch') + >>> c.update(d) # add elements from another counter + >>> c['h'] # four 'h' in which, witch, and watch + 4 + + ''' + # The regular dict.update() operation makes no sense here because the + # replace behavior results in the some of original untouched counts + # being mixed-in with all of the other counts for a mismash that + # doesn't have a straight-forward interpretation in most counting + # contexts. Instead, we implement straight-addition. Both the inputs + # and outputs are allowed to contain zero and negative counts. + + if not args: + raise TypeError("descriptor 'update' of 'Counter' object " + "needs an argument") + self = args[0] + args = args[1:] + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + iterable = args[0] if args else None + if iterable is not None: + if isinstance(iterable, Mapping): + if self: + self_get = self.get + for elem, count in iterable.items(): + self[elem] = count + self_get(elem, 0) + else: + super(Counter, self).update(iterable) # fast path when counter is empty + else: + _count_elements(self, iterable) + if kwds: + self.update(kwds) + + def subtract(*args, **kwds): + '''Like dict.update() but subtracts counts instead of replacing them. + Counts can be reduced below zero. Both the inputs and outputs are + allowed to contain zero and negative counts. + + Source can be an iterable, a dictionary, or another Counter instance. + + >>> c = Counter('which') + >>> c.subtract('witch') # subtract elements from another iterable + >>> c.subtract(Counter('watch')) # subtract elements from another counter + >>> c['h'] # 2 in which, minus 1 in witch, minus 1 in watch + 0 + >>> c['w'] # 1 in which, minus 1 in witch, minus 1 in watch + -1 + + ''' + if not args: + raise TypeError("descriptor 'subtract' of 'Counter' object " + "needs an argument") + self = args[0] + args = args[1:] + if len(args) > 1: + raise TypeError('expected at most 1 arguments, got %d' % len(args)) + iterable = args[0] if args else None + if iterable is not None: + self_get = self.get + if isinstance(iterable, Mapping): + for elem, count in iterable.items(): + self[elem] = self_get(elem, 0) - count + else: + for elem in iterable: + self[elem] = self_get(elem, 0) - 1 + if kwds: + self.subtract(kwds) + + def copy(self): + 'Return a shallow copy.' + return self.__class__(self) + + def __reduce__(self): + return self.__class__, (dict(self),) + + def __delitem__(self, elem): + 'Like dict.__delitem__() but does not raise KeyError for missing values.' + if elem in self: + super(Counter, self).__delitem__(elem) + + def __repr__(self): + if not self: + return '%s()' % self.__class__.__name__ + try: + items = ', '.join(map('%r: %r'.__mod__, self.most_common())) + return '%s({%s})' % (self.__class__.__name__, items) + except TypeError: + # handle case where values are not orderable + return '{0}({1!r})'.format(self.__class__.__name__, dict(self)) + + # Multiset-style mathematical operations discussed in: + # Knuth TAOCP Volume II section 4.6.3 exercise 19 + # and at http://en.wikipedia.org/wiki/Multiset + # + # Outputs guaranteed to only include positive counts. + # + # To strip negative and zero counts, add-in an empty counter: + # c += Counter() + + def __add__(self, other): + '''Add counts from two counters. + + >>> Counter('abbb') + Counter('bcc') + Counter({'b': 4, 'c': 2, 'a': 1}) + + ''' + if not isinstance(other, Counter): + return NotImplemented + result = Counter() + for elem, count in self.items(): + newcount = count + other[elem] + if newcount > 0: + result[elem] = newcount + for elem, count in other.items(): + if elem not in self and count > 0: + result[elem] = count + return result + + def __sub__(self, other): + ''' Subtract count, but keep only results with positive counts. + + >>> Counter('abbbc') - Counter('bccd') + Counter({'b': 2, 'a': 1}) + + ''' + if not isinstance(other, Counter): + return NotImplemented + result = Counter() + for elem, count in self.items(): + newcount = count - other[elem] + if newcount > 0: + result[elem] = newcount + for elem, count in other.items(): + if elem not in self and count < 0: + result[elem] = 0 - count + return result + + def __or__(self, other): + '''Union is the maximum of value in either of the input counters. + + >>> Counter('abbb') | Counter('bcc') + Counter({'b': 3, 'c': 2, 'a': 1}) + + ''' + if not isinstance(other, Counter): + return NotImplemented + result = Counter() + for elem, count in self.items(): + other_count = other[elem] + newcount = other_count if count < other_count else count + if newcount > 0: + result[elem] = newcount + for elem, count in other.items(): + if elem not in self and count > 0: + result[elem] = count + return result + + def __and__(self, other): + ''' Intersection is the minimum of corresponding counts. + + >>> Counter('abbb') & Counter('bcc') + Counter({'b': 1}) + + ''' + if not isinstance(other, Counter): + return NotImplemented + result = Counter() + for elem, count in self.items(): + other_count = other[elem] + newcount = count if count < other_count else other_count + if newcount > 0: + result[elem] = newcount + return result + + def __pos__(self): + 'Adds an empty counter, effectively stripping negative and zero counts' + return self + Counter() + + def __neg__(self): + '''Subtracts from an empty counter. Strips positive and zero counts, + and flips the sign on negative counts. + + ''' + return Counter() - self + + def _keep_positive(self): + '''Internal method to strip elements with a negative or zero count''' + nonpositive = [elem for elem, count in self.items() if not count > 0] + for elem in nonpositive: + del self[elem] + return self + + def __iadd__(self, other): + '''Inplace add from another counter, keeping only positive counts. + + >>> c = Counter('abbb') + >>> c += Counter('bcc') + >>> c + Counter({'b': 4, 'c': 2, 'a': 1}) + + ''' + for elem, count in other.items(): + self[elem] += count + return self._keep_positive() + + def __isub__(self, other): + '''Inplace subtract counter, but keep only results with positive counts. + + >>> c = Counter('abbbc') + >>> c -= Counter('bccd') + >>> c + Counter({'b': 2, 'a': 1}) + + ''' + for elem, count in other.items(): + self[elem] -= count + return self._keep_positive() + + def __ior__(self, other): + '''Inplace union is the maximum of value from either counter. + + >>> c = Counter('abbb') + >>> c |= Counter('bcc') + >>> c + Counter({'b': 3, 'c': 2, 'a': 1}) + + ''' + for elem, other_count in other.items(): + count = self[elem] + if other_count > count: + self[elem] = other_count + return self._keep_positive() + + def __iand__(self, other): + '''Inplace intersection is the minimum of corresponding counts. + + >>> c = Counter('abbb') + >>> c &= Counter('bcc') + >>> c + Counter({'b': 1}) + + ''' + for elem, count in self.items(): + other_count = other[elem] + if other_count < count: + self[elem] = other_count + return self._keep_positive() + + +def check_output(*popenargs, **kwargs): + """ + For Python 2.6 compatibility: see + http://stackoverflow.com/questions/4814970/ + """ + + if 'stdout' in kwargs: + raise ValueError('stdout argument not allowed, it will be overridden.') + process = subprocess.Popen(stdout=subprocess.PIPE, *popenargs, **kwargs) + output, unused_err = process.communicate() + retcode = process.poll() + if retcode: + cmd = kwargs.get("args") + if cmd is None: + cmd = popenargs[0] + raise subprocess.CalledProcessError(retcode, cmd) + return output + + +def count(start=0, step=1): + """ + ``itertools.count`` in Py 2.6 doesn't accept a step + parameter. This is an enhanced version of ``itertools.count`` + for Py2.6 equivalent to ``itertools.count`` in Python 2.7+. + """ + while True: + yield start + start += step + + +######################################################################## +### ChainMap (helper for configparser and string.Template) +### From the Py3.4 source code. See also: +### https://github.com/kkxue/Py2ChainMap/blob/master/py2chainmap.py +######################################################################## + +class ChainMap(MutableMapping): + ''' A ChainMap groups multiple dicts (or other mappings) together + to create a single, updateable view. + + The underlying mappings are stored in a list. That list is public and can + accessed or updated using the *maps* attribute. There is no other state. + + Lookups search the underlying mappings successively until a key is found. + In contrast, writes, updates, and deletions only operate on the first + mapping. + + ''' + + def __init__(self, *maps): + '''Initialize a ChainMap by setting *maps* to the given mappings. + If no mappings are provided, a single empty dictionary is used. + + ''' + self.maps = list(maps) or [{}] # always at least one map + + def __missing__(self, key): + raise KeyError(key) + + def __getitem__(self, key): + for mapping in self.maps: + try: + return mapping[key] # can't use 'key in mapping' with defaultdict + except KeyError: + pass + return self.__missing__(key) # support subclasses that define __missing__ + + def get(self, key, default=None): + return self[key] if key in self else default + + def __len__(self): + return len(set().union(*self.maps)) # reuses stored hash values if possible + + def __iter__(self): + return iter(set().union(*self.maps)) + + def __contains__(self, key): + return any(key in m for m in self.maps) + + def __bool__(self): + return any(self.maps) + + # Py2 compatibility: + __nonzero__ = __bool__ + + @recursive_repr() + def __repr__(self): + return '{0.__class__.__name__}({1})'.format( + self, ', '.join(map(repr, self.maps))) + + @classmethod + def fromkeys(cls, iterable, *args): + 'Create a ChainMap with a single dict created from the iterable.' + return cls(dict.fromkeys(iterable, *args)) + + def copy(self): + 'New ChainMap or subclass with a new copy of maps[0] and refs to maps[1:]' + return self.__class__(self.maps[0].copy(), *self.maps[1:]) + + __copy__ = copy + + def new_child(self, m=None): # like Django's Context.push() + ''' + New ChainMap with a new map followed by all previous maps. If no + map is provided, an empty dict is used. + ''' + if m is None: + m = {} + return self.__class__(m, *self.maps) + + @property + def parents(self): # like Django's Context.pop() + 'New ChainMap from maps[1:].' + return self.__class__(*self.maps[1:]) + + def __setitem__(self, key, value): + self.maps[0][key] = value + + def __delitem__(self, key): + try: + del self.maps[0][key] + except KeyError: + raise KeyError('Key not found in the first mapping: {0!r}'.format(key)) + + def popitem(self): + 'Remove and return an item pair from maps[0]. Raise KeyError is maps[0] is empty.' + try: + return self.maps[0].popitem() + except KeyError: + raise KeyError('No keys found in the first mapping.') + + def pop(self, key, *args): + 'Remove *key* from maps[0] and return its value. Raise KeyError if *key* not in maps[0].' + try: + return self.maps[0].pop(key, *args) + except KeyError: + raise KeyError('Key not found in the first mapping: {0!r}'.format(key)) + + def clear(self): + 'Clear maps[0], leaving maps[1:] intact.' + self.maps[0].clear() + + +# Re-use the same sentinel as in the Python stdlib socket module: +from socket import _GLOBAL_DEFAULT_TIMEOUT +# Was: _GLOBAL_DEFAULT_TIMEOUT = object() + + +def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, + source_address=None): + """Backport of 3-argument create_connection() for Py2.6. + + Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + err = None + for res in getaddrinfo(host, port, 0, SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket(af, socktype, proto) + if timeout is not _GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + return sock + + except error as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + raise err + else: + raise error("getaddrinfo returns an empty list") + +# Backport from Py2.7 for Py2.6: +def cmp_to_key(mycmp): + """Convert a cmp= function into a key= function""" + class K(object): + __slots__ = ['obj'] + def __init__(self, obj, *args): + self.obj = obj + def __lt__(self, other): + return mycmp(self.obj, other.obj) < 0 + def __gt__(self, other): + return mycmp(self.obj, other.obj) > 0 + def __eq__(self, other): + return mycmp(self.obj, other.obj) == 0 + def __le__(self, other): + return mycmp(self.obj, other.obj) <= 0 + def __ge__(self, other): + return mycmp(self.obj, other.obj) >= 0 + def __ne__(self, other): + return mycmp(self.obj, other.obj) != 0 + def __hash__(self): + raise TypeError('hash not implemented') + return K + +# Back up our definitions above in case they're useful +_OrderedDict = OrderedDict +_Counter = Counter +_check_output = check_output +_count = count +_ceil = ceil +__count_elements = _count_elements +_recursive_repr = recursive_repr +_ChainMap = ChainMap +_create_connection = create_connection +_cmp_to_key = cmp_to_key + +# Overwrite the definitions above with the usual ones +# from the standard library: +if sys.version_info >= (2, 7): + from collections import OrderedDict, Counter + from itertools import count + from functools import cmp_to_key + try: + from subprocess import check_output + except ImportError: + # Not available. This happens with Google App Engine: see issue #231 + pass + from socket import create_connection + +if sys.version_info >= (3, 0): + from math import ceil + from collections import _count_elements + +if sys.version_info >= (3, 3): + from reprlib import recursive_repr + from collections import ChainMap diff --git a/venv/lib/python3.10/site-packages/future/backports/socket.py b/venv/lib/python3.10/site-packages/future/backports/socket.py new file mode 100644 index 0000000..930e1da --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/socket.py @@ -0,0 +1,454 @@ +# Wrapper module for _socket, providing some additional facilities +# implemented in Python. + +"""\ +This module provides socket operations and some related functions. +On Unix, it supports IP (Internet Protocol) and Unix domain sockets. +On other systems, it only supports IP. Functions specific for a +socket are available as methods of the socket object. + +Functions: + +socket() -- create a new socket object +socketpair() -- create a pair of new socket objects [*] +fromfd() -- create a socket object from an open file descriptor [*] +fromshare() -- create a socket object from data received from socket.share() [*] +gethostname() -- return the current hostname +gethostbyname() -- map a hostname to its IP number +gethostbyaddr() -- map an IP number or hostname to DNS info +getservbyname() -- map a service name and a protocol name to a port number +getprotobyname() -- map a protocol name (e.g. 'tcp') to a number +ntohs(), ntohl() -- convert 16, 32 bit int from network to host byte order +htons(), htonl() -- convert 16, 32 bit int from host to network byte order +inet_aton() -- convert IP addr string (123.45.67.89) to 32-bit packed format +inet_ntoa() -- convert 32-bit packed format IP to string (123.45.67.89) +socket.getdefaulttimeout() -- get the default timeout value +socket.setdefaulttimeout() -- set the default timeout value +create_connection() -- connects to an address, with an optional timeout and + optional source address. + + [*] not available on all platforms! + +Special objects: + +SocketType -- type object for socket objects +error -- exception raised for I/O errors +has_ipv6 -- boolean value indicating if IPv6 is supported + +Integer constants: + +AF_INET, AF_UNIX -- socket domains (first argument to socket() call) +SOCK_STREAM, SOCK_DGRAM, SOCK_RAW -- socket types (second argument) + +Many other constants may be defined; these may be used in calls to +the setsockopt() and getsockopt() methods. +""" + +from __future__ import unicode_literals +from __future__ import print_function +from __future__ import division +from __future__ import absolute_import +from future.builtins import super + +import _socket +from _socket import * + +import os, sys, io + +try: + import errno +except ImportError: + errno = None +EBADF = getattr(errno, 'EBADF', 9) +EAGAIN = getattr(errno, 'EAGAIN', 11) +EWOULDBLOCK = getattr(errno, 'EWOULDBLOCK', 11) + +__all__ = ["getfqdn", "create_connection"] +__all__.extend(os._get_exports_list(_socket)) + + +_realsocket = socket + +# WSA error codes +if sys.platform.lower().startswith("win"): + errorTab = {} + errorTab[10004] = "The operation was interrupted." + errorTab[10009] = "A bad file handle was passed." + errorTab[10013] = "Permission denied." + errorTab[10014] = "A fault occurred on the network??" # WSAEFAULT + errorTab[10022] = "An invalid operation was attempted." + errorTab[10035] = "The socket operation would block" + errorTab[10036] = "A blocking operation is already in progress." + errorTab[10048] = "The network address is in use." + errorTab[10054] = "The connection has been reset." + errorTab[10058] = "The network has been shut down." + errorTab[10060] = "The operation timed out." + errorTab[10061] = "Connection refused." + errorTab[10063] = "The name is too long." + errorTab[10064] = "The host is down." + errorTab[10065] = "The host is unreachable." + __all__.append("errorTab") + + +class socket(_socket.socket): + + """A subclass of _socket.socket adding the makefile() method.""" + + __slots__ = ["__weakref__", "_io_refs", "_closed"] + + def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, fileno=None): + if fileno is None: + _socket.socket.__init__(self, family, type, proto) + else: + _socket.socket.__init__(self, family, type, proto, fileno) + self._io_refs = 0 + self._closed = False + + def __enter__(self): + return self + + def __exit__(self, *args): + if not self._closed: + self.close() + + def __repr__(self): + """Wrap __repr__() to reveal the real class name.""" + s = _socket.socket.__repr__(self) + if s.startswith(" socket object + + Return a new socket object connected to the same system resource. + """ + fd = dup(self.fileno()) + sock = self.__class__(self.family, self.type, self.proto, fileno=fd) + sock.settimeout(self.gettimeout()) + return sock + + def accept(self): + """accept() -> (socket object, address info) + + Wait for an incoming connection. Return a new socket + representing the connection, and the address of the client. + For IP sockets, the address info is a pair (hostaddr, port). + """ + fd, addr = self._accept() + sock = socket(self.family, self.type, self.proto, fileno=fd) + # Issue #7995: if no default timeout is set and the listening + # socket had a (non-zero) timeout, force the new socket in blocking + # mode to override platform-specific socket flags inheritance. + if getdefaulttimeout() is None and self.gettimeout(): + sock.setblocking(True) + return sock, addr + + def makefile(self, mode="r", buffering=None, **_3to2kwargs): + """makefile(...) -> an I/O stream connected to the socket + + The arguments are as for io.open() after the filename, + except the only mode characters supported are 'r', 'w' and 'b'. + The semantics are similar too. (XXX refactor to share code?) + """ + if 'newline' in _3to2kwargs: newline = _3to2kwargs['newline']; del _3to2kwargs['newline'] + else: newline = None + if 'errors' in _3to2kwargs: errors = _3to2kwargs['errors']; del _3to2kwargs['errors'] + else: errors = None + if 'encoding' in _3to2kwargs: encoding = _3to2kwargs['encoding']; del _3to2kwargs['encoding'] + else: encoding = None + for c in mode: + if c not in ("r", "w", "b"): + raise ValueError("invalid mode %r (only r, w, b allowed)") + writing = "w" in mode + reading = "r" in mode or not writing + assert reading or writing + binary = "b" in mode + rawmode = "" + if reading: + rawmode += "r" + if writing: + rawmode += "w" + raw = SocketIO(self, rawmode) + self._io_refs += 1 + if buffering is None: + buffering = -1 + if buffering < 0: + buffering = io.DEFAULT_BUFFER_SIZE + if buffering == 0: + if not binary: + raise ValueError("unbuffered streams must be binary") + return raw + if reading and writing: + buffer = io.BufferedRWPair(raw, raw, buffering) + elif reading: + buffer = io.BufferedReader(raw, buffering) + else: + assert writing + buffer = io.BufferedWriter(raw, buffering) + if binary: + return buffer + text = io.TextIOWrapper(buffer, encoding, errors, newline) + text.mode = mode + return text + + def _decref_socketios(self): + if self._io_refs > 0: + self._io_refs -= 1 + if self._closed: + self.close() + + def _real_close(self, _ss=_socket.socket): + # This function should not reference any globals. See issue #808164. + _ss.close(self) + + def close(self): + # This function should not reference any globals. See issue #808164. + self._closed = True + if self._io_refs <= 0: + self._real_close() + + def detach(self): + """detach() -> file descriptor + + Close the socket object without closing the underlying file descriptor. + The object cannot be used after this call, but the file descriptor + can be reused for other purposes. The file descriptor is returned. + """ + self._closed = True + return super().detach() + +def fromfd(fd, family, type, proto=0): + """ fromfd(fd, family, type[, proto]) -> socket object + + Create a socket object from a duplicate of the given file + descriptor. The remaining arguments are the same as for socket(). + """ + nfd = dup(fd) + return socket(family, type, proto, nfd) + +if hasattr(_socket.socket, "share"): + def fromshare(info): + """ fromshare(info) -> socket object + + Create a socket object from a the bytes object returned by + socket.share(pid). + """ + return socket(0, 0, 0, info) + +if hasattr(_socket, "socketpair"): + + def socketpair(family=None, type=SOCK_STREAM, proto=0): + """socketpair([family[, type[, proto]]]) -> (socket object, socket object) + + Create a pair of socket objects from the sockets returned by the platform + socketpair() function. + The arguments are the same as for socket() except the default family is + AF_UNIX if defined on the platform; otherwise, the default is AF_INET. + """ + if family is None: + try: + family = AF_UNIX + except NameError: + family = AF_INET + a, b = _socket.socketpair(family, type, proto) + a = socket(family, type, proto, a.detach()) + b = socket(family, type, proto, b.detach()) + return a, b + + +_blocking_errnos = set([EAGAIN, EWOULDBLOCK]) + +class SocketIO(io.RawIOBase): + + """Raw I/O implementation for stream sockets. + + This class supports the makefile() method on sockets. It provides + the raw I/O interface on top of a socket object. + """ + + # One might wonder why not let FileIO do the job instead. There are two + # main reasons why FileIO is not adapted: + # - it wouldn't work under Windows (where you can't used read() and + # write() on a socket handle) + # - it wouldn't work with socket timeouts (FileIO would ignore the + # timeout and consider the socket non-blocking) + + # XXX More docs + + def __init__(self, sock, mode): + if mode not in ("r", "w", "rw", "rb", "wb", "rwb"): + raise ValueError("invalid mode: %r" % mode) + io.RawIOBase.__init__(self) + self._sock = sock + if "b" not in mode: + mode += "b" + self._mode = mode + self._reading = "r" in mode + self._writing = "w" in mode + self._timeout_occurred = False + + def readinto(self, b): + """Read up to len(b) bytes into the writable buffer *b* and return + the number of bytes read. If the socket is non-blocking and no bytes + are available, None is returned. + + If *b* is non-empty, a 0 return value indicates that the connection + was shutdown at the other end. + """ + self._checkClosed() + self._checkReadable() + if self._timeout_occurred: + raise IOError("cannot read from timed out object") + while True: + try: + return self._sock.recv_into(b) + except timeout: + self._timeout_occurred = True + raise + # except InterruptedError: + # continue + except error as e: + if e.args[0] in _blocking_errnos: + return None + raise + + def write(self, b): + """Write the given bytes or bytearray object *b* to the socket + and return the number of bytes written. This can be less than + len(b) if not all data could be written. If the socket is + non-blocking and no bytes could be written None is returned. + """ + self._checkClosed() + self._checkWritable() + try: + return self._sock.send(b) + except error as e: + # XXX what about EINTR? + if e.args[0] in _blocking_errnos: + return None + raise + + def readable(self): + """True if the SocketIO is open for reading. + """ + if self.closed: + raise ValueError("I/O operation on closed socket.") + return self._reading + + def writable(self): + """True if the SocketIO is open for writing. + """ + if self.closed: + raise ValueError("I/O operation on closed socket.") + return self._writing + + def seekable(self): + """True if the SocketIO is open for seeking. + """ + if self.closed: + raise ValueError("I/O operation on closed socket.") + return super().seekable() + + def fileno(self): + """Return the file descriptor of the underlying socket. + """ + self._checkClosed() + return self._sock.fileno() + + @property + def name(self): + if not self.closed: + return self.fileno() + else: + return -1 + + @property + def mode(self): + return self._mode + + def close(self): + """Close the SocketIO object. This doesn't close the underlying + socket, except if all references to it have disappeared. + """ + if self.closed: + return + io.RawIOBase.close(self) + self._sock._decref_socketios() + self._sock = None + + +def getfqdn(name=''): + """Get fully qualified domain name from name. + + An empty argument is interpreted as meaning the local host. + + First the hostname returned by gethostbyaddr() is checked, then + possibly existing aliases. In case no FQDN is available, hostname + from gethostname() is returned. + """ + name = name.strip() + if not name or name == '0.0.0.0': + name = gethostname() + try: + hostname, aliases, ipaddrs = gethostbyaddr(name) + except error: + pass + else: + aliases.insert(0, hostname) + for name in aliases: + if '.' in name: + break + else: + name = hostname + return name + + +# Re-use the same sentinel as in the Python stdlib socket module: +from socket import _GLOBAL_DEFAULT_TIMEOUT +# Was: _GLOBAL_DEFAULT_TIMEOUT = object() + + +def create_connection(address, timeout=_GLOBAL_DEFAULT_TIMEOUT, + source_address=None): + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`getdefaulttimeout` + is used. If *source_address* is set it must be a tuple of (host, port) + for the socket to bind as a source address before making the connection. + An host of '' or port 0 tells the OS to use the default. + """ + + host, port = address + err = None + for res in getaddrinfo(host, port, 0, SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket(af, socktype, proto) + if timeout is not _GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + return sock + + except error as _: + err = _ + if sock is not None: + sock.close() + + if err is not None: + raise err + else: + raise error("getaddrinfo returns an empty list") diff --git a/venv/lib/python3.10/site-packages/future/backports/socketserver.py b/venv/lib/python3.10/site-packages/future/backports/socketserver.py new file mode 100644 index 0000000..d1e24a6 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/socketserver.py @@ -0,0 +1,747 @@ +"""Generic socket server classes. + +This module tries to capture the various aspects of defining a server: + +For socket-based servers: + +- address family: + - AF_INET{,6}: IP (Internet Protocol) sockets (default) + - AF_UNIX: Unix domain sockets + - others, e.g. AF_DECNET are conceivable (see +- socket type: + - SOCK_STREAM (reliable stream, e.g. TCP) + - SOCK_DGRAM (datagrams, e.g. UDP) + +For request-based servers (including socket-based): + +- client address verification before further looking at the request + (This is actually a hook for any processing that needs to look + at the request before anything else, e.g. logging) +- how to handle multiple requests: + - synchronous (one request is handled at a time) + - forking (each request is handled by a new process) + - threading (each request is handled by a new thread) + +The classes in this module favor the server type that is simplest to +write: a synchronous TCP/IP server. This is bad class design, but +save some typing. (There's also the issue that a deep class hierarchy +slows down method lookups.) + +There are five classes in an inheritance diagram, four of which represent +synchronous servers of four types: + + +------------+ + | BaseServer | + +------------+ + | + v + +-----------+ +------------------+ + | TCPServer |------->| UnixStreamServer | + +-----------+ +------------------+ + | + v + +-----------+ +--------------------+ + | UDPServer |------->| UnixDatagramServer | + +-----------+ +--------------------+ + +Note that UnixDatagramServer derives from UDPServer, not from +UnixStreamServer -- the only difference between an IP and a Unix +stream server is the address family, which is simply repeated in both +unix server classes. + +Forking and threading versions of each type of server can be created +using the ForkingMixIn and ThreadingMixIn mix-in classes. For +instance, a threading UDP server class is created as follows: + + class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass + +The Mix-in class must come first, since it overrides a method defined +in UDPServer! Setting the various member variables also changes +the behavior of the underlying server mechanism. + +To implement a service, you must derive a class from +BaseRequestHandler and redefine its handle() method. You can then run +various versions of the service by combining one of the server classes +with your request handler class. + +The request handler class must be different for datagram or stream +services. This can be hidden by using the request handler +subclasses StreamRequestHandler or DatagramRequestHandler. + +Of course, you still have to use your head! + +For instance, it makes no sense to use a forking server if the service +contains state in memory that can be modified by requests (since the +modifications in the child process would never reach the initial state +kept in the parent process and passed to each child). In this case, +you can use a threading server, but you will probably have to use +locks to avoid two requests that come in nearly simultaneous to apply +conflicting changes to the server state. + +On the other hand, if you are building e.g. an HTTP server, where all +data is stored externally (e.g. in the file system), a synchronous +class will essentially render the service "deaf" while one request is +being handled -- which may be for a very long time if a client is slow +to read all the data it has requested. Here a threading or forking +server is appropriate. + +In some cases, it may be appropriate to process part of a request +synchronously, but to finish processing in a forked child depending on +the request data. This can be implemented by using a synchronous +server and doing an explicit fork in the request handler class +handle() method. + +Another approach to handling multiple simultaneous requests in an +environment that supports neither threads nor fork (or where these are +too expensive or inappropriate for the service) is to maintain an +explicit table of partially finished requests and to use select() to +decide which request to work on next (or whether to handle a new +incoming request). This is particularly important for stream services +where each client can potentially be connected for a long time (if +threads or subprocesses cannot be used). + +Future work: +- Standard classes for Sun RPC (which uses either UDP or TCP) +- Standard mix-in classes to implement various authentication + and encryption schemes +- Standard framework for select-based multiplexing + +XXX Open problems: +- What to do with out-of-band data? + +BaseServer: +- split generic "request" functionality out into BaseServer class. + Copyright (C) 2000 Luke Kenneth Casson Leighton + + example: read entries from a SQL database (requires overriding + get_request() to return a table entry from the database). + entry is processed by a RequestHandlerClass. + +""" + +# Author of the BaseServer patch: Luke Kenneth Casson Leighton + +# XXX Warning! +# There is a test suite for this module, but it cannot be run by the +# standard regression test. +# To run it manually, run Lib/test/test_socketserver.py. + +from __future__ import (absolute_import, print_function) + +__version__ = "0.4" + + +import socket +import select +import sys +import os +import errno +try: + import threading +except ImportError: + import dummy_threading as threading + +__all__ = ["TCPServer","UDPServer","ForkingUDPServer","ForkingTCPServer", + "ThreadingUDPServer","ThreadingTCPServer","BaseRequestHandler", + "StreamRequestHandler","DatagramRequestHandler", + "ThreadingMixIn", "ForkingMixIn"] +if hasattr(socket, "AF_UNIX"): + __all__.extend(["UnixStreamServer","UnixDatagramServer", + "ThreadingUnixStreamServer", + "ThreadingUnixDatagramServer"]) + +def _eintr_retry(func, *args): + """restart a system call interrupted by EINTR""" + while True: + try: + return func(*args) + except OSError as e: + if e.errno != errno.EINTR: + raise + +class BaseServer(object): + + """Base class for server classes. + + Methods for the caller: + + - __init__(server_address, RequestHandlerClass) + - serve_forever(poll_interval=0.5) + - shutdown() + - handle_request() # if you do not use serve_forever() + - fileno() -> int # for select() + + Methods that may be overridden: + + - server_bind() + - server_activate() + - get_request() -> request, client_address + - handle_timeout() + - verify_request(request, client_address) + - server_close() + - process_request(request, client_address) + - shutdown_request(request) + - close_request(request) + - service_actions() + - handle_error() + + Methods for derived classes: + + - finish_request(request, client_address) + + Class variables that may be overridden by derived classes or + instances: + + - timeout + - address_family + - socket_type + - allow_reuse_address + + Instance variables: + + - RequestHandlerClass + - socket + + """ + + timeout = None + + def __init__(self, server_address, RequestHandlerClass): + """Constructor. May be extended, do not override.""" + self.server_address = server_address + self.RequestHandlerClass = RequestHandlerClass + self.__is_shut_down = threading.Event() + self.__shutdown_request = False + + def server_activate(self): + """Called by constructor to activate the server. + + May be overridden. + + """ + pass + + def serve_forever(self, poll_interval=0.5): + """Handle one request at a time until shutdown. + + Polls for shutdown every poll_interval seconds. Ignores + self.timeout. If you need to do periodic tasks, do them in + another thread. + """ + self.__is_shut_down.clear() + try: + while not self.__shutdown_request: + # XXX: Consider using another file descriptor or + # connecting to the socket to wake this up instead of + # polling. Polling reduces our responsiveness to a + # shutdown request and wastes cpu at all other times. + r, w, e = _eintr_retry(select.select, [self], [], [], + poll_interval) + if self in r: + self._handle_request_noblock() + + self.service_actions() + finally: + self.__shutdown_request = False + self.__is_shut_down.set() + + def shutdown(self): + """Stops the serve_forever loop. + + Blocks until the loop has finished. This must be called while + serve_forever() is running in another thread, or it will + deadlock. + """ + self.__shutdown_request = True + self.__is_shut_down.wait() + + def service_actions(self): + """Called by the serve_forever() loop. + + May be overridden by a subclass / Mixin to implement any code that + needs to be run during the loop. + """ + pass + + # The distinction between handling, getting, processing and + # finishing a request is fairly arbitrary. Remember: + # + # - handle_request() is the top-level call. It calls + # select, get_request(), verify_request() and process_request() + # - get_request() is different for stream or datagram sockets + # - process_request() is the place that may fork a new process + # or create a new thread to finish the request + # - finish_request() instantiates the request handler class; + # this constructor will handle the request all by itself + + def handle_request(self): + """Handle one request, possibly blocking. + + Respects self.timeout. + """ + # Support people who used socket.settimeout() to escape + # handle_request before self.timeout was available. + timeout = self.socket.gettimeout() + if timeout is None: + timeout = self.timeout + elif self.timeout is not None: + timeout = min(timeout, self.timeout) + fd_sets = _eintr_retry(select.select, [self], [], [], timeout) + if not fd_sets[0]: + self.handle_timeout() + return + self._handle_request_noblock() + + def _handle_request_noblock(self): + """Handle one request, without blocking. + + I assume that select.select has returned that the socket is + readable before this function was called, so there should be + no risk of blocking in get_request(). + """ + try: + request, client_address = self.get_request() + except socket.error: + return + if self.verify_request(request, client_address): + try: + self.process_request(request, client_address) + except: + self.handle_error(request, client_address) + self.shutdown_request(request) + + def handle_timeout(self): + """Called if no new request arrives within self.timeout. + + Overridden by ForkingMixIn. + """ + pass + + def verify_request(self, request, client_address): + """Verify the request. May be overridden. + + Return True if we should proceed with this request. + + """ + return True + + def process_request(self, request, client_address): + """Call finish_request. + + Overridden by ForkingMixIn and ThreadingMixIn. + + """ + self.finish_request(request, client_address) + self.shutdown_request(request) + + def server_close(self): + """Called to clean-up the server. + + May be overridden. + + """ + pass + + def finish_request(self, request, client_address): + """Finish one request by instantiating RequestHandlerClass.""" + self.RequestHandlerClass(request, client_address, self) + + def shutdown_request(self, request): + """Called to shutdown and close an individual request.""" + self.close_request(request) + + def close_request(self, request): + """Called to clean up an individual request.""" + pass + + def handle_error(self, request, client_address): + """Handle an error gracefully. May be overridden. + + The default is to print a traceback and continue. + + """ + print('-'*40) + print('Exception happened during processing of request from', end=' ') + print(client_address) + import traceback + traceback.print_exc() # XXX But this goes to stderr! + print('-'*40) + + +class TCPServer(BaseServer): + + """Base class for various socket-based server classes. + + Defaults to synchronous IP stream (i.e., TCP). + + Methods for the caller: + + - __init__(server_address, RequestHandlerClass, bind_and_activate=True) + - serve_forever(poll_interval=0.5) + - shutdown() + - handle_request() # if you don't use serve_forever() + - fileno() -> int # for select() + + Methods that may be overridden: + + - server_bind() + - server_activate() + - get_request() -> request, client_address + - handle_timeout() + - verify_request(request, client_address) + - process_request(request, client_address) + - shutdown_request(request) + - close_request(request) + - handle_error() + + Methods for derived classes: + + - finish_request(request, client_address) + + Class variables that may be overridden by derived classes or + instances: + + - timeout + - address_family + - socket_type + - request_queue_size (only for stream sockets) + - allow_reuse_address + + Instance variables: + + - server_address + - RequestHandlerClass + - socket + + """ + + address_family = socket.AF_INET + + socket_type = socket.SOCK_STREAM + + request_queue_size = 5 + + allow_reuse_address = False + + def __init__(self, server_address, RequestHandlerClass, bind_and_activate=True): + """Constructor. May be extended, do not override.""" + BaseServer.__init__(self, server_address, RequestHandlerClass) + self.socket = socket.socket(self.address_family, + self.socket_type) + if bind_and_activate: + self.server_bind() + self.server_activate() + + def server_bind(self): + """Called by constructor to bind the socket. + + May be overridden. + + """ + if self.allow_reuse_address: + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.socket.bind(self.server_address) + self.server_address = self.socket.getsockname() + + def server_activate(self): + """Called by constructor to activate the server. + + May be overridden. + + """ + self.socket.listen(self.request_queue_size) + + def server_close(self): + """Called to clean-up the server. + + May be overridden. + + """ + self.socket.close() + + def fileno(self): + """Return socket file number. + + Interface required by select(). + + """ + return self.socket.fileno() + + def get_request(self): + """Get the request and client address from the socket. + + May be overridden. + + """ + return self.socket.accept() + + def shutdown_request(self, request): + """Called to shutdown and close an individual request.""" + try: + #explicitly shutdown. socket.close() merely releases + #the socket and waits for GC to perform the actual close. + request.shutdown(socket.SHUT_WR) + except socket.error: + pass #some platforms may raise ENOTCONN here + self.close_request(request) + + def close_request(self, request): + """Called to clean up an individual request.""" + request.close() + + +class UDPServer(TCPServer): + + """UDP server class.""" + + allow_reuse_address = False + + socket_type = socket.SOCK_DGRAM + + max_packet_size = 8192 + + def get_request(self): + data, client_addr = self.socket.recvfrom(self.max_packet_size) + return (data, self.socket), client_addr + + def server_activate(self): + # No need to call listen() for UDP. + pass + + def shutdown_request(self, request): + # No need to shutdown anything. + self.close_request(request) + + def close_request(self, request): + # No need to close anything. + pass + +class ForkingMixIn(object): + + """Mix-in class to handle each request in a new process.""" + + timeout = 300 + active_children = None + max_children = 40 + + def collect_children(self): + """Internal routine to wait for children that have exited.""" + if self.active_children is None: return + while len(self.active_children) >= self.max_children: + # XXX: This will wait for any child process, not just ones + # spawned by this library. This could confuse other + # libraries that expect to be able to wait for their own + # children. + try: + pid, status = os.waitpid(0, 0) + except os.error: + pid = None + if pid not in self.active_children: continue + self.active_children.remove(pid) + + # XXX: This loop runs more system calls than it ought + # to. There should be a way to put the active_children into a + # process group and then use os.waitpid(-pgid) to wait for any + # of that set, but I couldn't find a way to allocate pgids + # that couldn't collide. + for child in self.active_children: + try: + pid, status = os.waitpid(child, os.WNOHANG) + except os.error: + pid = None + if not pid: continue + try: + self.active_children.remove(pid) + except ValueError as e: + raise ValueError('%s. x=%d and list=%r' % (e.message, pid, + self.active_children)) + + def handle_timeout(self): + """Wait for zombies after self.timeout seconds of inactivity. + + May be extended, do not override. + """ + self.collect_children() + + def service_actions(self): + """Collect the zombie child processes regularly in the ForkingMixIn. + + service_actions is called in the BaseServer's serve_forver loop. + """ + self.collect_children() + + def process_request(self, request, client_address): + """Fork a new subprocess to process the request.""" + pid = os.fork() + if pid: + # Parent process + if self.active_children is None: + self.active_children = [] + self.active_children.append(pid) + self.close_request(request) + return + else: + # Child process. + # This must never return, hence os._exit()! + try: + self.finish_request(request, client_address) + self.shutdown_request(request) + os._exit(0) + except: + try: + self.handle_error(request, client_address) + self.shutdown_request(request) + finally: + os._exit(1) + + +class ThreadingMixIn(object): + """Mix-in class to handle each request in a new thread.""" + + # Decides how threads will act upon termination of the + # main process + daemon_threads = False + + def process_request_thread(self, request, client_address): + """Same as in BaseServer but as a thread. + + In addition, exception handling is done here. + + """ + try: + self.finish_request(request, client_address) + self.shutdown_request(request) + except: + self.handle_error(request, client_address) + self.shutdown_request(request) + + def process_request(self, request, client_address): + """Start a new thread to process the request.""" + t = threading.Thread(target = self.process_request_thread, + args = (request, client_address)) + t.daemon = self.daemon_threads + t.start() + + +class ForkingUDPServer(ForkingMixIn, UDPServer): pass +class ForkingTCPServer(ForkingMixIn, TCPServer): pass + +class ThreadingUDPServer(ThreadingMixIn, UDPServer): pass +class ThreadingTCPServer(ThreadingMixIn, TCPServer): pass + +if hasattr(socket, 'AF_UNIX'): + + class UnixStreamServer(TCPServer): + address_family = socket.AF_UNIX + + class UnixDatagramServer(UDPServer): + address_family = socket.AF_UNIX + + class ThreadingUnixStreamServer(ThreadingMixIn, UnixStreamServer): pass + + class ThreadingUnixDatagramServer(ThreadingMixIn, UnixDatagramServer): pass + +class BaseRequestHandler(object): + + """Base class for request handler classes. + + This class is instantiated for each request to be handled. The + constructor sets the instance variables request, client_address + and server, and then calls the handle() method. To implement a + specific service, all you need to do is to derive a class which + defines a handle() method. + + The handle() method can find the request as self.request, the + client address as self.client_address, and the server (in case it + needs access to per-server information) as self.server. Since a + separate instance is created for each request, the handle() method + can define arbitrary other instance variariables. + + """ + + def __init__(self, request, client_address, server): + self.request = request + self.client_address = client_address + self.server = server + self.setup() + try: + self.handle() + finally: + self.finish() + + def setup(self): + pass + + def handle(self): + pass + + def finish(self): + pass + + +# The following two classes make it possible to use the same service +# class for stream or datagram servers. +# Each class sets up these instance variables: +# - rfile: a file object from which receives the request is read +# - wfile: a file object to which the reply is written +# When the handle() method returns, wfile is flushed properly + + +class StreamRequestHandler(BaseRequestHandler): + + """Define self.rfile and self.wfile for stream sockets.""" + + # Default buffer sizes for rfile, wfile. + # We default rfile to buffered because otherwise it could be + # really slow for large data (a getc() call per byte); we make + # wfile unbuffered because (a) often after a write() we want to + # read and we need to flush the line; (b) big writes to unbuffered + # files are typically optimized by stdio even when big reads + # aren't. + rbufsize = -1 + wbufsize = 0 + + # A timeout to apply to the request socket, if not None. + timeout = None + + # Disable nagle algorithm for this socket, if True. + # Use only when wbufsize != 0, to avoid small packets. + disable_nagle_algorithm = False + + def setup(self): + self.connection = self.request + if self.timeout is not None: + self.connection.settimeout(self.timeout) + if self.disable_nagle_algorithm: + self.connection.setsockopt(socket.IPPROTO_TCP, + socket.TCP_NODELAY, True) + self.rfile = self.connection.makefile('rb', self.rbufsize) + self.wfile = self.connection.makefile('wb', self.wbufsize) + + def finish(self): + if not self.wfile.closed: + try: + self.wfile.flush() + except socket.error: + # An final socket error may have occurred here, such as + # the local error ECONNABORTED. + pass + self.wfile.close() + self.rfile.close() + + +class DatagramRequestHandler(BaseRequestHandler): + + # XXX Regrettably, I cannot get this working on Linux; + # s.recvfrom() doesn't return a meaningful client address. + + """Define self.rfile and self.wfile for datagram sockets.""" + + def setup(self): + from io import BytesIO + self.packet, self.socket = self.request + self.rfile = BytesIO(self.packet) + self.wfile = BytesIO() + + def finish(self): + self.socket.sendto(self.wfile.getvalue(), self.client_address) diff --git a/venv/lib/python3.10/site-packages/future/backports/test/__init__.py b/venv/lib/python3.10/site-packages/future/backports/test/__init__.py new file mode 100644 index 0000000..0bba5e6 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/__init__.py @@ -0,0 +1,9 @@ +""" +test package backported for python-future. + +Its primary purpose is to allow use of "import test.support" for running +the Python standard library unit tests using the new Python 3 stdlib +import location. + +Python 3 renamed test.test_support to test.support. +""" diff --git a/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..920375c Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/pystone.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/pystone.cpython-310.pyc new file mode 100644 index 0000000..7f39593 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/pystone.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/ssl_servers.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/ssl_servers.cpython-310.pyc new file mode 100644 index 0000000..34f5410 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/ssl_servers.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/support.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/support.cpython-310.pyc new file mode 100644 index 0000000..81c72f3 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/test/__pycache__/support.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/test/badcert.pem b/venv/lib/python3.10/site-packages/future/backports/test/badcert.pem new file mode 100644 index 0000000..c419146 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/badcert.pem @@ -0,0 +1,36 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXwIBAAKBgQC8ddrhm+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9L +opdJhTvbGfEj0DQs1IE8M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVH +fhi/VwovESJlaBOp+WMnfhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQAB +AoGBAK0FZpaKj6WnJZN0RqhhK+ggtBWwBnc0U/ozgKz2j1s3fsShYeiGtW6CK5nU +D1dZ5wzhbGThI7LiOXDvRucc9n7vUgi0alqPQ/PFodPxAN/eEYkmXQ7W2k7zwsDA +IUK0KUhktQbLu8qF/m8qM86ba9y9/9YkXuQbZ3COl5ahTZrhAkEA301P08RKv3KM +oXnGU2UHTuJ1MAD2hOrPxjD4/wxA/39EWG9bZczbJyggB4RHu0I3NOSFjAm3HQm0 +ANOu5QK9owJBANgOeLfNNcF4pp+UikRFqxk5hULqRAWzVxVrWe85FlPm0VVmHbb/ +loif7mqjU8o1jTd/LM7RD9f2usZyE2psaw8CQQCNLhkpX3KO5kKJmS9N7JMZSc4j +oog58yeYO8BBqKKzpug0LXuQultYv2K4veaIO04iL9VLe5z9S/Q1jaCHBBuXAkEA +z8gjGoi1AOp6PBBLZNsncCvcV/0aC+1se4HxTNo2+duKSDnbq+ljqOM+E7odU+Nq +ewvIWOG//e8fssd0mq3HywJBAJ8l/c8GVmrpFTx8r/nZ2Pyyjt3dH1widooDXYSV +q6Gbf41Llo5sYAtmxdndTLASuHKecacTgZVhy0FryZpLKrU= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +Just bad cert data +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +MIICXwIBAAKBgQC8ddrhm+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9L +opdJhTvbGfEj0DQs1IE8M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVH +fhi/VwovESJlaBOp+WMnfhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQAB +AoGBAK0FZpaKj6WnJZN0RqhhK+ggtBWwBnc0U/ozgKz2j1s3fsShYeiGtW6CK5nU +D1dZ5wzhbGThI7LiOXDvRucc9n7vUgi0alqPQ/PFodPxAN/eEYkmXQ7W2k7zwsDA +IUK0KUhktQbLu8qF/m8qM86ba9y9/9YkXuQbZ3COl5ahTZrhAkEA301P08RKv3KM +oXnGU2UHTuJ1MAD2hOrPxjD4/wxA/39EWG9bZczbJyggB4RHu0I3NOSFjAm3HQm0 +ANOu5QK9owJBANgOeLfNNcF4pp+UikRFqxk5hULqRAWzVxVrWe85FlPm0VVmHbb/ +loif7mqjU8o1jTd/LM7RD9f2usZyE2psaw8CQQCNLhkpX3KO5kKJmS9N7JMZSc4j +oog58yeYO8BBqKKzpug0LXuQultYv2K4veaIO04iL9VLe5z9S/Q1jaCHBBuXAkEA +z8gjGoi1AOp6PBBLZNsncCvcV/0aC+1se4HxTNo2+duKSDnbq+ljqOM+E7odU+Nq +ewvIWOG//e8fssd0mq3HywJBAJ8l/c8GVmrpFTx8r/nZ2Pyyjt3dH1widooDXYSV +q6Gbf41Llo5sYAtmxdndTLASuHKecacTgZVhy0FryZpLKrU= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +Just bad cert data +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/badkey.pem b/venv/lib/python3.10/site-packages/future/backports/test/badkey.pem new file mode 100644 index 0000000..1c8a955 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/badkey.pem @@ -0,0 +1,40 @@ +-----BEGIN RSA PRIVATE KEY----- +Bad Key, though the cert should be OK +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIICpzCCAhCgAwIBAgIJAP+qStv1cIGNMA0GCSqGSIb3DQEBBQUAMIGJMQswCQYD +VQQGEwJVUzERMA8GA1UECBMIRGVsYXdhcmUxEzARBgNVBAcTCldpbG1pbmd0b24x +IzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMQwwCgYDVQQLEwNT +U0wxHzAdBgNVBAMTFnNvbWVtYWNoaW5lLnB5dGhvbi5vcmcwHhcNMDcwODI3MTY1 +NDUwWhcNMTMwMjE2MTY1NDUwWjCBiTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCERl +bGF3YXJlMRMwEQYDVQQHEwpXaWxtaW5ndG9uMSMwIQYDVQQKExpQeXRob24gU29m +dHdhcmUgRm91bmRhdGlvbjEMMAoGA1UECxMDU1NMMR8wHQYDVQQDExZzb21lbWFj +aGluZS5weXRob24ub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8ddrh +m+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9LopdJhTvbGfEj0DQs1IE8 +M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVHfhi/VwovESJlaBOp+WMn +fhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQABoxUwEzARBglghkgBhvhC +AQEEBAMCBkAwDQYJKoZIhvcNAQEFBQADgYEAF4Q5BVqmCOLv1n8je/Jw9K669VXb +08hyGzQhkemEBYQd6fzQ9A/1ZzHkJKb1P6yreOLSEh4KcxYPyrLRC1ll8nr5OlCx +CMhKkTnR6qBsdNV0XtdU2+N25hqW+Ma4ZeqsN/iiJVCGNOZGnvQuvCAGWF8+J/f/ +iHkC6gGdBJhogs4= +-----END CERTIFICATE----- +-----BEGIN RSA PRIVATE KEY----- +Bad Key, though the cert should be OK +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIICpzCCAhCgAwIBAgIJAP+qStv1cIGNMA0GCSqGSIb3DQEBBQUAMIGJMQswCQYD +VQQGEwJVUzERMA8GA1UECBMIRGVsYXdhcmUxEzARBgNVBAcTCldpbG1pbmd0b24x +IzAhBgNVBAoTGlB5dGhvbiBTb2Z0d2FyZSBGb3VuZGF0aW9uMQwwCgYDVQQLEwNT +U0wxHzAdBgNVBAMTFnNvbWVtYWNoaW5lLnB5dGhvbi5vcmcwHhcNMDcwODI3MTY1 +NDUwWhcNMTMwMjE2MTY1NDUwWjCBiTELMAkGA1UEBhMCVVMxETAPBgNVBAgTCERl +bGF3YXJlMRMwEQYDVQQHEwpXaWxtaW5ndG9uMSMwIQYDVQQKExpQeXRob24gU29m +dHdhcmUgRm91bmRhdGlvbjEMMAoGA1UECxMDU1NMMR8wHQYDVQQDExZzb21lbWFj +aGluZS5weXRob24ub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC8ddrh +m+LutBvjYcQlnH21PPIseJ1JVG2HMmN2CmZk2YukO+9LopdJhTvbGfEj0DQs1IE8 +M+kTUyOmuKfVrFMKwtVeCJphrAnhoz7TYOuLBSqt7lVHfhi/VwovESJlaBOp+WMn +fhcduPEYHYx/6cnVapIkZnLt30zu2um+DzA9jQIDAQABoxUwEzARBglghkgBhvhC +AQEEBAMCBkAwDQYJKoZIhvcNAQEFBQADgYEAF4Q5BVqmCOLv1n8je/Jw9K669VXb +08hyGzQhkemEBYQd6fzQ9A/1ZzHkJKb1P6yreOLSEh4KcxYPyrLRC1ll8nr5OlCx +CMhKkTnR6qBsdNV0XtdU2+N25hqW+Ma4ZeqsN/iiJVCGNOZGnvQuvCAGWF8+J/f/ +iHkC6gGdBJhogs4= +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/dh512.pem b/venv/lib/python3.10/site-packages/future/backports/test/dh512.pem new file mode 100644 index 0000000..200d16c --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/dh512.pem @@ -0,0 +1,9 @@ +-----BEGIN DH PARAMETERS----- +MEYCQQD1Kv884bEpQBgRjXyEpwpy1obEAxnIByl6ypUM2Zafq9AKUJsCRtMIPWak +XUGfnHy9iUsiGSa6q6Jew1XpKgVfAgEC +-----END DH PARAMETERS----- + +These are the 512 bit DH parameters from "Assigned Number for SKIP Protocols" +(http://www.skip-vpn.org/spec/numbers.html). +See there for how they were generated. +Note that g is not a generator, but this is not a problem since p is a safe prime. diff --git a/venv/lib/python3.10/site-packages/future/backports/test/https_svn_python_org_root.pem b/venv/lib/python3.10/site-packages/future/backports/test/https_svn_python_org_root.pem new file mode 100644 index 0000000..e7dfc82 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/https_svn_python_org_root.pem @@ -0,0 +1,41 @@ +-----BEGIN CERTIFICATE----- +MIIHPTCCBSWgAwIBAgIBADANBgkqhkiG9w0BAQQFADB5MRAwDgYDVQQKEwdSb290 +IENBMR4wHAYDVQQLExVodHRwOi8vd3d3LmNhY2VydC5vcmcxIjAgBgNVBAMTGUNB +IENlcnQgU2lnbmluZyBBdXRob3JpdHkxITAfBgkqhkiG9w0BCQEWEnN1cHBvcnRA +Y2FjZXJ0Lm9yZzAeFw0wMzAzMzAxMjI5NDlaFw0zMzAzMjkxMjI5NDlaMHkxEDAO +BgNVBAoTB1Jvb3QgQ0ExHjAcBgNVBAsTFWh0dHA6Ly93d3cuY2FjZXJ0Lm9yZzEi +MCAGA1UEAxMZQ0EgQ2VydCBTaWduaW5nIEF1dGhvcml0eTEhMB8GCSqGSIb3DQEJ +ARYSc3VwcG9ydEBjYWNlcnQub3JnMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC +CgKCAgEAziLA4kZ97DYoB1CW8qAzQIxL8TtmPzHlawI229Z89vGIj053NgVBlfkJ +8BLPRoZzYLdufujAWGSuzbCtRRcMY/pnCujW0r8+55jE8Ez64AO7NV1sId6eINm6 +zWYyN3L69wj1x81YyY7nDl7qPv4coRQKFWyGhFtkZip6qUtTefWIonvuLwphK42y +fk1WpRPs6tqSnqxEQR5YYGUFZvjARL3LlPdCfgv3ZWiYUQXw8wWRBB0bF4LsyFe7 +w2t6iPGwcswlWyCR7BYCEo8y6RcYSNDHBS4CMEK4JZwFaz+qOqfrU0j36NK2B5jc +G8Y0f3/JHIJ6BVgrCFvzOKKrF11myZjXnhCLotLddJr3cQxyYN/Nb5gznZY0dj4k +epKwDpUeb+agRThHqtdB7Uq3EvbXG4OKDy7YCbZZ16oE/9KTfWgu3YtLq1i6L43q +laegw1SJpfvbi1EinbLDvhG+LJGGi5Z4rSDTii8aP8bQUWWHIbEZAWV/RRyH9XzQ +QUxPKZgh/TMfdQwEUfoZd9vUFBzugcMd9Zi3aQaRIt0AUMyBMawSB3s42mhb5ivU +fslfrejrckzzAeVLIL+aplfKkQABi6F1ITe1Yw1nPkZPcCBnzsXWWdsC4PDSy826 +YreQQejdIOQpvGQpQsgi3Hia/0PsmBsJUUtaWsJx8cTLc6nloQsCAwEAAaOCAc4w +ggHKMB0GA1UdDgQWBBQWtTIb1Mfz4OaO873SsDrusjkY0TCBowYDVR0jBIGbMIGY +gBQWtTIb1Mfz4OaO873SsDrusjkY0aF9pHsweTEQMA4GA1UEChMHUm9vdCBDQTEe +MBwGA1UECxMVaHR0cDovL3d3dy5jYWNlcnQub3JnMSIwIAYDVQQDExlDQSBDZXJ0 +IFNpZ25pbmcgQXV0aG9yaXR5MSEwHwYJKoZIhvcNAQkBFhJzdXBwb3J0QGNhY2Vy +dC5vcmeCAQAwDwYDVR0TAQH/BAUwAwEB/zAyBgNVHR8EKzApMCegJaAjhiFodHRw +czovL3d3dy5jYWNlcnQub3JnL3Jldm9rZS5jcmwwMAYJYIZIAYb4QgEEBCMWIWh0 +dHBzOi8vd3d3LmNhY2VydC5vcmcvcmV2b2tlLmNybDA0BglghkgBhvhCAQgEJxYl +aHR0cDovL3d3dy5jYWNlcnQub3JnL2luZGV4LnBocD9pZD0xMDBWBglghkgBhvhC +AQ0ESRZHVG8gZ2V0IHlvdXIgb3duIGNlcnRpZmljYXRlIGZvciBGUkVFIGhlYWQg +b3ZlciB0byBodHRwOi8vd3d3LmNhY2VydC5vcmcwDQYJKoZIhvcNAQEEBQADggIB +ACjH7pyCArpcgBLKNQodgW+JapnM8mgPf6fhjViVPr3yBsOQWqy1YPaZQwGjiHCc +nWKdpIevZ1gNMDY75q1I08t0AoZxPuIrA2jxNGJARjtT6ij0rPtmlVOKTV39O9lg +18p5aTuxZZKmxoGCXJzN600BiqXfEVWqFcofN8CCmHBh22p8lqOOLlQ+TyGpkO/c +gr/c6EWtTZBzCDyUZbAEmXZ/4rzCahWqlwQ3JNgelE5tDlG+1sSPypZt90Pf6DBl +Jzt7u0NDY8RD97LsaMzhGY4i+5jhe1o+ATc7iwiwovOVThrLm82asduycPAtStvY +sONvRUgzEv/+PDIqVPfE94rwiCPCR/5kenHA0R6mY7AHfqQv0wGP3J8rtsYIqQ+T +SCX8Ev2fQtzzxD72V7DX3WnRBnc0CkvSyqD/HMaMyRa+xMwyN2hzXwj7UfdJUzYF +CpUCTPJ5GhD22Dp1nPMd8aINcGeGG7MW9S/lpOt5hvk9C8JzC6WZrG/8Z7jlLwum +GCSNe9FINSkYQKyTYOGWhlC0elnYjyELn8+CkcY7v2vcB5G5l1YjqrZslMZIBjzk +zk6q5PYvCdxTby78dOs6Y5nCpqyJvKeyRKANihDjbPIky/qbn3BHLt4Ui9SyIAmW +omTxJBzcoTWcFbLUvFUufQb1nA5V9FrWk9p2rSVzTMVD +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/keycert.passwd.pem b/venv/lib/python3.10/site-packages/future/backports/test/keycert.passwd.pem new file mode 100644 index 0000000..e905748 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/keycert.passwd.pem @@ -0,0 +1,33 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,1A8D9D2A02EC698A + +kJYbfZ8L0sfe9Oty3gw0aloNnY5E8fegRfQLZlNoxTl6jNt0nIwI8kDJ36CZgR9c +u3FDJm/KqrfUoz8vW+qEnWhSG7QPX2wWGPHd4K94Yz/FgrRzZ0DoK7XxXq9gOtVA +AVGQhnz32p+6WhfGsCr9ArXEwRZrTk/FvzEPaU5fHcoSkrNVAGX8IpSVkSDwEDQr +Gv17+cfk99UV1OCza6yKHoFkTtrC+PZU71LomBabivS2Oc4B9hYuSR2hF01wTHP+ +YlWNagZOOVtNz4oKK9x9eNQpmfQXQvPPTfusexKIbKfZrMvJoxcm1gfcZ0H/wK6P +6wmXSG35qMOOztCZNtperjs1wzEBXznyK8QmLcAJBjkfarABJX9vBEzZV0OUKhy+ +noORFwHTllphbmydLhu6ehLUZMHPhzAS5UN7srtpSN81eerDMy0RMUAwA7/PofX1 +94Me85Q8jP0PC9ETdsJcPqLzAPETEYu0ELewKRcrdyWi+tlLFrpE5KT/s5ecbl9l +7B61U4Kfd1PIXc/siINhU3A3bYK+845YyUArUOnKf1kEox7p1RpD7yFqVT04lRTo +cibNKATBusXSuBrp2G6GNuhWEOSafWCKJQAzgCYIp6ZTV2khhMUGppc/2H3CF6cO +zX0KtlPVZC7hLkB6HT8SxYUwF1zqWY7+/XPPdc37MeEZ87Q3UuZwqORLY+Z0hpgt +L5JXBCoklZhCAaN2GqwFLXtGiRSRFGY7xXIhbDTlE65Wv1WGGgDLMKGE1gOz3yAo +2jjG1+yAHJUdE69XTFHSqSkvaloA1W03LdMXZ9VuQJ/ySXCie6ABAQ== +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIICVDCCAb2gAwIBAgIJANfHOBkZr8JOMA0GCSqGSIb3DQEBBQUAMF8xCzAJBgNV +BAYTAlhZMRcwFQYDVQQHEw5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9u +IFNvZnR3YXJlIEZvdW5kYXRpb24xEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xMDEw +MDgyMzAxNTZaFw0yMDEwMDUyMzAxNTZaMF8xCzAJBgNVBAYTAlhZMRcwFQYDVQQH +Ew5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9uIFNvZnR3YXJlIEZvdW5k +YXRpb24xEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEA21vT5isq7F68amYuuNpSFlKDPrMUCa4YWYqZRt2OZ+/3NKaZ2xAiSwr7 +6MrQF70t5nLbSPpqE5+5VrS58SY+g/sXLiFd6AplH1wJZwh78DofbFYXUggktFMt +pTyiX8jtP66bkcPkDADA089RI1TQR6Ca+n7HFa7c1fabVV6i3zkCAwEAAaMYMBYw +FAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBBQUAA4GBAHPctQBEQ4wd +BJ6+JcpIraopLn8BGhbjNWj40mmRqWB/NAWF6M5ne7KpGAu7tLeG4hb1zLaldK8G +lxy2GPSRF6LFS48dpEj2HbMv2nvv6xxalDMJ9+DicWgAKTQ6bcX2j3GUkCR0g/T1 +CRlNBAAlvhKzO7Clpf9l0YKBEfraJByX +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/keycert.pem b/venv/lib/python3.10/site-packages/future/backports/test/keycert.pem new file mode 100644 index 0000000..64318aa --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/keycert.pem @@ -0,0 +1,31 @@ +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANtb0+YrKuxevGpm +LrjaUhZSgz6zFAmuGFmKmUbdjmfv9zSmmdsQIksK++jK0Be9LeZy20j6ahOfuVa0 +ufEmPoP7Fy4hXegKZR9cCWcIe/A6H2xWF1IIJLRTLaU8ol/I7T+um5HD5AwAwNPP +USNU0Eegmvp+xxWu3NX2m1Veot85AgMBAAECgYA3ZdZ673X0oexFlq7AAmrutkHt +CL7LvwrpOiaBjhyTxTeSNWzvtQBkIU8DOI0bIazA4UreAFffwtvEuPmonDb3F+Iq +SMAu42XcGyVZEl+gHlTPU9XRX7nTOXVt+MlRRRxL6t9GkGfUAXI3XxJDXW3c0vBK +UL9xqD8cORXOfE06rQJBAP8mEX1ERkR64Ptsoe4281vjTlNfIbs7NMPkUnrn9N/Y +BLhjNIfQ3HFZG8BTMLfX7kCS9D593DW5tV4Z9BP/c6cCQQDcFzCcVArNh2JSywOQ +ZfTfRbJg/Z5Lt9Fkngv1meeGNPgIMLN8Sg679pAOOWmzdMO3V706rNPzSVMME7E5 +oPIfAkEA8pDddarP5tCvTTgUpmTFbakm0KoTZm2+FzHcnA4jRh+XNTjTOv98Y6Ik +eO5d1ZnKXseWvkZncQgxfdnMqqpj5wJAcNq/RVne1DbYlwWchT2Si65MYmmJ8t+F +0mcsULqjOnEMwf5e+ptq5LzwbyrHZYq5FNk7ocufPv/ZQrcSSC+cFwJBAKvOJByS +x56qyGeZLOQlWS2JS3KJo59XuLFGqcbgN9Om9xFa41Yb4N9NvplFivsvZdw3m1Q/ +SPIXQuT8RMPDVNQ= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIICVDCCAb2gAwIBAgIJANfHOBkZr8JOMA0GCSqGSIb3DQEBBQUAMF8xCzAJBgNV +BAYTAlhZMRcwFQYDVQQHEw5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9u +IFNvZnR3YXJlIEZvdW5kYXRpb24xEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xMDEw +MDgyMzAxNTZaFw0yMDEwMDUyMzAxNTZaMF8xCzAJBgNVBAYTAlhZMRcwFQYDVQQH +Ew5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9uIFNvZnR3YXJlIEZvdW5k +YXRpb24xEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEA21vT5isq7F68amYuuNpSFlKDPrMUCa4YWYqZRt2OZ+/3NKaZ2xAiSwr7 +6MrQF70t5nLbSPpqE5+5VrS58SY+g/sXLiFd6AplH1wJZwh78DofbFYXUggktFMt +pTyiX8jtP66bkcPkDADA089RI1TQR6Ca+n7HFa7c1fabVV6i3zkCAwEAAaMYMBYw +FAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBBQUAA4GBAHPctQBEQ4wd +BJ6+JcpIraopLn8BGhbjNWj40mmRqWB/NAWF6M5ne7KpGAu7tLeG4hb1zLaldK8G +lxy2GPSRF6LFS48dpEj2HbMv2nvv6xxalDMJ9+DicWgAKTQ6bcX2j3GUkCR0g/T1 +CRlNBAAlvhKzO7Clpf9l0YKBEfraJByX +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/keycert2.pem b/venv/lib/python3.10/site-packages/future/backports/test/keycert2.pem new file mode 100644 index 0000000..e8a9e08 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/keycert2.pem @@ -0,0 +1,31 @@ +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJnsJZVrppL+W5I9 +zGQrrawWwE5QJpBK9nWw17mXrZ03R1cD9BamLGivVISbPlRlAVnZBEyh1ATpsB7d +CUQ+WHEvALquvx4+Yw5l+fXeiYRjrLRBYZuVy8yNtXzU3iWcGObcYRkUdiXdOyP7 +sLF2YZHRvQZpzgDBKkrraeQ81w21AgMBAAECgYBEm7n07FMHWlE+0kT0sXNsLYfy +YE+QKZnJw9WkaDN+zFEEPELkhZVt5BjsMraJr6v2fIEqF0gGGJPkbenffVq2B5dC +lWUOxvJHufMK4sM3Cp6s/gOp3LP+QkzVnvJSfAyZU6l+4PGX5pLdUsXYjPxgzjzL +S36tF7/2Uv1WePyLUQJBAMsPhYzUXOPRgmbhcJiqi9A9c3GO8kvSDYTCKt3VMnqz +HBn6MQ4VQasCD1F+7jWTI0FU/3vdw8non/Fj8hhYqZcCQQDCDRdvmZqDiZnpMqDq +L6ZSrLTVtMvZXZbgwForaAD9uHj51TME7+eYT7EG2YCgJTXJ4YvRJEnPNyskwdKt +vTSTAkEAtaaN/vyemEJ82BIGStwONNw0ILsSr5cZ9tBHzqiA/tipY+e36HRFiXhP +QcU9zXlxyWkDH8iz9DSAmE2jbfoqwwJANlMJ65E543cjIlitGcKLMnvtCCLcKpb7 +xSG0XJB6Lo11OKPJ66jp0gcFTSCY1Lx2CXVd+gfJrfwI1Pp562+bhwJBAJ9IfDPU +R8OpO9v1SGd8x33Owm7uXOpB9d63/T70AD1QOXjKUC4eXYbt0WWfWuny/RNPRuyh +w7DXSfUF+kPKolU= +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIICXTCCAcagAwIBAgIJAIO3upAG445fMA0GCSqGSIb3DQEBBQUAMGIxCzAJBgNV +BAYTAlhZMRcwFQYDVQQHEw5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9u +IFNvZnR3YXJlIEZvdW5kYXRpb24xFTATBgNVBAMTDGZha2Vob3N0bmFtZTAeFw0x +MDEwMDkxNTAxMDBaFw0yMDEwMDYxNTAxMDBaMGIxCzAJBgNVBAYTAlhZMRcwFQYD +VQQHEw5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9uIFNvZnR3YXJlIEZv +dW5kYXRpb24xFTATBgNVBAMTDGZha2Vob3N0bmFtZTCBnzANBgkqhkiG9w0BAQEF +AAOBjQAwgYkCgYEAmewllWumkv5bkj3MZCutrBbATlAmkEr2dbDXuZetnTdHVwP0 +FqYsaK9UhJs+VGUBWdkETKHUBOmwHt0JRD5YcS8Auq6/Hj5jDmX59d6JhGOstEFh +m5XLzI21fNTeJZwY5txhGRR2Jd07I/uwsXZhkdG9BmnOAMEqSutp5DzXDbUCAwEA +AaMbMBkwFwYDVR0RBBAwDoIMZmFrZWhvc3RuYW1lMA0GCSqGSIb3DQEBBQUAA4GB +AH+iMClLLGSaKWgwXsmdVo4FhTZZHo8Uprrtg3N9FxEeE50btpDVQysgRt5ias3K +m+bME9zbKwvbVWD5zZdjus4pDgzwF/iHyccL8JyYhxOvS/9zmvAtFXj/APIIbZFp +IT75d9f88ScIGEtknZQejnrdhB64tYki/EqluiuKBqKD +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/nokia.pem b/venv/lib/python3.10/site-packages/future/backports/test/nokia.pem new file mode 100644 index 0000000..0d044df --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/nokia.pem @@ -0,0 +1,31 @@ +# Certificate for projects.developer.nokia.com:443 (see issue 13034) +-----BEGIN CERTIFICATE----- +MIIFLDCCBBSgAwIBAgIQLubqdkCgdc7lAF9NfHlUmjANBgkqhkiG9w0BAQUFADCB +vDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL +ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTswOQYDVQQLEzJUZXJtcyBvZiB1c2Ug +YXQgaHR0cHM6Ly93d3cudmVyaXNpZ24uY29tL3JwYSAoYykxMDE2MDQGA1UEAxMt +VmVyaVNpZ24gQ2xhc3MgMyBJbnRlcm5hdGlvbmFsIFNlcnZlciBDQSAtIEczMB4X +DTExMDkyMTAwMDAwMFoXDTEyMDkyMDIzNTk1OVowcTELMAkGA1UEBhMCRkkxDjAM +BgNVBAgTBUVzcG9vMQ4wDAYDVQQHFAVFc3BvbzEOMAwGA1UEChQFTm9raWExCzAJ +BgNVBAsUAkJJMSUwIwYDVQQDFBxwcm9qZWN0cy5kZXZlbG9wZXIubm9raWEuY29t +MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCr92w1bpHYSYxUEx8N/8Iddda2 +lYi+aXNtQfV/l2Fw9Ykv3Ipw4nLeGTj18FFlAZgMdPRlgrzF/NNXGw/9l3/qKdow +CypkQf8lLaxb9Ze1E/KKmkRJa48QTOqvo6GqKuTI6HCeGlG1RxDb8YSKcQWLiytn +yj3Wp4MgRQO266xmMQIDAQABo4IB9jCCAfIwQQYDVR0RBDowOIIccHJvamVjdHMu +ZGV2ZWxvcGVyLm5va2lhLmNvbYIYcHJvamVjdHMuZm9ydW0ubm9raWEuY29tMAkG +A1UdEwQCMAAwCwYDVR0PBAQDAgWgMEEGA1UdHwQ6MDgwNqA0oDKGMGh0dHA6Ly9T +VlJJbnRsLUczLWNybC52ZXJpc2lnbi5jb20vU1ZSSW50bEczLmNybDBEBgNVHSAE +PTA7MDkGC2CGSAGG+EUBBxcDMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LnZl +cmlzaWduLmNvbS9ycGEwKAYDVR0lBCEwHwYJYIZIAYb4QgQBBggrBgEFBQcDAQYI +KwYBBQUHAwIwcgYIKwYBBQUHAQEEZjBkMCQGCCsGAQUFBzABhhhodHRwOi8vb2Nz +cC52ZXJpc2lnbi5jb20wPAYIKwYBBQUHMAKGMGh0dHA6Ly9TVlJJbnRsLUczLWFp +YS52ZXJpc2lnbi5jb20vU1ZSSW50bEczLmNlcjBuBggrBgEFBQcBDARiMGChXqBc +MFowWDBWFglpbWFnZS9naWYwITAfMAcGBSsOAwIaBBRLa7kolgYMu9BSOJsprEsH +iyEFGDAmFiRodHRwOi8vbG9nby52ZXJpc2lnbi5jb20vdnNsb2dvMS5naWYwDQYJ +KoZIhvcNAQEFBQADggEBACQuPyIJqXwUyFRWw9x5yDXgMW4zYFopQYOw/ItRY522 +O5BsySTh56BWS6mQB07XVfxmYUGAvRQDA5QHpmY8jIlNwSmN3s8RKo+fAtiNRlcL +x/mWSfuMs3D/S6ev3D6+dpEMZtjrhOdctsarMKp8n/hPbwhAbg5hVjpkW5n8vz2y +0KxvvkA1AxpLwpVv7OlK17ttzIHw8bp9HTlHBU5s8bKz4a565V/a5HI0CSEv/+0y +ko4/ghTnZc1CkmUngKKeFMSah/mT/xAh8XnE2l1AazFa8UKuYki1e+ArHaGZc4ix +UYOtiRphwfuYQhRZ7qX9q2MMkCMI65XNK/SaFrAbbG0= +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/nullbytecert.pem b/venv/lib/python3.10/site-packages/future/backports/test/nullbytecert.pem new file mode 100644 index 0000000..447186c --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/nullbytecert.pem @@ -0,0 +1,90 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: 0 (0x0) + Signature Algorithm: sha1WithRSAEncryption + Issuer: C=US, ST=Oregon, L=Beaverton, O=Python Software Foundation, OU=Python Core Development, CN=null.python.org\x00example.org/emailAddress=python-dev@python.org + Validity + Not Before: Aug 7 13:11:52 2013 GMT + Not After : Aug 7 13:12:52 2013 GMT + Subject: C=US, ST=Oregon, L=Beaverton, O=Python Software Foundation, OU=Python Core Development, CN=null.python.org\x00example.org/emailAddress=python-dev@python.org + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:b5:ea:ed:c9:fb:46:7d:6f:3b:76:80:dd:3a:f3: + 03:94:0b:a7:a6:db:ec:1d:df:ff:23:74:08:9d:97: + 16:3f:a3:a4:7b:3e:1b:0e:96:59:25:03:a7:26:e2: + 88:a9:cf:79:cd:f7:04:56:b0:ab:79:32:6e:59:c1: + 32:30:54:eb:58:a8:cb:91:f0:42:a5:64:27:cb:d4: + 56:31:88:52:ad:cf:bd:7f:f0:06:64:1f:cc:27:b8: + a3:8b:8c:f3:d8:29:1f:25:0b:f5:46:06:1b:ca:02: + 45:ad:7b:76:0a:9c:bf:bb:b9:ae:0d:16:ab:60:75: + ae:06:3e:9c:7c:31:dc:92:2f:29:1a:e0:4b:0c:91: + 90:6c:e9:37:c5:90:d7:2a:d7:97:15:a3:80:8f:5d: + 7b:49:8f:54:30:d4:97:2c:1c:5b:37:b5:ab:69:30: + 68:43:d3:33:78:4b:02:60:f5:3c:44:80:a1:8f:e7: + f0:0f:d1:5e:87:9e:46:cf:62:fc:f9:bf:0c:65:12: + f1:93:c8:35:79:3f:c8:ec:ec:47:f5:ef:be:44:d5: + ae:82:1e:2d:9a:9f:98:5a:67:65:e1:74:70:7c:cb: + d3:c2:ce:0e:45:49:27:dc:e3:2d:d4:fb:48:0e:2f: + 9e:77:b8:14:46:c0:c4:36:ca:02:ae:6a:91:8c:da: + 2f:85 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: critical + CA:FALSE + X509v3 Subject Key Identifier: + 88:5A:55:C0:52:FF:61:CD:52:A3:35:0F:EA:5A:9C:24:38:22:F7:5C + X509v3 Key Usage: + Digital Signature, Non Repudiation, Key Encipherment + X509v3 Subject Alternative Name: + ************************************************************* + WARNING: The values for DNS, email and URI are WRONG. OpenSSL + doesn't print the text after a NULL byte. + ************************************************************* + DNS:altnull.python.org, email:null@python.org, URI:http://null.python.org, IP Address:192.0.2.1, IP Address:2001:DB8:0:0:0:0:0:1 + Signature Algorithm: sha1WithRSAEncryption + ac:4f:45:ef:7d:49:a8:21:70:8e:88:59:3e:d4:36:42:70:f5: + a3:bd:8b:d7:a8:d0:58:f6:31:4a:b1:a4:a6:dd:6f:d9:e8:44: + 3c:b6:0a:71:d6:7f:b1:08:61:9d:60:ce:75:cf:77:0c:d2:37: + 86:02:8d:5e:5d:f9:0f:71:b4:16:a8:c1:3d:23:1c:f1:11:b3: + 56:6e:ca:d0:8d:34:94:e6:87:2a:99:f2:ae:ae:cc:c2:e8:86: + de:08:a8:7f:c5:05:fa:6f:81:a7:82:e6:d0:53:9d:34:f4:ac: + 3e:40:fe:89:57:7a:29:a4:91:7e:0b:c6:51:31:e5:10:2f:a4: + 60:76:cd:95:51:1a:be:8b:a1:b0:fd:ad:52:bd:d7:1b:87:60: + d2:31:c7:17:c4:18:4f:2d:08:25:a3:a7:4f:b7:92:ca:e2:f5: + 25:f1:54:75:81:9d:b3:3d:61:a2:f7:da:ed:e1:c6:6f:2c:60: + 1f:d8:6f:c5:92:05:ab:c9:09:62:49:a9:14:ad:55:11:cc:d6: + 4a:19:94:99:97:37:1d:81:5f:8b:cf:a3:a8:96:44:51:08:3d: + 0b:05:65:12:eb:b6:70:80:88:48:72:4f:c6:c2:da:cf:cd:8e: + 5b:ba:97:2f:60:b4:96:56:49:5e:3a:43:76:63:04:be:2a:f6: + c1:ca:a9:94 +-----BEGIN CERTIFICATE----- +MIIE2DCCA8CgAwIBAgIBADANBgkqhkiG9w0BAQUFADCBxTELMAkGA1UEBhMCVVMx +DzANBgNVBAgMBk9yZWdvbjESMBAGA1UEBwwJQmVhdmVydG9uMSMwIQYDVQQKDBpQ +eXRob24gU29mdHdhcmUgRm91bmRhdGlvbjEgMB4GA1UECwwXUHl0aG9uIENvcmUg +RGV2ZWxvcG1lbnQxJDAiBgNVBAMMG251bGwucHl0aG9uLm9yZwBleGFtcGxlLm9y +ZzEkMCIGCSqGSIb3DQEJARYVcHl0aG9uLWRldkBweXRob24ub3JnMB4XDTEzMDgw +NzEzMTE1MloXDTEzMDgwNzEzMTI1MlowgcUxCzAJBgNVBAYTAlVTMQ8wDQYDVQQI +DAZPcmVnb24xEjAQBgNVBAcMCUJlYXZlcnRvbjEjMCEGA1UECgwaUHl0aG9uIFNv +ZnR3YXJlIEZvdW5kYXRpb24xIDAeBgNVBAsMF1B5dGhvbiBDb3JlIERldmVsb3Bt +ZW50MSQwIgYDVQQDDBtudWxsLnB5dGhvbi5vcmcAZXhhbXBsZS5vcmcxJDAiBgkq +hkiG9w0BCQEWFXB5dGhvbi1kZXZAcHl0aG9uLm9yZzCCASIwDQYJKoZIhvcNAQEB +BQADggEPADCCAQoCggEBALXq7cn7Rn1vO3aA3TrzA5QLp6bb7B3f/yN0CJ2XFj+j +pHs+Gw6WWSUDpybiiKnPec33BFawq3kyblnBMjBU61ioy5HwQqVkJ8vUVjGIUq3P +vX/wBmQfzCe4o4uM89gpHyUL9UYGG8oCRa17dgqcv7u5rg0Wq2B1rgY+nHwx3JIv +KRrgSwyRkGzpN8WQ1yrXlxWjgI9de0mPVDDUlywcWze1q2kwaEPTM3hLAmD1PESA +oY/n8A/RXoeeRs9i/Pm/DGUS8ZPINXk/yOzsR/XvvkTVroIeLZqfmFpnZeF0cHzL +08LODkVJJ9zjLdT7SA4vnne4FEbAxDbKAq5qkYzaL4UCAwEAAaOB0DCBzTAMBgNV +HRMBAf8EAjAAMB0GA1UdDgQWBBSIWlXAUv9hzVKjNQ/qWpwkOCL3XDALBgNVHQ8E +BAMCBeAwgZAGA1UdEQSBiDCBhYIeYWx0bnVsbC5weXRob24ub3JnAGV4YW1wbGUu +Y29tgSBudWxsQHB5dGhvbi5vcmcAdXNlckBleGFtcGxlLm9yZ4YpaHR0cDovL251 +bGwucHl0aG9uLm9yZwBodHRwOi8vZXhhbXBsZS5vcmeHBMAAAgGHECABDbgAAAAA +AAAAAAAAAAEwDQYJKoZIhvcNAQEFBQADggEBAKxPRe99SaghcI6IWT7UNkJw9aO9 +i9eo0Fj2MUqxpKbdb9noRDy2CnHWf7EIYZ1gznXPdwzSN4YCjV5d+Q9xtBaowT0j +HPERs1ZuytCNNJTmhyqZ8q6uzMLoht4IqH/FBfpvgaeC5tBTnTT0rD5A/olXeimk +kX4LxlEx5RAvpGB2zZVRGr6LobD9rVK91xuHYNIxxxfEGE8tCCWjp0+3ksri9SXx +VHWBnbM9YaL32u3hxm8sYB/Yb8WSBavJCWJJqRStVRHM1koZlJmXNx2BX4vPo6iW +RFEIPQsFZRLrtnCAiEhyT8bC2s/Njlu6ly9gtJZWSV46Q3ZjBL4q9sHKqZQ= +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/nullcert.pem b/venv/lib/python3.10/site-packages/future/backports/test/nullcert.pem new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/future/backports/test/pystone.py b/venv/lib/python3.10/site-packages/future/backports/test/pystone.py new file mode 100644 index 0000000..7652027 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/pystone.py @@ -0,0 +1,272 @@ +#!/usr/bin/env python3 + +""" +"PYSTONE" Benchmark Program + +Version: Python/1.1 (corresponds to C/1.1 plus 2 Pystone fixes) + +Author: Reinhold P. Weicker, CACM Vol 27, No 10, 10/84 pg. 1013. + + Translated from ADA to C by Rick Richardson. + Every method to preserve ADA-likeness has been used, + at the expense of C-ness. + + Translated from C to Python by Guido van Rossum. + +Version History: + + Version 1.1 corrects two bugs in version 1.0: + + First, it leaked memory: in Proc1(), NextRecord ends + up having a pointer to itself. I have corrected this + by zapping NextRecord.PtrComp at the end of Proc1(). + + Second, Proc3() used the operator != to compare a + record to None. This is rather inefficient and not + true to the intention of the original benchmark (where + a pointer comparison to None is intended; the != + operator attempts to find a method __cmp__ to do value + comparison of the record). Version 1.1 runs 5-10 + percent faster than version 1.0, so benchmark figures + of different versions can't be compared directly. + +""" + +from __future__ import print_function + +from time import clock + +LOOPS = 50000 + +__version__ = "1.1" + +[Ident1, Ident2, Ident3, Ident4, Ident5] = range(1, 6) + +class Record(object): + + def __init__(self, PtrComp = None, Discr = 0, EnumComp = 0, + IntComp = 0, StringComp = 0): + self.PtrComp = PtrComp + self.Discr = Discr + self.EnumComp = EnumComp + self.IntComp = IntComp + self.StringComp = StringComp + + def copy(self): + return Record(self.PtrComp, self.Discr, self.EnumComp, + self.IntComp, self.StringComp) + +TRUE = 1 +FALSE = 0 + +def main(loops=LOOPS): + benchtime, stones = pystones(loops) + print("Pystone(%s) time for %d passes = %g" % \ + (__version__, loops, benchtime)) + print("This machine benchmarks at %g pystones/second" % stones) + + +def pystones(loops=LOOPS): + return Proc0(loops) + +IntGlob = 0 +BoolGlob = FALSE +Char1Glob = '\0' +Char2Glob = '\0' +Array1Glob = [0]*51 +Array2Glob = [x[:] for x in [Array1Glob]*51] +PtrGlb = None +PtrGlbNext = None + +def Proc0(loops=LOOPS): + global IntGlob + global BoolGlob + global Char1Glob + global Char2Glob + global Array1Glob + global Array2Glob + global PtrGlb + global PtrGlbNext + + starttime = clock() + for i in range(loops): + pass + nulltime = clock() - starttime + + PtrGlbNext = Record() + PtrGlb = Record() + PtrGlb.PtrComp = PtrGlbNext + PtrGlb.Discr = Ident1 + PtrGlb.EnumComp = Ident3 + PtrGlb.IntComp = 40 + PtrGlb.StringComp = "DHRYSTONE PROGRAM, SOME STRING" + String1Loc = "DHRYSTONE PROGRAM, 1'ST STRING" + Array2Glob[8][7] = 10 + + starttime = clock() + + for i in range(loops): + Proc5() + Proc4() + IntLoc1 = 2 + IntLoc2 = 3 + String2Loc = "DHRYSTONE PROGRAM, 2'ND STRING" + EnumLoc = Ident2 + BoolGlob = not Func2(String1Loc, String2Loc) + while IntLoc1 < IntLoc2: + IntLoc3 = 5 * IntLoc1 - IntLoc2 + IntLoc3 = Proc7(IntLoc1, IntLoc2) + IntLoc1 = IntLoc1 + 1 + Proc8(Array1Glob, Array2Glob, IntLoc1, IntLoc3) + PtrGlb = Proc1(PtrGlb) + CharIndex = 'A' + while CharIndex <= Char2Glob: + if EnumLoc == Func1(CharIndex, 'C'): + EnumLoc = Proc6(Ident1) + CharIndex = chr(ord(CharIndex)+1) + IntLoc3 = IntLoc2 * IntLoc1 + IntLoc2 = IntLoc3 / IntLoc1 + IntLoc2 = 7 * (IntLoc3 - IntLoc2) - IntLoc1 + IntLoc1 = Proc2(IntLoc1) + + benchtime = clock() - starttime - nulltime + if benchtime == 0.0: + loopsPerBenchtime = 0.0 + else: + loopsPerBenchtime = (loops / benchtime) + return benchtime, loopsPerBenchtime + +def Proc1(PtrParIn): + PtrParIn.PtrComp = NextRecord = PtrGlb.copy() + PtrParIn.IntComp = 5 + NextRecord.IntComp = PtrParIn.IntComp + NextRecord.PtrComp = PtrParIn.PtrComp + NextRecord.PtrComp = Proc3(NextRecord.PtrComp) + if NextRecord.Discr == Ident1: + NextRecord.IntComp = 6 + NextRecord.EnumComp = Proc6(PtrParIn.EnumComp) + NextRecord.PtrComp = PtrGlb.PtrComp + NextRecord.IntComp = Proc7(NextRecord.IntComp, 10) + else: + PtrParIn = NextRecord.copy() + NextRecord.PtrComp = None + return PtrParIn + +def Proc2(IntParIO): + IntLoc = IntParIO + 10 + while 1: + if Char1Glob == 'A': + IntLoc = IntLoc - 1 + IntParIO = IntLoc - IntGlob + EnumLoc = Ident1 + if EnumLoc == Ident1: + break + return IntParIO + +def Proc3(PtrParOut): + global IntGlob + + if PtrGlb is not None: + PtrParOut = PtrGlb.PtrComp + else: + IntGlob = 100 + PtrGlb.IntComp = Proc7(10, IntGlob) + return PtrParOut + +def Proc4(): + global Char2Glob + + BoolLoc = Char1Glob == 'A' + BoolLoc = BoolLoc or BoolGlob + Char2Glob = 'B' + +def Proc5(): + global Char1Glob + global BoolGlob + + Char1Glob = 'A' + BoolGlob = FALSE + +def Proc6(EnumParIn): + EnumParOut = EnumParIn + if not Func3(EnumParIn): + EnumParOut = Ident4 + if EnumParIn == Ident1: + EnumParOut = Ident1 + elif EnumParIn == Ident2: + if IntGlob > 100: + EnumParOut = Ident1 + else: + EnumParOut = Ident4 + elif EnumParIn == Ident3: + EnumParOut = Ident2 + elif EnumParIn == Ident4: + pass + elif EnumParIn == Ident5: + EnumParOut = Ident3 + return EnumParOut + +def Proc7(IntParI1, IntParI2): + IntLoc = IntParI1 + 2 + IntParOut = IntParI2 + IntLoc + return IntParOut + +def Proc8(Array1Par, Array2Par, IntParI1, IntParI2): + global IntGlob + + IntLoc = IntParI1 + 5 + Array1Par[IntLoc] = IntParI2 + Array1Par[IntLoc+1] = Array1Par[IntLoc] + Array1Par[IntLoc+30] = IntLoc + for IntIndex in range(IntLoc, IntLoc+2): + Array2Par[IntLoc][IntIndex] = IntLoc + Array2Par[IntLoc][IntLoc-1] = Array2Par[IntLoc][IntLoc-1] + 1 + Array2Par[IntLoc+20][IntLoc] = Array1Par[IntLoc] + IntGlob = 5 + +def Func1(CharPar1, CharPar2): + CharLoc1 = CharPar1 + CharLoc2 = CharLoc1 + if CharLoc2 != CharPar2: + return Ident1 + else: + return Ident2 + +def Func2(StrParI1, StrParI2): + IntLoc = 1 + while IntLoc <= 1: + if Func1(StrParI1[IntLoc], StrParI2[IntLoc+1]) == Ident1: + CharLoc = 'A' + IntLoc = IntLoc + 1 + if CharLoc >= 'W' and CharLoc <= 'Z': + IntLoc = 7 + if CharLoc == 'X': + return TRUE + else: + if StrParI1 > StrParI2: + IntLoc = IntLoc + 7 + return TRUE + else: + return FALSE + +def Func3(EnumParIn): + EnumLoc = EnumParIn + if EnumLoc == Ident3: return TRUE + return FALSE + +if __name__ == '__main__': + import sys + def error(msg): + print(msg, end=' ', file=sys.stderr) + print("usage: %s [number_of_loops]" % sys.argv[0], file=sys.stderr) + sys.exit(100) + nargs = len(sys.argv) - 1 + if nargs > 1: + error("%d arguments are too many;" % nargs) + elif nargs == 1: + try: loops = int(sys.argv[1]) + except ValueError: + error("Invalid argument %r;" % sys.argv[1]) + else: + loops = LOOPS + main(loops) diff --git a/venv/lib/python3.10/site-packages/future/backports/test/sha256.pem b/venv/lib/python3.10/site-packages/future/backports/test/sha256.pem new file mode 100644 index 0000000..d3db4b8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/sha256.pem @@ -0,0 +1,128 @@ +# Certificate chain for https://sha256.tbs-internet.com + 0 s:/C=FR/postalCode=14000/ST=Calvados/L=CAEN/street=22 rue de Bretagne/O=TBS INTERNET/OU=0002 440443810/OU=sha-256 production/CN=sha256.tbs-internet.com + i:/C=FR/ST=Calvados/L=Caen/O=TBS INTERNET/OU=Terms and Conditions: http://www.tbs-internet.com/CA/repository/OU=TBS INTERNET CA/CN=TBS X509 CA SGC +-----BEGIN CERTIFICATE----- +MIIGXDCCBUSgAwIBAgIRAKpVmHgg9nfCodAVwcP4siwwDQYJKoZIhvcNAQELBQAw +gcQxCzAJBgNVBAYTAkZSMREwDwYDVQQIEwhDYWx2YWRvczENMAsGA1UEBxMEQ2Fl +bjEVMBMGA1UEChMMVEJTIElOVEVSTkVUMUgwRgYDVQQLEz9UZXJtcyBhbmQgQ29u +ZGl0aW9uczogaHR0cDovL3d3dy50YnMtaW50ZXJuZXQuY29tL0NBL3JlcG9zaXRv +cnkxGDAWBgNVBAsTD1RCUyBJTlRFUk5FVCBDQTEYMBYGA1UEAxMPVEJTIFg1MDkg +Q0EgU0dDMB4XDTEyMDEwNDAwMDAwMFoXDTE0MDIxNzIzNTk1OVowgcsxCzAJBgNV +BAYTAkZSMQ4wDAYDVQQREwUxNDAwMDERMA8GA1UECBMIQ2FsdmFkb3MxDTALBgNV +BAcTBENBRU4xGzAZBgNVBAkTEjIyIHJ1ZSBkZSBCcmV0YWduZTEVMBMGA1UEChMM +VEJTIElOVEVSTkVUMRcwFQYDVQQLEw4wMDAyIDQ0MDQ0MzgxMDEbMBkGA1UECxMS +c2hhLTI1NiBwcm9kdWN0aW9uMSAwHgYDVQQDExdzaGEyNTYudGJzLWludGVybmV0 +LmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKQIX/zdJcyxty0m +PM1XQSoSSifueS3AVcgqMsaIKS/u+rYzsv4hQ/qA6vLn5m5/ewUcZDj7zdi6rBVf +PaVNXJ6YinLX0tkaW8TEjeVuZG5yksGZlhCt1CJ1Ho9XLiLaP4uJ7MCoNUntpJ+E +LfrOdgsIj91kPmwjDJeztVcQCvKzhjVJA/KxdInc0JvOATn7rpaSmQI5bvIjufgo +qVsTPwVFzuUYULXBk7KxRT7MiEqnd5HvviNh0285QC478zl3v0I0Fb5El4yD3p49 +IthcRnxzMKc0UhU5ogi0SbONyBfm/mzONVfSxpM+MlyvZmJqrbuuLoEDzJD+t8PU +xSuzgbcCAwEAAaOCAj4wggI6MB8GA1UdIwQYMBaAFAdEdoWTKLx/bXjSCuv6TEvf +2YIfMB0GA1UdDgQWBBT/qTGYdaj+f61c2IRFL/B1eEsM8DAOBgNVHQ8BAf8EBAMC +BaAwDAYDVR0TAQH/BAIwADA0BgNVHSUELTArBggrBgEFBQcDAQYIKwYBBQUHAwIG +CisGAQQBgjcKAwMGCWCGSAGG+EIEATBLBgNVHSAERDBCMEAGCisGAQQB5TcCBAEw +MjAwBggrBgEFBQcCARYkaHR0cHM6Ly93d3cudGJzLWludGVybmV0LmNvbS9DQS9D +UFM0MG0GA1UdHwRmMGQwMqAwoC6GLGh0dHA6Ly9jcmwudGJzLWludGVybmV0LmNv +bS9UQlNYNTA5Q0FTR0MuY3JsMC6gLKAqhihodHRwOi8vY3JsLnRicy14NTA5LmNv +bS9UQlNYNTA5Q0FTR0MuY3JsMIGmBggrBgEFBQcBAQSBmTCBljA4BggrBgEFBQcw +AoYsaHR0cDovL2NydC50YnMtaW50ZXJuZXQuY29tL1RCU1g1MDlDQVNHQy5jcnQw +NAYIKwYBBQUHMAKGKGh0dHA6Ly9jcnQudGJzLXg1MDkuY29tL1RCU1g1MDlDQVNH +Qy5jcnQwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLnRicy14NTA5LmNvbTA/BgNV +HREEODA2ghdzaGEyNTYudGJzLWludGVybmV0LmNvbYIbd3d3LnNoYTI1Ni50YnMt +aW50ZXJuZXQuY29tMA0GCSqGSIb3DQEBCwUAA4IBAQA0pOuL8QvAa5yksTbGShzX +ABApagunUGoEydv4YJT1MXy9tTp7DrWaozZSlsqBxrYAXP1d9r2fuKbEniYHxaQ0 +UYaf1VSIlDo1yuC8wE7wxbHDIpQ/E5KAyxiaJ8obtDhFstWAPAH+UoGXq0kj2teN +21sFQ5dXgA95nldvVFsFhrRUNB6xXAcaj0VZFhttI0ZfQZmQwEI/P+N9Jr40OGun +aa+Dn0TMeUH4U20YntfLbu2nDcJcYfyurm+8/0Tr4HznLnedXu9pCPYj0TaddrgT +XO0oFiyy7qGaY6+qKh71yD64Y3ycCJ/HR9Wm39mjZYc9ezYwT4noP6r7Lk8YO7/q +-----END CERTIFICATE----- + 1 s:/C=FR/ST=Calvados/L=Caen/O=TBS INTERNET/OU=Terms and Conditions: http://www.tbs-internet.com/CA/repository/OU=TBS INTERNET CA/CN=TBS X509 CA SGC + i:/C=SE/O=AddTrust AB/OU=AddTrust External TTP Network/CN=AddTrust External CA Root +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQXpDZ0ETJMV02WTx3GTnhhTANBgkqhkiG9w0BAQUFADBv +MQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFk +ZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBF +eHRlcm5hbCBDQSBSb290MB4XDTA1MTIwMTAwMDAwMFoXDTE5MDYyNDE5MDYzMFow +gcQxCzAJBgNVBAYTAkZSMREwDwYDVQQIEwhDYWx2YWRvczENMAsGA1UEBxMEQ2Fl +bjEVMBMGA1UEChMMVEJTIElOVEVSTkVUMUgwRgYDVQQLEz9UZXJtcyBhbmQgQ29u +ZGl0aW9uczogaHR0cDovL3d3dy50YnMtaW50ZXJuZXQuY29tL0NBL3JlcG9zaXRv +cnkxGDAWBgNVBAsTD1RCUyBJTlRFUk5FVCBDQTEYMBYGA1UEAxMPVEJTIFg1MDkg +Q0EgU0dDMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsgOkO3f7wzN6 +rOjg45tR5vjBfzK7qmV9IBxb/QW9EEXxG+E7FNhZqQLtwGBKoSsHTnQqV75wWMk0 +9tinWvftBkSpj5sTi/8cbzJfUvTSVYh3Qxv6AVVjMMH/ruLjE6y+4PoaPs8WoYAQ +ts5R4Z1g8c/WnTepLst2x0/Wv7GmuoQi+gXvHU6YrBiu7XkeYhzc95QdviWSJRDk +owhb5K43qhcvjRmBfO/paGlCliDGZp8mHwrI21mwobWpVjTxZRwYO3bd4+TGcI4G +Ie5wmHwE8F7SK1tgSqbBacKjDa93j7txKkfz/Yd2n7TGqOXiHPsJpG655vrKtnXk +9vs1zoDeJQIDAQABo4IBljCCAZIwHQYDVR0OBBYEFAdEdoWTKLx/bXjSCuv6TEvf +2YIfMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAGAQH/AgEAMCAGA1UdJQQZ +MBcGCisGAQQBgjcKAwMGCWCGSAGG+EIEATAYBgNVHSAEETAPMA0GCysGAQQBgOU3 +AgQBMHsGA1UdHwR0MHIwOKA2oDSGMmh0dHA6Ly9jcmwuY29tb2RvY2EuY29tL0Fk +ZFRydXN0RXh0ZXJuYWxDQVJvb3QuY3JsMDagNKAyhjBodHRwOi8vY3JsLmNvbW9k +by5uZXQvQWRkVHJ1c3RFeHRlcm5hbENBUm9vdC5jcmwwgYAGCCsGAQUFBwEBBHQw +cjA4BggrBgEFBQcwAoYsaHR0cDovL2NydC5jb21vZG9jYS5jb20vQWRkVHJ1c3RV +VE5TR0NDQS5jcnQwNgYIKwYBBQUHMAKGKmh0dHA6Ly9jcnQuY29tb2RvLm5ldC9B +ZGRUcnVzdFVUTlNHQ0NBLmNydDARBglghkgBhvhCAQEEBAMCAgQwDQYJKoZIhvcN +AQEFBQADggEBAK2zEzs+jcIrVK9oDkdDZNvhuBYTdCfpxfFs+OAujW0bIfJAy232 +euVsnJm6u/+OrqKudD2tad2BbejLLXhMZViaCmK7D9nrXHx4te5EP8rL19SUVqLY +1pTnv5dhNgEgvA7n5lIzDSYs7yRLsr7HJsYPr6SeYSuZizyX1SNz7ooJ32/F3X98 +RB0Mlc/E0OyOrkQ9/y5IrnpnaSora8CnUrV5XNOg+kyCz9edCyx4D5wXYcwZPVWz +8aDqquESrezPyjtfi4WRO4s/VD3HLZvOxzMrWAVYCDG9FxaOhF0QGuuG1F7F3GKV +v6prNyCl016kRl2j1UT+a7gLd8fA25A4C9E= +-----END CERTIFICATE----- + 2 s:/C=SE/O=AddTrust AB/OU=AddTrust External TTP Network/CN=AddTrust External CA Root + i:/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN - DATACorp SGC +-----BEGIN CERTIFICATE----- +MIIEZjCCA06gAwIBAgIQUSYKkxzif5zDpV954HKugjANBgkqhkiG9w0BAQUFADCB +kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw +IFNHQzAeFw0wNTA2MDcwODA5MTBaFw0xOTA2MjQxOTA2MzBaMG8xCzAJBgNVBAYT +AlNFMRQwEgYDVQQKEwtBZGRUcnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0 +ZXJuYWwgVFRQIE5ldHdvcmsxIjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENB +IFJvb3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC39xoz5vIABC05 +4E5b7R+8bA/Ntfojts7emxEzl6QpTH2Tn71KvJPtAxrjj8/lbVBa1pcplFqAsEl6 +2y6V/bjKvzc4LR4+kUGtcFbH8E8/6DKedMrIkFTpxl8PeJ2aQDwOrGGqXhSPnoeh +alDc15pOrwWzpnGUnHGzUGAKxxOdOAeGAqjpqGkmGJCrTLBPI6s6T4TY386f4Wlv +u9dC12tE5Met7m1BX3JacQg3s3llpFmglDf3AC8NwpJy2tA4ctsUqEXEXSp9t7TW +xO6szRNEt8kr3UMAJfphuWlqWCMRt6czj1Z1WfXNKddGtworZbbTQm8Vsrh7++/p +XVPVNFonAgMBAAGjgdgwgdUwHwYDVR0jBBgwFoAUUzLRs89/+uDxoF2FTpLSnkUd +tE8wHQYDVR0OBBYEFK29mHo0tCb3+sQmVO8DveAky1QaMA4GA1UdDwEB/wQEAwIB +BjAPBgNVHRMBAf8EBTADAQH/MBEGCWCGSAGG+EIBAQQEAwIBAjAgBgNVHSUEGTAX +BgorBgEEAYI3CgMDBglghkgBhvhCBAEwPQYDVR0fBDYwNDAyoDCgLoYsaHR0cDov +L2NybC51c2VydHJ1c3QuY29tL1VUTi1EQVRBQ29ycFNHQy5jcmwwDQYJKoZIhvcN +AQEFBQADggEBAMbuUxdoFLJRIh6QWA2U/b3xcOWGLcM2MY9USEbnLQg3vGwKYOEO +rVE04BKT6b64q7gmtOmWPSiPrmQH/uAB7MXjkesYoPF1ftsK5p+R26+udd8jkWjd +FwBaS/9kbHDrARrQkNnHptZt9hPk/7XJ0h4qy7ElQyZ42TCbTg0evmnv3+r+LbPM ++bDdtRTKkdSytaX7ARmjR3mfnYyVhzT4HziS2jamEfpr62vp3EV4FTkG101B5CHI +3C+H0be/SGB1pWLLJN47YaApIKa+xWycxOkKaSLvkTr6Jq/RW0GnOuL4OAdCq8Fb ++M5tug8EPzI0rNwEKNdwMBQmBsTkm5jVz3g= +-----END CERTIFICATE----- + 3 s:/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN - DATACorp SGC + i:/C=US/ST=UT/L=Salt Lake City/O=The USERTRUST Network/OU=http://www.usertrust.com/CN=UTN - DATACorp SGC +-----BEGIN CERTIFICATE----- +MIIEXjCCA0agAwIBAgIQRL4Mi1AAIbQR0ypoBqmtaTANBgkqhkiG9w0BAQUFADCB +kzELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAlVUMRcwFQYDVQQHEw5TYWx0IExha2Ug +Q2l0eTEeMBwGA1UEChMVVGhlIFVTRVJUUlVTVCBOZXR3b3JrMSEwHwYDVQQLExho +dHRwOi8vd3d3LnVzZXJ0cnVzdC5jb20xGzAZBgNVBAMTElVUTiAtIERBVEFDb3Jw +IFNHQzAeFw05OTA2MjQxODU3MjFaFw0xOTA2MjQxOTA2MzBaMIGTMQswCQYDVQQG +EwJVUzELMAkGA1UECBMCVVQxFzAVBgNVBAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYD +VQQKExVUaGUgVVNFUlRSVVNUIE5ldHdvcmsxITAfBgNVBAsTGGh0dHA6Ly93d3cu +dXNlcnRydXN0LmNvbTEbMBkGA1UEAxMSVVROIC0gREFUQUNvcnAgU0dDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA3+5YEKIrblXEjr8uRgnn4AgPLit6 +E5Qbvfa2gI5lBZMAHryv4g+OGQ0SR+ysraP6LnD43m77VkIVni5c7yPeIbkFdicZ +D0/Ww5y0vpQZY/KmEQrrU0icvvIpOxboGqBMpsn0GFlowHDyUwDAXlCCpVZvNvlK +4ESGoE1O1kduSUrLZ9emxAW5jh70/P/N5zbgnAVssjMiFdC04MwXwLLA9P4yPykq +lXvY8qdOD1R8oQ2AswkDwf9c3V6aPryuvEeKaq5xyh+xKrhfQgUL7EYw0XILyulW +bfXv33i+Ybqypa4ETLyorGkVl73v67SMvzX41MPRKA5cOp9wGDMgd8SirwIDAQAB +o4GrMIGoMAsGA1UdDwQEAwIBxjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRT +MtGzz3/64PGgXYVOktKeRR20TzA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY3Js +LnVzZXJ0cnVzdC5jb20vVVROLURBVEFDb3JwU0dDLmNybDAqBgNVHSUEIzAhBggr +BgEFBQcDAQYKKwYBBAGCNwoDAwYJYIZIAYb4QgQBMA0GCSqGSIb3DQEBBQUAA4IB +AQAnNZcAiosovcYzMB4p/OL31ZjUQLtgyr+rFywJNn9Q+kHcrpY6CiM+iVnJowft +Gzet/Hy+UUla3joKVAgWRcKZsYfNjGjgaQPpxE6YsjuMFrMOoAyYUJuTqXAJyCyj +j98C5OBxOvG0I3KgqgHf35g+FFCgMSa9KOlaMCZ1+XtgHI3zzVAmbQQnmt/VDUVH +KWss5nbZqSl9Mt3JNjy9rjXxEZ4du5A/EkdOjtd+D2JzHVImOBwYSf0wdJrE5SIv +2MCN7ZF6TACPcn9d2t0bi0Vr591pl6jFVkwPDPafepE39peC4N1xaf92P2BNPM/3 +mfnGV/TJVTl4uix5yaaIK/QI +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/ssl_cert.pem b/venv/lib/python3.10/site-packages/future/backports/test/ssl_cert.pem new file mode 100644 index 0000000..47a7d7e --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/ssl_cert.pem @@ -0,0 +1,15 @@ +-----BEGIN CERTIFICATE----- +MIICVDCCAb2gAwIBAgIJANfHOBkZr8JOMA0GCSqGSIb3DQEBBQUAMF8xCzAJBgNV +BAYTAlhZMRcwFQYDVQQHEw5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9u +IFNvZnR3YXJlIEZvdW5kYXRpb24xEjAQBgNVBAMTCWxvY2FsaG9zdDAeFw0xMDEw +MDgyMzAxNTZaFw0yMDEwMDUyMzAxNTZaMF8xCzAJBgNVBAYTAlhZMRcwFQYDVQQH +Ew5DYXN0bGUgQW50aHJheDEjMCEGA1UEChMaUHl0aG9uIFNvZnR3YXJlIEZvdW5k +YXRpb24xEjAQBgNVBAMTCWxvY2FsaG9zdDCBnzANBgkqhkiG9w0BAQEFAAOBjQAw +gYkCgYEA21vT5isq7F68amYuuNpSFlKDPrMUCa4YWYqZRt2OZ+/3NKaZ2xAiSwr7 +6MrQF70t5nLbSPpqE5+5VrS58SY+g/sXLiFd6AplH1wJZwh78DofbFYXUggktFMt +pTyiX8jtP66bkcPkDADA089RI1TQR6Ca+n7HFa7c1fabVV6i3zkCAwEAAaMYMBYw +FAYDVR0RBA0wC4IJbG9jYWxob3N0MA0GCSqGSIb3DQEBBQUAA4GBAHPctQBEQ4wd +BJ6+JcpIraopLn8BGhbjNWj40mmRqWB/NAWF6M5ne7KpGAu7tLeG4hb1zLaldK8G +lxy2GPSRF6LFS48dpEj2HbMv2nvv6xxalDMJ9+DicWgAKTQ6bcX2j3GUkCR0g/T1 +CRlNBAAlvhKzO7Clpf9l0YKBEfraJByX +-----END CERTIFICATE----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/ssl_key.passwd.pem b/venv/lib/python3.10/site-packages/future/backports/test/ssl_key.passwd.pem new file mode 100644 index 0000000..2524672 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/ssl_key.passwd.pem @@ -0,0 +1,18 @@ +-----BEGIN RSA PRIVATE KEY----- +Proc-Type: 4,ENCRYPTED +DEK-Info: DES-EDE3-CBC,1A8D9D2A02EC698A + +kJYbfZ8L0sfe9Oty3gw0aloNnY5E8fegRfQLZlNoxTl6jNt0nIwI8kDJ36CZgR9c +u3FDJm/KqrfUoz8vW+qEnWhSG7QPX2wWGPHd4K94Yz/FgrRzZ0DoK7XxXq9gOtVA +AVGQhnz32p+6WhfGsCr9ArXEwRZrTk/FvzEPaU5fHcoSkrNVAGX8IpSVkSDwEDQr +Gv17+cfk99UV1OCza6yKHoFkTtrC+PZU71LomBabivS2Oc4B9hYuSR2hF01wTHP+ +YlWNagZOOVtNz4oKK9x9eNQpmfQXQvPPTfusexKIbKfZrMvJoxcm1gfcZ0H/wK6P +6wmXSG35qMOOztCZNtperjs1wzEBXznyK8QmLcAJBjkfarABJX9vBEzZV0OUKhy+ +noORFwHTllphbmydLhu6ehLUZMHPhzAS5UN7srtpSN81eerDMy0RMUAwA7/PofX1 +94Me85Q8jP0PC9ETdsJcPqLzAPETEYu0ELewKRcrdyWi+tlLFrpE5KT/s5ecbl9l +7B61U4Kfd1PIXc/siINhU3A3bYK+845YyUArUOnKf1kEox7p1RpD7yFqVT04lRTo +cibNKATBusXSuBrp2G6GNuhWEOSafWCKJQAzgCYIp6ZTV2khhMUGppc/2H3CF6cO +zX0KtlPVZC7hLkB6HT8SxYUwF1zqWY7+/XPPdc37MeEZ87Q3UuZwqORLY+Z0hpgt +L5JXBCoklZhCAaN2GqwFLXtGiRSRFGY7xXIhbDTlE65Wv1WGGgDLMKGE1gOz3yAo +2jjG1+yAHJUdE69XTFHSqSkvaloA1W03LdMXZ9VuQJ/ySXCie6ABAQ== +-----END RSA PRIVATE KEY----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/ssl_key.pem b/venv/lib/python3.10/site-packages/future/backports/test/ssl_key.pem new file mode 100644 index 0000000..3fd3bbd --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/ssl_key.pem @@ -0,0 +1,16 @@ +-----BEGIN PRIVATE KEY----- +MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBANtb0+YrKuxevGpm +LrjaUhZSgz6zFAmuGFmKmUbdjmfv9zSmmdsQIksK++jK0Be9LeZy20j6ahOfuVa0 +ufEmPoP7Fy4hXegKZR9cCWcIe/A6H2xWF1IIJLRTLaU8ol/I7T+um5HD5AwAwNPP +USNU0Eegmvp+xxWu3NX2m1Veot85AgMBAAECgYA3ZdZ673X0oexFlq7AAmrutkHt +CL7LvwrpOiaBjhyTxTeSNWzvtQBkIU8DOI0bIazA4UreAFffwtvEuPmonDb3F+Iq +SMAu42XcGyVZEl+gHlTPU9XRX7nTOXVt+MlRRRxL6t9GkGfUAXI3XxJDXW3c0vBK +UL9xqD8cORXOfE06rQJBAP8mEX1ERkR64Ptsoe4281vjTlNfIbs7NMPkUnrn9N/Y +BLhjNIfQ3HFZG8BTMLfX7kCS9D593DW5tV4Z9BP/c6cCQQDcFzCcVArNh2JSywOQ +ZfTfRbJg/Z5Lt9Fkngv1meeGNPgIMLN8Sg679pAOOWmzdMO3V706rNPzSVMME7E5 +oPIfAkEA8pDddarP5tCvTTgUpmTFbakm0KoTZm2+FzHcnA4jRh+XNTjTOv98Y6Ik +eO5d1ZnKXseWvkZncQgxfdnMqqpj5wJAcNq/RVne1DbYlwWchT2Si65MYmmJ8t+F +0mcsULqjOnEMwf5e+ptq5LzwbyrHZYq5FNk7ocufPv/ZQrcSSC+cFwJBAKvOJByS +x56qyGeZLOQlWS2JS3KJo59XuLFGqcbgN9Om9xFa41Yb4N9NvplFivsvZdw3m1Q/ +SPIXQuT8RMPDVNQ= +-----END PRIVATE KEY----- diff --git a/venv/lib/python3.10/site-packages/future/backports/test/ssl_servers.py b/venv/lib/python3.10/site-packages/future/backports/test/ssl_servers.py new file mode 100644 index 0000000..87a3fb8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/ssl_servers.py @@ -0,0 +1,207 @@ +from __future__ import absolute_import, division, print_function, unicode_literals +from future.builtins import filter, str +from future import utils +import os +import sys +import ssl +import pprint +import socket +from future.backports.urllib import parse as urllib_parse +from future.backports.http.server import (HTTPServer as _HTTPServer, + SimpleHTTPRequestHandler, BaseHTTPRequestHandler) +from future.backports.test import support +threading = support.import_module("threading") + +here = os.path.dirname(__file__) + +HOST = support.HOST +CERTFILE = os.path.join(here, 'keycert.pem') + +# This one's based on HTTPServer, which is based on SocketServer + +class HTTPSServer(_HTTPServer): + + def __init__(self, server_address, handler_class, context): + _HTTPServer.__init__(self, server_address, handler_class) + self.context = context + + def __str__(self): + return ('<%s %s:%s>' % + (self.__class__.__name__, + self.server_name, + self.server_port)) + + def get_request(self): + # override this to wrap socket with SSL + try: + sock, addr = self.socket.accept() + sslconn = self.context.wrap_socket(sock, server_side=True) + except socket.error as e: + # socket errors are silenced by the caller, print them here + if support.verbose: + sys.stderr.write("Got an error:\n%s\n" % e) + raise + return sslconn, addr + +class RootedHTTPRequestHandler(SimpleHTTPRequestHandler): + # need to override translate_path to get a known root, + # instead of using os.curdir, since the test could be + # run from anywhere + + server_version = "TestHTTPS/1.0" + root = here + # Avoid hanging when a request gets interrupted by the client + timeout = 5 + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = urllib.parse.urlparse(path)[2] + path = os.path.normpath(urllib.parse.unquote(path)) + words = path.split('/') + words = filter(None, words) + path = self.root + for word in words: + drive, word = os.path.splitdrive(word) + head, word = os.path.split(word) + path = os.path.join(path, word) + return path + + def log_message(self, format, *args): + # we override this to suppress logging unless "verbose" + if support.verbose: + sys.stdout.write(" server (%s:%d %s):\n [%s] %s\n" % + (self.server.server_address, + self.server.server_port, + self.request.cipher(), + self.log_date_time_string(), + format%args)) + + +class StatsRequestHandler(BaseHTTPRequestHandler): + """Example HTTP request handler which returns SSL statistics on GET + requests. + """ + + server_version = "StatsHTTPS/1.0" + + def do_GET(self, send_body=True): + """Serve a GET request.""" + sock = self.rfile.raw._sock + context = sock.context + stats = { + 'session_cache': context.session_stats(), + 'cipher': sock.cipher(), + 'compression': sock.compression(), + } + body = pprint.pformat(stats) + body = body.encode('utf-8') + self.send_response(200) + self.send_header("Content-type", "text/plain; charset=utf-8") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + if send_body: + self.wfile.write(body) + + def do_HEAD(self): + """Serve a HEAD request.""" + self.do_GET(send_body=False) + + def log_request(self, format, *args): + if support.verbose: + BaseHTTPRequestHandler.log_request(self, format, *args) + + +class HTTPSServerThread(threading.Thread): + + def __init__(self, context, host=HOST, handler_class=None): + self.flag = None + self.server = HTTPSServer((host, 0), + handler_class or RootedHTTPRequestHandler, + context) + self.port = self.server.server_port + threading.Thread.__init__(self) + self.daemon = True + + def __str__(self): + return "<%s %s>" % (self.__class__.__name__, self.server) + + def start(self, flag=None): + self.flag = flag + threading.Thread.start(self) + + def run(self): + if self.flag: + self.flag.set() + try: + self.server.serve_forever(0.05) + finally: + self.server.server_close() + + def stop(self): + self.server.shutdown() + + +def make_https_server(case, certfile=CERTFILE, host=HOST, handler_class=None): + # we assume the certfile contains both private key and certificate + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.load_cert_chain(certfile) + server = HTTPSServerThread(context, host, handler_class) + flag = threading.Event() + server.start(flag) + flag.wait() + def cleanup(): + if support.verbose: + sys.stdout.write('stopping HTTPS server\n') + server.stop() + if support.verbose: + sys.stdout.write('joining HTTPS thread\n') + server.join() + case.addCleanup(cleanup) + return server + + +if __name__ == "__main__": + import argparse + parser = argparse.ArgumentParser( + description='Run a test HTTPS server. ' + 'By default, the current directory is served.') + parser.add_argument('-p', '--port', type=int, default=4433, + help='port to listen on (default: %(default)s)') + parser.add_argument('-q', '--quiet', dest='verbose', default=True, + action='store_false', help='be less verbose') + parser.add_argument('-s', '--stats', dest='use_stats_handler', default=False, + action='store_true', help='always return stats page') + parser.add_argument('--curve-name', dest='curve_name', type=str, + action='store', + help='curve name for EC-based Diffie-Hellman') + parser.add_argument('--dh', dest='dh_file', type=str, action='store', + help='PEM file containing DH parameters') + args = parser.parse_args() + + support.verbose = args.verbose + if args.use_stats_handler: + handler_class = StatsRequestHandler + else: + handler_class = RootedHTTPRequestHandler + if utils.PY2: + handler_class.root = os.getcwdu() + else: + handler_class.root = os.getcwd() + context = ssl.SSLContext(ssl.PROTOCOL_TLSv1) + context.load_cert_chain(CERTFILE) + if args.curve_name: + context.set_ecdh_curve(args.curve_name) + if args.dh_file: + context.load_dh_params(args.dh_file) + + server = HTTPSServer(("", args.port), handler_class, context) + if args.verbose: + print("Listening on https://localhost:{0.port}".format(args)) + server.serve_forever(0.1) diff --git a/venv/lib/python3.10/site-packages/future/backports/test/support.py b/venv/lib/python3.10/site-packages/future/backports/test/support.py new file mode 100644 index 0000000..6639372 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/test/support.py @@ -0,0 +1,2016 @@ +# -*- coding: utf-8 -*- +"""Supporting definitions for the Python regression tests. + +Backported for python-future from Python 3.3 test/support.py. +""" + +from __future__ import (absolute_import, division, + print_function, unicode_literals) +from future import utils +from future.builtins import str, range, open, int, map, list + +import contextlib +import errno +import functools +import gc +import socket +import sys +import os +import platform +import shutil +import warnings +import unittest +# For Python 2.6 compatibility: +if not hasattr(unittest, 'skip'): + import unittest2 as unittest + +import importlib +# import collections.abc # not present on Py2.7 +import re +import subprocess +import time +try: + import sysconfig +except ImportError: + # sysconfig is not available on Python 2.6. Try using distutils.sysconfig instead: + from distutils import sysconfig +import fnmatch +import logging.handlers +import struct +import tempfile + +try: + if utils.PY3: + import _thread, threading + else: + import thread as _thread, threading +except ImportError: + _thread = None + threading = None +try: + import multiprocessing.process +except ImportError: + multiprocessing = None + +try: + import zlib +except ImportError: + zlib = None + +try: + import gzip +except ImportError: + gzip = None + +try: + import bz2 +except ImportError: + bz2 = None + +try: + import lzma +except ImportError: + lzma = None + +__all__ = [ + "Error", "TestFailed", "ResourceDenied", "import_module", "verbose", + "use_resources", "max_memuse", "record_original_stdout", + "get_original_stdout", "unload", "unlink", "rmtree", "forget", + "is_resource_enabled", "requires", "requires_freebsd_version", + "requires_linux_version", "requires_mac_ver", "find_unused_port", + "bind_port", "IPV6_ENABLED", "is_jython", "TESTFN", "HOST", "SAVEDCWD", + "temp_cwd", "findfile", "create_empty_file", "sortdict", + "check_syntax_error", "open_urlresource", "check_warnings", "CleanImport", + "EnvironmentVarGuard", "TransientResource", "captured_stdout", + "captured_stdin", "captured_stderr", "time_out", "socket_peer_reset", + "ioerror_peer_reset", "run_with_locale", 'temp_umask', + "transient_internet", "set_memlimit", "bigmemtest", "bigaddrspacetest", + "BasicTestRunner", "run_unittest", "run_doctest", "threading_setup", + "threading_cleanup", "reap_children", "cpython_only", "check_impl_detail", + "get_attribute", "swap_item", "swap_attr", "requires_IEEE_754", + "TestHandler", "Matcher", "can_symlink", "skip_unless_symlink", + "skip_unless_xattr", "import_fresh_module", "requires_zlib", + "PIPE_MAX_SIZE", "failfast", "anticipate_failure", "run_with_tz", + "requires_gzip", "requires_bz2", "requires_lzma", "suppress_crash_popup", + ] + +class Error(Exception): + """Base class for regression test exceptions.""" + +class TestFailed(Error): + """Test failed.""" + +class ResourceDenied(unittest.SkipTest): + """Test skipped because it requested a disallowed resource. + + This is raised when a test calls requires() for a resource that + has not be enabled. It is used to distinguish between expected + and unexpected skips. + """ + +@contextlib.contextmanager +def _ignore_deprecated_imports(ignore=True): + """Context manager to suppress package and module deprecation + warnings when importing them. + + If ignore is False, this context manager has no effect.""" + if ignore: + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", ".+ (module|package)", + DeprecationWarning) + yield + else: + yield + + +def import_module(name, deprecated=False): + """Import and return the module to be tested, raising SkipTest if + it is not available. + + If deprecated is True, any module or package deprecation messages + will be suppressed.""" + with _ignore_deprecated_imports(deprecated): + try: + return importlib.import_module(name) + except ImportError as msg: + raise unittest.SkipTest(str(msg)) + + +def _save_and_remove_module(name, orig_modules): + """Helper function to save and remove a module from sys.modules + + Raise ImportError if the module can't be imported. + """ + # try to import the module and raise an error if it can't be imported + if name not in sys.modules: + __import__(name) + del sys.modules[name] + for modname in list(sys.modules): + if modname == name or modname.startswith(name + '.'): + orig_modules[modname] = sys.modules[modname] + del sys.modules[modname] + +def _save_and_block_module(name, orig_modules): + """Helper function to save and block a module in sys.modules + + Return True if the module was in sys.modules, False otherwise. + """ + saved = True + try: + orig_modules[name] = sys.modules[name] + except KeyError: + saved = False + sys.modules[name] = None + return saved + + +def anticipate_failure(condition): + """Decorator to mark a test that is known to be broken in some cases + + Any use of this decorator should have a comment identifying the + associated tracker issue. + """ + if condition: + return unittest.expectedFailure + return lambda f: f + + +def import_fresh_module(name, fresh=(), blocked=(), deprecated=False): + """Import and return a module, deliberately bypassing sys.modules. + This function imports and returns a fresh copy of the named Python module + by removing the named module from sys.modules before doing the import. + Note that unlike reload, the original module is not affected by + this operation. + + *fresh* is an iterable of additional module names that are also removed + from the sys.modules cache before doing the import. + + *blocked* is an iterable of module names that are replaced with None + in the module cache during the import to ensure that attempts to import + them raise ImportError. + + The named module and any modules named in the *fresh* and *blocked* + parameters are saved before starting the import and then reinserted into + sys.modules when the fresh import is complete. + + Module and package deprecation messages are suppressed during this import + if *deprecated* is True. + + This function will raise ImportError if the named module cannot be + imported. + + If deprecated is True, any module or package deprecation messages + will be suppressed. + """ + # NOTE: test_heapq, test_json and test_warnings include extra sanity checks + # to make sure that this utility function is working as expected + with _ignore_deprecated_imports(deprecated): + # Keep track of modules saved for later restoration as well + # as those which just need a blocking entry removed + orig_modules = {} + names_to_remove = [] + _save_and_remove_module(name, orig_modules) + try: + for fresh_name in fresh: + _save_and_remove_module(fresh_name, orig_modules) + for blocked_name in blocked: + if not _save_and_block_module(blocked_name, orig_modules): + names_to_remove.append(blocked_name) + fresh_module = importlib.import_module(name) + except ImportError: + fresh_module = None + finally: + for orig_name, module in orig_modules.items(): + sys.modules[orig_name] = module + for name_to_remove in names_to_remove: + del sys.modules[name_to_remove] + return fresh_module + + +def get_attribute(obj, name): + """Get an attribute, raising SkipTest if AttributeError is raised.""" + try: + attribute = getattr(obj, name) + except AttributeError: + raise unittest.SkipTest("object %r has no attribute %r" % (obj, name)) + else: + return attribute + +verbose = 1 # Flag set to 0 by regrtest.py +use_resources = None # Flag set to [] by regrtest.py +max_memuse = 0 # Disable bigmem tests (they will still be run with + # small sizes, to make sure they work.) +real_max_memuse = 0 +failfast = False +match_tests = None + +# _original_stdout is meant to hold stdout at the time regrtest began. +# This may be "the real" stdout, or IDLE's emulation of stdout, or whatever. +# The point is to have some flavor of stdout the user can actually see. +_original_stdout = None +def record_original_stdout(stdout): + global _original_stdout + _original_stdout = stdout + +def get_original_stdout(): + return _original_stdout or sys.stdout + +def unload(name): + try: + del sys.modules[name] + except KeyError: + pass + +if sys.platform.startswith("win"): + def _waitfor(func, pathname, waitall=False): + # Perform the operation + func(pathname) + # Now setup the wait loop + if waitall: + dirname = pathname + else: + dirname, name = os.path.split(pathname) + dirname = dirname or '.' + # Check for `pathname` to be removed from the filesystem. + # The exponential backoff of the timeout amounts to a total + # of ~1 second after which the deletion is probably an error + # anyway. + # Testing on a i7@4.3GHz shows that usually only 1 iteration is + # required when contention occurs. + timeout = 0.001 + while timeout < 1.0: + # Note we are only testing for the existence of the file(s) in + # the contents of the directory regardless of any security or + # access rights. If we have made it this far, we have sufficient + # permissions to do that much using Python's equivalent of the + # Windows API FindFirstFile. + # Other Windows APIs can fail or give incorrect results when + # dealing with files that are pending deletion. + L = os.listdir(dirname) + if not (L if waitall else name in L): + return + # Increase the timeout and try again + time.sleep(timeout) + timeout *= 2 + warnings.warn('tests may fail, delete still pending for ' + pathname, + RuntimeWarning, stacklevel=4) + + def _unlink(filename): + _waitfor(os.unlink, filename) + + def _rmdir(dirname): + _waitfor(os.rmdir, dirname) + + def _rmtree(path): + def _rmtree_inner(path): + for name in os.listdir(path): + fullname = os.path.join(path, name) + if os.path.isdir(fullname): + _waitfor(_rmtree_inner, fullname, waitall=True) + os.rmdir(fullname) + else: + os.unlink(fullname) + _waitfor(_rmtree_inner, path, waitall=True) + _waitfor(os.rmdir, path) +else: + _unlink = os.unlink + _rmdir = os.rmdir + _rmtree = shutil.rmtree + +def unlink(filename): + try: + _unlink(filename) + except OSError as error: + # The filename need not exist. + if error.errno not in (errno.ENOENT, errno.ENOTDIR): + raise + +def rmdir(dirname): + try: + _rmdir(dirname) + except OSError as error: + # The directory need not exist. + if error.errno != errno.ENOENT: + raise + +def rmtree(path): + try: + _rmtree(path) + except OSError as error: + if error.errno != errno.ENOENT: + raise + + +# On some platforms, should not run gui test even if it is allowed +# in `use_resources'. +if sys.platform.startswith('win'): + import ctypes + import ctypes.wintypes + def _is_gui_available(): + UOI_FLAGS = 1 + WSF_VISIBLE = 0x0001 + class USEROBJECTFLAGS(ctypes.Structure): + _fields_ = [("fInherit", ctypes.wintypes.BOOL), + ("fReserved", ctypes.wintypes.BOOL), + ("dwFlags", ctypes.wintypes.DWORD)] + dll = ctypes.windll.user32 + h = dll.GetProcessWindowStation() + if not h: + raise ctypes.WinError() + uof = USEROBJECTFLAGS() + needed = ctypes.wintypes.DWORD() + res = dll.GetUserObjectInformationW(h, + UOI_FLAGS, + ctypes.byref(uof), + ctypes.sizeof(uof), + ctypes.byref(needed)) + if not res: + raise ctypes.WinError() + return bool(uof.dwFlags & WSF_VISIBLE) +else: + def _is_gui_available(): + return True + +def is_resource_enabled(resource): + """Test whether a resource is enabled. Known resources are set by + regrtest.py.""" + return use_resources is not None and resource in use_resources + +def requires(resource, msg=None): + """Raise ResourceDenied if the specified resource is not available. + + If the caller's module is __main__ then automatically return True. The + possibility of False being returned occurs when regrtest.py is + executing. + """ + if resource == 'gui' and not _is_gui_available(): + raise unittest.SkipTest("Cannot use the 'gui' resource") + # see if the caller's module is __main__ - if so, treat as if + # the resource was set + if sys._getframe(1).f_globals.get("__name__") == "__main__": + return + if not is_resource_enabled(resource): + if msg is None: + msg = "Use of the %r resource not enabled" % resource + raise ResourceDenied(msg) + +def _requires_unix_version(sysname, min_version): + """Decorator raising SkipTest if the OS is `sysname` and the version is less + than `min_version`. + + For example, @_requires_unix_version('FreeBSD', (7, 2)) raises SkipTest if + the FreeBSD version is less than 7.2. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kw): + if platform.system() == sysname: + version_txt = platform.release().split('-', 1)[0] + try: + version = tuple(map(int, version_txt.split('.'))) + except ValueError: + pass + else: + if version < min_version: + min_version_txt = '.'.join(map(str, min_version)) + raise unittest.SkipTest( + "%s version %s or higher required, not %s" + % (sysname, min_version_txt, version_txt)) + return func(*args, **kw) + wrapper.min_version = min_version + return wrapper + return decorator + +def requires_freebsd_version(*min_version): + """Decorator raising SkipTest if the OS is FreeBSD and the FreeBSD version is + less than `min_version`. + + For example, @requires_freebsd_version(7, 2) raises SkipTest if the FreeBSD + version is less than 7.2. + """ + return _requires_unix_version('FreeBSD', min_version) + +def requires_linux_version(*min_version): + """Decorator raising SkipTest if the OS is Linux and the Linux version is + less than `min_version`. + + For example, @requires_linux_version(2, 6, 32) raises SkipTest if the Linux + version is less than 2.6.32. + """ + return _requires_unix_version('Linux', min_version) + +def requires_mac_ver(*min_version): + """Decorator raising SkipTest if the OS is Mac OS X and the OS X + version if less than min_version. + + For example, @requires_mac_ver(10, 5) raises SkipTest if the OS X version + is lesser than 10.5. + """ + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kw): + if sys.platform == 'darwin': + version_txt = platform.mac_ver()[0] + try: + version = tuple(map(int, version_txt.split('.'))) + except ValueError: + pass + else: + if version < min_version: + min_version_txt = '.'.join(map(str, min_version)) + raise unittest.SkipTest( + "Mac OS X %s or higher required, not %s" + % (min_version_txt, version_txt)) + return func(*args, **kw) + wrapper.min_version = min_version + return wrapper + return decorator + +# Don't use "localhost", since resolving it uses the DNS under recent +# Windows versions (see issue #18792). +HOST = "127.0.0.1" +HOSTv6 = "::1" + + +def find_unused_port(family=socket.AF_INET, socktype=socket.SOCK_STREAM): + """Returns an unused port that should be suitable for binding. This is + achieved by creating a temporary socket with the same family and type as + the 'sock' parameter (default is AF_INET, SOCK_STREAM), and binding it to + the specified host address (defaults to 0.0.0.0) with the port set to 0, + eliciting an unused ephemeral port from the OS. The temporary socket is + then closed and deleted, and the ephemeral port is returned. + + Either this method or bind_port() should be used for any tests where a + server socket needs to be bound to a particular port for the duration of + the test. Which one to use depends on whether the calling code is creating + a python socket, or if an unused port needs to be provided in a constructor + or passed to an external program (i.e. the -accept argument to openssl's + s_server mode). Always prefer bind_port() over find_unused_port() where + possible. Hard coded ports should *NEVER* be used. As soon as a server + socket is bound to a hard coded port, the ability to run multiple instances + of the test simultaneously on the same host is compromised, which makes the + test a ticking time bomb in a buildbot environment. On Unix buildbots, this + may simply manifest as a failed test, which can be recovered from without + intervention in most cases, but on Windows, the entire python process can + completely and utterly wedge, requiring someone to log in to the buildbot + and manually kill the affected process. + + (This is easy to reproduce on Windows, unfortunately, and can be traced to + the SO_REUSEADDR socket option having different semantics on Windows versus + Unix/Linux. On Unix, you can't have two AF_INET SOCK_STREAM sockets bind, + listen and then accept connections on identical host/ports. An EADDRINUSE + socket.error will be raised at some point (depending on the platform and + the order bind and listen were called on each socket). + + However, on Windows, if SO_REUSEADDR is set on the sockets, no EADDRINUSE + will ever be raised when attempting to bind two identical host/ports. When + accept() is called on each socket, the second caller's process will steal + the port from the first caller, leaving them both in an awkwardly wedged + state where they'll no longer respond to any signals or graceful kills, and + must be forcibly killed via OpenProcess()/TerminateProcess(). + + The solution on Windows is to use the SO_EXCLUSIVEADDRUSE socket option + instead of SO_REUSEADDR, which effectively affords the same semantics as + SO_REUSEADDR on Unix. Given the propensity of Unix developers in the Open + Source world compared to Windows ones, this is a common mistake. A quick + look over OpenSSL's 0.9.8g source shows that they use SO_REUSEADDR when + openssl.exe is called with the 's_server' option, for example. See + http://bugs.python.org/issue2550 for more info. The following site also + has a very thorough description about the implications of both REUSEADDR + and EXCLUSIVEADDRUSE on Windows: + http://msdn2.microsoft.com/en-us/library/ms740621(VS.85).aspx) + + XXX: although this approach is a vast improvement on previous attempts to + elicit unused ports, it rests heavily on the assumption that the ephemeral + port returned to us by the OS won't immediately be dished back out to some + other process when we close and delete our temporary socket but before our + calling code has a chance to bind the returned port. We can deal with this + issue if/when we come across it. + """ + + tempsock = socket.socket(family, socktype) + port = bind_port(tempsock) + tempsock.close() + del tempsock + return port + +def bind_port(sock, host=HOST): + """Bind the socket to a free port and return the port number. Relies on + ephemeral ports in order to ensure we are using an unbound port. This is + important as many tests may be running simultaneously, especially in a + buildbot environment. This method raises an exception if the sock.family + is AF_INET and sock.type is SOCK_STREAM, *and* the socket has SO_REUSEADDR + or SO_REUSEPORT set on it. Tests should *never* set these socket options + for TCP/IP sockets. The only case for setting these options is testing + multicasting via multiple UDP sockets. + + Additionally, if the SO_EXCLUSIVEADDRUSE socket option is available (i.e. + on Windows), it will be set on the socket. This will prevent anyone else + from bind()'ing to our host/port for the duration of the test. + """ + + if sock.family == socket.AF_INET and sock.type == socket.SOCK_STREAM: + if hasattr(socket, 'SO_REUSEADDR'): + if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR) == 1: + raise TestFailed("tests should never set the SO_REUSEADDR " \ + "socket option on TCP/IP sockets!") + if hasattr(socket, 'SO_REUSEPORT'): + try: + if sock.getsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT) == 1: + raise TestFailed("tests should never set the SO_REUSEPORT " \ + "socket option on TCP/IP sockets!") + except socket.error: + # Python's socket module was compiled using modern headers + # thus defining SO_REUSEPORT but this process is running + # under an older kernel that does not support SO_REUSEPORT. + pass + if hasattr(socket, 'SO_EXCLUSIVEADDRUSE'): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) + + sock.bind((host, 0)) + port = sock.getsockname()[1] + return port + +def _is_ipv6_enabled(): + """Check whether IPv6 is enabled on this host.""" + if socket.has_ipv6: + sock = None + try: + sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) + sock.bind(('::1', 0)) + return True + except (socket.error, socket.gaierror): + pass + finally: + if sock: + sock.close() + return False + +IPV6_ENABLED = _is_ipv6_enabled() + + +# A constant likely larger than the underlying OS pipe buffer size, to +# make writes blocking. +# Windows limit seems to be around 512 B, and many Unix kernels have a +# 64 KiB pipe buffer size or 16 * PAGE_SIZE: take a few megs to be sure. +# (see issue #17835 for a discussion of this number). +PIPE_MAX_SIZE = 4 * 1024 * 1024 + 1 + +# A constant likely larger than the underlying OS socket buffer size, to make +# writes blocking. +# The socket buffer sizes can usually be tuned system-wide (e.g. through sysctl +# on Linux), or on a per-socket basis (SO_SNDBUF/SO_RCVBUF). See issue #18643 +# for a discussion of this number). +SOCK_MAX_SIZE = 16 * 1024 * 1024 + 1 + +# # decorator for skipping tests on non-IEEE 754 platforms +# requires_IEEE_754 = unittest.skipUnless( +# float.__getformat__("double").startswith("IEEE"), +# "test requires IEEE 754 doubles") + +requires_zlib = unittest.skipUnless(zlib, 'requires zlib') + +requires_bz2 = unittest.skipUnless(bz2, 'requires bz2') + +requires_lzma = unittest.skipUnless(lzma, 'requires lzma') + +is_jython = sys.platform.startswith('java') + +# Filename used for testing +if os.name == 'java': + # Jython disallows @ in module names + TESTFN = '$test' +else: + TESTFN = '@test' + +# Disambiguate TESTFN for parallel testing, while letting it remain a valid +# module name. +TESTFN = "{0}_{1}_tmp".format(TESTFN, os.getpid()) + +# # FS_NONASCII: non-ASCII character encodable by os.fsencode(), +# # or None if there is no such character. +# FS_NONASCII = None +# for character in ( +# # First try printable and common characters to have a readable filename. +# # For each character, the encoding list are just example of encodings able +# # to encode the character (the list is not exhaustive). +# +# # U+00E6 (Latin Small Letter Ae): cp1252, iso-8859-1 +# '\u00E6', +# # U+0130 (Latin Capital Letter I With Dot Above): cp1254, iso8859_3 +# '\u0130', +# # U+0141 (Latin Capital Letter L With Stroke): cp1250, cp1257 +# '\u0141', +# # U+03C6 (Greek Small Letter Phi): cp1253 +# '\u03C6', +# # U+041A (Cyrillic Capital Letter Ka): cp1251 +# '\u041A', +# # U+05D0 (Hebrew Letter Alef): Encodable to cp424 +# '\u05D0', +# # U+060C (Arabic Comma): cp864, cp1006, iso8859_6, mac_arabic +# '\u060C', +# # U+062A (Arabic Letter Teh): cp720 +# '\u062A', +# # U+0E01 (Thai Character Ko Kai): cp874 +# '\u0E01', +# +# # Then try more "special" characters. "special" because they may be +# # interpreted or displayed differently depending on the exact locale +# # encoding and the font. +# +# # U+00A0 (No-Break Space) +# '\u00A0', +# # U+20AC (Euro Sign) +# '\u20AC', +# ): +# try: +# os.fsdecode(os.fsencode(character)) +# except UnicodeError: +# pass +# else: +# FS_NONASCII = character +# break +# +# # TESTFN_UNICODE is a non-ascii filename +# TESTFN_UNICODE = TESTFN + "-\xe0\xf2\u0258\u0141\u011f" +# if sys.platform == 'darwin': +# # In Mac OS X's VFS API file names are, by definition, canonically +# # decomposed Unicode, encoded using UTF-8. See QA1173: +# # http://developer.apple.com/mac/library/qa/qa2001/qa1173.html +# import unicodedata +# TESTFN_UNICODE = unicodedata.normalize('NFD', TESTFN_UNICODE) +# TESTFN_ENCODING = sys.getfilesystemencoding() +# +# # TESTFN_UNENCODABLE is a filename (str type) that should *not* be able to be +# # encoded by the filesystem encoding (in strict mode). It can be None if we +# # cannot generate such filename. +# TESTFN_UNENCODABLE = None +# if os.name in ('nt', 'ce'): +# # skip win32s (0) or Windows 9x/ME (1) +# if sys.getwindowsversion().platform >= 2: +# # Different kinds of characters from various languages to minimize the +# # probability that the whole name is encodable to MBCS (issue #9819) +# TESTFN_UNENCODABLE = TESTFN + "-\u5171\u0141\u2661\u0363\uDC80" +# try: +# TESTFN_UNENCODABLE.encode(TESTFN_ENCODING) +# except UnicodeEncodeError: +# pass +# else: +# print('WARNING: The filename %r CAN be encoded by the filesystem encoding (%s). ' +# 'Unicode filename tests may not be effective' +# % (TESTFN_UNENCODABLE, TESTFN_ENCODING)) +# TESTFN_UNENCODABLE = None +# # Mac OS X denies unencodable filenames (invalid utf-8) +# elif sys.platform != 'darwin': +# try: +# # ascii and utf-8 cannot encode the byte 0xff +# b'\xff'.decode(TESTFN_ENCODING) +# except UnicodeDecodeError: +# # 0xff will be encoded using the surrogate character u+DCFF +# TESTFN_UNENCODABLE = TESTFN \ +# + b'-\xff'.decode(TESTFN_ENCODING, 'surrogateescape') +# else: +# # File system encoding (eg. ISO-8859-* encodings) can encode +# # the byte 0xff. Skip some unicode filename tests. +# pass +# +# # TESTFN_UNDECODABLE is a filename (bytes type) that should *not* be able to be +# # decoded from the filesystem encoding (in strict mode). It can be None if we +# # cannot generate such filename (ex: the latin1 encoding can decode any byte +# # sequence). On UNIX, TESTFN_UNDECODABLE can be decoded by os.fsdecode() thanks +# # to the surrogateescape error handler (PEP 383), but not from the filesystem +# # encoding in strict mode. +# TESTFN_UNDECODABLE = None +# for name in ( +# # b'\xff' is not decodable by os.fsdecode() with code page 932. Windows +# # accepts it to create a file or a directory, or don't accept to enter to +# # such directory (when the bytes name is used). So test b'\xe7' first: it is +# # not decodable from cp932. +# b'\xe7w\xf0', +# # undecodable from ASCII, UTF-8 +# b'\xff', +# # undecodable from iso8859-3, iso8859-6, iso8859-7, cp424, iso8859-8, cp856 +# # and cp857 +# b'\xae\xd5' +# # undecodable from UTF-8 (UNIX and Mac OS X) +# b'\xed\xb2\x80', b'\xed\xb4\x80', +# # undecodable from shift_jis, cp869, cp874, cp932, cp1250, cp1251, cp1252, +# # cp1253, cp1254, cp1255, cp1257, cp1258 +# b'\x81\x98', +# ): +# try: +# name.decode(TESTFN_ENCODING) +# except UnicodeDecodeError: +# TESTFN_UNDECODABLE = os.fsencode(TESTFN) + name +# break +# +# if FS_NONASCII: +# TESTFN_NONASCII = TESTFN + '-' + FS_NONASCII +# else: +# TESTFN_NONASCII = None + +# Save the initial cwd +SAVEDCWD = os.getcwd() + +@contextlib.contextmanager +def temp_cwd(name='tempcwd', quiet=False, path=None): + """ + Context manager that temporarily changes the CWD. + + An existing path may be provided as *path*, in which case this + function makes no changes to the file system. + + Otherwise, the new CWD is created in the current directory and it's + named *name*. If *quiet* is False (default) and it's not possible to + create or change the CWD, an error is raised. If it's True, only a + warning is raised and the original CWD is used. + """ + saved_dir = os.getcwd() + is_temporary = False + if path is None: + path = name + try: + os.mkdir(name) + is_temporary = True + except OSError: + if not quiet: + raise + warnings.warn('tests may fail, unable to create temp CWD ' + name, + RuntimeWarning, stacklevel=3) + try: + os.chdir(path) + except OSError: + if not quiet: + raise + warnings.warn('tests may fail, unable to change the CWD to ' + path, + RuntimeWarning, stacklevel=3) + try: + yield os.getcwd() + finally: + os.chdir(saved_dir) + if is_temporary: + rmtree(name) + + +if hasattr(os, "umask"): + @contextlib.contextmanager + def temp_umask(umask): + """Context manager that temporarily sets the process umask.""" + oldmask = os.umask(umask) + try: + yield + finally: + os.umask(oldmask) + + +def findfile(file, here=__file__, subdir=None): + """Try to find a file on sys.path and the working directory. If it is not + found the argument passed to the function is returned (this does not + necessarily signal failure; could still be the legitimate path).""" + if os.path.isabs(file): + return file + if subdir is not None: + file = os.path.join(subdir, file) + path = sys.path + path = [os.path.dirname(here)] + path + for dn in path: + fn = os.path.join(dn, file) + if os.path.exists(fn): return fn + return file + +def create_empty_file(filename): + """Create an empty file. If the file already exists, truncate it.""" + fd = os.open(filename, os.O_WRONLY | os.O_CREAT | os.O_TRUNC) + os.close(fd) + +def sortdict(dict): + "Like repr(dict), but in sorted order." + items = sorted(dict.items()) + reprpairs = ["%r: %r" % pair for pair in items] + withcommas = ", ".join(reprpairs) + return "{%s}" % withcommas + +def make_bad_fd(): + """ + Create an invalid file descriptor by opening and closing a file and return + its fd. + """ + file = open(TESTFN, "wb") + try: + return file.fileno() + finally: + file.close() + unlink(TESTFN) + +def check_syntax_error(testcase, statement): + testcase.assertRaises(SyntaxError, compile, statement, + '', 'exec') + +def open_urlresource(url, *args, **kw): + from future.backports.urllib import (request as urllib_request, + parse as urllib_parse) + + check = kw.pop('check', None) + + filename = urllib_parse.urlparse(url)[2].split('/')[-1] # '/': it's URL! + + fn = os.path.join(os.path.dirname(__file__), "data", filename) + + def check_valid_file(fn): + f = open(fn, *args, **kw) + if check is None: + return f + elif check(f): + f.seek(0) + return f + f.close() + + if os.path.exists(fn): + f = check_valid_file(fn) + if f is not None: + return f + unlink(fn) + + # Verify the requirement before downloading the file + requires('urlfetch') + + print('\tfetching %s ...' % url, file=get_original_stdout()) + f = urllib_request.urlopen(url, timeout=15) + try: + with open(fn, "wb") as out: + s = f.read() + while s: + out.write(s) + s = f.read() + finally: + f.close() + + f = check_valid_file(fn) + if f is not None: + return f + raise TestFailed('invalid resource %r' % fn) + + +class WarningsRecorder(object): + """Convenience wrapper for the warnings list returned on + entry to the warnings.catch_warnings() context manager. + """ + def __init__(self, warnings_list): + self._warnings = warnings_list + self._last = 0 + + def __getattr__(self, attr): + if len(self._warnings) > self._last: + return getattr(self._warnings[-1], attr) + elif attr in warnings.WarningMessage._WARNING_DETAILS: + return None + raise AttributeError("%r has no attribute %r" % (self, attr)) + + @property + def warnings(self): + return self._warnings[self._last:] + + def reset(self): + self._last = len(self._warnings) + + +def _filterwarnings(filters, quiet=False): + """Catch the warnings, then check if all the expected + warnings have been raised and re-raise unexpected warnings. + If 'quiet' is True, only re-raise the unexpected warnings. + """ + # Clear the warning registry of the calling module + # in order to re-raise the warnings. + frame = sys._getframe(2) + registry = frame.f_globals.get('__warningregistry__') + if registry: + if utils.PY3: + registry.clear() + else: + # Py2-compatible: + for i in range(len(registry)): + registry.pop() + with warnings.catch_warnings(record=True) as w: + # Set filter "always" to record all warnings. Because + # test_warnings swap the module, we need to look up in + # the sys.modules dictionary. + sys.modules['warnings'].simplefilter("always") + yield WarningsRecorder(w) + # Filter the recorded warnings + reraise = list(w) + missing = [] + for msg, cat in filters: + seen = False + for w in reraise[:]: + warning = w.message + # Filter out the matching messages + if (re.match(msg, str(warning), re.I) and + issubclass(warning.__class__, cat)): + seen = True + reraise.remove(w) + if not seen and not quiet: + # This filter caught nothing + missing.append((msg, cat.__name__)) + if reraise: + raise AssertionError("unhandled warning %s" % reraise[0]) + if missing: + raise AssertionError("filter (%r, %s) did not catch any warning" % + missing[0]) + + +@contextlib.contextmanager +def check_warnings(*filters, **kwargs): + """Context manager to silence warnings. + + Accept 2-tuples as positional arguments: + ("message regexp", WarningCategory) + + Optional argument: + - if 'quiet' is True, it does not fail if a filter catches nothing + (default True without argument, + default False if some filters are defined) + + Without argument, it defaults to: + check_warnings(("", Warning), quiet=True) + """ + quiet = kwargs.get('quiet') + if not filters: + filters = (("", Warning),) + # Preserve backward compatibility + if quiet is None: + quiet = True + return _filterwarnings(filters, quiet) + + +class CleanImport(object): + """Context manager to force import to return a new module reference. + + This is useful for testing module-level behaviours, such as + the emission of a DeprecationWarning on import. + + Use like this: + + with CleanImport("foo"): + importlib.import_module("foo") # new reference + """ + + def __init__(self, *module_names): + self.original_modules = sys.modules.copy() + for module_name in module_names: + if module_name in sys.modules: + module = sys.modules[module_name] + # It is possible that module_name is just an alias for + # another module (e.g. stub for modules renamed in 3.x). + # In that case, we also need delete the real module to clear + # the import cache. + if module.__name__ != module_name: + del sys.modules[module.__name__] + del sys.modules[module_name] + + def __enter__(self): + return self + + def __exit__(self, *ignore_exc): + sys.modules.update(self.original_modules) + +### Added for python-future: +if utils.PY3: + import collections.abc + mybase = collections.abc.MutableMapping +else: + import UserDict + mybase = UserDict.DictMixin +### + +class EnvironmentVarGuard(mybase): + + """Class to help protect the environment variable properly. Can be used as + a context manager.""" + + def __init__(self): + self._environ = os.environ + self._changed = {} + + def __getitem__(self, envvar): + return self._environ[envvar] + + def __setitem__(self, envvar, value): + # Remember the initial value on the first access + if envvar not in self._changed: + self._changed[envvar] = self._environ.get(envvar) + self._environ[envvar] = value + + def __delitem__(self, envvar): + # Remember the initial value on the first access + if envvar not in self._changed: + self._changed[envvar] = self._environ.get(envvar) + if envvar in self._environ: + del self._environ[envvar] + + def keys(self): + return self._environ.keys() + + def __iter__(self): + return iter(self._environ) + + def __len__(self): + return len(self._environ) + + def set(self, envvar, value): + self[envvar] = value + + def unset(self, envvar): + del self[envvar] + + def __enter__(self): + return self + + def __exit__(self, *ignore_exc): + for (k, v) in self._changed.items(): + if v is None: + if k in self._environ: + del self._environ[k] + else: + self._environ[k] = v + os.environ = self._environ + + +class DirsOnSysPath(object): + """Context manager to temporarily add directories to sys.path. + + This makes a copy of sys.path, appends any directories given + as positional arguments, then reverts sys.path to the copied + settings when the context ends. + + Note that *all* sys.path modifications in the body of the + context manager, including replacement of the object, + will be reverted at the end of the block. + """ + + def __init__(self, *paths): + self.original_value = sys.path[:] + self.original_object = sys.path + sys.path.extend(paths) + + def __enter__(self): + return self + + def __exit__(self, *ignore_exc): + sys.path = self.original_object + sys.path[:] = self.original_value + + +class TransientResource(object): + + """Raise ResourceDenied if an exception is raised while the context manager + is in effect that matches the specified exception and attributes.""" + + def __init__(self, exc, **kwargs): + self.exc = exc + self.attrs = kwargs + + def __enter__(self): + return self + + def __exit__(self, type_=None, value=None, traceback=None): + """If type_ is a subclass of self.exc and value has attributes matching + self.attrs, raise ResourceDenied. Otherwise let the exception + propagate (if any).""" + if type_ is not None and issubclass(self.exc, type_): + for attr, attr_value in self.attrs.items(): + if not hasattr(value, attr): + break + if getattr(value, attr) != attr_value: + break + else: + raise ResourceDenied("an optional resource is not available") + +# Context managers that raise ResourceDenied when various issues +# with the Internet connection manifest themselves as exceptions. +# XXX deprecate these and use transient_internet() instead +time_out = TransientResource(IOError, errno=errno.ETIMEDOUT) +socket_peer_reset = TransientResource(socket.error, errno=errno.ECONNRESET) +ioerror_peer_reset = TransientResource(IOError, errno=errno.ECONNRESET) + + +@contextlib.contextmanager +def transient_internet(resource_name, timeout=30.0, errnos=()): + """Return a context manager that raises ResourceDenied when various issues + with the Internet connection manifest themselves as exceptions.""" + default_errnos = [ + ('ECONNREFUSED', 111), + ('ECONNRESET', 104), + ('EHOSTUNREACH', 113), + ('ENETUNREACH', 101), + ('ETIMEDOUT', 110), + ] + default_gai_errnos = [ + ('EAI_AGAIN', -3), + ('EAI_FAIL', -4), + ('EAI_NONAME', -2), + ('EAI_NODATA', -5), + # Encountered when trying to resolve IPv6-only hostnames + ('WSANO_DATA', 11004), + ] + + denied = ResourceDenied("Resource %r is not available" % resource_name) + captured_errnos = errnos + gai_errnos = [] + if not captured_errnos: + captured_errnos = [getattr(errno, name, num) + for (name, num) in default_errnos] + gai_errnos = [getattr(socket, name, num) + for (name, num) in default_gai_errnos] + + def filter_error(err): + n = getattr(err, 'errno', None) + if (isinstance(err, socket.timeout) or + (isinstance(err, socket.gaierror) and n in gai_errnos) or + n in captured_errnos): + if not verbose: + sys.stderr.write(denied.args[0] + "\n") + # Was: raise denied from err + # For Python-Future: + exc = denied + exc.__cause__ = err + raise exc + + old_timeout = socket.getdefaulttimeout() + try: + if timeout is not None: + socket.setdefaulttimeout(timeout) + yield + except IOError as err: + # urllib can wrap original socket errors multiple times (!), we must + # unwrap to get at the original error. + while True: + a = err.args + if len(a) >= 1 and isinstance(a[0], IOError): + err = a[0] + # The error can also be wrapped as args[1]: + # except socket.error as msg: + # raise IOError('socket error', msg).with_traceback(sys.exc_info()[2]) + elif len(a) >= 2 and isinstance(a[1], IOError): + err = a[1] + else: + break + filter_error(err) + raise + # XXX should we catch generic exceptions and look for their + # __cause__ or __context__? + finally: + socket.setdefaulttimeout(old_timeout) + + +@contextlib.contextmanager +def captured_output(stream_name): + """Return a context manager used by captured_stdout/stdin/stderr + that temporarily replaces the sys stream *stream_name* with a StringIO.""" + import io + orig_stdout = getattr(sys, stream_name) + setattr(sys, stream_name, io.StringIO()) + try: + yield getattr(sys, stream_name) + finally: + setattr(sys, stream_name, orig_stdout) + +def captured_stdout(): + """Capture the output of sys.stdout: + + with captured_stdout() as s: + print("hello") + self.assertEqual(s.getvalue(), "hello") + """ + return captured_output("stdout") + +def captured_stderr(): + return captured_output("stderr") + +def captured_stdin(): + return captured_output("stdin") + + +def gc_collect(): + """Force as many objects as possible to be collected. + + In non-CPython implementations of Python, this is needed because timely + deallocation is not guaranteed by the garbage collector. (Even in CPython + this can be the case in case of reference cycles.) This means that __del__ + methods may be called later than expected and weakrefs may remain alive for + longer than expected. This function tries its best to force all garbage + objects to disappear. + """ + gc.collect() + if is_jython: + time.sleep(0.1) + gc.collect() + gc.collect() + +@contextlib.contextmanager +def disable_gc(): + have_gc = gc.isenabled() + gc.disable() + try: + yield + finally: + if have_gc: + gc.enable() + + +def python_is_optimized(): + """Find if Python was built with optimizations.""" + # We don't have sysconfig on Py2.6: + import sysconfig + cflags = sysconfig.get_config_var('PY_CFLAGS') or '' + final_opt = "" + for opt in cflags.split(): + if opt.startswith('-O'): + final_opt = opt + return final_opt != '' and final_opt != '-O0' + + +_header = 'nP' +_align = '0n' +if hasattr(sys, "gettotalrefcount"): + _header = '2P' + _header + _align = '0P' +_vheader = _header + 'n' + +def calcobjsize(fmt): + return struct.calcsize(_header + fmt + _align) + +def calcvobjsize(fmt): + return struct.calcsize(_vheader + fmt + _align) + + +_TPFLAGS_HAVE_GC = 1<<14 +_TPFLAGS_HEAPTYPE = 1<<9 + +def check_sizeof(test, o, size): + result = sys.getsizeof(o) + # add GC header size + if ((type(o) == type) and (o.__flags__ & _TPFLAGS_HEAPTYPE) or\ + ((type(o) != type) and (type(o).__flags__ & _TPFLAGS_HAVE_GC))): + size += _testcapi.SIZEOF_PYGC_HEAD + msg = 'wrong size for %s: got %d, expected %d' \ + % (type(o), result, size) + test.assertEqual(result, size, msg) + +#======================================================================= +# Decorator for running a function in a different locale, correctly resetting +# it afterwards. + +def run_with_locale(catstr, *locales): + def decorator(func): + def inner(*args, **kwds): + try: + import locale + category = getattr(locale, catstr) + orig_locale = locale.setlocale(category) + except AttributeError: + # if the test author gives us an invalid category string + raise + except: + # cannot retrieve original locale, so do nothing + locale = orig_locale = None + else: + for loc in locales: + try: + locale.setlocale(category, loc) + break + except: + pass + + # now run the function, resetting the locale on exceptions + try: + return func(*args, **kwds) + finally: + if locale and orig_locale: + locale.setlocale(category, orig_locale) + inner.__name__ = func.__name__ + inner.__doc__ = func.__doc__ + return inner + return decorator + +#======================================================================= +# Decorator for running a function in a specific timezone, correctly +# resetting it afterwards. + +def run_with_tz(tz): + def decorator(func): + def inner(*args, **kwds): + try: + tzset = time.tzset + except AttributeError: + raise unittest.SkipTest("tzset required") + if 'TZ' in os.environ: + orig_tz = os.environ['TZ'] + else: + orig_tz = None + os.environ['TZ'] = tz + tzset() + + # now run the function, resetting the tz on exceptions + try: + return func(*args, **kwds) + finally: + if orig_tz is None: + del os.environ['TZ'] + else: + os.environ['TZ'] = orig_tz + time.tzset() + + inner.__name__ = func.__name__ + inner.__doc__ = func.__doc__ + return inner + return decorator + +#======================================================================= +# Big-memory-test support. Separate from 'resources' because memory use +# should be configurable. + +# Some handy shorthands. Note that these are used for byte-limits as well +# as size-limits, in the various bigmem tests +_1M = 1024*1024 +_1G = 1024 * _1M +_2G = 2 * _1G +_4G = 4 * _1G + +MAX_Py_ssize_t = sys.maxsize + +def set_memlimit(limit): + global max_memuse + global real_max_memuse + sizes = { + 'k': 1024, + 'm': _1M, + 'g': _1G, + 't': 1024*_1G, + } + m = re.match(r'(\d+(\.\d+)?) (K|M|G|T)b?$', limit, + re.IGNORECASE | re.VERBOSE) + if m is None: + raise ValueError('Invalid memory limit %r' % (limit,)) + memlimit = int(float(m.group(1)) * sizes[m.group(3).lower()]) + real_max_memuse = memlimit + if memlimit > MAX_Py_ssize_t: + memlimit = MAX_Py_ssize_t + if memlimit < _2G - 1: + raise ValueError('Memory limit %r too low to be useful' % (limit,)) + max_memuse = memlimit + +class _MemoryWatchdog(object): + """An object which periodically watches the process' memory consumption + and prints it out. + """ + + def __init__(self): + self.procfile = '/proc/{pid}/statm'.format(pid=os.getpid()) + self.started = False + + def start(self): + try: + f = open(self.procfile, 'r') + except OSError as e: + warnings.warn('/proc not available for stats: {0}'.format(e), + RuntimeWarning) + sys.stderr.flush() + return + + watchdog_script = findfile("memory_watchdog.py") + self.mem_watchdog = subprocess.Popen([sys.executable, watchdog_script], + stdin=f, stderr=subprocess.DEVNULL) + f.close() + self.started = True + + def stop(self): + if self.started: + self.mem_watchdog.terminate() + self.mem_watchdog.wait() + + +def bigmemtest(size, memuse, dry_run=True): + """Decorator for bigmem tests. + + 'minsize' is the minimum useful size for the test (in arbitrary, + test-interpreted units.) 'memuse' is the number of 'bytes per size' for + the test, or a good estimate of it. + + if 'dry_run' is False, it means the test doesn't support dummy runs + when -M is not specified. + """ + def decorator(f): + def wrapper(self): + size = wrapper.size + memuse = wrapper.memuse + if not real_max_memuse: + maxsize = 5147 + else: + maxsize = size + + if ((real_max_memuse or not dry_run) + and real_max_memuse < maxsize * memuse): + raise unittest.SkipTest( + "not enough memory: %.1fG minimum needed" + % (size * memuse / (1024 ** 3))) + + if real_max_memuse and verbose: + print() + print(" ... expected peak memory use: {peak:.1f}G" + .format(peak=size * memuse / (1024 ** 3))) + watchdog = _MemoryWatchdog() + watchdog.start() + else: + watchdog = None + + try: + return f(self, maxsize) + finally: + if watchdog: + watchdog.stop() + + wrapper.size = size + wrapper.memuse = memuse + return wrapper + return decorator + +def bigaddrspacetest(f): + """Decorator for tests that fill the address space.""" + def wrapper(self): + if max_memuse < MAX_Py_ssize_t: + if MAX_Py_ssize_t >= 2**63 - 1 and max_memuse >= 2**31: + raise unittest.SkipTest( + "not enough memory: try a 32-bit build instead") + else: + raise unittest.SkipTest( + "not enough memory: %.1fG minimum needed" + % (MAX_Py_ssize_t / (1024 ** 3))) + else: + return f(self) + return wrapper + +#======================================================================= +# unittest integration. + +class BasicTestRunner(object): + def run(self, test): + result = unittest.TestResult() + test(result) + return result + +def _id(obj): + return obj + +def requires_resource(resource): + if resource == 'gui' and not _is_gui_available(): + return unittest.skip("resource 'gui' is not available") + if is_resource_enabled(resource): + return _id + else: + return unittest.skip("resource {0!r} is not enabled".format(resource)) + +def cpython_only(test): + """ + Decorator for tests only applicable on CPython. + """ + return impl_detail(cpython=True)(test) + +def impl_detail(msg=None, **guards): + if check_impl_detail(**guards): + return _id + if msg is None: + guardnames, default = _parse_guards(guards) + if default: + msg = "implementation detail not available on {0}" + else: + msg = "implementation detail specific to {0}" + guardnames = sorted(guardnames.keys()) + msg = msg.format(' or '.join(guardnames)) + return unittest.skip(msg) + +def _parse_guards(guards): + # Returns a tuple ({platform_name: run_me}, default_value) + if not guards: + return ({'cpython': True}, False) + is_true = list(guards.values())[0] + assert list(guards.values()) == [is_true] * len(guards) # all True or all False + return (guards, not is_true) + +# Use the following check to guard CPython's implementation-specific tests -- +# or to run them only on the implementation(s) guarded by the arguments. +def check_impl_detail(**guards): + """This function returns True or False depending on the host platform. + Examples: + if check_impl_detail(): # only on CPython (default) + if check_impl_detail(jython=True): # only on Jython + if check_impl_detail(cpython=False): # everywhere except on CPython + """ + guards, default = _parse_guards(guards) + return guards.get(platform.python_implementation().lower(), default) + + +def no_tracing(func): + """Decorator to temporarily turn off tracing for the duration of a test.""" + if not hasattr(sys, 'gettrace'): + return func + else: + @functools.wraps(func) + def wrapper(*args, **kwargs): + original_trace = sys.gettrace() + try: + sys.settrace(None) + return func(*args, **kwargs) + finally: + sys.settrace(original_trace) + return wrapper + + +def refcount_test(test): + """Decorator for tests which involve reference counting. + + To start, the decorator does not run the test if is not run by CPython. + After that, any trace function is unset during the test to prevent + unexpected refcounts caused by the trace function. + + """ + return no_tracing(cpython_only(test)) + + +def _filter_suite(suite, pred): + """Recursively filter test cases in a suite based on a predicate.""" + newtests = [] + for test in suite._tests: + if isinstance(test, unittest.TestSuite): + _filter_suite(test, pred) + newtests.append(test) + else: + if pred(test): + newtests.append(test) + suite._tests = newtests + +def _run_suite(suite): + """Run tests from a unittest.TestSuite-derived class.""" + if verbose: + runner = unittest.TextTestRunner(sys.stdout, verbosity=2, + failfast=failfast) + else: + runner = BasicTestRunner() + + result = runner.run(suite) + if not result.wasSuccessful(): + if len(result.errors) == 1 and not result.failures: + err = result.errors[0][1] + elif len(result.failures) == 1 and not result.errors: + err = result.failures[0][1] + else: + err = "multiple errors occurred" + if not verbose: err += "; run in verbose mode for details" + raise TestFailed(err) + + +def run_unittest(*classes): + """Run tests from unittest.TestCase-derived classes.""" + valid_types = (unittest.TestSuite, unittest.TestCase) + suite = unittest.TestSuite() + for cls in classes: + if isinstance(cls, str): + if cls in sys.modules: + suite.addTest(unittest.findTestCases(sys.modules[cls])) + else: + raise ValueError("str arguments must be keys in sys.modules") + elif isinstance(cls, valid_types): + suite.addTest(cls) + else: + suite.addTest(unittest.makeSuite(cls)) + def case_pred(test): + if match_tests is None: + return True + for name in test.id().split("."): + if fnmatch.fnmatchcase(name, match_tests): + return True + return False + _filter_suite(suite, case_pred) + _run_suite(suite) + +# We don't have sysconfig on Py2.6: +# #======================================================================= +# # Check for the presence of docstrings. +# +# HAVE_DOCSTRINGS = (check_impl_detail(cpython=False) or +# sys.platform == 'win32' or +# sysconfig.get_config_var('WITH_DOC_STRINGS')) +# +# requires_docstrings = unittest.skipUnless(HAVE_DOCSTRINGS, +# "test requires docstrings") +# +# +# #======================================================================= +# doctest driver. + +def run_doctest(module, verbosity=None, optionflags=0): + """Run doctest on the given module. Return (#failures, #tests). + + If optional argument verbosity is not specified (or is None), pass + support's belief about verbosity on to doctest. Else doctest's + usual behavior is used (it searches sys.argv for -v). + """ + + import doctest + + if verbosity is None: + verbosity = verbose + else: + verbosity = None + + f, t = doctest.testmod(module, verbose=verbosity, optionflags=optionflags) + if f: + raise TestFailed("%d of %d doctests failed" % (f, t)) + if verbose: + print('doctest (%s) ... %d tests with zero failures' % + (module.__name__, t)) + return f, t + + +#======================================================================= +# Support for saving and restoring the imported modules. + +def modules_setup(): + return sys.modules.copy(), + +def modules_cleanup(oldmodules): + # Encoders/decoders are registered permanently within the internal + # codec cache. If we destroy the corresponding modules their + # globals will be set to None which will trip up the cached functions. + encodings = [(k, v) for k, v in sys.modules.items() + if k.startswith('encodings.')] + # Was: + # sys.modules.clear() + # Py2-compatible: + for i in range(len(sys.modules)): + sys.modules.pop() + + sys.modules.update(encodings) + # XXX: This kind of problem can affect more than just encodings. In particular + # extension modules (such as _ssl) don't cope with reloading properly. + # Really, test modules should be cleaning out the test specific modules they + # know they added (ala test_runpy) rather than relying on this function (as + # test_importhooks and test_pkg do currently). + # Implicitly imported *real* modules should be left alone (see issue 10556). + sys.modules.update(oldmodules) + +#======================================================================= +# Backported versions of threading_setup() and threading_cleanup() which don't refer +# to threading._dangling (not available on Py2.7). + +# Threading support to prevent reporting refleaks when running regrtest.py -R + +# NOTE: we use thread._count() rather than threading.enumerate() (or the +# moral equivalent thereof) because a threading.Thread object is still alive +# until its __bootstrap() method has returned, even after it has been +# unregistered from the threading module. +# thread._count(), on the other hand, only gets decremented *after* the +# __bootstrap() method has returned, which gives us reliable reference counts +# at the end of a test run. + +def threading_setup(): + if _thread: + return _thread._count(), + else: + return 1, + +def threading_cleanup(nb_threads): + if not _thread: + return + + _MAX_COUNT = 10 + for count in range(_MAX_COUNT): + n = _thread._count() + if n == nb_threads: + break + time.sleep(0.1) + # XXX print a warning in case of failure? + +def reap_threads(func): + """Use this function when threads are being used. This will + ensure that the threads are cleaned up even when the test fails. + If threading is unavailable this function does nothing. + """ + if not _thread: + return func + + @functools.wraps(func) + def decorator(*args): + key = threading_setup() + try: + return func(*args) + finally: + threading_cleanup(*key) + return decorator + +def reap_children(): + """Use this function at the end of test_main() whenever sub-processes + are started. This will help ensure that no extra children (zombies) + stick around to hog resources and create problems when looking + for refleaks. + """ + + # Reap all our dead child processes so we don't leave zombies around. + # These hog resources and might be causing some of the buildbots to die. + if hasattr(os, 'waitpid'): + any_process = -1 + while True: + try: + # This will raise an exception on Windows. That's ok. + pid, status = os.waitpid(any_process, os.WNOHANG) + if pid == 0: + break + except: + break + +@contextlib.contextmanager +def swap_attr(obj, attr, new_val): + """Temporary swap out an attribute with a new object. + + Usage: + with swap_attr(obj, "attr", 5): + ... + + This will set obj.attr to 5 for the duration of the with: block, + restoring the old value at the end of the block. If `attr` doesn't + exist on `obj`, it will be created and then deleted at the end of the + block. + """ + if hasattr(obj, attr): + real_val = getattr(obj, attr) + setattr(obj, attr, new_val) + try: + yield + finally: + setattr(obj, attr, real_val) + else: + setattr(obj, attr, new_val) + try: + yield + finally: + delattr(obj, attr) + +@contextlib.contextmanager +def swap_item(obj, item, new_val): + """Temporary swap out an item with a new object. + + Usage: + with swap_item(obj, "item", 5): + ... + + This will set obj["item"] to 5 for the duration of the with: block, + restoring the old value at the end of the block. If `item` doesn't + exist on `obj`, it will be created and then deleted at the end of the + block. + """ + if item in obj: + real_val = obj[item] + obj[item] = new_val + try: + yield + finally: + obj[item] = real_val + else: + obj[item] = new_val + try: + yield + finally: + del obj[item] + +def strip_python_stderr(stderr): + """Strip the stderr of a Python process from potential debug output + emitted by the interpreter. + + This will typically be run on the result of the communicate() method + of a subprocess.Popen object. + """ + stderr = re.sub(br"\[\d+ refs\]\r?\n?", b"", stderr).strip() + return stderr + +def args_from_interpreter_flags(): + """Return a list of command-line arguments reproducing the current + settings in sys.flags and sys.warnoptions.""" + return subprocess._args_from_interpreter_flags() + +#============================================================ +# Support for assertions about logging. +#============================================================ + +class TestHandler(logging.handlers.BufferingHandler): + def __init__(self, matcher): + # BufferingHandler takes a "capacity" argument + # so as to know when to flush. As we're overriding + # shouldFlush anyway, we can set a capacity of zero. + # You can call flush() manually to clear out the + # buffer. + logging.handlers.BufferingHandler.__init__(self, 0) + self.matcher = matcher + + def shouldFlush(self): + return False + + def emit(self, record): + self.format(record) + self.buffer.append(record.__dict__) + + def matches(self, **kwargs): + """ + Look for a saved dict whose keys/values match the supplied arguments. + """ + result = False + for d in self.buffer: + if self.matcher.matches(d, **kwargs): + result = True + break + return result + +class Matcher(object): + + _partial_matches = ('msg', 'message') + + def matches(self, d, **kwargs): + """ + Try to match a single dict with the supplied arguments. + + Keys whose values are strings and which are in self._partial_matches + will be checked for partial (i.e. substring) matches. You can extend + this scheme to (for example) do regular expression matching, etc. + """ + result = True + for k in kwargs: + v = kwargs[k] + dv = d.get(k) + if not self.match_value(k, dv, v): + result = False + break + return result + + def match_value(self, k, dv, v): + """ + Try to match a single stored value (dv) with a supplied value (v). + """ + if type(v) != type(dv): + result = False + elif type(dv) is not str or k not in self._partial_matches: + result = (v == dv) + else: + result = dv.find(v) >= 0 + return result + + +_can_symlink = None +def can_symlink(): + global _can_symlink + if _can_symlink is not None: + return _can_symlink + symlink_path = TESTFN + "can_symlink" + try: + os.symlink(TESTFN, symlink_path) + can = True + except (OSError, NotImplementedError, AttributeError): + can = False + else: + os.remove(symlink_path) + _can_symlink = can + return can + +def skip_unless_symlink(test): + """Skip decorator for tests that require functional symlink""" + ok = can_symlink() + msg = "Requires functional symlink implementation" + return test if ok else unittest.skip(msg)(test) + +_can_xattr = None +def can_xattr(): + global _can_xattr + if _can_xattr is not None: + return _can_xattr + if not hasattr(os, "setxattr"): + can = False + else: + tmp_fp, tmp_name = tempfile.mkstemp() + try: + with open(TESTFN, "wb") as fp: + try: + # TESTFN & tempfile may use different file systems with + # different capabilities + os.setxattr(tmp_fp, b"user.test", b"") + os.setxattr(fp.fileno(), b"user.test", b"") + # Kernels < 2.6.39 don't respect setxattr flags. + kernel_version = platform.release() + m = re.match("2.6.(\d{1,2})", kernel_version) + can = m is None or int(m.group(1)) >= 39 + except OSError: + can = False + finally: + unlink(TESTFN) + unlink(tmp_name) + _can_xattr = can + return can + +def skip_unless_xattr(test): + """Skip decorator for tests that require functional extended attributes""" + ok = can_xattr() + msg = "no non-broken extended attribute support" + return test if ok else unittest.skip(msg)(test) + + +if sys.platform.startswith('win'): + @contextlib.contextmanager + def suppress_crash_popup(): + """Disable Windows Error Reporting dialogs using SetErrorMode.""" + # see http://msdn.microsoft.com/en-us/library/windows/desktop/ms680621%28v=vs.85%29.aspx + # GetErrorMode is not available on Windows XP and Windows Server 2003, + # but SetErrorMode returns the previous value, so we can use that + import ctypes + k32 = ctypes.windll.kernel32 + SEM_NOGPFAULTERRORBOX = 0x02 + old_error_mode = k32.SetErrorMode(SEM_NOGPFAULTERRORBOX) + k32.SetErrorMode(old_error_mode | SEM_NOGPFAULTERRORBOX) + try: + yield + finally: + k32.SetErrorMode(old_error_mode) +else: + # this is a no-op for other platforms + @contextlib.contextmanager + def suppress_crash_popup(): + yield + + +def patch(test_instance, object_to_patch, attr_name, new_value): + """Override 'object_to_patch'.'attr_name' with 'new_value'. + + Also, add a cleanup procedure to 'test_instance' to restore + 'object_to_patch' value for 'attr_name'. + The 'attr_name' should be a valid attribute for 'object_to_patch'. + + """ + # check that 'attr_name' is a real attribute for 'object_to_patch' + # will raise AttributeError if it does not exist + getattr(object_to_patch, attr_name) + + # keep a copy of the old value + attr_is_local = False + try: + old_value = object_to_patch.__dict__[attr_name] + except (AttributeError, KeyError): + old_value = getattr(object_to_patch, attr_name, None) + else: + attr_is_local = True + + # restore the value when the test is done + def cleanup(): + if attr_is_local: + setattr(object_to_patch, attr_name, old_value) + else: + delattr(object_to_patch, attr_name) + + test_instance.addCleanup(cleanup) + + # actually override the attribute + setattr(object_to_patch, attr_name, new_value) diff --git a/venv/lib/python3.10/site-packages/future/backports/total_ordering.py b/venv/lib/python3.10/site-packages/future/backports/total_ordering.py new file mode 100644 index 0000000..760f06d --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/total_ordering.py @@ -0,0 +1,38 @@ +""" +For Python < 2.7.2. total_ordering in versions prior to 2.7.2 is buggy. +See http://bugs.python.org/issue10042 for details. For these versions use +code borrowed from Python 2.7.3. + +From django.utils. +""" + +import sys +if sys.version_info >= (2, 7, 2): + from functools import total_ordering +else: + def total_ordering(cls): + """Class decorator that fills in missing ordering methods""" + convert = { + '__lt__': [('__gt__', lambda self, other: not (self < other or self == other)), + ('__le__', lambda self, other: self < other or self == other), + ('__ge__', lambda self, other: not self < other)], + '__le__': [('__ge__', lambda self, other: not self <= other or self == other), + ('__lt__', lambda self, other: self <= other and not self == other), + ('__gt__', lambda self, other: not self <= other)], + '__gt__': [('__lt__', lambda self, other: not (self > other or self == other)), + ('__ge__', lambda self, other: self > other or self == other), + ('__le__', lambda self, other: not self > other)], + '__ge__': [('__le__', lambda self, other: (not self >= other) or self == other), + ('__gt__', lambda self, other: self >= other and not self == other), + ('__lt__', lambda self, other: not self >= other)] + } + roots = set(dir(cls)) & set(convert) + if not roots: + raise ValueError('must define at least one ordering operation: < > <= >=') + root = max(roots) # prefer __lt__ to __le__ to __gt__ to __ge__ + for opname, opfunc in convert[root]: + if opname not in roots: + opfunc.__name__ = opname + opfunc.__doc__ = getattr(int, opname).__doc__ + setattr(cls, opname, opfunc) + return cls diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/__init__.py b/venv/lib/python3.10/site-packages/future/backports/urllib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..483d645 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/error.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/error.cpython-310.pyc new file mode 100644 index 0000000..f320481 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/error.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/parse.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/parse.cpython-310.pyc new file mode 100644 index 0000000..0f23bfe Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/parse.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/request.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/request.cpython-310.pyc new file mode 100644 index 0000000..5176d65 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/request.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/response.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/response.cpython-310.pyc new file mode 100644 index 0000000..d2691c7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/response.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/robotparser.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/robotparser.cpython-310.pyc new file mode 100644 index 0000000..d4be30e Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/urllib/__pycache__/robotparser.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/error.py b/venv/lib/python3.10/site-packages/future/backports/urllib/error.py new file mode 100644 index 0000000..a473e44 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/urllib/error.py @@ -0,0 +1,75 @@ +"""Exception classes raised by urllib. + +The base exception class is URLError, which inherits from IOError. It +doesn't define any behavior of its own, but is the base class for all +exceptions defined in this package. + +HTTPError is an exception class that is also a valid HTTP response +instance. It behaves this way because HTTP protocol errors are valid +responses, with a status code, headers, and a body. In some contexts, +an application may want to handle an exception like a regular +response. +""" +from __future__ import absolute_import, division, unicode_literals +from future import standard_library + +from future.backports.urllib import response as urllib_response + + +__all__ = ['URLError', 'HTTPError', 'ContentTooShortError'] + + +# do these error classes make sense? +# make sure all of the IOError stuff is overridden. we just want to be +# subtypes. + +class URLError(IOError): + # URLError is a sub-type of IOError, but it doesn't share any of + # the implementation. need to override __init__ and __str__. + # It sets self.args for compatibility with other EnvironmentError + # subclasses, but args doesn't have the typical format with errno in + # slot 0 and strerror in slot 1. This may be better than nothing. + def __init__(self, reason, filename=None): + self.args = reason, + self.reason = reason + if filename is not None: + self.filename = filename + + def __str__(self): + return '' % self.reason + +class HTTPError(URLError, urllib_response.addinfourl): + """Raised when HTTP error occurs, but also acts like non-error return""" + __super_init = urllib_response.addinfourl.__init__ + + def __init__(self, url, code, msg, hdrs, fp): + self.code = code + self.msg = msg + self.hdrs = hdrs + self.fp = fp + self.filename = url + # The addinfourl classes depend on fp being a valid file + # object. In some cases, the HTTPError may not have a valid + # file object. If this happens, the simplest workaround is to + # not initialize the base classes. + if fp is not None: + self.__super_init(fp, hdrs, url, code) + + def __str__(self): + return 'HTTP Error %s: %s' % (self.code, self.msg) + + # since URLError specifies a .reason attribute, HTTPError should also + # provide this attribute. See issue13211 for discussion. + @property + def reason(self): + return self.msg + + def info(self): + return self.hdrs + + +# exception raised when downloaded size does not match content-length +class ContentTooShortError(URLError): + def __init__(self, message, content): + URLError.__init__(self, message) + self.content = content diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/parse.py b/venv/lib/python3.10/site-packages/future/backports/urllib/parse.py new file mode 100644 index 0000000..04e52d4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/urllib/parse.py @@ -0,0 +1,991 @@ +""" +Ported using Python-Future from the Python 3.3 standard library. + +Parse (absolute and relative) URLs. + +urlparse module is based upon the following RFC specifications. + +RFC 3986 (STD66): "Uniform Resource Identifiers" by T. Berners-Lee, R. Fielding +and L. Masinter, January 2005. + +RFC 2732 : "Format for Literal IPv6 Addresses in URL's by R.Hinden, B.Carpenter +and L.Masinter, December 1999. + +RFC 2396: "Uniform Resource Identifiers (URI)": Generic Syntax by T. +Berners-Lee, R. Fielding, and L. Masinter, August 1998. + +RFC 2368: "The mailto URL scheme", by P.Hoffman , L Masinter, J. Zawinski, July 1998. + +RFC 1808: "Relative Uniform Resource Locators", by R. Fielding, UC Irvine, June +1995. + +RFC 1738: "Uniform Resource Locators (URL)" by T. Berners-Lee, L. Masinter, M. +McCahill, December 1994 + +RFC 3986 is considered the current standard and any future changes to +urlparse module should conform with it. The urlparse module is +currently not entirely compliant with this RFC due to defacto +scenarios for parsing, and for backward compatibility purposes, some +parsing quirks from older RFCs are retained. The testcases in +test_urlparse.py provides a good indicator of parsing behavior. +""" +from __future__ import absolute_import, division, unicode_literals +from future.builtins import bytes, chr, dict, int, range, str +from future.utils import raise_with_traceback + +import re +import sys +import collections + +__all__ = ["urlparse", "urlunparse", "urljoin", "urldefrag", + "urlsplit", "urlunsplit", "urlencode", "parse_qs", + "parse_qsl", "quote", "quote_plus", "quote_from_bytes", + "unquote", "unquote_plus", "unquote_to_bytes"] + +# A classification of schemes ('' means apply by default) +uses_relative = ['ftp', 'http', 'gopher', 'nntp', 'imap', + 'wais', 'file', 'https', 'shttp', 'mms', + 'prospero', 'rtsp', 'rtspu', '', 'sftp', + 'svn', 'svn+ssh'] +uses_netloc = ['ftp', 'http', 'gopher', 'nntp', 'telnet', + 'imap', 'wais', 'file', 'mms', 'https', 'shttp', + 'snews', 'prospero', 'rtsp', 'rtspu', 'rsync', '', + 'svn', 'svn+ssh', 'sftp', 'nfs', 'git', 'git+ssh'] +uses_params = ['ftp', 'hdl', 'prospero', 'http', 'imap', + 'https', 'shttp', 'rtsp', 'rtspu', 'sip', 'sips', + 'mms', '', 'sftp', 'tel'] + +# These are not actually used anymore, but should stay for backwards +# compatibility. (They are undocumented, but have a public-looking name.) +non_hierarchical = ['gopher', 'hdl', 'mailto', 'news', + 'telnet', 'wais', 'imap', 'snews', 'sip', 'sips'] +uses_query = ['http', 'wais', 'imap', 'https', 'shttp', 'mms', + 'gopher', 'rtsp', 'rtspu', 'sip', 'sips', ''] +uses_fragment = ['ftp', 'hdl', 'http', 'gopher', 'news', + 'nntp', 'wais', 'https', 'shttp', 'snews', + 'file', 'prospero', ''] + +# Characters valid in scheme names +scheme_chars = ('abcdefghijklmnopqrstuvwxyz' + 'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + '0123456789' + '+-.') + +# XXX: Consider replacing with functools.lru_cache +MAX_CACHE_SIZE = 20 +_parse_cache = {} + +def clear_cache(): + """Clear the parse cache and the quoters cache.""" + _parse_cache.clear() + _safe_quoters.clear() + + +# Helpers for bytes handling +# For 3.2, we deliberately require applications that +# handle improperly quoted URLs to do their own +# decoding and encoding. If valid use cases are +# presented, we may relax this by using latin-1 +# decoding internally for 3.3 +_implicit_encoding = 'ascii' +_implicit_errors = 'strict' + +def _noop(obj): + return obj + +def _encode_result(obj, encoding=_implicit_encoding, + errors=_implicit_errors): + return obj.encode(encoding, errors) + +def _decode_args(args, encoding=_implicit_encoding, + errors=_implicit_errors): + return tuple(x.decode(encoding, errors) if x else '' for x in args) + +def _coerce_args(*args): + # Invokes decode if necessary to create str args + # and returns the coerced inputs along with + # an appropriate result coercion function + # - noop for str inputs + # - encoding function otherwise + str_input = isinstance(args[0], str) + for arg in args[1:]: + # We special-case the empty string to support the + # "scheme=''" default argument to some functions + if arg and isinstance(arg, str) != str_input: + raise TypeError("Cannot mix str and non-str arguments") + if str_input: + return args + (_noop,) + return _decode_args(args) + (_encode_result,) + +# Result objects are more helpful than simple tuples +class _ResultMixinStr(object): + """Standard approach to encoding parsed results from str to bytes""" + __slots__ = () + + def encode(self, encoding='ascii', errors='strict'): + return self._encoded_counterpart(*(x.encode(encoding, errors) for x in self)) + + +class _ResultMixinBytes(object): + """Standard approach to decoding parsed results from bytes to str""" + __slots__ = () + + def decode(self, encoding='ascii', errors='strict'): + return self._decoded_counterpart(*(x.decode(encoding, errors) for x in self)) + + +class _NetlocResultMixinBase(object): + """Shared methods for the parsed result objects containing a netloc element""" + __slots__ = () + + @property + def username(self): + return self._userinfo[0] + + @property + def password(self): + return self._userinfo[1] + + @property + def hostname(self): + hostname = self._hostinfo[0] + if not hostname: + hostname = None + elif hostname is not None: + hostname = hostname.lower() + return hostname + + @property + def port(self): + port = self._hostinfo[1] + if port is not None: + port = int(port, 10) + # Return None on an illegal port + if not ( 0 <= port <= 65535): + return None + return port + + +class _NetlocResultMixinStr(_NetlocResultMixinBase, _ResultMixinStr): + __slots__ = () + + @property + def _userinfo(self): + netloc = self.netloc + userinfo, have_info, hostinfo = netloc.rpartition('@') + if have_info: + username, have_password, password = userinfo.partition(':') + if not have_password: + password = None + else: + username = password = None + return username, password + + @property + def _hostinfo(self): + netloc = self.netloc + _, _, hostinfo = netloc.rpartition('@') + _, have_open_br, bracketed = hostinfo.partition('[') + if have_open_br: + hostname, _, port = bracketed.partition(']') + _, have_port, port = port.partition(':') + else: + hostname, have_port, port = hostinfo.partition(':') + if not have_port: + port = None + return hostname, port + + +class _NetlocResultMixinBytes(_NetlocResultMixinBase, _ResultMixinBytes): + __slots__ = () + + @property + def _userinfo(self): + netloc = self.netloc + userinfo, have_info, hostinfo = netloc.rpartition(b'@') + if have_info: + username, have_password, password = userinfo.partition(b':') + if not have_password: + password = None + else: + username = password = None + return username, password + + @property + def _hostinfo(self): + netloc = self.netloc + _, _, hostinfo = netloc.rpartition(b'@') + _, have_open_br, bracketed = hostinfo.partition(b'[') + if have_open_br: + hostname, _, port = bracketed.partition(b']') + _, have_port, port = port.partition(b':') + else: + hostname, have_port, port = hostinfo.partition(b':') + if not have_port: + port = None + return hostname, port + + +from collections import namedtuple + +_DefragResultBase = namedtuple('DefragResult', 'url fragment') +_SplitResultBase = namedtuple('SplitResult', 'scheme netloc path query fragment') +_ParseResultBase = namedtuple('ParseResult', 'scheme netloc path params query fragment') + +# For backwards compatibility, alias _NetlocResultMixinStr +# ResultBase is no longer part of the documented API, but it is +# retained since deprecating it isn't worth the hassle +ResultBase = _NetlocResultMixinStr + +# Structured result objects for string data +class DefragResult(_DefragResultBase, _ResultMixinStr): + __slots__ = () + def geturl(self): + if self.fragment: + return self.url + '#' + self.fragment + else: + return self.url + +class SplitResult(_SplitResultBase, _NetlocResultMixinStr): + __slots__ = () + def geturl(self): + return urlunsplit(self) + +class ParseResult(_ParseResultBase, _NetlocResultMixinStr): + __slots__ = () + def geturl(self): + return urlunparse(self) + +# Structured result objects for bytes data +class DefragResultBytes(_DefragResultBase, _ResultMixinBytes): + __slots__ = () + def geturl(self): + if self.fragment: + return self.url + b'#' + self.fragment + else: + return self.url + +class SplitResultBytes(_SplitResultBase, _NetlocResultMixinBytes): + __slots__ = () + def geturl(self): + return urlunsplit(self) + +class ParseResultBytes(_ParseResultBase, _NetlocResultMixinBytes): + __slots__ = () + def geturl(self): + return urlunparse(self) + +# Set up the encode/decode result pairs +def _fix_result_transcoding(): + _result_pairs = ( + (DefragResult, DefragResultBytes), + (SplitResult, SplitResultBytes), + (ParseResult, ParseResultBytes), + ) + for _decoded, _encoded in _result_pairs: + _decoded._encoded_counterpart = _encoded + _encoded._decoded_counterpart = _decoded + +_fix_result_transcoding() +del _fix_result_transcoding + +def urlparse(url, scheme='', allow_fragments=True): + """Parse a URL into 6 components: + :///;?# + Return a 6-tuple: (scheme, netloc, path, params, query, fragment). + Note that we don't break the components up in smaller bits + (e.g. netloc is a single string) and we don't expand % escapes.""" + url, scheme, _coerce_result = _coerce_args(url, scheme) + splitresult = urlsplit(url, scheme, allow_fragments) + scheme, netloc, url, query, fragment = splitresult + if scheme in uses_params and ';' in url: + url, params = _splitparams(url) + else: + params = '' + result = ParseResult(scheme, netloc, url, params, query, fragment) + return _coerce_result(result) + +def _splitparams(url): + if '/' in url: + i = url.find(';', url.rfind('/')) + if i < 0: + return url, '' + else: + i = url.find(';') + return url[:i], url[i+1:] + +def _splitnetloc(url, start=0): + delim = len(url) # position of end of domain part of url, default is end + for c in '/?#': # look for delimiters; the order is NOT important + wdelim = url.find(c, start) # find first of this delim + if wdelim >= 0: # if found + delim = min(delim, wdelim) # use earliest delim position + return url[start:delim], url[delim:] # return (domain, rest) + +def urlsplit(url, scheme='', allow_fragments=True): + """Parse a URL into 5 components: + :///?# + Return a 5-tuple: (scheme, netloc, path, query, fragment). + Note that we don't break the components up in smaller bits + (e.g. netloc is a single string) and we don't expand % escapes.""" + url, scheme, _coerce_result = _coerce_args(url, scheme) + allow_fragments = bool(allow_fragments) + key = url, scheme, allow_fragments, type(url), type(scheme) + cached = _parse_cache.get(key, None) + if cached: + return _coerce_result(cached) + if len(_parse_cache) >= MAX_CACHE_SIZE: # avoid runaway growth + clear_cache() + netloc = query = fragment = '' + i = url.find(':') + if i > 0: + if url[:i] == 'http': # optimize the common case + scheme = url[:i].lower() + url = url[i+1:] + if url[:2] == '//': + netloc, url = _splitnetloc(url, 2) + if (('[' in netloc and ']' not in netloc) or + (']' in netloc and '[' not in netloc)): + raise ValueError("Invalid IPv6 URL") + if allow_fragments and '#' in url: + url, fragment = url.split('#', 1) + if '?' in url: + url, query = url.split('?', 1) + v = SplitResult(scheme, netloc, url, query, fragment) + _parse_cache[key] = v + return _coerce_result(v) + for c in url[:i]: + if c not in scheme_chars: + break + else: + # make sure "url" is not actually a port number (in which case + # "scheme" is really part of the path) + rest = url[i+1:] + if not rest or any(c not in '0123456789' for c in rest): + # not a port number + scheme, url = url[:i].lower(), rest + + if url[:2] == '//': + netloc, url = _splitnetloc(url, 2) + if (('[' in netloc and ']' not in netloc) or + (']' in netloc and '[' not in netloc)): + raise ValueError("Invalid IPv6 URL") + if allow_fragments and '#' in url: + url, fragment = url.split('#', 1) + if '?' in url: + url, query = url.split('?', 1) + v = SplitResult(scheme, netloc, url, query, fragment) + _parse_cache[key] = v + return _coerce_result(v) + +def urlunparse(components): + """Put a parsed URL back together again. This may result in a + slightly different, but equivalent URL, if the URL that was parsed + originally had redundant delimiters, e.g. a ? with an empty query + (the draft states that these are equivalent).""" + scheme, netloc, url, params, query, fragment, _coerce_result = ( + _coerce_args(*components)) + if params: + url = "%s;%s" % (url, params) + return _coerce_result(urlunsplit((scheme, netloc, url, query, fragment))) + +def urlunsplit(components): + """Combine the elements of a tuple as returned by urlsplit() into a + complete URL as a string. The data argument can be any five-item iterable. + This may result in a slightly different, but equivalent URL, if the URL that + was parsed originally had unnecessary delimiters (for example, a ? with an + empty query; the RFC states that these are equivalent).""" + scheme, netloc, url, query, fragment, _coerce_result = ( + _coerce_args(*components)) + if netloc or (scheme and scheme in uses_netloc and url[:2] != '//'): + if url and url[:1] != '/': url = '/' + url + url = '//' + (netloc or '') + url + if scheme: + url = scheme + ':' + url + if query: + url = url + '?' + query + if fragment: + url = url + '#' + fragment + return _coerce_result(url) + +def urljoin(base, url, allow_fragments=True): + """Join a base URL and a possibly relative URL to form an absolute + interpretation of the latter.""" + if not base: + return url + if not url: + return base + base, url, _coerce_result = _coerce_args(base, url) + bscheme, bnetloc, bpath, bparams, bquery, bfragment = \ + urlparse(base, '', allow_fragments) + scheme, netloc, path, params, query, fragment = \ + urlparse(url, bscheme, allow_fragments) + if scheme != bscheme or scheme not in uses_relative: + return _coerce_result(url) + if scheme in uses_netloc: + if netloc: + return _coerce_result(urlunparse((scheme, netloc, path, + params, query, fragment))) + netloc = bnetloc + if path[:1] == '/': + return _coerce_result(urlunparse((scheme, netloc, path, + params, query, fragment))) + if not path and not params: + path = bpath + params = bparams + if not query: + query = bquery + return _coerce_result(urlunparse((scheme, netloc, path, + params, query, fragment))) + segments = bpath.split('/')[:-1] + path.split('/') + # XXX The stuff below is bogus in various ways... + if segments[-1] == '.': + segments[-1] = '' + while '.' in segments: + segments.remove('.') + while 1: + i = 1 + n = len(segments) - 1 + while i < n: + if (segments[i] == '..' + and segments[i-1] not in ('', '..')): + del segments[i-1:i+1] + break + i = i+1 + else: + break + if segments == ['', '..']: + segments[-1] = '' + elif len(segments) >= 2 and segments[-1] == '..': + segments[-2:] = [''] + return _coerce_result(urlunparse((scheme, netloc, '/'.join(segments), + params, query, fragment))) + +def urldefrag(url): + """Removes any existing fragment from URL. + + Returns a tuple of the defragmented URL and the fragment. If + the URL contained no fragments, the second element is the + empty string. + """ + url, _coerce_result = _coerce_args(url) + if '#' in url: + s, n, p, a, q, frag = urlparse(url) + defrag = urlunparse((s, n, p, a, q, '')) + else: + frag = '' + defrag = url + return _coerce_result(DefragResult(defrag, frag)) + +_hexdig = '0123456789ABCDEFabcdef' +_hextobyte = dict(((a + b).encode(), bytes([int(a + b, 16)])) + for a in _hexdig for b in _hexdig) + +def unquote_to_bytes(string): + """unquote_to_bytes('abc%20def') -> b'abc def'.""" + # Note: strings are encoded as UTF-8. This is only an issue if it contains + # unescaped non-ASCII characters, which URIs should not. + if not string: + # Is it a string-like object? + string.split + return bytes(b'') + if isinstance(string, str): + string = string.encode('utf-8') + ### For Python-Future: + # It is already a byte-string object, but force it to be newbytes here on + # Py2: + string = bytes(string) + ### + bits = string.split(b'%') + if len(bits) == 1: + return string + res = [bits[0]] + append = res.append + for item in bits[1:]: + try: + append(_hextobyte[item[:2]]) + append(item[2:]) + except KeyError: + append(b'%') + append(item) + return bytes(b'').join(res) + +_asciire = re.compile('([\x00-\x7f]+)') + +def unquote(string, encoding='utf-8', errors='replace'): + """Replace %xx escapes by their single-character equivalent. The optional + encoding and errors parameters specify how to decode percent-encoded + sequences into Unicode characters, as accepted by the bytes.decode() + method. + By default, percent-encoded sequences are decoded with UTF-8, and invalid + sequences are replaced by a placeholder character. + + unquote('abc%20def') -> 'abc def'. + """ + if '%' not in string: + string.split + return string + if encoding is None: + encoding = 'utf-8' + if errors is None: + errors = 'replace' + bits = _asciire.split(string) + res = [bits[0]] + append = res.append + for i in range(1, len(bits), 2): + append(unquote_to_bytes(bits[i]).decode(encoding, errors)) + append(bits[i + 1]) + return ''.join(res) + +def parse_qs(qs, keep_blank_values=False, strict_parsing=False, + encoding='utf-8', errors='replace'): + """Parse a query given as a string argument. + + Arguments: + + qs: percent-encoded query string to be parsed + + keep_blank_values: flag indicating whether blank values in + percent-encoded queries should be treated as blank strings. + A true value indicates that blanks should be retained as + blank strings. The default false value indicates that + blank values are to be ignored and treated as if they were + not included. + + strict_parsing: flag indicating what to do with parsing errors. + If false (the default), errors are silently ignored. + If true, errors raise a ValueError exception. + + encoding and errors: specify how to decode percent-encoded sequences + into Unicode characters, as accepted by the bytes.decode() method. + """ + parsed_result = {} + pairs = parse_qsl(qs, keep_blank_values, strict_parsing, + encoding=encoding, errors=errors) + for name, value in pairs: + if name in parsed_result: + parsed_result[name].append(value) + else: + parsed_result[name] = [value] + return parsed_result + +def parse_qsl(qs, keep_blank_values=False, strict_parsing=False, + encoding='utf-8', errors='replace'): + """Parse a query given as a string argument. + + Arguments: + + qs: percent-encoded query string to be parsed + + keep_blank_values: flag indicating whether blank values in + percent-encoded queries should be treated as blank strings. A + true value indicates that blanks should be retained as blank + strings. The default false value indicates that blank values + are to be ignored and treated as if they were not included. + + strict_parsing: flag indicating what to do with parsing errors. If + false (the default), errors are silently ignored. If true, + errors raise a ValueError exception. + + encoding and errors: specify how to decode percent-encoded sequences + into Unicode characters, as accepted by the bytes.decode() method. + + Returns a list, as G-d intended. + """ + qs, _coerce_result = _coerce_args(qs) + pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] + r = [] + for name_value in pairs: + if not name_value and not strict_parsing: + continue + nv = name_value.split('=', 1) + if len(nv) != 2: + if strict_parsing: + raise ValueError("bad query field: %r" % (name_value,)) + # Handle case of a control-name with no equal sign + if keep_blank_values: + nv.append('') + else: + continue + if len(nv[1]) or keep_blank_values: + name = nv[0].replace('+', ' ') + name = unquote(name, encoding=encoding, errors=errors) + name = _coerce_result(name) + value = nv[1].replace('+', ' ') + value = unquote(value, encoding=encoding, errors=errors) + value = _coerce_result(value) + r.append((name, value)) + return r + +def unquote_plus(string, encoding='utf-8', errors='replace'): + """Like unquote(), but also replace plus signs by spaces, as required for + unquoting HTML form values. + + unquote_plus('%7e/abc+def') -> '~/abc def' + """ + string = string.replace('+', ' ') + return unquote(string, encoding, errors) + +_ALWAYS_SAFE = frozenset(bytes(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ' + b'abcdefghijklmnopqrstuvwxyz' + b'0123456789' + b'_.-')) +_ALWAYS_SAFE_BYTES = bytes(_ALWAYS_SAFE) +_safe_quoters = {} + +class Quoter(collections.defaultdict): + """A mapping from bytes (in range(0,256)) to strings. + + String values are percent-encoded byte values, unless the key < 128, and + in the "safe" set (either the specified safe set, or default set). + """ + # Keeps a cache internally, using defaultdict, for efficiency (lookups + # of cached keys don't call Python code at all). + def __init__(self, safe): + """safe: bytes object.""" + self.safe = _ALWAYS_SAFE.union(bytes(safe)) + + def __repr__(self): + # Without this, will just display as a defaultdict + return "" % dict(self) + + def __missing__(self, b): + # Handle a cache miss. Store quoted string in cache and return. + res = chr(b) if b in self.safe else '%{0:02X}'.format(b) + self[b] = res + return res + +def quote(string, safe='/', encoding=None, errors=None): + """quote('abc def') -> 'abc%20def' + + Each part of a URL, e.g. the path info, the query, etc., has a + different set of reserved characters that must be quoted. + + RFC 2396 Uniform Resource Identifiers (URI): Generic Syntax lists + the following reserved characters. + + reserved = ";" | "/" | "?" | ":" | "@" | "&" | "=" | "+" | + "$" | "," + + Each of these characters is reserved in some component of a URL, + but not necessarily in all of them. + + By default, the quote function is intended for quoting the path + section of a URL. Thus, it will not encode '/'. This character + is reserved, but in typical usage the quote function is being + called on a path where the existing slash characters are used as + reserved characters. + + string and safe may be either str or bytes objects. encoding must + not be specified if string is a str. + + The optional encoding and errors parameters specify how to deal with + non-ASCII characters, as accepted by the str.encode method. + By default, encoding='utf-8' (characters are encoded with UTF-8), and + errors='strict' (unsupported characters raise a UnicodeEncodeError). + """ + if isinstance(string, str): + if not string: + return string + if encoding is None: + encoding = 'utf-8' + if errors is None: + errors = 'strict' + string = string.encode(encoding, errors) + else: + if encoding is not None: + raise TypeError("quote() doesn't support 'encoding' for bytes") + if errors is not None: + raise TypeError("quote() doesn't support 'errors' for bytes") + return quote_from_bytes(string, safe) + +def quote_plus(string, safe='', encoding=None, errors=None): + """Like quote(), but also replace ' ' with '+', as required for quoting + HTML form values. Plus signs in the original string are escaped unless + they are included in safe. It also does not have safe default to '/'. + """ + # Check if ' ' in string, where string may either be a str or bytes. If + # there are no spaces, the regular quote will produce the right answer. + if ((isinstance(string, str) and ' ' not in string) or + (isinstance(string, bytes) and b' ' not in string)): + return quote(string, safe, encoding, errors) + if isinstance(safe, str): + space = str(' ') + else: + space = bytes(b' ') + string = quote(string, safe + space, encoding, errors) + return string.replace(' ', '+') + +def quote_from_bytes(bs, safe='/'): + """Like quote(), but accepts a bytes object rather than a str, and does + not perform string-to-bytes encoding. It always returns an ASCII string. + quote_from_bytes(b'abc def\x3f') -> 'abc%20def%3f' + """ + if not isinstance(bs, (bytes, bytearray)): + raise TypeError("quote_from_bytes() expected bytes") + if not bs: + return str('') + ### For Python-Future: + bs = bytes(bs) + ### + if isinstance(safe, str): + # Normalize 'safe' by converting to bytes and removing non-ASCII chars + safe = str(safe).encode('ascii', 'ignore') + else: + ### For Python-Future: + safe = bytes(safe) + ### + safe = bytes([c for c in safe if c < 128]) + if not bs.rstrip(_ALWAYS_SAFE_BYTES + safe): + return bs.decode() + try: + quoter = _safe_quoters[safe] + except KeyError: + _safe_quoters[safe] = quoter = Quoter(safe).__getitem__ + return str('').join([quoter(char) for char in bs]) + +def urlencode(query, doseq=False, safe='', encoding=None, errors=None): + """Encode a sequence of two-element tuples or dictionary into a URL query string. + + If any values in the query arg are sequences and doseq is true, each + sequence element is converted to a separate parameter. + + If the query arg is a sequence of two-element tuples, the order of the + parameters in the output will match the order of parameters in the + input. + + The query arg may be either a string or a bytes type. When query arg is a + string, the safe, encoding and error parameters are sent the quote_plus for + encoding. + """ + + if hasattr(query, "items"): + query = query.items() + else: + # It's a bother at times that strings and string-like objects are + # sequences. + try: + # non-sequence items should not work with len() + # non-empty strings will fail this + if len(query) and not isinstance(query[0], tuple): + raise TypeError + # Zero-length sequences of all types will get here and succeed, + # but that's a minor nit. Since the original implementation + # allowed empty dicts that type of behavior probably should be + # preserved for consistency + except TypeError: + ty, va, tb = sys.exc_info() + raise_with_traceback(TypeError("not a valid non-string sequence " + "or mapping object"), tb) + + l = [] + if not doseq: + for k, v in query: + if isinstance(k, bytes): + k = quote_plus(k, safe) + else: + k = quote_plus(str(k), safe, encoding, errors) + + if isinstance(v, bytes): + v = quote_plus(v, safe) + else: + v = quote_plus(str(v), safe, encoding, errors) + l.append(k + '=' + v) + else: + for k, v in query: + if isinstance(k, bytes): + k = quote_plus(k, safe) + else: + k = quote_plus(str(k), safe, encoding, errors) + + if isinstance(v, bytes): + v = quote_plus(v, safe) + l.append(k + '=' + v) + elif isinstance(v, str): + v = quote_plus(v, safe, encoding, errors) + l.append(k + '=' + v) + else: + try: + # Is this a sufficient test for sequence-ness? + x = len(v) + except TypeError: + # not a sequence + v = quote_plus(str(v), safe, encoding, errors) + l.append(k + '=' + v) + else: + # loop over the sequence + for elt in v: + if isinstance(elt, bytes): + elt = quote_plus(elt, safe) + else: + elt = quote_plus(str(elt), safe, encoding, errors) + l.append(k + '=' + elt) + return str('&').join(l) + +# Utilities to parse URLs (most of these return None for missing parts): +# unwrap('') --> 'type://host/path' +# splittype('type:opaquestring') --> 'type', 'opaquestring' +# splithost('//host[:port]/path') --> 'host[:port]', '/path' +# splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]' +# splitpasswd('user:passwd') -> 'user', 'passwd' +# splitport('host:port') --> 'host', 'port' +# splitquery('/path?query') --> '/path', 'query' +# splittag('/path#tag') --> '/path', 'tag' +# splitattr('/path;attr1=value1;attr2=value2;...') -> +# '/path', ['attr1=value1', 'attr2=value2', ...] +# splitvalue('attr=value') --> 'attr', 'value' +# urllib.parse.unquote('abc%20def') -> 'abc def' +# quote('abc def') -> 'abc%20def') + +def to_bytes(url): + """to_bytes(u"URL") --> 'URL'.""" + # Most URL schemes require ASCII. If that changes, the conversion + # can be relaxed. + # XXX get rid of to_bytes() + if isinstance(url, str): + try: + url = url.encode("ASCII").decode() + except UnicodeError: + raise UnicodeError("URL " + repr(url) + + " contains non-ASCII characters") + return url + +def unwrap(url): + """unwrap('') --> 'type://host/path'.""" + url = str(url).strip() + if url[:1] == '<' and url[-1:] == '>': + url = url[1:-1].strip() + if url[:4] == 'URL:': url = url[4:].strip() + return url + +_typeprog = None +def splittype(url): + """splittype('type:opaquestring') --> 'type', 'opaquestring'.""" + global _typeprog + if _typeprog is None: + import re + _typeprog = re.compile('^([^/:]+):') + + match = _typeprog.match(url) + if match: + scheme = match.group(1) + return scheme.lower(), url[len(scheme) + 1:] + return None, url + +_hostprog = None +def splithost(url): + """splithost('//host[:port]/path') --> 'host[:port]', '/path'.""" + global _hostprog + if _hostprog is None: + import re + _hostprog = re.compile('^//([^/?]*)(.*)$') + + match = _hostprog.match(url) + if match: + host_port = match.group(1) + path = match.group(2) + if path and not path.startswith('/'): + path = '/' + path + return host_port, path + return None, url + +_userprog = None +def splituser(host): + """splituser('user[:passwd]@host[:port]') --> 'user[:passwd]', 'host[:port]'.""" + global _userprog + if _userprog is None: + import re + _userprog = re.compile('^(.*)@(.*)$') + + match = _userprog.match(host) + if match: return match.group(1, 2) + return None, host + +_passwdprog = None +def splitpasswd(user): + """splitpasswd('user:passwd') -> 'user', 'passwd'.""" + global _passwdprog + if _passwdprog is None: + import re + _passwdprog = re.compile('^([^:]*):(.*)$',re.S) + + match = _passwdprog.match(user) + if match: return match.group(1, 2) + return user, None + +# splittag('/path#tag') --> '/path', 'tag' +_portprog = None +def splitport(host): + """splitport('host:port') --> 'host', 'port'.""" + global _portprog + if _portprog is None: + import re + _portprog = re.compile('^(.*):([0-9]+)$') + + match = _portprog.match(host) + if match: return match.group(1, 2) + return host, None + +_nportprog = None +def splitnport(host, defport=-1): + """Split host and port, returning numeric port. + Return given default port if no ':' found; defaults to -1. + Return numerical port if a valid number are found after ':'. + Return None if ':' but not a valid number.""" + global _nportprog + if _nportprog is None: + import re + _nportprog = re.compile('^(.*):(.*)$') + + match = _nportprog.match(host) + if match: + host, port = match.group(1, 2) + try: + if not port: raise ValueError("no digits") + nport = int(port) + except ValueError: + nport = None + return host, nport + return host, defport + +_queryprog = None +def splitquery(url): + """splitquery('/path?query') --> '/path', 'query'.""" + global _queryprog + if _queryprog is None: + import re + _queryprog = re.compile('^(.*)\?([^?]*)$') + + match = _queryprog.match(url) + if match: return match.group(1, 2) + return url, None + +_tagprog = None +def splittag(url): + """splittag('/path#tag') --> '/path', 'tag'.""" + global _tagprog + if _tagprog is None: + import re + _tagprog = re.compile('^(.*)#([^#]*)$') + + match = _tagprog.match(url) + if match: return match.group(1, 2) + return url, None + +def splitattr(url): + """splitattr('/path;attr1=value1;attr2=value2;...') -> + '/path', ['attr1=value1', 'attr2=value2', ...].""" + words = url.split(';') + return words[0], words[1:] + +_valueprog = None +def splitvalue(attr): + """splitvalue('attr=value') --> 'attr', 'value'.""" + global _valueprog + if _valueprog is None: + import re + _valueprog = re.compile('^([^=]*)=(.*)$') + + match = _valueprog.match(attr) + if match: return match.group(1, 2) + return attr, None diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/request.py b/venv/lib/python3.10/site-packages/future/backports/urllib/request.py new file mode 100644 index 0000000..baee540 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/urllib/request.py @@ -0,0 +1,2647 @@ +""" +Ported using Python-Future from the Python 3.3 standard library. + +An extensible library for opening URLs using a variety of protocols + +The simplest way to use this module is to call the urlopen function, +which accepts a string containing a URL or a Request object (described +below). It opens the URL and returns the results as file-like +object; the returned object has some extra methods described below. + +The OpenerDirector manages a collection of Handler objects that do +all the actual work. Each Handler implements a particular protocol or +option. The OpenerDirector is a composite object that invokes the +Handlers needed to open the requested URL. For example, the +HTTPHandler performs HTTP GET and POST requests and deals with +non-error returns. The HTTPRedirectHandler automatically deals with +HTTP 301, 302, 303 and 307 redirect errors, and the HTTPDigestAuthHandler +deals with digest authentication. + +urlopen(url, data=None) -- Basic usage is the same as original +urllib. pass the url and optionally data to post to an HTTP URL, and +get a file-like object back. One difference is that you can also pass +a Request instance instead of URL. Raises a URLError (subclass of +IOError); for HTTP errors, raises an HTTPError, which can also be +treated as a valid response. + +build_opener -- Function that creates a new OpenerDirector instance. +Will install the default handlers. Accepts one or more Handlers as +arguments, either instances or Handler classes that it will +instantiate. If one of the argument is a subclass of the default +handler, the argument will be installed instead of the default. + +install_opener -- Installs a new opener as the default opener. + +objects of interest: + +OpenerDirector -- Sets up the User Agent as the Python-urllib client and manages +the Handler classes, while dealing with requests and responses. + +Request -- An object that encapsulates the state of a request. The +state can be as simple as the URL. It can also include extra HTTP +headers, e.g. a User-Agent. + +BaseHandler -- + +internals: +BaseHandler and parent +_call_chain conventions + +Example usage: + +import urllib.request + +# set up authentication info +authinfo = urllib.request.HTTPBasicAuthHandler() +authinfo.add_password(realm='PDQ Application', + uri='https://mahler:8092/site-updates.py', + user='klem', + passwd='geheim$parole') + +proxy_support = urllib.request.ProxyHandler({"http" : "http://ahad-haam:3128"}) + +# build a new opener that adds authentication and caching FTP handlers +opener = urllib.request.build_opener(proxy_support, authinfo, + urllib.request.CacheFTPHandler) + +# install it +urllib.request.install_opener(opener) + +f = urllib.request.urlopen('http://www.python.org/') +""" + +# XXX issues: +# If an authentication error handler that tries to perform +# authentication for some reason but fails, how should the error be +# signalled? The client needs to know the HTTP error code. But if +# the handler knows that the problem was, e.g., that it didn't know +# that hash algo that requested in the challenge, it would be good to +# pass that information along to the client, too. +# ftp errors aren't handled cleanly +# check digest against correct (i.e. non-apache) implementation + +# Possible extensions: +# complex proxies XXX not sure what exactly was meant by this +# abstract factory for opener + +from __future__ import absolute_import, division, print_function, unicode_literals +from future.builtins import bytes, dict, filter, input, int, map, open, str +from future.utils import PY2, PY3, raise_with_traceback + +import base64 +import bisect +import hashlib +import array + +from future.backports import email +from future.backports.http import client as http_client +from .error import URLError, HTTPError, ContentTooShortError +from .parse import ( + urlparse, urlsplit, urljoin, unwrap, quote, unquote, + splittype, splithost, splitport, splituser, splitpasswd, + splitattr, splitquery, splitvalue, splittag, to_bytes, urlunparse) +from .response import addinfourl, addclosehook + +import io +import os +import posixpath +import re +import socket +import sys +import time +import tempfile +import contextlib +import warnings + +from future.utils import PY2 + +if PY2: + from collections import Iterable +else: + from collections.abc import Iterable + +# check for SSL +try: + import ssl + # Not available in the SSL module in Py2: + from ssl import SSLContext +except ImportError: + _have_ssl = False +else: + _have_ssl = True + +__all__ = [ + # Classes + 'Request', 'OpenerDirector', 'BaseHandler', 'HTTPDefaultErrorHandler', + 'HTTPRedirectHandler', 'HTTPCookieProcessor', 'ProxyHandler', + 'HTTPPasswordMgr', 'HTTPPasswordMgrWithDefaultRealm', + 'AbstractBasicAuthHandler', 'HTTPBasicAuthHandler', 'ProxyBasicAuthHandler', + 'AbstractDigestAuthHandler', 'HTTPDigestAuthHandler', 'ProxyDigestAuthHandler', + 'HTTPHandler', 'FileHandler', 'FTPHandler', 'CacheFTPHandler', + 'UnknownHandler', 'HTTPErrorProcessor', + # Functions + 'urlopen', 'install_opener', 'build_opener', + 'pathname2url', 'url2pathname', 'getproxies', + # Legacy interface + 'urlretrieve', 'urlcleanup', 'URLopener', 'FancyURLopener', +] + +# used in User-Agent header sent +__version__ = sys.version[:3] + +_opener = None +def urlopen(url, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, **_3to2kwargs): + if 'cadefault' in _3to2kwargs: cadefault = _3to2kwargs['cadefault']; del _3to2kwargs['cadefault'] + else: cadefault = False + if 'capath' in _3to2kwargs: capath = _3to2kwargs['capath']; del _3to2kwargs['capath'] + else: capath = None + if 'cafile' in _3to2kwargs: cafile = _3to2kwargs['cafile']; del _3to2kwargs['cafile'] + else: cafile = None + global _opener + if cafile or capath or cadefault: + if not _have_ssl: + raise ValueError('SSL support not available') + context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + context.options |= ssl.OP_NO_SSLv2 + context.verify_mode = ssl.CERT_REQUIRED + if cafile or capath: + context.load_verify_locations(cafile, capath) + else: + context.set_default_verify_paths() + https_handler = HTTPSHandler(context=context, check_hostname=True) + opener = build_opener(https_handler) + elif _opener is None: + _opener = opener = build_opener() + else: + opener = _opener + return opener.open(url, data, timeout) + +def install_opener(opener): + global _opener + _opener = opener + +_url_tempfiles = [] +def urlretrieve(url, filename=None, reporthook=None, data=None): + """ + Retrieve a URL into a temporary location on disk. + + Requires a URL argument. If a filename is passed, it is used as + the temporary file location. The reporthook argument should be + a callable that accepts a block number, a read size, and the + total file size of the URL target. The data argument should be + valid URL encoded data. + + If a filename is passed and the URL points to a local resource, + the result is a copy from local file to new file. + + Returns a tuple containing the path to the newly created + data file as well as the resulting HTTPMessage object. + """ + url_type, path = splittype(url) + + with contextlib.closing(urlopen(url, data)) as fp: + headers = fp.info() + + # Just return the local path and the "headers" for file:// + # URLs. No sense in performing a copy unless requested. + if url_type == "file" and not filename: + return os.path.normpath(path), headers + + # Handle temporary file setup. + if filename: + tfp = open(filename, 'wb') + else: + tfp = tempfile.NamedTemporaryFile(delete=False) + filename = tfp.name + _url_tempfiles.append(filename) + + with tfp: + result = filename, headers + bs = 1024*8 + size = -1 + read = 0 + blocknum = 0 + if "content-length" in headers: + size = int(headers["Content-Length"]) + + if reporthook: + reporthook(blocknum, bs, size) + + while True: + block = fp.read(bs) + if not block: + break + read += len(block) + tfp.write(block) + blocknum += 1 + if reporthook: + reporthook(blocknum, bs, size) + + if size >= 0 and read < size: + raise ContentTooShortError( + "retrieval incomplete: got only %i out of %i bytes" + % (read, size), result) + + return result + +def urlcleanup(): + for temp_file in _url_tempfiles: + try: + os.unlink(temp_file) + except EnvironmentError: + pass + + del _url_tempfiles[:] + global _opener + if _opener: + _opener = None + +if PY3: + _cut_port_re = re.compile(r":\d+$", re.ASCII) +else: + _cut_port_re = re.compile(r":\d+$") + +def request_host(request): + + """Return request-host, as defined by RFC 2965. + + Variation from RFC: returned value is lowercased, for convenient + comparison. + + """ + url = request.full_url + host = urlparse(url)[1] + if host == "": + host = request.get_header("Host", "") + + # remove port, if present + host = _cut_port_re.sub("", host, 1) + return host.lower() + +class Request(object): + + def __init__(self, url, data=None, headers={}, + origin_req_host=None, unverifiable=False, + method=None): + # unwrap('') --> 'type://host/path' + self.full_url = unwrap(url) + self.full_url, self.fragment = splittag(self.full_url) + self.data = data + self.headers = {} + self._tunnel_host = None + for key, value in headers.items(): + self.add_header(key, value) + self.unredirected_hdrs = {} + if origin_req_host is None: + origin_req_host = request_host(self) + self.origin_req_host = origin_req_host + self.unverifiable = unverifiable + self.method = method + self._parse() + + def _parse(self): + self.type, rest = splittype(self.full_url) + if self.type is None: + raise ValueError("unknown url type: %r" % self.full_url) + self.host, self.selector = splithost(rest) + if self.host: + self.host = unquote(self.host) + + def get_method(self): + """Return a string indicating the HTTP request method.""" + if self.method is not None: + return self.method + elif self.data is not None: + return "POST" + else: + return "GET" + + def get_full_url(self): + if self.fragment: + return '%s#%s' % (self.full_url, self.fragment) + else: + return self.full_url + + # Begin deprecated methods + + def add_data(self, data): + msg = "Request.add_data method is deprecated." + warnings.warn(msg, DeprecationWarning, stacklevel=1) + self.data = data + + def has_data(self): + msg = "Request.has_data method is deprecated." + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return self.data is not None + + def get_data(self): + msg = "Request.get_data method is deprecated." + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return self.data + + def get_type(self): + msg = "Request.get_type method is deprecated." + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return self.type + + def get_host(self): + msg = "Request.get_host method is deprecated." + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return self.host + + def get_selector(self): + msg = "Request.get_selector method is deprecated." + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return self.selector + + def is_unverifiable(self): + msg = "Request.is_unverifiable method is deprecated." + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return self.unverifiable + + def get_origin_req_host(self): + msg = "Request.get_origin_req_host method is deprecated." + warnings.warn(msg, DeprecationWarning, stacklevel=1) + return self.origin_req_host + + # End deprecated methods + + def set_proxy(self, host, type): + if self.type == 'https' and not self._tunnel_host: + self._tunnel_host = self.host + else: + self.type= type + self.selector = self.full_url + self.host = host + + def has_proxy(self): + return self.selector == self.full_url + + def add_header(self, key, val): + # useful for something like authentication + self.headers[key.capitalize()] = val + + def add_unredirected_header(self, key, val): + # will not be added to a redirected request + self.unredirected_hdrs[key.capitalize()] = val + + def has_header(self, header_name): + return (header_name in self.headers or + header_name in self.unredirected_hdrs) + + def get_header(self, header_name, default=None): + return self.headers.get( + header_name, + self.unredirected_hdrs.get(header_name, default)) + + def header_items(self): + hdrs = self.unredirected_hdrs.copy() + hdrs.update(self.headers) + return list(hdrs.items()) + +class OpenerDirector(object): + def __init__(self): + client_version = "Python-urllib/%s" % __version__ + self.addheaders = [('User-agent', client_version)] + # self.handlers is retained only for backward compatibility + self.handlers = [] + # manage the individual handlers + self.handle_open = {} + self.handle_error = {} + self.process_response = {} + self.process_request = {} + + def add_handler(self, handler): + if not hasattr(handler, "add_parent"): + raise TypeError("expected BaseHandler instance, got %r" % + type(handler)) + + added = False + for meth in dir(handler): + if meth in ["redirect_request", "do_open", "proxy_open"]: + # oops, coincidental match + continue + + i = meth.find("_") + protocol = meth[:i] + condition = meth[i+1:] + + if condition.startswith("error"): + j = condition.find("_") + i + 1 + kind = meth[j+1:] + try: + kind = int(kind) + except ValueError: + pass + lookup = self.handle_error.get(protocol, {}) + self.handle_error[protocol] = lookup + elif condition == "open": + kind = protocol + lookup = self.handle_open + elif condition == "response": + kind = protocol + lookup = self.process_response + elif condition == "request": + kind = protocol + lookup = self.process_request + else: + continue + + handlers = lookup.setdefault(kind, []) + if handlers: + bisect.insort(handlers, handler) + else: + handlers.append(handler) + added = True + + if added: + bisect.insort(self.handlers, handler) + handler.add_parent(self) + + def close(self): + # Only exists for backwards compatibility. + pass + + def _call_chain(self, chain, kind, meth_name, *args): + # Handlers raise an exception if no one else should try to handle + # the request, or return None if they can't but another handler + # could. Otherwise, they return the response. + handlers = chain.get(kind, ()) + for handler in handlers: + func = getattr(handler, meth_name) + result = func(*args) + if result is not None: + return result + + def open(self, fullurl, data=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT): + """ + Accept a URL or a Request object + + Python-Future: if the URL is passed as a byte-string, decode it first. + """ + if isinstance(fullurl, bytes): + fullurl = fullurl.decode() + if isinstance(fullurl, str): + req = Request(fullurl, data) + else: + req = fullurl + if data is not None: + req.data = data + + req.timeout = timeout + protocol = req.type + + # pre-process request + meth_name = protocol+"_request" + for processor in self.process_request.get(protocol, []): + meth = getattr(processor, meth_name) + req = meth(req) + + response = self._open(req, data) + + # post-process response + meth_name = protocol+"_response" + for processor in self.process_response.get(protocol, []): + meth = getattr(processor, meth_name) + response = meth(req, response) + + return response + + def _open(self, req, data=None): + result = self._call_chain(self.handle_open, 'default', + 'default_open', req) + if result: + return result + + protocol = req.type + result = self._call_chain(self.handle_open, protocol, protocol + + '_open', req) + if result: + return result + + return self._call_chain(self.handle_open, 'unknown', + 'unknown_open', req) + + def error(self, proto, *args): + if proto in ('http', 'https'): + # XXX http[s] protocols are special-cased + dict = self.handle_error['http'] # https is not different than http + proto = args[2] # YUCK! + meth_name = 'http_error_%s' % proto + http_err = 1 + orig_args = args + else: + dict = self.handle_error + meth_name = proto + '_error' + http_err = 0 + args = (dict, proto, meth_name) + args + result = self._call_chain(*args) + if result: + return result + + if http_err: + args = (dict, 'default', 'http_error_default') + orig_args + return self._call_chain(*args) + +# XXX probably also want an abstract factory that knows when it makes +# sense to skip a superclass in favor of a subclass and when it might +# make sense to include both + +def build_opener(*handlers): + """Create an opener object from a list of handlers. + + The opener will use several default handlers, including support + for HTTP, FTP and when applicable HTTPS. + + If any of the handlers passed as arguments are subclasses of the + default handlers, the default handlers will not be used. + """ + def isclass(obj): + return isinstance(obj, type) or hasattr(obj, "__bases__") + + opener = OpenerDirector() + default_classes = [ProxyHandler, UnknownHandler, HTTPHandler, + HTTPDefaultErrorHandler, HTTPRedirectHandler, + FTPHandler, FileHandler, HTTPErrorProcessor] + if hasattr(http_client, "HTTPSConnection"): + default_classes.append(HTTPSHandler) + skip = set() + for klass in default_classes: + for check in handlers: + if isclass(check): + if issubclass(check, klass): + skip.add(klass) + elif isinstance(check, klass): + skip.add(klass) + for klass in skip: + default_classes.remove(klass) + + for klass in default_classes: + opener.add_handler(klass()) + + for h in handlers: + if isclass(h): + h = h() + opener.add_handler(h) + return opener + +class BaseHandler(object): + handler_order = 500 + + def add_parent(self, parent): + self.parent = parent + + def close(self): + # Only exists for backwards compatibility + pass + + def __lt__(self, other): + if not hasattr(other, "handler_order"): + # Try to preserve the old behavior of having custom classes + # inserted after default ones (works only for custom user + # classes which are not aware of handler_order). + return True + return self.handler_order < other.handler_order + + +class HTTPErrorProcessor(BaseHandler): + """Process HTTP error responses.""" + handler_order = 1000 # after all other processing + + def http_response(self, request, response): + code, msg, hdrs = response.code, response.msg, response.info() + + # According to RFC 2616, "2xx" code indicates that the client's + # request was successfully received, understood, and accepted. + if not (200 <= code < 300): + response = self.parent.error( + 'http', request, response, code, msg, hdrs) + + return response + + https_response = http_response + +class HTTPDefaultErrorHandler(BaseHandler): + def http_error_default(self, req, fp, code, msg, hdrs): + raise HTTPError(req.full_url, code, msg, hdrs, fp) + +class HTTPRedirectHandler(BaseHandler): + # maximum number of redirections to any single URL + # this is needed because of the state that cookies introduce + max_repeats = 4 + # maximum total number of redirections (regardless of URL) before + # assuming we're in a loop + max_redirections = 10 + + def redirect_request(self, req, fp, code, msg, headers, newurl): + """Return a Request or None in response to a redirect. + + This is called by the http_error_30x methods when a + redirection response is received. If a redirection should + take place, return a new Request to allow http_error_30x to + perform the redirect. Otherwise, raise HTTPError if no-one + else should try to handle this url. Return None if you can't + but another Handler might. + """ + m = req.get_method() + if (not (code in (301, 302, 303, 307) and m in ("GET", "HEAD") + or code in (301, 302, 303) and m == "POST")): + raise HTTPError(req.full_url, code, msg, headers, fp) + + # Strictly (according to RFC 2616), 301 or 302 in response to + # a POST MUST NOT cause a redirection without confirmation + # from the user (of urllib.request, in this case). In practice, + # essentially all clients do redirect in this case, so we do + # the same. + # be conciliant with URIs containing a space + newurl = newurl.replace(' ', '%20') + CONTENT_HEADERS = ("content-length", "content-type") + newheaders = dict((k, v) for k, v in req.headers.items() + if k.lower() not in CONTENT_HEADERS) + return Request(newurl, + headers=newheaders, + origin_req_host=req.origin_req_host, + unverifiable=True) + + # Implementation note: To avoid the server sending us into an + # infinite loop, the request object needs to track what URLs we + # have already seen. Do this by adding a handler-specific + # attribute to the Request object. + def http_error_302(self, req, fp, code, msg, headers): + # Some servers (incorrectly) return multiple Location headers + # (so probably same goes for URI). Use first header. + if "location" in headers: + newurl = headers["location"] + elif "uri" in headers: + newurl = headers["uri"] + else: + return + + # fix a possible malformed URL + urlparts = urlparse(newurl) + + # For security reasons we don't allow redirection to anything other + # than http, https or ftp. + + if urlparts.scheme not in ('http', 'https', 'ftp', ''): + raise HTTPError( + newurl, code, + "%s - Redirection to url '%s' is not allowed" % (msg, newurl), + headers, fp) + + if not urlparts.path: + urlparts = list(urlparts) + urlparts[2] = "/" + newurl = urlunparse(urlparts) + + newurl = urljoin(req.full_url, newurl) + + # XXX Probably want to forget about the state of the current + # request, although that might interact poorly with other + # handlers that also use handler-specific request attributes + new = self.redirect_request(req, fp, code, msg, headers, newurl) + if new is None: + return + + # loop detection + # .redirect_dict has a key url if url was previously visited. + if hasattr(req, 'redirect_dict'): + visited = new.redirect_dict = req.redirect_dict + if (visited.get(newurl, 0) >= self.max_repeats or + len(visited) >= self.max_redirections): + raise HTTPError(req.full_url, code, + self.inf_msg + msg, headers, fp) + else: + visited = new.redirect_dict = req.redirect_dict = {} + visited[newurl] = visited.get(newurl, 0) + 1 + + # Don't close the fp until we are sure that we won't use it + # with HTTPError. + fp.read() + fp.close() + + return self.parent.open(new, timeout=req.timeout) + + http_error_301 = http_error_303 = http_error_307 = http_error_302 + + inf_msg = "The HTTP server returned a redirect error that would " \ + "lead to an infinite loop.\n" \ + "The last 30x error message was:\n" + + +def _parse_proxy(proxy): + """Return (scheme, user, password, host/port) given a URL or an authority. + + If a URL is supplied, it must have an authority (host:port) component. + According to RFC 3986, having an authority component means the URL must + have two slashes after the scheme: + + >>> _parse_proxy('file:/ftp.example.com/') + Traceback (most recent call last): + ValueError: proxy URL with no authority: 'file:/ftp.example.com/' + + The first three items of the returned tuple may be None. + + Examples of authority parsing: + + >>> _parse_proxy('proxy.example.com') + (None, None, None, 'proxy.example.com') + >>> _parse_proxy('proxy.example.com:3128') + (None, None, None, 'proxy.example.com:3128') + + The authority component may optionally include userinfo (assumed to be + username:password): + + >>> _parse_proxy('joe:password@proxy.example.com') + (None, 'joe', 'password', 'proxy.example.com') + >>> _parse_proxy('joe:password@proxy.example.com:3128') + (None, 'joe', 'password', 'proxy.example.com:3128') + + Same examples, but with URLs instead: + + >>> _parse_proxy('http://proxy.example.com/') + ('http', None, None, 'proxy.example.com') + >>> _parse_proxy('http://proxy.example.com:3128/') + ('http', None, None, 'proxy.example.com:3128') + >>> _parse_proxy('http://joe:password@proxy.example.com/') + ('http', 'joe', 'password', 'proxy.example.com') + >>> _parse_proxy('http://joe:password@proxy.example.com:3128') + ('http', 'joe', 'password', 'proxy.example.com:3128') + + Everything after the authority is ignored: + + >>> _parse_proxy('ftp://joe:password@proxy.example.com/rubbish:3128') + ('ftp', 'joe', 'password', 'proxy.example.com') + + Test for no trailing '/' case: + + >>> _parse_proxy('http://joe:password@proxy.example.com') + ('http', 'joe', 'password', 'proxy.example.com') + + """ + scheme, r_scheme = splittype(proxy) + if not r_scheme.startswith("/"): + # authority + scheme = None + authority = proxy + else: + # URL + if not r_scheme.startswith("//"): + raise ValueError("proxy URL with no authority: %r" % proxy) + # We have an authority, so for RFC 3986-compliant URLs (by ss 3. + # and 3.3.), path is empty or starts with '/' + end = r_scheme.find("/", 2) + if end == -1: + end = None + authority = r_scheme[2:end] + userinfo, hostport = splituser(authority) + if userinfo is not None: + user, password = splitpasswd(userinfo) + else: + user = password = None + return scheme, user, password, hostport + +class ProxyHandler(BaseHandler): + # Proxies must be in front + handler_order = 100 + + def __init__(self, proxies=None): + if proxies is None: + proxies = getproxies() + assert hasattr(proxies, 'keys'), "proxies must be a mapping" + self.proxies = proxies + for type, url in proxies.items(): + setattr(self, '%s_open' % type, + lambda r, proxy=url, type=type, meth=self.proxy_open: + meth(r, proxy, type)) + + def proxy_open(self, req, proxy, type): + orig_type = req.type + proxy_type, user, password, hostport = _parse_proxy(proxy) + if proxy_type is None: + proxy_type = orig_type + + if req.host and proxy_bypass(req.host): + return None + + if user and password: + user_pass = '%s:%s' % (unquote(user), + unquote(password)) + creds = base64.b64encode(user_pass.encode()).decode("ascii") + req.add_header('Proxy-authorization', 'Basic ' + creds) + hostport = unquote(hostport) + req.set_proxy(hostport, proxy_type) + if orig_type == proxy_type or orig_type == 'https': + # let other handlers take care of it + return None + else: + # need to start over, because the other handlers don't + # grok the proxy's URL type + # e.g. if we have a constructor arg proxies like so: + # {'http': 'ftp://proxy.example.com'}, we may end up turning + # a request for http://acme.example.com/a into one for + # ftp://proxy.example.com/a + return self.parent.open(req, timeout=req.timeout) + +class HTTPPasswordMgr(object): + + def __init__(self): + self.passwd = {} + + def add_password(self, realm, uri, user, passwd): + # uri could be a single URI or a sequence + if isinstance(uri, str): + uri = [uri] + if realm not in self.passwd: + self.passwd[realm] = {} + for default_port in True, False: + reduced_uri = tuple( + [self.reduce_uri(u, default_port) for u in uri]) + self.passwd[realm][reduced_uri] = (user, passwd) + + def find_user_password(self, realm, authuri): + domains = self.passwd.get(realm, {}) + for default_port in True, False: + reduced_authuri = self.reduce_uri(authuri, default_port) + for uris, authinfo in domains.items(): + for uri in uris: + if self.is_suburi(uri, reduced_authuri): + return authinfo + return None, None + + def reduce_uri(self, uri, default_port=True): + """Accept authority or URI and extract only the authority and path.""" + # note HTTP URLs do not have a userinfo component + parts = urlsplit(uri) + if parts[1]: + # URI + scheme = parts[0] + authority = parts[1] + path = parts[2] or '/' + else: + # host or host:port + scheme = None + authority = uri + path = '/' + host, port = splitport(authority) + if default_port and port is None and scheme is not None: + dport = {"http": 80, + "https": 443, + }.get(scheme) + if dport is not None: + authority = "%s:%d" % (host, dport) + return authority, path + + def is_suburi(self, base, test): + """Check if test is below base in a URI tree + + Both args must be URIs in reduced form. + """ + if base == test: + return True + if base[0] != test[0]: + return False + common = posixpath.commonprefix((base[1], test[1])) + if len(common) == len(base[1]): + return True + return False + + +class HTTPPasswordMgrWithDefaultRealm(HTTPPasswordMgr): + + def find_user_password(self, realm, authuri): + user, password = HTTPPasswordMgr.find_user_password(self, realm, + authuri) + if user is not None: + return user, password + return HTTPPasswordMgr.find_user_password(self, None, authuri) + + +class AbstractBasicAuthHandler(object): + + # XXX this allows for multiple auth-schemes, but will stupidly pick + # the last one with a realm specified. + + # allow for double- and single-quoted realm values + # (single quotes are a violation of the RFC, but appear in the wild) + rx = re.compile('(?:.*,)*[ \t]*([^ \t]+)[ \t]+' + 'realm=(["\']?)([^"\']*)\\2', re.I) + + # XXX could pre-emptively send auth info already accepted (RFC 2617, + # end of section 2, and section 1.2 immediately after "credentials" + # production). + + def __init__(self, password_mgr=None): + if password_mgr is None: + password_mgr = HTTPPasswordMgr() + self.passwd = password_mgr + self.add_password = self.passwd.add_password + self.retried = 0 + + def reset_retry_count(self): + self.retried = 0 + + def http_error_auth_reqed(self, authreq, host, req, headers): + # host may be an authority (without userinfo) or a URL with an + # authority + # XXX could be multiple headers + authreq = headers.get(authreq, None) + + if self.retried > 5: + # retry sending the username:password 5 times before failing. + raise HTTPError(req.get_full_url(), 401, "basic auth failed", + headers, None) + else: + self.retried += 1 + + if authreq: + scheme = authreq.split()[0] + if scheme.lower() != 'basic': + raise ValueError("AbstractBasicAuthHandler does not" + " support the following scheme: '%s'" % + scheme) + else: + mo = AbstractBasicAuthHandler.rx.search(authreq) + if mo: + scheme, quote, realm = mo.groups() + if quote not in ['"',"'"]: + warnings.warn("Basic Auth Realm was unquoted", + UserWarning, 2) + if scheme.lower() == 'basic': + response = self.retry_http_basic_auth(host, req, realm) + if response and response.code != 401: + self.retried = 0 + return response + + def retry_http_basic_auth(self, host, req, realm): + user, pw = self.passwd.find_user_password(realm, host) + if pw is not None: + raw = "%s:%s" % (user, pw) + auth = "Basic " + base64.b64encode(raw.encode()).decode("ascii") + if req.headers.get(self.auth_header, None) == auth: + return None + req.add_unredirected_header(self.auth_header, auth) + return self.parent.open(req, timeout=req.timeout) + else: + return None + + +class HTTPBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler): + + auth_header = 'Authorization' + + def http_error_401(self, req, fp, code, msg, headers): + url = req.full_url + response = self.http_error_auth_reqed('www-authenticate', + url, req, headers) + self.reset_retry_count() + return response + + +class ProxyBasicAuthHandler(AbstractBasicAuthHandler, BaseHandler): + + auth_header = 'Proxy-authorization' + + def http_error_407(self, req, fp, code, msg, headers): + # http_error_auth_reqed requires that there is no userinfo component in + # authority. Assume there isn't one, since urllib.request does not (and + # should not, RFC 3986 s. 3.2.1) support requests for URLs containing + # userinfo. + authority = req.host + response = self.http_error_auth_reqed('proxy-authenticate', + authority, req, headers) + self.reset_retry_count() + return response + + +# Return n random bytes. +_randombytes = os.urandom + + +class AbstractDigestAuthHandler(object): + # Digest authentication is specified in RFC 2617. + + # XXX The client does not inspect the Authentication-Info header + # in a successful response. + + # XXX It should be possible to test this implementation against + # a mock server that just generates a static set of challenges. + + # XXX qop="auth-int" supports is shaky + + def __init__(self, passwd=None): + if passwd is None: + passwd = HTTPPasswordMgr() + self.passwd = passwd + self.add_password = self.passwd.add_password + self.retried = 0 + self.nonce_count = 0 + self.last_nonce = None + + def reset_retry_count(self): + self.retried = 0 + + def http_error_auth_reqed(self, auth_header, host, req, headers): + authreq = headers.get(auth_header, None) + if self.retried > 5: + # Don't fail endlessly - if we failed once, we'll probably + # fail a second time. Hm. Unless the Password Manager is + # prompting for the information. Crap. This isn't great + # but it's better than the current 'repeat until recursion + # depth exceeded' approach + raise HTTPError(req.full_url, 401, "digest auth failed", + headers, None) + else: + self.retried += 1 + if authreq: + scheme = authreq.split()[0] + if scheme.lower() == 'digest': + return self.retry_http_digest_auth(req, authreq) + elif scheme.lower() != 'basic': + raise ValueError("AbstractDigestAuthHandler does not support" + " the following scheme: '%s'" % scheme) + + def retry_http_digest_auth(self, req, auth): + token, challenge = auth.split(' ', 1) + chal = parse_keqv_list(filter(None, parse_http_list(challenge))) + auth = self.get_authorization(req, chal) + if auth: + auth_val = 'Digest %s' % auth + if req.headers.get(self.auth_header, None) == auth_val: + return None + req.add_unredirected_header(self.auth_header, auth_val) + resp = self.parent.open(req, timeout=req.timeout) + return resp + + def get_cnonce(self, nonce): + # The cnonce-value is an opaque + # quoted string value provided by the client and used by both client + # and server to avoid chosen plaintext attacks, to provide mutual + # authentication, and to provide some message integrity protection. + # This isn't a fabulous effort, but it's probably Good Enough. + s = "%s:%s:%s:" % (self.nonce_count, nonce, time.ctime()) + b = s.encode("ascii") + _randombytes(8) + dig = hashlib.sha1(b).hexdigest() + return dig[:16] + + def get_authorization(self, req, chal): + try: + realm = chal['realm'] + nonce = chal['nonce'] + qop = chal.get('qop') + algorithm = chal.get('algorithm', 'MD5') + # mod_digest doesn't send an opaque, even though it isn't + # supposed to be optional + opaque = chal.get('opaque', None) + except KeyError: + return None + + H, KD = self.get_algorithm_impls(algorithm) + if H is None: + return None + + user, pw = self.passwd.find_user_password(realm, req.full_url) + if user is None: + return None + + # XXX not implemented yet + if req.data is not None: + entdig = self.get_entity_digest(req.data, chal) + else: + entdig = None + + A1 = "%s:%s:%s" % (user, realm, pw) + A2 = "%s:%s" % (req.get_method(), + # XXX selector: what about proxies and full urls + req.selector) + if qop == 'auth': + if nonce == self.last_nonce: + self.nonce_count += 1 + else: + self.nonce_count = 1 + self.last_nonce = nonce + ncvalue = '%08x' % self.nonce_count + cnonce = self.get_cnonce(nonce) + noncebit = "%s:%s:%s:%s:%s" % (nonce, ncvalue, cnonce, qop, H(A2)) + respdig = KD(H(A1), noncebit) + elif qop is None: + respdig = KD(H(A1), "%s:%s" % (nonce, H(A2))) + else: + # XXX handle auth-int. + raise URLError("qop '%s' is not supported." % qop) + + # XXX should the partial digests be encoded too? + + base = 'username="%s", realm="%s", nonce="%s", uri="%s", ' \ + 'response="%s"' % (user, realm, nonce, req.selector, + respdig) + if opaque: + base += ', opaque="%s"' % opaque + if entdig: + base += ', digest="%s"' % entdig + base += ', algorithm="%s"' % algorithm + if qop: + base += ', qop=auth, nc=%s, cnonce="%s"' % (ncvalue, cnonce) + return base + + def get_algorithm_impls(self, algorithm): + # lambdas assume digest modules are imported at the top level + if algorithm == 'MD5': + H = lambda x: hashlib.md5(x.encode("ascii")).hexdigest() + elif algorithm == 'SHA': + H = lambda x: hashlib.sha1(x.encode("ascii")).hexdigest() + # XXX MD5-sess + KD = lambda s, d: H("%s:%s" % (s, d)) + return H, KD + + def get_entity_digest(self, data, chal): + # XXX not implemented yet + return None + + +class HTTPDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler): + """An authentication protocol defined by RFC 2069 + + Digest authentication improves on basic authentication because it + does not transmit passwords in the clear. + """ + + auth_header = 'Authorization' + handler_order = 490 # before Basic auth + + def http_error_401(self, req, fp, code, msg, headers): + host = urlparse(req.full_url)[1] + retry = self.http_error_auth_reqed('www-authenticate', + host, req, headers) + self.reset_retry_count() + return retry + + +class ProxyDigestAuthHandler(BaseHandler, AbstractDigestAuthHandler): + + auth_header = 'Proxy-Authorization' + handler_order = 490 # before Basic auth + + def http_error_407(self, req, fp, code, msg, headers): + host = req.host + retry = self.http_error_auth_reqed('proxy-authenticate', + host, req, headers) + self.reset_retry_count() + return retry + +class AbstractHTTPHandler(BaseHandler): + + def __init__(self, debuglevel=0): + self._debuglevel = debuglevel + + def set_http_debuglevel(self, level): + self._debuglevel = level + + def do_request_(self, request): + host = request.host + if not host: + raise URLError('no host given') + + if request.data is not None: # POST + data = request.data + if isinstance(data, str): + msg = "POST data should be bytes or an iterable of bytes. " \ + "It cannot be of type str." + raise TypeError(msg) + if not request.has_header('Content-type'): + request.add_unredirected_header( + 'Content-type', + 'application/x-www-form-urlencoded') + if not request.has_header('Content-length'): + size = None + try: + ### For Python-Future: + if PY2 and isinstance(data, array.array): + # memoryviews of arrays aren't supported + # in Py2.7. (e.g. memoryview(array.array('I', + # [1, 2, 3, 4])) raises a TypeError.) + # So we calculate the size manually instead: + size = len(data) * data.itemsize + ### + else: + mv = memoryview(data) + size = len(mv) * mv.itemsize + except TypeError: + if isinstance(data, Iterable): + raise ValueError("Content-Length should be specified " + "for iterable data of type %r %r" % (type(data), + data)) + else: + request.add_unredirected_header( + 'Content-length', '%d' % size) + + sel_host = host + if request.has_proxy(): + scheme, sel = splittype(request.selector) + sel_host, sel_path = splithost(sel) + if not request.has_header('Host'): + request.add_unredirected_header('Host', sel_host) + for name, value in self.parent.addheaders: + name = name.capitalize() + if not request.has_header(name): + request.add_unredirected_header(name, value) + + return request + + def do_open(self, http_class, req, **http_conn_args): + """Return an HTTPResponse object for the request, using http_class. + + http_class must implement the HTTPConnection API from http.client. + """ + host = req.host + if not host: + raise URLError('no host given') + + # will parse host:port + h = http_class(host, timeout=req.timeout, **http_conn_args) + + headers = dict(req.unredirected_hdrs) + headers.update(dict((k, v) for k, v in req.headers.items() + if k not in headers)) + + # TODO(jhylton): Should this be redesigned to handle + # persistent connections? + + # We want to make an HTTP/1.1 request, but the addinfourl + # class isn't prepared to deal with a persistent connection. + # It will try to read all remaining data from the socket, + # which will block while the server waits for the next request. + # So make sure the connection gets closed after the (only) + # request. + headers["Connection"] = "close" + headers = dict((name.title(), val) for name, val in headers.items()) + + if req._tunnel_host: + tunnel_headers = {} + proxy_auth_hdr = "Proxy-Authorization" + if proxy_auth_hdr in headers: + tunnel_headers[proxy_auth_hdr] = headers[proxy_auth_hdr] + # Proxy-Authorization should not be sent to origin + # server. + del headers[proxy_auth_hdr] + h.set_tunnel(req._tunnel_host, headers=tunnel_headers) + + try: + h.request(req.get_method(), req.selector, req.data, headers) + except socket.error as err: # timeout error + h.close() + raise URLError(err) + else: + r = h.getresponse() + # If the server does not send us a 'Connection: close' header, + # HTTPConnection assumes the socket should be left open. Manually + # mark the socket to be closed when this response object goes away. + if h.sock: + h.sock.close() + h.sock = None + + + r.url = req.get_full_url() + # This line replaces the .msg attribute of the HTTPResponse + # with .headers, because urllib clients expect the response to + # have the reason in .msg. It would be good to mark this + # attribute is deprecated and get then to use info() or + # .headers. + r.msg = r.reason + return r + + +class HTTPHandler(AbstractHTTPHandler): + + def http_open(self, req): + return self.do_open(http_client.HTTPConnection, req) + + http_request = AbstractHTTPHandler.do_request_ + +if hasattr(http_client, 'HTTPSConnection'): + + class HTTPSHandler(AbstractHTTPHandler): + + def __init__(self, debuglevel=0, context=None, check_hostname=None): + AbstractHTTPHandler.__init__(self, debuglevel) + self._context = context + self._check_hostname = check_hostname + + def https_open(self, req): + return self.do_open(http_client.HTTPSConnection, req, + context=self._context, check_hostname=self._check_hostname) + + https_request = AbstractHTTPHandler.do_request_ + + __all__.append('HTTPSHandler') + +class HTTPCookieProcessor(BaseHandler): + def __init__(self, cookiejar=None): + import future.backports.http.cookiejar as http_cookiejar + if cookiejar is None: + cookiejar = http_cookiejar.CookieJar() + self.cookiejar = cookiejar + + def http_request(self, request): + self.cookiejar.add_cookie_header(request) + return request + + def http_response(self, request, response): + self.cookiejar.extract_cookies(response, request) + return response + + https_request = http_request + https_response = http_response + +class UnknownHandler(BaseHandler): + def unknown_open(self, req): + type = req.type + raise URLError('unknown url type: %s' % type) + +def parse_keqv_list(l): + """Parse list of key=value strings where keys are not duplicated.""" + parsed = {} + for elt in l: + k, v = elt.split('=', 1) + if v[0] == '"' and v[-1] == '"': + v = v[1:-1] + parsed[k] = v + return parsed + +def parse_http_list(s): + """Parse lists as described by RFC 2068 Section 2. + + In particular, parse comma-separated lists where the elements of + the list may include quoted-strings. A quoted-string could + contain a comma. A non-quoted string could have quotes in the + middle. Neither commas nor quotes count if they are escaped. + Only double-quotes count, not single-quotes. + """ + res = [] + part = '' + + escape = quote = False + for cur in s: + if escape: + part += cur + escape = False + continue + if quote: + if cur == '\\': + escape = True + continue + elif cur == '"': + quote = False + part += cur + continue + + if cur == ',': + res.append(part) + part = '' + continue + + if cur == '"': + quote = True + + part += cur + + # append last part + if part: + res.append(part) + + return [part.strip() for part in res] + +class FileHandler(BaseHandler): + # Use local file or FTP depending on form of URL + def file_open(self, req): + url = req.selector + if url[:2] == '//' and url[2:3] != '/' and (req.host and + req.host != 'localhost'): + if not req.host is self.get_names(): + raise URLError("file:// scheme is supported only on localhost") + else: + return self.open_local_file(req) + + # names for the localhost + names = None + def get_names(self): + if FileHandler.names is None: + try: + FileHandler.names = tuple( + socket.gethostbyname_ex('localhost')[2] + + socket.gethostbyname_ex(socket.gethostname())[2]) + except socket.gaierror: + FileHandler.names = (socket.gethostbyname('localhost'),) + return FileHandler.names + + # not entirely sure what the rules are here + def open_local_file(self, req): + import future.backports.email.utils as email_utils + import mimetypes + host = req.host + filename = req.selector + localfile = url2pathname(filename) + try: + stats = os.stat(localfile) + size = stats.st_size + modified = email_utils.formatdate(stats.st_mtime, usegmt=True) + mtype = mimetypes.guess_type(filename)[0] + headers = email.message_from_string( + 'Content-type: %s\nContent-length: %d\nLast-modified: %s\n' % + (mtype or 'text/plain', size, modified)) + if host: + host, port = splitport(host) + if not host or \ + (not port and _safe_gethostbyname(host) in self.get_names()): + if host: + origurl = 'file://' + host + filename + else: + origurl = 'file://' + filename + return addinfourl(open(localfile, 'rb'), headers, origurl) + except OSError as exp: + # users shouldn't expect OSErrors coming from urlopen() + raise URLError(exp) + raise URLError('file not on local host') + +def _safe_gethostbyname(host): + try: + return socket.gethostbyname(host) + except socket.gaierror: + return None + +class FTPHandler(BaseHandler): + def ftp_open(self, req): + import ftplib + import mimetypes + host = req.host + if not host: + raise URLError('ftp error: no host given') + host, port = splitport(host) + if port is None: + port = ftplib.FTP_PORT + else: + port = int(port) + + # username/password handling + user, host = splituser(host) + if user: + user, passwd = splitpasswd(user) + else: + passwd = None + host = unquote(host) + user = user or '' + passwd = passwd or '' + + try: + host = socket.gethostbyname(host) + except socket.error as msg: + raise URLError(msg) + path, attrs = splitattr(req.selector) + dirs = path.split('/') + dirs = list(map(unquote, dirs)) + dirs, file = dirs[:-1], dirs[-1] + if dirs and not dirs[0]: + dirs = dirs[1:] + try: + fw = self.connect_ftp(user, passwd, host, port, dirs, req.timeout) + type = file and 'I' or 'D' + for attr in attrs: + attr, value = splitvalue(attr) + if attr.lower() == 'type' and \ + value in ('a', 'A', 'i', 'I', 'd', 'D'): + type = value.upper() + fp, retrlen = fw.retrfile(file, type) + headers = "" + mtype = mimetypes.guess_type(req.full_url)[0] + if mtype: + headers += "Content-type: %s\n" % mtype + if retrlen is not None and retrlen >= 0: + headers += "Content-length: %d\n" % retrlen + headers = email.message_from_string(headers) + return addinfourl(fp, headers, req.full_url) + except ftplib.all_errors as exp: + exc = URLError('ftp error: %r' % exp) + raise_with_traceback(exc) + + def connect_ftp(self, user, passwd, host, port, dirs, timeout): + return ftpwrapper(user, passwd, host, port, dirs, timeout, + persistent=False) + +class CacheFTPHandler(FTPHandler): + # XXX would be nice to have pluggable cache strategies + # XXX this stuff is definitely not thread safe + def __init__(self): + self.cache = {} + self.timeout = {} + self.soonest = 0 + self.delay = 60 + self.max_conns = 16 + + def setTimeout(self, t): + self.delay = t + + def setMaxConns(self, m): + self.max_conns = m + + def connect_ftp(self, user, passwd, host, port, dirs, timeout): + key = user, host, port, '/'.join(dirs), timeout + if key in self.cache: + self.timeout[key] = time.time() + self.delay + else: + self.cache[key] = ftpwrapper(user, passwd, host, port, + dirs, timeout) + self.timeout[key] = time.time() + self.delay + self.check_cache() + return self.cache[key] + + def check_cache(self): + # first check for old ones + t = time.time() + if self.soonest <= t: + for k, v in list(self.timeout.items()): + if v < t: + self.cache[k].close() + del self.cache[k] + del self.timeout[k] + self.soonest = min(list(self.timeout.values())) + + # then check the size + if len(self.cache) == self.max_conns: + for k, v in list(self.timeout.items()): + if v == self.soonest: + del self.cache[k] + del self.timeout[k] + break + self.soonest = min(list(self.timeout.values())) + + def clear_cache(self): + for conn in self.cache.values(): + conn.close() + self.cache.clear() + self.timeout.clear() + + +# Code move from the old urllib module + +MAXFTPCACHE = 10 # Trim the ftp cache beyond this size + +# Helper for non-unix systems +if os.name == 'nt': + from nturl2path import url2pathname, pathname2url +else: + def url2pathname(pathname): + """OS-specific conversion from a relative URL of the 'file' scheme + to a file system path; not recommended for general use.""" + return unquote(pathname) + + def pathname2url(pathname): + """OS-specific conversion from a file system path to a relative URL + of the 'file' scheme; not recommended for general use.""" + return quote(pathname) + +# This really consists of two pieces: +# (1) a class which handles opening of all sorts of URLs +# (plus assorted utilities etc.) +# (2) a set of functions for parsing URLs +# XXX Should these be separated out into different modules? + + +ftpcache = {} +class URLopener(object): + """Class to open URLs. + This is a class rather than just a subroutine because we may need + more than one set of global protocol-specific options. + Note -- this is a base class for those who don't want the + automatic handling of errors type 302 (relocated) and 401 + (authorization needed).""" + + __tempfiles = None + + version = "Python-urllib/%s" % __version__ + + # Constructor + def __init__(self, proxies=None, **x509): + msg = "%(class)s style of invoking requests is deprecated. " \ + "Use newer urlopen functions/methods" % {'class': self.__class__.__name__} + warnings.warn(msg, DeprecationWarning, stacklevel=3) + if proxies is None: + proxies = getproxies() + assert hasattr(proxies, 'keys'), "proxies must be a mapping" + self.proxies = proxies + self.key_file = x509.get('key_file') + self.cert_file = x509.get('cert_file') + self.addheaders = [('User-Agent', self.version)] + self.__tempfiles = [] + self.__unlink = os.unlink # See cleanup() + self.tempcache = None + # Undocumented feature: if you assign {} to tempcache, + # it is used to cache files retrieved with + # self.retrieve(). This is not enabled by default + # since it does not work for changing documents (and I + # haven't got the logic to check expiration headers + # yet). + self.ftpcache = ftpcache + # Undocumented feature: you can use a different + # ftp cache by assigning to the .ftpcache member; + # in case you want logically independent URL openers + # XXX This is not threadsafe. Bah. + + def __del__(self): + self.close() + + def close(self): + self.cleanup() + + def cleanup(self): + # This code sometimes runs when the rest of this module + # has already been deleted, so it can't use any globals + # or import anything. + if self.__tempfiles: + for file in self.__tempfiles: + try: + self.__unlink(file) + except OSError: + pass + del self.__tempfiles[:] + if self.tempcache: + self.tempcache.clear() + + def addheader(self, *args): + """Add a header to be used by the HTTP interface only + e.g. u.addheader('Accept', 'sound/basic')""" + self.addheaders.append(args) + + # External interface + def open(self, fullurl, data=None): + """Use URLopener().open(file) instead of open(file, 'r').""" + fullurl = unwrap(to_bytes(fullurl)) + fullurl = quote(fullurl, safe="%/:=&?~#+!$,;'@()*[]|") + if self.tempcache and fullurl in self.tempcache: + filename, headers = self.tempcache[fullurl] + fp = open(filename, 'rb') + return addinfourl(fp, headers, fullurl) + urltype, url = splittype(fullurl) + if not urltype: + urltype = 'file' + if urltype in self.proxies: + proxy = self.proxies[urltype] + urltype, proxyhost = splittype(proxy) + host, selector = splithost(proxyhost) + url = (host, fullurl) # Signal special case to open_*() + else: + proxy = None + name = 'open_' + urltype + self.type = urltype + name = name.replace('-', '_') + if not hasattr(self, name): + if proxy: + return self.open_unknown_proxy(proxy, fullurl, data) + else: + return self.open_unknown(fullurl, data) + try: + if data is None: + return getattr(self, name)(url) + else: + return getattr(self, name)(url, data) + except HTTPError: + raise + except socket.error as msg: + raise_with_traceback(IOError('socket error', msg)) + + def open_unknown(self, fullurl, data=None): + """Overridable interface to open unknown URL type.""" + type, url = splittype(fullurl) + raise IOError('url error', 'unknown url type', type) + + def open_unknown_proxy(self, proxy, fullurl, data=None): + """Overridable interface to open unknown URL type.""" + type, url = splittype(fullurl) + raise IOError('url error', 'invalid proxy for %s' % type, proxy) + + # External interface + def retrieve(self, url, filename=None, reporthook=None, data=None): + """retrieve(url) returns (filename, headers) for a local object + or (tempfilename, headers) for a remote object.""" + url = unwrap(to_bytes(url)) + if self.tempcache and url in self.tempcache: + return self.tempcache[url] + type, url1 = splittype(url) + if filename is None and (not type or type == 'file'): + try: + fp = self.open_local_file(url1) + hdrs = fp.info() + fp.close() + return url2pathname(splithost(url1)[1]), hdrs + except IOError as msg: + pass + fp = self.open(url, data) + try: + headers = fp.info() + if filename: + tfp = open(filename, 'wb') + else: + import tempfile + garbage, path = splittype(url) + garbage, path = splithost(path or "") + path, garbage = splitquery(path or "") + path, garbage = splitattr(path or "") + suffix = os.path.splitext(path)[1] + (fd, filename) = tempfile.mkstemp(suffix) + self.__tempfiles.append(filename) + tfp = os.fdopen(fd, 'wb') + try: + result = filename, headers + if self.tempcache is not None: + self.tempcache[url] = result + bs = 1024*8 + size = -1 + read = 0 + blocknum = 0 + if "content-length" in headers: + size = int(headers["Content-Length"]) + if reporthook: + reporthook(blocknum, bs, size) + while 1: + block = fp.read(bs) + if not block: + break + read += len(block) + tfp.write(block) + blocknum += 1 + if reporthook: + reporthook(blocknum, bs, size) + finally: + tfp.close() + finally: + fp.close() + + # raise exception if actual size does not match content-length header + if size >= 0 and read < size: + raise ContentTooShortError( + "retrieval incomplete: got only %i out of %i bytes" + % (read, size), result) + + return result + + # Each method named open_ knows how to open that type of URL + + def _open_generic_http(self, connection_factory, url, data): + """Make an HTTP connection using connection_class. + + This is an internal method that should be called from + open_http() or open_https(). + + Arguments: + - connection_factory should take a host name and return an + HTTPConnection instance. + - url is the url to retrieval or a host, relative-path pair. + - data is payload for a POST request or None. + """ + + user_passwd = None + proxy_passwd= None + if isinstance(url, str): + host, selector = splithost(url) + if host: + user_passwd, host = splituser(host) + host = unquote(host) + realhost = host + else: + host, selector = url + # check whether the proxy contains authorization information + proxy_passwd, host = splituser(host) + # now we proceed with the url we want to obtain + urltype, rest = splittype(selector) + url = rest + user_passwd = None + if urltype.lower() != 'http': + realhost = None + else: + realhost, rest = splithost(rest) + if realhost: + user_passwd, realhost = splituser(realhost) + if user_passwd: + selector = "%s://%s%s" % (urltype, realhost, rest) + if proxy_bypass(realhost): + host = realhost + + if not host: raise IOError('http error', 'no host given') + + if proxy_passwd: + proxy_passwd = unquote(proxy_passwd) + proxy_auth = base64.b64encode(proxy_passwd.encode()).decode('ascii') + else: + proxy_auth = None + + if user_passwd: + user_passwd = unquote(user_passwd) + auth = base64.b64encode(user_passwd.encode()).decode('ascii') + else: + auth = None + http_conn = connection_factory(host) + headers = {} + if proxy_auth: + headers["Proxy-Authorization"] = "Basic %s" % proxy_auth + if auth: + headers["Authorization"] = "Basic %s" % auth + if realhost: + headers["Host"] = realhost + + # Add Connection:close as we don't support persistent connections yet. + # This helps in closing the socket and avoiding ResourceWarning + + headers["Connection"] = "close" + + for header, value in self.addheaders: + headers[header] = value + + if data is not None: + headers["Content-Type"] = "application/x-www-form-urlencoded" + http_conn.request("POST", selector, data, headers) + else: + http_conn.request("GET", selector, headers=headers) + + try: + response = http_conn.getresponse() + except http_client.BadStatusLine: + # something went wrong with the HTTP status line + raise URLError("http protocol error: bad status line") + + # According to RFC 2616, "2xx" code indicates that the client's + # request was successfully received, understood, and accepted. + if 200 <= response.status < 300: + return addinfourl(response, response.msg, "http:" + url, + response.status) + else: + return self.http_error( + url, response.fp, + response.status, response.reason, response.msg, data) + + def open_http(self, url, data=None): + """Use HTTP protocol.""" + return self._open_generic_http(http_client.HTTPConnection, url, data) + + def http_error(self, url, fp, errcode, errmsg, headers, data=None): + """Handle http errors. + + Derived class can override this, or provide specific handlers + named http_error_DDD where DDD is the 3-digit error code.""" + # First check if there's a specific handler for this error + name = 'http_error_%d' % errcode + if hasattr(self, name): + method = getattr(self, name) + if data is None: + result = method(url, fp, errcode, errmsg, headers) + else: + result = method(url, fp, errcode, errmsg, headers, data) + if result: return result + return self.http_error_default(url, fp, errcode, errmsg, headers) + + def http_error_default(self, url, fp, errcode, errmsg, headers): + """Default error handler: close the connection and raise IOError.""" + fp.close() + raise HTTPError(url, errcode, errmsg, headers, None) + + if _have_ssl: + def _https_connection(self, host): + return http_client.HTTPSConnection(host, + key_file=self.key_file, + cert_file=self.cert_file) + + def open_https(self, url, data=None): + """Use HTTPS protocol.""" + return self._open_generic_http(self._https_connection, url, data) + + def open_file(self, url): + """Use local file or FTP depending on form of URL.""" + if not isinstance(url, str): + raise URLError('file error: proxy support for file protocol currently not implemented') + if url[:2] == '//' and url[2:3] != '/' and url[2:12].lower() != 'localhost/': + raise ValueError("file:// scheme is supported only on localhost") + else: + return self.open_local_file(url) + + def open_local_file(self, url): + """Use local file.""" + import future.backports.email.utils as email_utils + import mimetypes + host, file = splithost(url) + localname = url2pathname(file) + try: + stats = os.stat(localname) + except OSError as e: + raise URLError(e.strerror, e.filename) + size = stats.st_size + modified = email_utils.formatdate(stats.st_mtime, usegmt=True) + mtype = mimetypes.guess_type(url)[0] + headers = email.message_from_string( + 'Content-Type: %s\nContent-Length: %d\nLast-modified: %s\n' % + (mtype or 'text/plain', size, modified)) + if not host: + urlfile = file + if file[:1] == '/': + urlfile = 'file://' + file + return addinfourl(open(localname, 'rb'), headers, urlfile) + host, port = splitport(host) + if (not port + and socket.gethostbyname(host) in ((localhost(),) + thishost())): + urlfile = file + if file[:1] == '/': + urlfile = 'file://' + file + elif file[:2] == './': + raise ValueError("local file url may start with / or file:. Unknown url of type: %s" % url) + return addinfourl(open(localname, 'rb'), headers, urlfile) + raise URLError('local file error: not on local host') + + def open_ftp(self, url): + """Use FTP protocol.""" + if not isinstance(url, str): + raise URLError('ftp error: proxy support for ftp protocol currently not implemented') + import mimetypes + host, path = splithost(url) + if not host: raise URLError('ftp error: no host given') + host, port = splitport(host) + user, host = splituser(host) + if user: user, passwd = splitpasswd(user) + else: passwd = None + host = unquote(host) + user = unquote(user or '') + passwd = unquote(passwd or '') + host = socket.gethostbyname(host) + if not port: + import ftplib + port = ftplib.FTP_PORT + else: + port = int(port) + path, attrs = splitattr(path) + path = unquote(path) + dirs = path.split('/') + dirs, file = dirs[:-1], dirs[-1] + if dirs and not dirs[0]: dirs = dirs[1:] + if dirs and not dirs[0]: dirs[0] = '/' + key = user, host, port, '/'.join(dirs) + # XXX thread unsafe! + if len(self.ftpcache) > MAXFTPCACHE: + # Prune the cache, rather arbitrarily + for k in self.ftpcache.keys(): + if k != key: + v = self.ftpcache[k] + del self.ftpcache[k] + v.close() + try: + if key not in self.ftpcache: + self.ftpcache[key] = \ + ftpwrapper(user, passwd, host, port, dirs) + if not file: type = 'D' + else: type = 'I' + for attr in attrs: + attr, value = splitvalue(attr) + if attr.lower() == 'type' and \ + value in ('a', 'A', 'i', 'I', 'd', 'D'): + type = value.upper() + (fp, retrlen) = self.ftpcache[key].retrfile(file, type) + mtype = mimetypes.guess_type("ftp:" + url)[0] + headers = "" + if mtype: + headers += "Content-Type: %s\n" % mtype + if retrlen is not None and retrlen >= 0: + headers += "Content-Length: %d\n" % retrlen + headers = email.message_from_string(headers) + return addinfourl(fp, headers, "ftp:" + url) + except ftperrors() as exp: + raise_with_traceback(URLError('ftp error %r' % exp)) + + def open_data(self, url, data=None): + """Use "data" URL.""" + if not isinstance(url, str): + raise URLError('data error: proxy support for data protocol currently not implemented') + # ignore POSTed data + # + # syntax of data URLs: + # dataurl := "data:" [ mediatype ] [ ";base64" ] "," data + # mediatype := [ type "/" subtype ] *( ";" parameter ) + # data := *urlchar + # parameter := attribute "=" value + try: + [type, data] = url.split(',', 1) + except ValueError: + raise IOError('data error', 'bad data URL') + if not type: + type = 'text/plain;charset=US-ASCII' + semi = type.rfind(';') + if semi >= 0 and '=' not in type[semi:]: + encoding = type[semi+1:] + type = type[:semi] + else: + encoding = '' + msg = [] + msg.append('Date: %s'%time.strftime('%a, %d %b %Y %H:%M:%S GMT', + time.gmtime(time.time()))) + msg.append('Content-type: %s' % type) + if encoding == 'base64': + # XXX is this encoding/decoding ok? + data = base64.decodebytes(data.encode('ascii')).decode('latin-1') + else: + data = unquote(data) + msg.append('Content-Length: %d' % len(data)) + msg.append('') + msg.append(data) + msg = '\n'.join(msg) + headers = email.message_from_string(msg) + f = io.StringIO(msg) + #f.fileno = None # needed for addinfourl + return addinfourl(f, headers, url) + + +class FancyURLopener(URLopener): + """Derived class with handlers for errors we can handle (perhaps).""" + + def __init__(self, *args, **kwargs): + URLopener.__init__(self, *args, **kwargs) + self.auth_cache = {} + self.tries = 0 + self.maxtries = 10 + + def http_error_default(self, url, fp, errcode, errmsg, headers): + """Default error handling -- don't raise an exception.""" + return addinfourl(fp, headers, "http:" + url, errcode) + + def http_error_302(self, url, fp, errcode, errmsg, headers, data=None): + """Error 302 -- relocated (temporarily).""" + self.tries += 1 + if self.maxtries and self.tries >= self.maxtries: + if hasattr(self, "http_error_500"): + meth = self.http_error_500 + else: + meth = self.http_error_default + self.tries = 0 + return meth(url, fp, 500, + "Internal Server Error: Redirect Recursion", headers) + result = self.redirect_internal(url, fp, errcode, errmsg, headers, + data) + self.tries = 0 + return result + + def redirect_internal(self, url, fp, errcode, errmsg, headers, data): + if 'location' in headers: + newurl = headers['location'] + elif 'uri' in headers: + newurl = headers['uri'] + else: + return + fp.close() + + # In case the server sent a relative URL, join with original: + newurl = urljoin(self.type + ":" + url, newurl) + + urlparts = urlparse(newurl) + + # For security reasons, we don't allow redirection to anything other + # than http, https and ftp. + + # We are using newer HTTPError with older redirect_internal method + # This older method will get deprecated in 3.3 + + if urlparts.scheme not in ('http', 'https', 'ftp', ''): + raise HTTPError(newurl, errcode, + errmsg + + " Redirection to url '%s' is not allowed." % newurl, + headers, fp) + + return self.open(newurl) + + def http_error_301(self, url, fp, errcode, errmsg, headers, data=None): + """Error 301 -- also relocated (permanently).""" + return self.http_error_302(url, fp, errcode, errmsg, headers, data) + + def http_error_303(self, url, fp, errcode, errmsg, headers, data=None): + """Error 303 -- also relocated (essentially identical to 302).""" + return self.http_error_302(url, fp, errcode, errmsg, headers, data) + + def http_error_307(self, url, fp, errcode, errmsg, headers, data=None): + """Error 307 -- relocated, but turn POST into error.""" + if data is None: + return self.http_error_302(url, fp, errcode, errmsg, headers, data) + else: + return self.http_error_default(url, fp, errcode, errmsg, headers) + + def http_error_401(self, url, fp, errcode, errmsg, headers, data=None, + retry=False): + """Error 401 -- authentication required. + This function supports Basic authentication only.""" + if 'www-authenticate' not in headers: + URLopener.http_error_default(self, url, fp, + errcode, errmsg, headers) + stuff = headers['www-authenticate'] + match = re.match('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', stuff) + if not match: + URLopener.http_error_default(self, url, fp, + errcode, errmsg, headers) + scheme, realm = match.groups() + if scheme.lower() != 'basic': + URLopener.http_error_default(self, url, fp, + errcode, errmsg, headers) + if not retry: + URLopener.http_error_default(self, url, fp, errcode, errmsg, + headers) + name = 'retry_' + self.type + '_basic_auth' + if data is None: + return getattr(self,name)(url, realm) + else: + return getattr(self,name)(url, realm, data) + + def http_error_407(self, url, fp, errcode, errmsg, headers, data=None, + retry=False): + """Error 407 -- proxy authentication required. + This function supports Basic authentication only.""" + if 'proxy-authenticate' not in headers: + URLopener.http_error_default(self, url, fp, + errcode, errmsg, headers) + stuff = headers['proxy-authenticate'] + match = re.match('[ \t]*([^ \t]+)[ \t]+realm="([^"]*)"', stuff) + if not match: + URLopener.http_error_default(self, url, fp, + errcode, errmsg, headers) + scheme, realm = match.groups() + if scheme.lower() != 'basic': + URLopener.http_error_default(self, url, fp, + errcode, errmsg, headers) + if not retry: + URLopener.http_error_default(self, url, fp, errcode, errmsg, + headers) + name = 'retry_proxy_' + self.type + '_basic_auth' + if data is None: + return getattr(self,name)(url, realm) + else: + return getattr(self,name)(url, realm, data) + + def retry_proxy_http_basic_auth(self, url, realm, data=None): + host, selector = splithost(url) + newurl = 'http://' + host + selector + proxy = self.proxies['http'] + urltype, proxyhost = splittype(proxy) + proxyhost, proxyselector = splithost(proxyhost) + i = proxyhost.find('@') + 1 + proxyhost = proxyhost[i:] + user, passwd = self.get_user_passwd(proxyhost, realm, i) + if not (user or passwd): return None + proxyhost = "%s:%s@%s" % (quote(user, safe=''), + quote(passwd, safe=''), proxyhost) + self.proxies['http'] = 'http://' + proxyhost + proxyselector + if data is None: + return self.open(newurl) + else: + return self.open(newurl, data) + + def retry_proxy_https_basic_auth(self, url, realm, data=None): + host, selector = splithost(url) + newurl = 'https://' + host + selector + proxy = self.proxies['https'] + urltype, proxyhost = splittype(proxy) + proxyhost, proxyselector = splithost(proxyhost) + i = proxyhost.find('@') + 1 + proxyhost = proxyhost[i:] + user, passwd = self.get_user_passwd(proxyhost, realm, i) + if not (user or passwd): return None + proxyhost = "%s:%s@%s" % (quote(user, safe=''), + quote(passwd, safe=''), proxyhost) + self.proxies['https'] = 'https://' + proxyhost + proxyselector + if data is None: + return self.open(newurl) + else: + return self.open(newurl, data) + + def retry_http_basic_auth(self, url, realm, data=None): + host, selector = splithost(url) + i = host.find('@') + 1 + host = host[i:] + user, passwd = self.get_user_passwd(host, realm, i) + if not (user or passwd): return None + host = "%s:%s@%s" % (quote(user, safe=''), + quote(passwd, safe=''), host) + newurl = 'http://' + host + selector + if data is None: + return self.open(newurl) + else: + return self.open(newurl, data) + + def retry_https_basic_auth(self, url, realm, data=None): + host, selector = splithost(url) + i = host.find('@') + 1 + host = host[i:] + user, passwd = self.get_user_passwd(host, realm, i) + if not (user or passwd): return None + host = "%s:%s@%s" % (quote(user, safe=''), + quote(passwd, safe=''), host) + newurl = 'https://' + host + selector + if data is None: + return self.open(newurl) + else: + return self.open(newurl, data) + + def get_user_passwd(self, host, realm, clear_cache=0): + key = realm + '@' + host.lower() + if key in self.auth_cache: + if clear_cache: + del self.auth_cache[key] + else: + return self.auth_cache[key] + user, passwd = self.prompt_user_passwd(host, realm) + if user or passwd: self.auth_cache[key] = (user, passwd) + return user, passwd + + def prompt_user_passwd(self, host, realm): + """Override this in a GUI environment!""" + import getpass + try: + user = input("Enter username for %s at %s: " % (realm, host)) + passwd = getpass.getpass("Enter password for %s in %s at %s: " % + (user, realm, host)) + return user, passwd + except KeyboardInterrupt: + print() + return None, None + + +# Utility functions + +_localhost = None +def localhost(): + """Return the IP address of the magic hostname 'localhost'.""" + global _localhost + if _localhost is None: + _localhost = socket.gethostbyname('localhost') + return _localhost + +_thishost = None +def thishost(): + """Return the IP addresses of the current host.""" + global _thishost + if _thishost is None: + try: + _thishost = tuple(socket.gethostbyname_ex(socket.gethostname())[2]) + except socket.gaierror: + _thishost = tuple(socket.gethostbyname_ex('localhost')[2]) + return _thishost + +_ftperrors = None +def ftperrors(): + """Return the set of errors raised by the FTP class.""" + global _ftperrors + if _ftperrors is None: + import ftplib + _ftperrors = ftplib.all_errors + return _ftperrors + +_noheaders = None +def noheaders(): + """Return an empty email Message object.""" + global _noheaders + if _noheaders is None: + _noheaders = email.message_from_string("") + return _noheaders + + +# Utility classes + +class ftpwrapper(object): + """Class used by open_ftp() for cache of open FTP connections.""" + + def __init__(self, user, passwd, host, port, dirs, timeout=None, + persistent=True): + self.user = user + self.passwd = passwd + self.host = host + self.port = port + self.dirs = dirs + self.timeout = timeout + self.refcount = 0 + self.keepalive = persistent + self.init() + + def init(self): + import ftplib + self.busy = 0 + self.ftp = ftplib.FTP() + self.ftp.connect(self.host, self.port, self.timeout) + self.ftp.login(self.user, self.passwd) + _target = '/'.join(self.dirs) + self.ftp.cwd(_target) + + def retrfile(self, file, type): + import ftplib + self.endtransfer() + if type in ('d', 'D'): cmd = 'TYPE A'; isdir = 1 + else: cmd = 'TYPE ' + type; isdir = 0 + try: + self.ftp.voidcmd(cmd) + except ftplib.all_errors: + self.init() + self.ftp.voidcmd(cmd) + conn = None + if file and not isdir: + # Try to retrieve as a file + try: + cmd = 'RETR ' + file + conn, retrlen = self.ftp.ntransfercmd(cmd) + except ftplib.error_perm as reason: + if str(reason)[:3] != '550': + raise_with_traceback(URLError('ftp error: %r' % reason)) + if not conn: + # Set transfer mode to ASCII! + self.ftp.voidcmd('TYPE A') + # Try a directory listing. Verify that directory exists. + if file: + pwd = self.ftp.pwd() + try: + try: + self.ftp.cwd(file) + except ftplib.error_perm as reason: + ### Was: + # raise URLError('ftp error: %r' % reason) from reason + exc = URLError('ftp error: %r' % reason) + exc.__cause__ = reason + raise exc + finally: + self.ftp.cwd(pwd) + cmd = 'LIST ' + file + else: + cmd = 'LIST' + conn, retrlen = self.ftp.ntransfercmd(cmd) + self.busy = 1 + + ftpobj = addclosehook(conn.makefile('rb'), self.file_close) + self.refcount += 1 + conn.close() + # Pass back both a suitably decorated object and a retrieval length + return (ftpobj, retrlen) + + def endtransfer(self): + self.busy = 0 + + def close(self): + self.keepalive = False + if self.refcount <= 0: + self.real_close() + + def file_close(self): + self.endtransfer() + self.refcount -= 1 + if self.refcount <= 0 and not self.keepalive: + self.real_close() + + def real_close(self): + self.endtransfer() + try: + self.ftp.close() + except ftperrors(): + pass + +# Proxy handling +def getproxies_environment(): + """Return a dictionary of scheme -> proxy server URL mappings. + + Scan the environment for variables named _proxy; + this seems to be the standard convention. If you need a + different way, you can pass a proxies dictionary to the + [Fancy]URLopener constructor. + + """ + proxies = {} + for name, value in os.environ.items(): + name = name.lower() + if value and name[-6:] == '_proxy': + proxies[name[:-6]] = value + return proxies + +def proxy_bypass_environment(host): + """Test if proxies should not be used for a particular host. + + Checks the environment for a variable named no_proxy, which should + be a list of DNS suffixes separated by commas, or '*' for all hosts. + """ + no_proxy = os.environ.get('no_proxy', '') or os.environ.get('NO_PROXY', '') + # '*' is special case for always bypass + if no_proxy == '*': + return 1 + # strip port off host + hostonly, port = splitport(host) + # check if the host ends with any of the DNS suffixes + no_proxy_list = [proxy.strip() for proxy in no_proxy.split(',')] + for name in no_proxy_list: + if name and (hostonly.endswith(name) or host.endswith(name)): + return 1 + # otherwise, don't bypass + return 0 + + +# This code tests an OSX specific data structure but is testable on all +# platforms +def _proxy_bypass_macosx_sysconf(host, proxy_settings): + """ + Return True iff this host shouldn't be accessed using a proxy + + This function uses the MacOSX framework SystemConfiguration + to fetch the proxy information. + + proxy_settings come from _scproxy._get_proxy_settings or get mocked ie: + { 'exclude_simple': bool, + 'exceptions': ['foo.bar', '*.bar.com', '127.0.0.1', '10.1', '10.0/16'] + } + """ + from fnmatch import fnmatch + + hostonly, port = splitport(host) + + def ip2num(ipAddr): + parts = ipAddr.split('.') + parts = list(map(int, parts)) + if len(parts) != 4: + parts = (parts + [0, 0, 0, 0])[:4] + return (parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3] + + # Check for simple host names: + if '.' not in host: + if proxy_settings['exclude_simple']: + return True + + hostIP = None + + for value in proxy_settings.get('exceptions', ()): + # Items in the list are strings like these: *.local, 169.254/16 + if not value: continue + + m = re.match(r"(\d+(?:\.\d+)*)(/\d+)?", value) + if m is not None: + if hostIP is None: + try: + hostIP = socket.gethostbyname(hostonly) + hostIP = ip2num(hostIP) + except socket.error: + continue + + base = ip2num(m.group(1)) + mask = m.group(2) + if mask is None: + mask = 8 * (m.group(1).count('.') + 1) + else: + mask = int(mask[1:]) + mask = 32 - mask + + if (hostIP >> mask) == (base >> mask): + return True + + elif fnmatch(host, value): + return True + + return False + + +if sys.platform == 'darwin': + from _scproxy import _get_proxy_settings, _get_proxies + + def proxy_bypass_macosx_sysconf(host): + proxy_settings = _get_proxy_settings() + return _proxy_bypass_macosx_sysconf(host, proxy_settings) + + def getproxies_macosx_sysconf(): + """Return a dictionary of scheme -> proxy server URL mappings. + + This function uses the MacOSX framework SystemConfiguration + to fetch the proxy information. + """ + return _get_proxies() + + + + def proxy_bypass(host): + if getproxies_environment(): + return proxy_bypass_environment(host) + else: + return proxy_bypass_macosx_sysconf(host) + + def getproxies(): + return getproxies_environment() or getproxies_macosx_sysconf() + + +elif os.name == 'nt': + def getproxies_registry(): + """Return a dictionary of scheme -> proxy server URL mappings. + + Win32 uses the registry to store proxies. + + """ + proxies = {} + try: + import winreg + except ImportError: + # Std module, so should be around - but you never know! + return proxies + try: + internetSettings = winreg.OpenKey(winreg.HKEY_CURRENT_USER, + r'Software\Microsoft\Windows\CurrentVersion\Internet Settings') + proxyEnable = winreg.QueryValueEx(internetSettings, + 'ProxyEnable')[0] + if proxyEnable: + # Returned as Unicode but problems if not converted to ASCII + proxyServer = str(winreg.QueryValueEx(internetSettings, + 'ProxyServer')[0]) + if '=' in proxyServer: + # Per-protocol settings + for p in proxyServer.split(';'): + protocol, address = p.split('=', 1) + # See if address has a type:// prefix + if not re.match('^([^/:]+)://', address): + address = '%s://%s' % (protocol, address) + proxies[protocol] = address + else: + # Use one setting for all protocols + if proxyServer[:5] == 'http:': + proxies['http'] = proxyServer + else: + proxies['http'] = 'http://%s' % proxyServer + proxies['https'] = 'https://%s' % proxyServer + proxies['ftp'] = 'ftp://%s' % proxyServer + internetSettings.Close() + except (WindowsError, ValueError, TypeError): + # Either registry key not found etc, or the value in an + # unexpected format. + # proxies already set up to be empty so nothing to do + pass + return proxies + + def getproxies(): + """Return a dictionary of scheme -> proxy server URL mappings. + + Returns settings gathered from the environment, if specified, + or the registry. + + """ + return getproxies_environment() or getproxies_registry() + + def proxy_bypass_registry(host): + try: + import winreg + except ImportError: + # Std modules, so should be around - but you never know! + return 0 + try: + internetSettings = winreg.OpenKey(winreg.HKEY_CURRENT_USER, + r'Software\Microsoft\Windows\CurrentVersion\Internet Settings') + proxyEnable = winreg.QueryValueEx(internetSettings, + 'ProxyEnable')[0] + proxyOverride = str(winreg.QueryValueEx(internetSettings, + 'ProxyOverride')[0]) + # ^^^^ Returned as Unicode but problems if not converted to ASCII + except WindowsError: + return 0 + if not proxyEnable or not proxyOverride: + return 0 + # try to make a host list from name and IP address. + rawHost, port = splitport(host) + host = [rawHost] + try: + addr = socket.gethostbyname(rawHost) + if addr != rawHost: + host.append(addr) + except socket.error: + pass + try: + fqdn = socket.getfqdn(rawHost) + if fqdn != rawHost: + host.append(fqdn) + except socket.error: + pass + # make a check value list from the registry entry: replace the + # '' string by the localhost entry and the corresponding + # canonical entry. + proxyOverride = proxyOverride.split(';') + # now check if we match one of the registry values. + for test in proxyOverride: + if test == '': + if '.' not in rawHost: + return 1 + test = test.replace(".", r"\.") # mask dots + test = test.replace("*", r".*") # change glob sequence + test = test.replace("?", r".") # change glob char + for val in host: + if re.match(test, val, re.I): + return 1 + return 0 + + def proxy_bypass(host): + """Return a dictionary of scheme -> proxy server URL mappings. + + Returns settings gathered from the environment, if specified, + or the registry. + + """ + if getproxies_environment(): + return proxy_bypass_environment(host) + else: + return proxy_bypass_registry(host) + +else: + # By default use environment variables + getproxies = getproxies_environment + proxy_bypass = proxy_bypass_environment diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/response.py b/venv/lib/python3.10/site-packages/future/backports/urllib/response.py new file mode 100644 index 0000000..adbf6e5 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/urllib/response.py @@ -0,0 +1,103 @@ +"""Response classes used by urllib. + +The base class, addbase, defines a minimal file-like interface, +including read() and readline(). The typical response object is an +addinfourl instance, which defines an info() method that returns +headers and a geturl() method that returns the url. +""" +from __future__ import absolute_import, division, unicode_literals +from future.builtins import object + +class addbase(object): + """Base class for addinfo and addclosehook.""" + + # XXX Add a method to expose the timeout on the underlying socket? + + def __init__(self, fp): + # TODO(jhylton): Is there a better way to delegate using io? + self.fp = fp + self.read = self.fp.read + self.readline = self.fp.readline + # TODO(jhylton): Make sure an object with readlines() is also iterable + if hasattr(self.fp, "readlines"): + self.readlines = self.fp.readlines + if hasattr(self.fp, "fileno"): + self.fileno = self.fp.fileno + else: + self.fileno = lambda: None + + def __iter__(self): + # Assigning `__iter__` to the instance doesn't work as intended + # because the iter builtin does something like `cls.__iter__(obj)` + # and thus fails to find the _bound_ method `obj.__iter__`. + # Returning just `self.fp` works for built-in file objects but + # might not work for general file-like objects. + return iter(self.fp) + + def __repr__(self): + return '<%s at %r whose fp = %r>' % (self.__class__.__name__, + id(self), self.fp) + + def close(self): + if self.fp: + self.fp.close() + self.fp = None + self.read = None + self.readline = None + self.readlines = None + self.fileno = None + self.__iter__ = None + self.__next__ = None + + def __enter__(self): + if self.fp is None: + raise ValueError("I/O operation on closed file") + return self + + def __exit__(self, type, value, traceback): + self.close() + +class addclosehook(addbase): + """Class to add a close hook to an open file.""" + + def __init__(self, fp, closehook, *hookargs): + addbase.__init__(self, fp) + self.closehook = closehook + self.hookargs = hookargs + + def close(self): + if self.closehook: + self.closehook(*self.hookargs) + self.closehook = None + self.hookargs = None + addbase.close(self) + +class addinfo(addbase): + """class to add an info() method to an open file.""" + + def __init__(self, fp, headers): + addbase.__init__(self, fp) + self.headers = headers + + def info(self): + return self.headers + +class addinfourl(addbase): + """class to add info() and geturl() methods to an open file.""" + + def __init__(self, fp, headers, url, code=None): + addbase.__init__(self, fp) + self.headers = headers + self.url = url + self.code = code + + def info(self): + return self.headers + + def getcode(self): + return self.code + + def geturl(self): + return self.url + +del absolute_import, division, unicode_literals, object diff --git a/venv/lib/python3.10/site-packages/future/backports/urllib/robotparser.py b/venv/lib/python3.10/site-packages/future/backports/urllib/robotparser.py new file mode 100644 index 0000000..a0f3651 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/urllib/robotparser.py @@ -0,0 +1,211 @@ +from __future__ import absolute_import, division, unicode_literals +from future.builtins import str +""" robotparser.py + + Copyright (C) 2000 Bastian Kleineidam + + You can choose between two licenses when using this package: + 1) GNU GPLv2 + 2) PSF license for Python 2.2 + + The robots.txt Exclusion Protocol is implemented as specified in + http://info.webcrawler.com/mak/projects/robots/norobots-rfc.html +""" + +# Was: import urllib.parse, urllib.request +from future.backports import urllib +from future.backports.urllib import parse as _parse, request as _request +urllib.parse = _parse +urllib.request = _request + + +__all__ = ["RobotFileParser"] + +class RobotFileParser(object): + """ This class provides a set of methods to read, parse and answer + questions about a single robots.txt file. + + """ + + def __init__(self, url=''): + self.entries = [] + self.default_entry = None + self.disallow_all = False + self.allow_all = False + self.set_url(url) + self.last_checked = 0 + + def mtime(self): + """Returns the time the robots.txt file was last fetched. + + This is useful for long-running web spiders that need to + check for new robots.txt files periodically. + + """ + return self.last_checked + + def modified(self): + """Sets the time the robots.txt file was last fetched to the + current time. + + """ + import time + self.last_checked = time.time() + + def set_url(self, url): + """Sets the URL referring to a robots.txt file.""" + self.url = url + self.host, self.path = urllib.parse.urlparse(url)[1:3] + + def read(self): + """Reads the robots.txt URL and feeds it to the parser.""" + try: + f = urllib.request.urlopen(self.url) + except urllib.error.HTTPError as err: + if err.code in (401, 403): + self.disallow_all = True + elif err.code >= 400: + self.allow_all = True + else: + raw = f.read() + self.parse(raw.decode("utf-8").splitlines()) + + def _add_entry(self, entry): + if "*" in entry.useragents: + # the default entry is considered last + if self.default_entry is None: + # the first default entry wins + self.default_entry = entry + else: + self.entries.append(entry) + + def parse(self, lines): + """Parse the input lines from a robots.txt file. + + We allow that a user-agent: line is not preceded by + one or more blank lines. + """ + # states: + # 0: start state + # 1: saw user-agent line + # 2: saw an allow or disallow line + state = 0 + entry = Entry() + + for line in lines: + if not line: + if state == 1: + entry = Entry() + state = 0 + elif state == 2: + self._add_entry(entry) + entry = Entry() + state = 0 + # remove optional comment and strip line + i = line.find('#') + if i >= 0: + line = line[:i] + line = line.strip() + if not line: + continue + line = line.split(':', 1) + if len(line) == 2: + line[0] = line[0].strip().lower() + line[1] = urllib.parse.unquote(line[1].strip()) + if line[0] == "user-agent": + if state == 2: + self._add_entry(entry) + entry = Entry() + entry.useragents.append(line[1]) + state = 1 + elif line[0] == "disallow": + if state != 0: + entry.rulelines.append(RuleLine(line[1], False)) + state = 2 + elif line[0] == "allow": + if state != 0: + entry.rulelines.append(RuleLine(line[1], True)) + state = 2 + if state == 2: + self._add_entry(entry) + + + def can_fetch(self, useragent, url): + """using the parsed robots.txt decide if useragent can fetch url""" + if self.disallow_all: + return False + if self.allow_all: + return True + # search for given user agent matches + # the first match counts + parsed_url = urllib.parse.urlparse(urllib.parse.unquote(url)) + url = urllib.parse.urlunparse(('','',parsed_url.path, + parsed_url.params,parsed_url.query, parsed_url.fragment)) + url = urllib.parse.quote(url) + if not url: + url = "/" + for entry in self.entries: + if entry.applies_to(useragent): + return entry.allowance(url) + # try the default entry last + if self.default_entry: + return self.default_entry.allowance(url) + # agent not found ==> access granted + return True + + def __str__(self): + return ''.join([str(entry) + "\n" for entry in self.entries]) + + +class RuleLine(object): + """A rule line is a single "Allow:" (allowance==True) or "Disallow:" + (allowance==False) followed by a path.""" + def __init__(self, path, allowance): + if path == '' and not allowance: + # an empty value means allow all + allowance = True + self.path = urllib.parse.quote(path) + self.allowance = allowance + + def applies_to(self, filename): + return self.path == "*" or filename.startswith(self.path) + + def __str__(self): + return (self.allowance and "Allow" or "Disallow") + ": " + self.path + + +class Entry(object): + """An entry has one or more user-agents and zero or more rulelines""" + def __init__(self): + self.useragents = [] + self.rulelines = [] + + def __str__(self): + ret = [] + for agent in self.useragents: + ret.extend(["User-agent: ", agent, "\n"]) + for line in self.rulelines: + ret.extend([str(line), "\n"]) + return ''.join(ret) + + def applies_to(self, useragent): + """check if this entry applies to the specified agent""" + # split the name token and make it lower case + useragent = useragent.split("/")[0].lower() + for agent in self.useragents: + if agent == '*': + # we have the catch-all agent + return True + agent = agent.lower() + if agent in useragent: + return True + return False + + def allowance(self, filename): + """Preconditions: + - our agent applies to this entry + - filename is URL decoded""" + for line in self.rulelines: + if line.applies_to(filename): + return line.allowance + return True diff --git a/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__init__.py b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__init__.py new file mode 100644 index 0000000..196d378 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__init__.py @@ -0,0 +1 @@ +# This directory is a Python package. diff --git a/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..82d0098 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__pycache__/client.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__pycache__/client.cpython-310.pyc new file mode 100644 index 0000000..71d1785 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__pycache__/client.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__pycache__/server.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__pycache__/server.cpython-310.pyc new file mode 100644 index 0000000..557628b Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/__pycache__/server.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/backports/xmlrpc/client.py b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/client.py new file mode 100644 index 0000000..0838f61 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/client.py @@ -0,0 +1,1497 @@ +# +# XML-RPC CLIENT LIBRARY +# $Id$ +# +# an XML-RPC client interface for Python. +# +# the marshalling and response parser code can also be used to +# implement XML-RPC servers. +# +# Notes: +# this version is designed to work with Python 2.1 or newer. +# +# History: +# 1999-01-14 fl Created +# 1999-01-15 fl Changed dateTime to use localtime +# 1999-01-16 fl Added Binary/base64 element, default to RPC2 service +# 1999-01-19 fl Fixed array data element (from Skip Montanaro) +# 1999-01-21 fl Fixed dateTime constructor, etc. +# 1999-02-02 fl Added fault handling, handle empty sequences, etc. +# 1999-02-10 fl Fixed problem with empty responses (from Skip Montanaro) +# 1999-06-20 fl Speed improvements, pluggable parsers/transports (0.9.8) +# 2000-11-28 fl Changed boolean to check the truth value of its argument +# 2001-02-24 fl Added encoding/Unicode/SafeTransport patches +# 2001-02-26 fl Added compare support to wrappers (0.9.9/1.0b1) +# 2001-03-28 fl Make sure response tuple is a singleton +# 2001-03-29 fl Don't require empty params element (from Nicholas Riley) +# 2001-06-10 fl Folded in _xmlrpclib accelerator support (1.0b2) +# 2001-08-20 fl Base xmlrpclib.Error on built-in Exception (from Paul Prescod) +# 2001-09-03 fl Allow Transport subclass to override getparser +# 2001-09-10 fl Lazy import of urllib, cgi, xmllib (20x import speedup) +# 2001-10-01 fl Remove containers from memo cache when done with them +# 2001-10-01 fl Use faster escape method (80% dumps speedup) +# 2001-10-02 fl More dumps microtuning +# 2001-10-04 fl Make sure import expat gets a parser (from Guido van Rossum) +# 2001-10-10 sm Allow long ints to be passed as ints if they don't overflow +# 2001-10-17 sm Test for int and long overflow (allows use on 64-bit systems) +# 2001-11-12 fl Use repr() to marshal doubles (from Paul Felix) +# 2002-03-17 fl Avoid buffered read when possible (from James Rucker) +# 2002-04-07 fl Added pythondoc comments +# 2002-04-16 fl Added __str__ methods to datetime/binary wrappers +# 2002-05-15 fl Added error constants (from Andrew Kuchling) +# 2002-06-27 fl Merged with Python CVS version +# 2002-10-22 fl Added basic authentication (based on code from Phillip Eby) +# 2003-01-22 sm Add support for the bool type +# 2003-02-27 gvr Remove apply calls +# 2003-04-24 sm Use cStringIO if available +# 2003-04-25 ak Add support for nil +# 2003-06-15 gn Add support for time.struct_time +# 2003-07-12 gp Correct marshalling of Faults +# 2003-10-31 mvl Add multicall support +# 2004-08-20 mvl Bump minimum supported Python version to 2.1 +# +# Copyright (c) 1999-2002 by Secret Labs AB. +# Copyright (c) 1999-2002 by Fredrik Lundh. +# +# info@pythonware.com +# http://www.pythonware.com +# +# -------------------------------------------------------------------- +# The XML-RPC client interface is +# +# Copyright (c) 1999-2002 by Secret Labs AB +# Copyright (c) 1999-2002 by Fredrik Lundh +# +# By obtaining, using, and/or copying this software and/or its +# associated documentation, you agree that you have read, understood, +# and will comply with the following terms and conditions: +# +# Permission to use, copy, modify, and distribute this software and +# its associated documentation for any purpose and without fee is +# hereby granted, provided that the above copyright notice appears in +# all copies, and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Secret Labs AB or the author not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD +# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT- +# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR +# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY +# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE +# OF THIS SOFTWARE. +# -------------------------------------------------------------------- + +""" +Ported using Python-Future from the Python 3.3 standard library. + +An XML-RPC client interface for Python. + +The marshalling and response parser code can also be used to +implement XML-RPC servers. + +Exported exceptions: + + Error Base class for client errors + ProtocolError Indicates an HTTP protocol error + ResponseError Indicates a broken response package + Fault Indicates an XML-RPC fault package + +Exported classes: + + ServerProxy Represents a logical connection to an XML-RPC server + + MultiCall Executor of boxcared xmlrpc requests + DateTime dateTime wrapper for an ISO 8601 string or time tuple or + localtime integer value to generate a "dateTime.iso8601" + XML-RPC value + Binary binary data wrapper + + Marshaller Generate an XML-RPC params chunk from a Python data structure + Unmarshaller Unmarshal an XML-RPC response from incoming XML event message + Transport Handles an HTTP transaction to an XML-RPC server + SafeTransport Handles an HTTPS transaction to an XML-RPC server + +Exported constants: + + (none) + +Exported functions: + + getparser Create instance of the fastest available parser & attach + to an unmarshalling object + dumps Convert an argument tuple or a Fault instance to an XML-RPC + request (or response, if the methodresponse option is used). + loads Convert an XML-RPC packet to unmarshalled data plus a method + name (None if not present). +""" + +from __future__ import (absolute_import, division, print_function, + unicode_literals) +from future.builtins import bytes, dict, int, range, str + +import base64 +import sys +if sys.version_info < (3, 9): + # Py2.7 compatibility hack + base64.encodebytes = base64.encodestring + base64.decodebytes = base64.decodestring +import time +from datetime import datetime +from future.backports.http import client as http_client +from future.backports.urllib import parse as urllib_parse +from future.utils import ensure_new_type +from xml.parsers import expat +import socket +import errno +from io import BytesIO +try: + import gzip +except ImportError: + gzip = None #python can be built without zlib/gzip support + +# -------------------------------------------------------------------- +# Internal stuff + +def escape(s): + s = s.replace("&", "&") + s = s.replace("<", "<") + return s.replace(">", ">",) + +# used in User-Agent header sent +__version__ = sys.version[:3] + +# xmlrpc integer limits +MAXINT = 2**31-1 +MININT = -2**31 + +# -------------------------------------------------------------------- +# Error constants (from Dan Libby's specification at +# http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php) + +# Ranges of errors +PARSE_ERROR = -32700 +SERVER_ERROR = -32600 +APPLICATION_ERROR = -32500 +SYSTEM_ERROR = -32400 +TRANSPORT_ERROR = -32300 + +# Specific errors +NOT_WELLFORMED_ERROR = -32700 +UNSUPPORTED_ENCODING = -32701 +INVALID_ENCODING_CHAR = -32702 +INVALID_XMLRPC = -32600 +METHOD_NOT_FOUND = -32601 +INVALID_METHOD_PARAMS = -32602 +INTERNAL_ERROR = -32603 + +# -------------------------------------------------------------------- +# Exceptions + +## +# Base class for all kinds of client-side errors. + +class Error(Exception): + """Base class for client errors.""" + def __str__(self): + return repr(self) + +## +# Indicates an HTTP-level protocol error. This is raised by the HTTP +# transport layer, if the server returns an error code other than 200 +# (OK). +# +# @param url The target URL. +# @param errcode The HTTP error code. +# @param errmsg The HTTP error message. +# @param headers The HTTP header dictionary. + +class ProtocolError(Error): + """Indicates an HTTP protocol error.""" + def __init__(self, url, errcode, errmsg, headers): + Error.__init__(self) + self.url = url + self.errcode = errcode + self.errmsg = errmsg + self.headers = headers + def __repr__(self): + return ( + "" % + (self.url, self.errcode, self.errmsg) + ) + +## +# Indicates a broken XML-RPC response package. This exception is +# raised by the unmarshalling layer, if the XML-RPC response is +# malformed. + +class ResponseError(Error): + """Indicates a broken response package.""" + pass + +## +# Indicates an XML-RPC fault response package. This exception is +# raised by the unmarshalling layer, if the XML-RPC response contains +# a fault string. This exception can also be used as a class, to +# generate a fault XML-RPC message. +# +# @param faultCode The XML-RPC fault code. +# @param faultString The XML-RPC fault string. + +class Fault(Error): + """Indicates an XML-RPC fault package.""" + def __init__(self, faultCode, faultString, **extra): + Error.__init__(self) + self.faultCode = faultCode + self.faultString = faultString + def __repr__(self): + return "" % (ensure_new_type(self.faultCode), + ensure_new_type(self.faultString)) + +# -------------------------------------------------------------------- +# Special values + +## +# Backwards compatibility + +boolean = Boolean = bool + +## +# Wrapper for XML-RPC DateTime values. This converts a time value to +# the format used by XML-RPC. +#

+# The value can be given as a datetime object, as a string in the +# format "yyyymmddThh:mm:ss", as a 9-item time tuple (as returned by +# time.localtime()), or an integer value (as returned by time.time()). +# The wrapper uses time.localtime() to convert an integer to a time +# tuple. +# +# @param value The time, given as a datetime object, an ISO 8601 string, +# a time tuple, or an integer time value. + + +### For Python-Future: +def _iso8601_format(value): + return "%04d%02d%02dT%02d:%02d:%02d" % ( + value.year, value.month, value.day, + value.hour, value.minute, value.second) +### +# Issue #13305: different format codes across platforms +# _day0 = datetime(1, 1, 1) +# if _day0.strftime('%Y') == '0001': # Mac OS X +# def _iso8601_format(value): +# return value.strftime("%Y%m%dT%H:%M:%S") +# elif _day0.strftime('%4Y') == '0001': # Linux +# def _iso8601_format(value): +# return value.strftime("%4Y%m%dT%H:%M:%S") +# else: +# def _iso8601_format(value): +# return value.strftime("%Y%m%dT%H:%M:%S").zfill(17) +# del _day0 + + +def _strftime(value): + if isinstance(value, datetime): + return _iso8601_format(value) + + if not isinstance(value, (tuple, time.struct_time)): + if value == 0: + value = time.time() + value = time.localtime(value) + + return "%04d%02d%02dT%02d:%02d:%02d" % value[:6] + +class DateTime(object): + """DateTime wrapper for an ISO 8601 string or time tuple or + localtime integer value to generate 'dateTime.iso8601' XML-RPC + value. + """ + + def __init__(self, value=0): + if isinstance(value, str): + self.value = value + else: + self.value = _strftime(value) + + def make_comparable(self, other): + if isinstance(other, DateTime): + s = self.value + o = other.value + elif isinstance(other, datetime): + s = self.value + o = _iso8601_format(other) + elif isinstance(other, str): + s = self.value + o = other + elif hasattr(other, "timetuple"): + s = self.timetuple() + o = other.timetuple() + else: + otype = (hasattr(other, "__class__") + and other.__class__.__name__ + or type(other)) + raise TypeError("Can't compare %s and %s" % + (self.__class__.__name__, otype)) + return s, o + + def __lt__(self, other): + s, o = self.make_comparable(other) + return s < o + + def __le__(self, other): + s, o = self.make_comparable(other) + return s <= o + + def __gt__(self, other): + s, o = self.make_comparable(other) + return s > o + + def __ge__(self, other): + s, o = self.make_comparable(other) + return s >= o + + def __eq__(self, other): + s, o = self.make_comparable(other) + return s == o + + def __ne__(self, other): + s, o = self.make_comparable(other) + return s != o + + def timetuple(self): + return time.strptime(self.value, "%Y%m%dT%H:%M:%S") + + ## + # Get date/time value. + # + # @return Date/time value, as an ISO 8601 string. + + def __str__(self): + return self.value + + def __repr__(self): + return "" % (ensure_new_type(self.value), id(self)) + + def decode(self, data): + self.value = str(data).strip() + + def encode(self, out): + out.write("") + out.write(self.value) + out.write("\n") + +def _datetime(data): + # decode xml element contents into a DateTime structure. + value = DateTime() + value.decode(data) + return value + +def _datetime_type(data): + return datetime.strptime(data, "%Y%m%dT%H:%M:%S") + +## +# Wrapper for binary data. This can be used to transport any kind +# of binary data over XML-RPC, using BASE64 encoding. +# +# @param data An 8-bit string containing arbitrary data. + +class Binary(object): + """Wrapper for binary data.""" + + def __init__(self, data=None): + if data is None: + data = b"" + else: + if not isinstance(data, (bytes, bytearray)): + raise TypeError("expected bytes or bytearray, not %s" % + data.__class__.__name__) + data = bytes(data) # Make a copy of the bytes! + self.data = data + + ## + # Get buffer contents. + # + # @return Buffer contents, as an 8-bit string. + + def __str__(self): + return str(self.data, "latin-1") # XXX encoding?! + + def __eq__(self, other): + if isinstance(other, Binary): + other = other.data + return self.data == other + + def __ne__(self, other): + if isinstance(other, Binary): + other = other.data + return self.data != other + + def decode(self, data): + self.data = base64.decodebytes(data) + + def encode(self, out): + out.write("\n") + encoded = base64.encodebytes(self.data) + out.write(encoded.decode('ascii')) + out.write("\n") + +def _binary(data): + # decode xml element contents into a Binary structure + value = Binary() + value.decode(data) + return value + +WRAPPERS = (DateTime, Binary) + +# -------------------------------------------------------------------- +# XML parsers + +class ExpatParser(object): + # fast expat parser for Python 2.0 and later. + def __init__(self, target): + self._parser = parser = expat.ParserCreate(None, None) + self._target = target + parser.StartElementHandler = target.start + parser.EndElementHandler = target.end + parser.CharacterDataHandler = target.data + encoding = None + target.xml(encoding, None) + + def feed(self, data): + self._parser.Parse(data, 0) + + def close(self): + self._parser.Parse("", 1) # end of data + del self._target, self._parser # get rid of circular references + +# -------------------------------------------------------------------- +# XML-RPC marshalling and unmarshalling code + +## +# XML-RPC marshaller. +# +# @param encoding Default encoding for 8-bit strings. The default +# value is None (interpreted as UTF-8). +# @see dumps + +class Marshaller(object): + """Generate an XML-RPC params chunk from a Python data structure. + + Create a Marshaller instance for each set of parameters, and use + the "dumps" method to convert your data (represented as a tuple) + to an XML-RPC params chunk. To write a fault response, pass a + Fault instance instead. You may prefer to use the "dumps" module + function for this purpose. + """ + + # by the way, if you don't understand what's going on in here, + # that's perfectly ok. + + def __init__(self, encoding=None, allow_none=False): + self.memo = {} + self.data = None + self.encoding = encoding + self.allow_none = allow_none + + dispatch = {} + + def dumps(self, values): + out = [] + write = out.append + dump = self.__dump + if isinstance(values, Fault): + # fault instance + write("\n") + dump({'faultCode': values.faultCode, + 'faultString': values.faultString}, + write) + write("\n") + else: + # parameter block + # FIXME: the xml-rpc specification allows us to leave out + # the entire block if there are no parameters. + # however, changing this may break older code (including + # old versions of xmlrpclib.py), so this is better left as + # is for now. See @XMLRPC3 for more information. /F + write("\n") + for v in values: + write("\n") + dump(v, write) + write("\n") + write("\n") + result = "".join(out) + return str(result) + + def __dump(self, value, write): + try: + f = self.dispatch[type(ensure_new_type(value))] + except KeyError: + # check if this object can be marshalled as a structure + if not hasattr(value, '__dict__'): + raise TypeError("cannot marshal %s objects" % type(value)) + # check if this class is a sub-class of a basic type, + # because we don't know how to marshal these types + # (e.g. a string sub-class) + for type_ in type(value).__mro__: + if type_ in self.dispatch.keys(): + raise TypeError("cannot marshal %s objects" % type(value)) + # XXX(twouters): using "_arbitrary_instance" as key as a quick-fix + # for the p3yk merge, this should probably be fixed more neatly. + f = self.dispatch["_arbitrary_instance"] + f(self, value, write) + + def dump_nil (self, value, write): + if not self.allow_none: + raise TypeError("cannot marshal None unless allow_none is enabled") + write("") + dispatch[type(None)] = dump_nil + + def dump_bool(self, value, write): + write("") + write(value and "1" or "0") + write("\n") + dispatch[bool] = dump_bool + + def dump_long(self, value, write): + if value > MAXINT or value < MININT: + raise OverflowError("long int exceeds XML-RPC limits") + write("") + write(str(int(value))) + write("\n") + dispatch[int] = dump_long + + # backward compatible + dump_int = dump_long + + def dump_double(self, value, write): + write("") + write(repr(ensure_new_type(value))) + write("\n") + dispatch[float] = dump_double + + def dump_unicode(self, value, write, escape=escape): + write("") + write(escape(value)) + write("\n") + dispatch[str] = dump_unicode + + def dump_bytes(self, value, write): + write("\n") + encoded = base64.encodebytes(value) + write(encoded.decode('ascii')) + write("\n") + dispatch[bytes] = dump_bytes + dispatch[bytearray] = dump_bytes + + def dump_array(self, value, write): + i = id(value) + if i in self.memo: + raise TypeError("cannot marshal recursive sequences") + self.memo[i] = None + dump = self.__dump + write("\n") + for v in value: + dump(v, write) + write("\n") + del self.memo[i] + dispatch[tuple] = dump_array + dispatch[list] = dump_array + + def dump_struct(self, value, write, escape=escape): + i = id(value) + if i in self.memo: + raise TypeError("cannot marshal recursive dictionaries") + self.memo[i] = None + dump = self.__dump + write("\n") + for k, v in value.items(): + write("\n") + if not isinstance(k, str): + raise TypeError("dictionary key must be string") + write("%s\n" % escape(k)) + dump(v, write) + write("\n") + write("\n") + del self.memo[i] + dispatch[dict] = dump_struct + + def dump_datetime(self, value, write): + write("") + write(_strftime(value)) + write("\n") + dispatch[datetime] = dump_datetime + + def dump_instance(self, value, write): + # check for special wrappers + if value.__class__ in WRAPPERS: + self.write = write + value.encode(self) + del self.write + else: + # store instance attributes as a struct (really?) + self.dump_struct(value.__dict__, write) + dispatch[DateTime] = dump_instance + dispatch[Binary] = dump_instance + # XXX(twouters): using "_arbitrary_instance" as key as a quick-fix + # for the p3yk merge, this should probably be fixed more neatly. + dispatch["_arbitrary_instance"] = dump_instance + +## +# XML-RPC unmarshaller. +# +# @see loads + +class Unmarshaller(object): + """Unmarshal an XML-RPC response, based on incoming XML event + messages (start, data, end). Call close() to get the resulting + data structure. + + Note that this reader is fairly tolerant, and gladly accepts bogus + XML-RPC data without complaining (but not bogus XML). + """ + + # and again, if you don't understand what's going on in here, + # that's perfectly ok. + + def __init__(self, use_datetime=False, use_builtin_types=False): + self._type = None + self._stack = [] + self._marks = [] + self._data = [] + self._methodname = None + self._encoding = "utf-8" + self.append = self._stack.append + self._use_datetime = use_builtin_types or use_datetime + self._use_bytes = use_builtin_types + + def close(self): + # return response tuple and target method + if self._type is None or self._marks: + raise ResponseError() + if self._type == "fault": + raise Fault(**self._stack[0]) + return tuple(self._stack) + + def getmethodname(self): + return self._methodname + + # + # event handlers + + def xml(self, encoding, standalone): + self._encoding = encoding + # FIXME: assert standalone == 1 ??? + + def start(self, tag, attrs): + # prepare to handle this element + if tag == "array" or tag == "struct": + self._marks.append(len(self._stack)) + self._data = [] + self._value = (tag == "value") + + def data(self, text): + self._data.append(text) + + def end(self, tag): + # call the appropriate end tag handler + try: + f = self.dispatch[tag] + except KeyError: + pass # unknown tag ? + else: + return f(self, "".join(self._data)) + + # + # accelerator support + + def end_dispatch(self, tag, data): + # dispatch data + try: + f = self.dispatch[tag] + except KeyError: + pass # unknown tag ? + else: + return f(self, data) + + # + # element decoders + + dispatch = {} + + def end_nil (self, data): + self.append(None) + self._value = 0 + dispatch["nil"] = end_nil + + def end_boolean(self, data): + if data == "0": + self.append(False) + elif data == "1": + self.append(True) + else: + raise TypeError("bad boolean value") + self._value = 0 + dispatch["boolean"] = end_boolean + + def end_int(self, data): + self.append(int(data)) + self._value = 0 + dispatch["i4"] = end_int + dispatch["i8"] = end_int + dispatch["int"] = end_int + + def end_double(self, data): + self.append(float(data)) + self._value = 0 + dispatch["double"] = end_double + + def end_string(self, data): + if self._encoding: + data = data.decode(self._encoding) + self.append(data) + self._value = 0 + dispatch["string"] = end_string + dispatch["name"] = end_string # struct keys are always strings + + def end_array(self, data): + mark = self._marks.pop() + # map arrays to Python lists + self._stack[mark:] = [self._stack[mark:]] + self._value = 0 + dispatch["array"] = end_array + + def end_struct(self, data): + mark = self._marks.pop() + # map structs to Python dictionaries + dict = {} + items = self._stack[mark:] + for i in range(0, len(items), 2): + dict[items[i]] = items[i+1] + self._stack[mark:] = [dict] + self._value = 0 + dispatch["struct"] = end_struct + + def end_base64(self, data): + value = Binary() + value.decode(data.encode("ascii")) + if self._use_bytes: + value = value.data + self.append(value) + self._value = 0 + dispatch["base64"] = end_base64 + + def end_dateTime(self, data): + value = DateTime() + value.decode(data) + if self._use_datetime: + value = _datetime_type(data) + self.append(value) + dispatch["dateTime.iso8601"] = end_dateTime + + def end_value(self, data): + # if we stumble upon a value element with no internal + # elements, treat it as a string element + if self._value: + self.end_string(data) + dispatch["value"] = end_value + + def end_params(self, data): + self._type = "params" + dispatch["params"] = end_params + + def end_fault(self, data): + self._type = "fault" + dispatch["fault"] = end_fault + + def end_methodName(self, data): + if self._encoding: + data = data.decode(self._encoding) + self._methodname = data + self._type = "methodName" # no params + dispatch["methodName"] = end_methodName + +## Multicall support +# + +class _MultiCallMethod(object): + # some lesser magic to store calls made to a MultiCall object + # for batch execution + def __init__(self, call_list, name): + self.__call_list = call_list + self.__name = name + def __getattr__(self, name): + return _MultiCallMethod(self.__call_list, "%s.%s" % (self.__name, name)) + def __call__(self, *args): + self.__call_list.append((self.__name, args)) + +class MultiCallIterator(object): + """Iterates over the results of a multicall. Exceptions are + raised in response to xmlrpc faults.""" + + def __init__(self, results): + self.results = results + + def __getitem__(self, i): + item = self.results[i] + if isinstance(type(item), dict): + raise Fault(item['faultCode'], item['faultString']) + elif type(item) == type([]): + return item[0] + else: + raise ValueError("unexpected type in multicall result") + +class MultiCall(object): + """server -> a object used to boxcar method calls + + server should be a ServerProxy object. + + Methods can be added to the MultiCall using normal + method call syntax e.g.: + + multicall = MultiCall(server_proxy) + multicall.add(2,3) + multicall.get_address("Guido") + + To execute the multicall, call the MultiCall object e.g.: + + add_result, address = multicall() + """ + + def __init__(self, server): + self.__server = server + self.__call_list = [] + + def __repr__(self): + return "" % id(self) + + __str__ = __repr__ + + def __getattr__(self, name): + return _MultiCallMethod(self.__call_list, name) + + def __call__(self): + marshalled_list = [] + for name, args in self.__call_list: + marshalled_list.append({'methodName' : name, 'params' : args}) + + return MultiCallIterator(self.__server.system.multicall(marshalled_list)) + +# -------------------------------------------------------------------- +# convenience functions + +FastMarshaller = FastParser = FastUnmarshaller = None + +## +# Create a parser object, and connect it to an unmarshalling instance. +# This function picks the fastest available XML parser. +# +# return A (parser, unmarshaller) tuple. + +def getparser(use_datetime=False, use_builtin_types=False): + """getparser() -> parser, unmarshaller + + Create an instance of the fastest available parser, and attach it + to an unmarshalling object. Return both objects. + """ + if FastParser and FastUnmarshaller: + if use_builtin_types: + mkdatetime = _datetime_type + mkbytes = base64.decodebytes + elif use_datetime: + mkdatetime = _datetime_type + mkbytes = _binary + else: + mkdatetime = _datetime + mkbytes = _binary + target = FastUnmarshaller(True, False, mkbytes, mkdatetime, Fault) + parser = FastParser(target) + else: + target = Unmarshaller(use_datetime=use_datetime, use_builtin_types=use_builtin_types) + if FastParser: + parser = FastParser(target) + else: + parser = ExpatParser(target) + return parser, target + +## +# Convert a Python tuple or a Fault instance to an XML-RPC packet. +# +# @def dumps(params, **options) +# @param params A tuple or Fault instance. +# @keyparam methodname If given, create a methodCall request for +# this method name. +# @keyparam methodresponse If given, create a methodResponse packet. +# If used with a tuple, the tuple must be a singleton (that is, +# it must contain exactly one element). +# @keyparam encoding The packet encoding. +# @return A string containing marshalled data. + +def dumps(params, methodname=None, methodresponse=None, encoding=None, + allow_none=False): + """data [,options] -> marshalled data + + Convert an argument tuple or a Fault instance to an XML-RPC + request (or response, if the methodresponse option is used). + + In addition to the data object, the following options can be given + as keyword arguments: + + methodname: the method name for a methodCall packet + + methodresponse: true to create a methodResponse packet. + If this option is used with a tuple, the tuple must be + a singleton (i.e. it can contain only one element). + + encoding: the packet encoding (default is UTF-8) + + All byte strings in the data structure are assumed to use the + packet encoding. Unicode strings are automatically converted, + where necessary. + """ + + assert isinstance(params, (tuple, Fault)), "argument must be tuple or Fault instance" + if isinstance(params, Fault): + methodresponse = 1 + elif methodresponse and isinstance(params, tuple): + assert len(params) == 1, "response tuple must be a singleton" + + if not encoding: + encoding = "utf-8" + + if FastMarshaller: + m = FastMarshaller(encoding) + else: + m = Marshaller(encoding, allow_none) + + data = m.dumps(params) + + if encoding != "utf-8": + xmlheader = "\n" % str(encoding) + else: + xmlheader = "\n" # utf-8 is default + + # standard XML-RPC wrappings + if methodname: + # a method call + if not isinstance(methodname, str): + methodname = methodname.encode(encoding) + data = ( + xmlheader, + "\n" + "", methodname, "\n", + data, + "\n" + ) + elif methodresponse: + # a method response, or a fault structure + data = ( + xmlheader, + "\n", + data, + "\n" + ) + else: + return data # return as is + return str("").join(data) + +## +# Convert an XML-RPC packet to a Python object. If the XML-RPC packet +# represents a fault condition, this function raises a Fault exception. +# +# @param data An XML-RPC packet, given as an 8-bit string. +# @return A tuple containing the unpacked data, and the method name +# (None if not present). +# @see Fault + +def loads(data, use_datetime=False, use_builtin_types=False): + """data -> unmarshalled data, method name + + Convert an XML-RPC packet to unmarshalled data plus a method + name (None if not present). + + If the XML-RPC packet represents a fault condition, this function + raises a Fault exception. + """ + p, u = getparser(use_datetime=use_datetime, use_builtin_types=use_builtin_types) + p.feed(data) + p.close() + return u.close(), u.getmethodname() + +## +# Encode a string using the gzip content encoding such as specified by the +# Content-Encoding: gzip +# in the HTTP header, as described in RFC 1952 +# +# @param data the unencoded data +# @return the encoded data + +def gzip_encode(data): + """data -> gzip encoded data + + Encode data using the gzip content encoding as described in RFC 1952 + """ + if not gzip: + raise NotImplementedError + f = BytesIO() + gzf = gzip.GzipFile(mode="wb", fileobj=f, compresslevel=1) + gzf.write(data) + gzf.close() + encoded = f.getvalue() + f.close() + return encoded + +## +# Decode a string using the gzip content encoding such as specified by the +# Content-Encoding: gzip +# in the HTTP header, as described in RFC 1952 +# +# @param data The encoded data +# @return the unencoded data +# @raises ValueError if data is not correctly coded. + +def gzip_decode(data): + """gzip encoded data -> unencoded data + + Decode data using the gzip content encoding as described in RFC 1952 + """ + if not gzip: + raise NotImplementedError + f = BytesIO(data) + gzf = gzip.GzipFile(mode="rb", fileobj=f) + try: + decoded = gzf.read() + except IOError: + raise ValueError("invalid data") + f.close() + gzf.close() + return decoded + +## +# Return a decoded file-like object for the gzip encoding +# as described in RFC 1952. +# +# @param response A stream supporting a read() method +# @return a file-like object that the decoded data can be read() from + +class GzipDecodedResponse(gzip.GzipFile if gzip else object): + """a file-like object to decode a response encoded with the gzip + method, as described in RFC 1952. + """ + def __init__(self, response): + #response doesn't support tell() and read(), required by + #GzipFile + if not gzip: + raise NotImplementedError + self.io = BytesIO(response.read()) + gzip.GzipFile.__init__(self, mode="rb", fileobj=self.io) + + def close(self): + gzip.GzipFile.close(self) + self.io.close() + + +# -------------------------------------------------------------------- +# request dispatcher + +class _Method(object): + # some magic to bind an XML-RPC method to an RPC server. + # supports "nested" methods (e.g. examples.getStateName) + def __init__(self, send, name): + self.__send = send + self.__name = name + def __getattr__(self, name): + return _Method(self.__send, "%s.%s" % (self.__name, name)) + def __call__(self, *args): + return self.__send(self.__name, args) + +## +# Standard transport class for XML-RPC over HTTP. +#

+# You can create custom transports by subclassing this method, and +# overriding selected methods. + +class Transport(object): + """Handles an HTTP transaction to an XML-RPC server.""" + + # client identifier (may be overridden) + user_agent = "Python-xmlrpc/%s" % __version__ + + #if true, we'll request gzip encoding + accept_gzip_encoding = True + + # if positive, encode request using gzip if it exceeds this threshold + # note that many server will get confused, so only use it if you know + # that they can decode such a request + encode_threshold = None #None = don't encode + + def __init__(self, use_datetime=False, use_builtin_types=False): + self._use_datetime = use_datetime + self._use_builtin_types = use_builtin_types + self._connection = (None, None) + self._extra_headers = [] + + ## + # Send a complete request, and parse the response. + # Retry request if a cached connection has disconnected. + # + # @param host Target host. + # @param handler Target PRC handler. + # @param request_body XML-RPC request body. + # @param verbose Debugging flag. + # @return Parsed response. + + def request(self, host, handler, request_body, verbose=False): + #retry request once if cached connection has gone cold + for i in (0, 1): + try: + return self.single_request(host, handler, request_body, verbose) + except socket.error as e: + if i or e.errno not in (errno.ECONNRESET, errno.ECONNABORTED, errno.EPIPE): + raise + except http_client.BadStatusLine: #close after we sent request + if i: + raise + + def single_request(self, host, handler, request_body, verbose=False): + # issue XML-RPC request + try: + http_conn = self.send_request(host, handler, request_body, verbose) + resp = http_conn.getresponse() + if resp.status == 200: + self.verbose = verbose + return self.parse_response(resp) + + except Fault: + raise + except Exception: + #All unexpected errors leave connection in + # a strange state, so we clear it. + self.close() + raise + + #We got an error response. + #Discard any response data and raise exception + if resp.getheader("content-length", ""): + resp.read() + raise ProtocolError( + host + handler, + resp.status, resp.reason, + dict(resp.getheaders()) + ) + + + ## + # Create parser. + # + # @return A 2-tuple containing a parser and a unmarshaller. + + def getparser(self): + # get parser and unmarshaller + return getparser(use_datetime=self._use_datetime, + use_builtin_types=self._use_builtin_types) + + ## + # Get authorization info from host parameter + # Host may be a string, or a (host, x509-dict) tuple; if a string, + # it is checked for a "user:pw@host" format, and a "Basic + # Authentication" header is added if appropriate. + # + # @param host Host descriptor (URL or (URL, x509 info) tuple). + # @return A 3-tuple containing (actual host, extra headers, + # x509 info). The header and x509 fields may be None. + + def get_host_info(self, host): + + x509 = {} + if isinstance(host, tuple): + host, x509 = host + + auth, host = urllib_parse.splituser(host) + + if auth: + auth = urllib_parse.unquote_to_bytes(auth) + auth = base64.encodebytes(auth).decode("utf-8") + auth = "".join(auth.split()) # get rid of whitespace + extra_headers = [ + ("Authorization", "Basic " + auth) + ] + else: + extra_headers = [] + + return host, extra_headers, x509 + + ## + # Connect to server. + # + # @param host Target host. + # @return An HTTPConnection object + + def make_connection(self, host): + #return an existing connection if possible. This allows + #HTTP/1.1 keep-alive. + if self._connection and host == self._connection[0]: + return self._connection[1] + # create a HTTP connection object from a host descriptor + chost, self._extra_headers, x509 = self.get_host_info(host) + self._connection = host, http_client.HTTPConnection(chost) + return self._connection[1] + + ## + # Clear any cached connection object. + # Used in the event of socket errors. + # + def close(self): + if self._connection[1]: + self._connection[1].close() + self._connection = (None, None) + + ## + # Send HTTP request. + # + # @param host Host descriptor (URL or (URL, x509 info) tuple). + # @param handler Target RPC handler (a path relative to host) + # @param request_body The XML-RPC request body + # @param debug Enable debugging if debug is true. + # @return An HTTPConnection. + + def send_request(self, host, handler, request_body, debug): + connection = self.make_connection(host) + headers = self._extra_headers[:] + if debug: + connection.set_debuglevel(1) + if self.accept_gzip_encoding and gzip: + connection.putrequest("POST", handler, skip_accept_encoding=True) + headers.append(("Accept-Encoding", "gzip")) + else: + connection.putrequest("POST", handler) + headers.append(("Content-Type", "text/xml")) + headers.append(("User-Agent", self.user_agent)) + self.send_headers(connection, headers) + self.send_content(connection, request_body) + return connection + + ## + # Send request headers. + # This function provides a useful hook for subclassing + # + # @param connection httpConnection. + # @param headers list of key,value pairs for HTTP headers + + def send_headers(self, connection, headers): + for key, val in headers: + connection.putheader(key, val) + + ## + # Send request body. + # This function provides a useful hook for subclassing + # + # @param connection httpConnection. + # @param request_body XML-RPC request body. + + def send_content(self, connection, request_body): + #optionally encode the request + if (self.encode_threshold is not None and + self.encode_threshold < len(request_body) and + gzip): + connection.putheader("Content-Encoding", "gzip") + request_body = gzip_encode(request_body) + + connection.putheader("Content-Length", str(len(request_body))) + connection.endheaders(request_body) + + ## + # Parse response. + # + # @param file Stream. + # @return Response tuple and target method. + + def parse_response(self, response): + # read response data from httpresponse, and parse it + # Check for new http response object, otherwise it is a file object. + if hasattr(response, 'getheader'): + if response.getheader("Content-Encoding", "") == "gzip": + stream = GzipDecodedResponse(response) + else: + stream = response + else: + stream = response + + p, u = self.getparser() + + while 1: + data = stream.read(1024) + if not data: + break + if self.verbose: + print("body:", repr(data)) + p.feed(data) + + if stream is not response: + stream.close() + p.close() + + return u.close() + +## +# Standard transport class for XML-RPC over HTTPS. + +class SafeTransport(Transport): + """Handles an HTTPS transaction to an XML-RPC server.""" + + # FIXME: mostly untested + + def make_connection(self, host): + if self._connection and host == self._connection[0]: + return self._connection[1] + + if not hasattr(http_client, "HTTPSConnection"): + raise NotImplementedError( + "your version of http.client doesn't support HTTPS") + # create a HTTPS connection object from a host descriptor + # host may be a string, or a (host, x509-dict) tuple + chost, self._extra_headers, x509 = self.get_host_info(host) + self._connection = host, http_client.HTTPSConnection(chost, + None, **(x509 or {})) + return self._connection[1] + +## +# Standard server proxy. This class establishes a virtual connection +# to an XML-RPC server. +#

+# This class is available as ServerProxy and Server. New code should +# use ServerProxy, to avoid confusion. +# +# @def ServerProxy(uri, **options) +# @param uri The connection point on the server. +# @keyparam transport A transport factory, compatible with the +# standard transport class. +# @keyparam encoding The default encoding used for 8-bit strings +# (default is UTF-8). +# @keyparam verbose Use a true value to enable debugging output. +# (printed to standard output). +# @see Transport + +class ServerProxy(object): + """uri [,options] -> a logical connection to an XML-RPC server + + uri is the connection point on the server, given as + scheme://host/target. + + The standard implementation always supports the "http" scheme. If + SSL socket support is available (Python 2.0), it also supports + "https". + + If the target part and the slash preceding it are both omitted, + "/RPC2" is assumed. + + The following options can be given as keyword arguments: + + transport: a transport factory + encoding: the request encoding (default is UTF-8) + + All 8-bit strings passed to the server proxy are assumed to use + the given encoding. + """ + + def __init__(self, uri, transport=None, encoding=None, verbose=False, + allow_none=False, use_datetime=False, use_builtin_types=False): + # establish a "logical" server connection + + # get the url + type, uri = urllib_parse.splittype(uri) + if type not in ("http", "https"): + raise IOError("unsupported XML-RPC protocol") + self.__host, self.__handler = urllib_parse.splithost(uri) + if not self.__handler: + self.__handler = "/RPC2" + + if transport is None: + if type == "https": + handler = SafeTransport + else: + handler = Transport + transport = handler(use_datetime=use_datetime, + use_builtin_types=use_builtin_types) + self.__transport = transport + + self.__encoding = encoding or 'utf-8' + self.__verbose = verbose + self.__allow_none = allow_none + + def __close(self): + self.__transport.close() + + def __request(self, methodname, params): + # call a method on the remote server + + request = dumps(params, methodname, encoding=self.__encoding, + allow_none=self.__allow_none).encode(self.__encoding) + + response = self.__transport.request( + self.__host, + self.__handler, + request, + verbose=self.__verbose + ) + + if len(response) == 1: + response = response[0] + + return response + + def __repr__(self): + return ( + "" % + (self.__host, self.__handler) + ) + + __str__ = __repr__ + + def __getattr__(self, name): + # magic method dispatcher + return _Method(self.__request, name) + + # note: to call a remote object with an non-standard name, use + # result getattr(server, "strange-python-name")(args) + + def __call__(self, attr): + """A workaround to get special attributes on the ServerProxy + without interfering with the magic __getattr__ + """ + if attr == "close": + return self.__close + elif attr == "transport": + return self.__transport + raise AttributeError("Attribute %r not found" % (attr,)) + +# compatibility + +Server = ServerProxy + +# -------------------------------------------------------------------- +# test code + +if __name__ == "__main__": + + # simple test program (from the XML-RPC specification) + + # local server, available from Lib/xmlrpc/server.py + server = ServerProxy("http://localhost:8000") + + try: + print(server.currentTime.getCurrentTime()) + except Error as v: + print("ERROR", v) + + multi = MultiCall(server) + multi.getData() + multi.pow(2,9) + multi.add(1,2) + try: + for response in multi(): + print(response) + except Error as v: + print("ERROR", v) diff --git a/venv/lib/python3.10/site-packages/future/backports/xmlrpc/server.py b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/server.py new file mode 100644 index 0000000..28072bf --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/backports/xmlrpc/server.py @@ -0,0 +1,999 @@ +r""" +Ported using Python-Future from the Python 3.3 standard library. + +XML-RPC Servers. + +This module can be used to create simple XML-RPC servers +by creating a server and either installing functions, a +class instance, or by extending the SimpleXMLRPCServer +class. + +It can also be used to handle XML-RPC requests in a CGI +environment using CGIXMLRPCRequestHandler. + +The Doc* classes can be used to create XML-RPC servers that +serve pydoc-style documentation in response to HTTP +GET requests. This documentation is dynamically generated +based on the functions and methods registered with the +server. + +A list of possible usage patterns follows: + +1. Install functions: + +server = SimpleXMLRPCServer(("localhost", 8000)) +server.register_function(pow) +server.register_function(lambda x,y: x+y, 'add') +server.serve_forever() + +2. Install an instance: + +class MyFuncs: + def __init__(self): + # make all of the sys functions available through sys.func_name + import sys + self.sys = sys + def _listMethods(self): + # implement this method so that system.listMethods + # knows to advertise the sys methods + return list_public_methods(self) + \ + ['sys.' + method for method in list_public_methods(self.sys)] + def pow(self, x, y): return pow(x, y) + def add(self, x, y) : return x + y + +server = SimpleXMLRPCServer(("localhost", 8000)) +server.register_introspection_functions() +server.register_instance(MyFuncs()) +server.serve_forever() + +3. Install an instance with custom dispatch method: + +class Math: + def _listMethods(self): + # this method must be present for system.listMethods + # to work + return ['add', 'pow'] + def _methodHelp(self, method): + # this method must be present for system.methodHelp + # to work + if method == 'add': + return "add(2,3) => 5" + elif method == 'pow': + return "pow(x, y[, z]) => number" + else: + # By convention, return empty + # string if no help is available + return "" + def _dispatch(self, method, params): + if method == 'pow': + return pow(*params) + elif method == 'add': + return params[0] + params[1] + else: + raise ValueError('bad method') + +server = SimpleXMLRPCServer(("localhost", 8000)) +server.register_introspection_functions() +server.register_instance(Math()) +server.serve_forever() + +4. Subclass SimpleXMLRPCServer: + +class MathServer(SimpleXMLRPCServer): + def _dispatch(self, method, params): + try: + # We are forcing the 'export_' prefix on methods that are + # callable through XML-RPC to prevent potential security + # problems + func = getattr(self, 'export_' + method) + except AttributeError: + raise Exception('method "%s" is not supported' % method) + else: + return func(*params) + + def export_add(self, x, y): + return x + y + +server = MathServer(("localhost", 8000)) +server.serve_forever() + +5. CGI script: + +server = CGIXMLRPCRequestHandler() +server.register_function(pow) +server.handle_request() +""" + +from __future__ import absolute_import, division, print_function, unicode_literals +from future.builtins import int, str + +# Written by Brian Quinlan (brian@sweetapp.com). +# Based on code written by Fredrik Lundh. + +from future.backports.xmlrpc.client import Fault, dumps, loads, gzip_encode, gzip_decode +from future.backports.http.server import BaseHTTPRequestHandler +import future.backports.http.server as http_server +from future.backports import socketserver +import sys +import os +import re +import pydoc +import inspect +import traceback +try: + import fcntl +except ImportError: + fcntl = None + +def resolve_dotted_attribute(obj, attr, allow_dotted_names=True): + """resolve_dotted_attribute(a, 'b.c.d') => a.b.c.d + + Resolves a dotted attribute name to an object. Raises + an AttributeError if any attribute in the chain starts with a '_'. + + If the optional allow_dotted_names argument is false, dots are not + supported and this function operates similar to getattr(obj, attr). + """ + + if allow_dotted_names: + attrs = attr.split('.') + else: + attrs = [attr] + + for i in attrs: + if i.startswith('_'): + raise AttributeError( + 'attempt to access private attribute "%s"' % i + ) + else: + obj = getattr(obj,i) + return obj + +def list_public_methods(obj): + """Returns a list of attribute strings, found in the specified + object, which represent callable attributes""" + + return [member for member in dir(obj) + if not member.startswith('_') and + callable(getattr(obj, member))] + +class SimpleXMLRPCDispatcher(object): + """Mix-in class that dispatches XML-RPC requests. + + This class is used to register XML-RPC method handlers + and then to dispatch them. This class doesn't need to be + instanced directly when used by SimpleXMLRPCServer but it + can be instanced when used by the MultiPathXMLRPCServer + """ + + def __init__(self, allow_none=False, encoding=None, + use_builtin_types=False): + self.funcs = {} + self.instance = None + self.allow_none = allow_none + self.encoding = encoding or 'utf-8' + self.use_builtin_types = use_builtin_types + + def register_instance(self, instance, allow_dotted_names=False): + """Registers an instance to respond to XML-RPC requests. + + Only one instance can be installed at a time. + + If the registered instance has a _dispatch method then that + method will be called with the name of the XML-RPC method and + its parameters as a tuple + e.g. instance._dispatch('add',(2,3)) + + If the registered instance does not have a _dispatch method + then the instance will be searched to find a matching method + and, if found, will be called. Methods beginning with an '_' + are considered private and will not be called by + SimpleXMLRPCServer. + + If a registered function matches a XML-RPC request, then it + will be called instead of the registered instance. + + If the optional allow_dotted_names argument is true and the + instance does not have a _dispatch method, method names + containing dots are supported and resolved, as long as none of + the name segments start with an '_'. + + *** SECURITY WARNING: *** + + Enabling the allow_dotted_names options allows intruders + to access your module's global variables and may allow + intruders to execute arbitrary code on your machine. Only + use this option on a secure, closed network. + + """ + + self.instance = instance + self.allow_dotted_names = allow_dotted_names + + def register_function(self, function, name=None): + """Registers a function to respond to XML-RPC requests. + + The optional name argument can be used to set a Unicode name + for the function. + """ + + if name is None: + name = function.__name__ + self.funcs[name] = function + + def register_introspection_functions(self): + """Registers the XML-RPC introspection methods in the system + namespace. + + see http://xmlrpc.usefulinc.com/doc/reserved.html + """ + + self.funcs.update({'system.listMethods' : self.system_listMethods, + 'system.methodSignature' : self.system_methodSignature, + 'system.methodHelp' : self.system_methodHelp}) + + def register_multicall_functions(self): + """Registers the XML-RPC multicall method in the system + namespace. + + see http://www.xmlrpc.com/discuss/msgReader$1208""" + + self.funcs.update({'system.multicall' : self.system_multicall}) + + def _marshaled_dispatch(self, data, dispatch_method = None, path = None): + """Dispatches an XML-RPC method from marshalled (XML) data. + + XML-RPC methods are dispatched from the marshalled (XML) data + using the _dispatch method and the result is returned as + marshalled data. For backwards compatibility, a dispatch + function can be provided as an argument (see comment in + SimpleXMLRPCRequestHandler.do_POST) but overriding the + existing method through subclassing is the preferred means + of changing method dispatch behavior. + """ + + try: + params, method = loads(data, use_builtin_types=self.use_builtin_types) + + # generate response + if dispatch_method is not None: + response = dispatch_method(method, params) + else: + response = self._dispatch(method, params) + # wrap response in a singleton tuple + response = (response,) + response = dumps(response, methodresponse=1, + allow_none=self.allow_none, encoding=self.encoding) + except Fault as fault: + response = dumps(fault, allow_none=self.allow_none, + encoding=self.encoding) + except: + # report exception back to server + exc_type, exc_value, exc_tb = sys.exc_info() + response = dumps( + Fault(1, "%s:%s" % (exc_type, exc_value)), + encoding=self.encoding, allow_none=self.allow_none, + ) + + return response.encode(self.encoding) + + def system_listMethods(self): + """system.listMethods() => ['add', 'subtract', 'multiple'] + + Returns a list of the methods supported by the server.""" + + methods = set(self.funcs.keys()) + if self.instance is not None: + # Instance can implement _listMethod to return a list of + # methods + if hasattr(self.instance, '_listMethods'): + methods |= set(self.instance._listMethods()) + # if the instance has a _dispatch method then we + # don't have enough information to provide a list + # of methods + elif not hasattr(self.instance, '_dispatch'): + methods |= set(list_public_methods(self.instance)) + return sorted(methods) + + def system_methodSignature(self, method_name): + """system.methodSignature('add') => [double, int, int] + + Returns a list describing the signature of the method. In the + above example, the add method takes two integers as arguments + and returns a double result. + + This server does NOT support system.methodSignature.""" + + # See http://xmlrpc.usefulinc.com/doc/sysmethodsig.html + + return 'signatures not supported' + + def system_methodHelp(self, method_name): + """system.methodHelp('add') => "Adds two integers together" + + Returns a string containing documentation for the specified method.""" + + method = None + if method_name in self.funcs: + method = self.funcs[method_name] + elif self.instance is not None: + # Instance can implement _methodHelp to return help for a method + if hasattr(self.instance, '_methodHelp'): + return self.instance._methodHelp(method_name) + # if the instance has a _dispatch method then we + # don't have enough information to provide help + elif not hasattr(self.instance, '_dispatch'): + try: + method = resolve_dotted_attribute( + self.instance, + method_name, + self.allow_dotted_names + ) + except AttributeError: + pass + + # Note that we aren't checking that the method actually + # be a callable object of some kind + if method is None: + return "" + else: + return pydoc.getdoc(method) + + def system_multicall(self, call_list): + """system.multicall([{'methodName': 'add', 'params': [2, 2]}, ...]) => \ +[[4], ...] + + Allows the caller to package multiple XML-RPC calls into a single + request. + + See http://www.xmlrpc.com/discuss/msgReader$1208 + """ + + results = [] + for call in call_list: + method_name = call['methodName'] + params = call['params'] + + try: + # XXX A marshalling error in any response will fail the entire + # multicall. If someone cares they should fix this. + results.append([self._dispatch(method_name, params)]) + except Fault as fault: + results.append( + {'faultCode' : fault.faultCode, + 'faultString' : fault.faultString} + ) + except: + exc_type, exc_value, exc_tb = sys.exc_info() + results.append( + {'faultCode' : 1, + 'faultString' : "%s:%s" % (exc_type, exc_value)} + ) + return results + + def _dispatch(self, method, params): + """Dispatches the XML-RPC method. + + XML-RPC calls are forwarded to a registered function that + matches the called XML-RPC method name. If no such function + exists then the call is forwarded to the registered instance, + if available. + + If the registered instance has a _dispatch method then that + method will be called with the name of the XML-RPC method and + its parameters as a tuple + e.g. instance._dispatch('add',(2,3)) + + If the registered instance does not have a _dispatch method + then the instance will be searched to find a matching method + and, if found, will be called. + + Methods beginning with an '_' are considered private and will + not be called. + """ + + func = None + try: + # check to see if a matching function has been registered + func = self.funcs[method] + except KeyError: + if self.instance is not None: + # check for a _dispatch method + if hasattr(self.instance, '_dispatch'): + return self.instance._dispatch(method, params) + else: + # call instance method directly + try: + func = resolve_dotted_attribute( + self.instance, + method, + self.allow_dotted_names + ) + except AttributeError: + pass + + if func is not None: + return func(*params) + else: + raise Exception('method "%s" is not supported' % method) + +class SimpleXMLRPCRequestHandler(BaseHTTPRequestHandler): + """Simple XML-RPC request handler class. + + Handles all HTTP POST requests and attempts to decode them as + XML-RPC requests. + """ + + # Class attribute listing the accessible path components; + # paths not on this list will result in a 404 error. + rpc_paths = ('/', '/RPC2') + + #if not None, encode responses larger than this, if possible + encode_threshold = 1400 #a common MTU + + #Override form StreamRequestHandler: full buffering of output + #and no Nagle. + wbufsize = -1 + disable_nagle_algorithm = True + + # a re to match a gzip Accept-Encoding + aepattern = re.compile(r""" + \s* ([^\s;]+) \s* #content-coding + (;\s* q \s*=\s* ([0-9\.]+))? #q + """, re.VERBOSE | re.IGNORECASE) + + def accept_encodings(self): + r = {} + ae = self.headers.get("Accept-Encoding", "") + for e in ae.split(","): + match = self.aepattern.match(e) + if match: + v = match.group(3) + v = float(v) if v else 1.0 + r[match.group(1)] = v + return r + + def is_rpc_path_valid(self): + if self.rpc_paths: + return self.path in self.rpc_paths + else: + # If .rpc_paths is empty, just assume all paths are legal + return True + + def do_POST(self): + """Handles the HTTP POST request. + + Attempts to interpret all HTTP POST requests as XML-RPC calls, + which are forwarded to the server's _dispatch method for handling. + """ + + # Check that the path is legal + if not self.is_rpc_path_valid(): + self.report_404() + return + + try: + # Get arguments by reading body of request. + # We read this in chunks to avoid straining + # socket.read(); around the 10 or 15Mb mark, some platforms + # begin to have problems (bug #792570). + max_chunk_size = 10*1024*1024 + size_remaining = int(self.headers["content-length"]) + L = [] + while size_remaining: + chunk_size = min(size_remaining, max_chunk_size) + chunk = self.rfile.read(chunk_size) + if not chunk: + break + L.append(chunk) + size_remaining -= len(L[-1]) + data = b''.join(L) + + data = self.decode_request_content(data) + if data is None: + return #response has been sent + + # In previous versions of SimpleXMLRPCServer, _dispatch + # could be overridden in this class, instead of in + # SimpleXMLRPCDispatcher. To maintain backwards compatibility, + # check to see if a subclass implements _dispatch and dispatch + # using that method if present. + response = self.server._marshaled_dispatch( + data, getattr(self, '_dispatch', None), self.path + ) + except Exception as e: # This should only happen if the module is buggy + # internal error, report as HTTP server error + self.send_response(500) + + # Send information about the exception if requested + if hasattr(self.server, '_send_traceback_header') and \ + self.server._send_traceback_header: + self.send_header("X-exception", str(e)) + trace = traceback.format_exc() + trace = str(trace.encode('ASCII', 'backslashreplace'), 'ASCII') + self.send_header("X-traceback", trace) + + self.send_header("Content-length", "0") + self.end_headers() + else: + self.send_response(200) + self.send_header("Content-type", "text/xml") + if self.encode_threshold is not None: + if len(response) > self.encode_threshold: + q = self.accept_encodings().get("gzip", 0) + if q: + try: + response = gzip_encode(response) + self.send_header("Content-Encoding", "gzip") + except NotImplementedError: + pass + self.send_header("Content-length", str(len(response))) + self.end_headers() + self.wfile.write(response) + + def decode_request_content(self, data): + #support gzip encoding of request + encoding = self.headers.get("content-encoding", "identity").lower() + if encoding == "identity": + return data + if encoding == "gzip": + try: + return gzip_decode(data) + except NotImplementedError: + self.send_response(501, "encoding %r not supported" % encoding) + except ValueError: + self.send_response(400, "error decoding gzip content") + else: + self.send_response(501, "encoding %r not supported" % encoding) + self.send_header("Content-length", "0") + self.end_headers() + + def report_404 (self): + # Report a 404 error + self.send_response(404) + response = b'No such page' + self.send_header("Content-type", "text/plain") + self.send_header("Content-length", str(len(response))) + self.end_headers() + self.wfile.write(response) + + def log_request(self, code='-', size='-'): + """Selectively log an accepted request.""" + + if self.server.logRequests: + BaseHTTPRequestHandler.log_request(self, code, size) + +class SimpleXMLRPCServer(socketserver.TCPServer, + SimpleXMLRPCDispatcher): + """Simple XML-RPC server. + + Simple XML-RPC server that allows functions and a single instance + to be installed to handle requests. The default implementation + attempts to dispatch XML-RPC calls to the functions or instance + installed in the server. Override the _dispatch method inherited + from SimpleXMLRPCDispatcher to change this behavior. + """ + + allow_reuse_address = True + + # Warning: this is for debugging purposes only! Never set this to True in + # production code, as will be sending out sensitive information (exception + # and stack trace details) when exceptions are raised inside + # SimpleXMLRPCRequestHandler.do_POST + _send_traceback_header = False + + def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, + logRequests=True, allow_none=False, encoding=None, + bind_and_activate=True, use_builtin_types=False): + self.logRequests = logRequests + + SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types) + socketserver.TCPServer.__init__(self, addr, requestHandler, bind_and_activate) + + # [Bug #1222790] If possible, set close-on-exec flag; if a + # method spawns a subprocess, the subprocess shouldn't have + # the listening socket open. + if fcntl is not None and hasattr(fcntl, 'FD_CLOEXEC'): + flags = fcntl.fcntl(self.fileno(), fcntl.F_GETFD) + flags |= fcntl.FD_CLOEXEC + fcntl.fcntl(self.fileno(), fcntl.F_SETFD, flags) + +class MultiPathXMLRPCServer(SimpleXMLRPCServer): + """Multipath XML-RPC Server + This specialization of SimpleXMLRPCServer allows the user to create + multiple Dispatcher instances and assign them to different + HTTP request paths. This makes it possible to run two or more + 'virtual XML-RPC servers' at the same port. + Make sure that the requestHandler accepts the paths in question. + """ + def __init__(self, addr, requestHandler=SimpleXMLRPCRequestHandler, + logRequests=True, allow_none=False, encoding=None, + bind_and_activate=True, use_builtin_types=False): + + SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, allow_none, + encoding, bind_and_activate, use_builtin_types) + self.dispatchers = {} + self.allow_none = allow_none + self.encoding = encoding or 'utf-8' + + def add_dispatcher(self, path, dispatcher): + self.dispatchers[path] = dispatcher + return dispatcher + + def get_dispatcher(self, path): + return self.dispatchers[path] + + def _marshaled_dispatch(self, data, dispatch_method = None, path = None): + try: + response = self.dispatchers[path]._marshaled_dispatch( + data, dispatch_method, path) + except: + # report low level exception back to server + # (each dispatcher should have handled their own + # exceptions) + exc_type, exc_value = sys.exc_info()[:2] + response = dumps( + Fault(1, "%s:%s" % (exc_type, exc_value)), + encoding=self.encoding, allow_none=self.allow_none) + response = response.encode(self.encoding) + return response + +class CGIXMLRPCRequestHandler(SimpleXMLRPCDispatcher): + """Simple handler for XML-RPC data passed through CGI.""" + + def __init__(self, allow_none=False, encoding=None, use_builtin_types=False): + SimpleXMLRPCDispatcher.__init__(self, allow_none, encoding, use_builtin_types) + + def handle_xmlrpc(self, request_text): + """Handle a single XML-RPC request""" + + response = self._marshaled_dispatch(request_text) + + print('Content-Type: text/xml') + print('Content-Length: %d' % len(response)) + print() + sys.stdout.flush() + sys.stdout.buffer.write(response) + sys.stdout.buffer.flush() + + def handle_get(self): + """Handle a single HTTP GET request. + + Default implementation indicates an error because + XML-RPC uses the POST method. + """ + + code = 400 + message, explain = BaseHTTPRequestHandler.responses[code] + + response = http_server.DEFAULT_ERROR_MESSAGE % \ + { + 'code' : code, + 'message' : message, + 'explain' : explain + } + response = response.encode('utf-8') + print('Status: %d %s' % (code, message)) + print('Content-Type: %s' % http_server.DEFAULT_ERROR_CONTENT_TYPE) + print('Content-Length: %d' % len(response)) + print() + sys.stdout.flush() + sys.stdout.buffer.write(response) + sys.stdout.buffer.flush() + + def handle_request(self, request_text=None): + """Handle a single XML-RPC request passed through a CGI post method. + + If no XML data is given then it is read from stdin. The resulting + XML-RPC response is printed to stdout along with the correct HTTP + headers. + """ + + if request_text is None and \ + os.environ.get('REQUEST_METHOD', None) == 'GET': + self.handle_get() + else: + # POST data is normally available through stdin + try: + length = int(os.environ.get('CONTENT_LENGTH', None)) + except (ValueError, TypeError): + length = -1 + if request_text is None: + request_text = sys.stdin.read(length) + + self.handle_xmlrpc(request_text) + + +# ----------------------------------------------------------------------------- +# Self documenting XML-RPC Server. + +class ServerHTMLDoc(pydoc.HTMLDoc): + """Class used to generate pydoc HTML document for a server""" + + def markup(self, text, escape=None, funcs={}, classes={}, methods={}): + """Mark up some plain text, given a context of symbols to look for. + Each context dictionary maps object names to anchor names.""" + escape = escape or self.escape + results = [] + here = 0 + + # XXX Note that this regular expression does not allow for the + # hyperlinking of arbitrary strings being used as method + # names. Only methods with names consisting of word characters + # and '.'s are hyperlinked. + pattern = re.compile(r'\b((http|ftp)://\S+[\w/]|' + r'RFC[- ]?(\d+)|' + r'PEP[- ]?(\d+)|' + r'(self\.)?((?:\w|\.)+))\b') + while 1: + match = pattern.search(text, here) + if not match: break + start, end = match.span() + results.append(escape(text[here:start])) + + all, scheme, rfc, pep, selfdot, name = match.groups() + if scheme: + url = escape(all).replace('"', '"') + results.append('%s' % (url, url)) + elif rfc: + url = 'http://www.rfc-editor.org/rfc/rfc%d.txt' % int(rfc) + results.append('%s' % (url, escape(all))) + elif pep: + url = 'http://www.python.org/dev/peps/pep-%04d/' % int(pep) + results.append('%s' % (url, escape(all))) + elif text[end:end+1] == '(': + results.append(self.namelink(name, methods, funcs, classes)) + elif selfdot: + results.append('self.%s' % name) + else: + results.append(self.namelink(name, classes)) + here = end + results.append(escape(text[here:])) + return ''.join(results) + + def docroutine(self, object, name, mod=None, + funcs={}, classes={}, methods={}, cl=None): + """Produce HTML documentation for a function or method object.""" + + anchor = (cl and cl.__name__ or '') + '-' + name + note = '' + + title = '%s' % ( + self.escape(anchor), self.escape(name)) + + if inspect.ismethod(object): + args = inspect.getfullargspec(object) + # exclude the argument bound to the instance, it will be + # confusing to the non-Python user + argspec = inspect.formatargspec ( + args.args[1:], + args.varargs, + args.varkw, + args.defaults, + annotations=args.annotations, + formatvalue=self.formatvalue + ) + elif inspect.isfunction(object): + args = inspect.getfullargspec(object) + argspec = inspect.formatargspec( + args.args, args.varargs, args.varkw, args.defaults, + annotations=args.annotations, + formatvalue=self.formatvalue) + else: + argspec = '(...)' + + if isinstance(object, tuple): + argspec = object[0] or argspec + docstring = object[1] or "" + else: + docstring = pydoc.getdoc(object) + + decl = title + argspec + (note and self.grey( + '%s' % note)) + + doc = self.markup( + docstring, self.preformat, funcs, classes, methods) + doc = doc and '

%s
' % doc + return '
%s
%s
\n' % (decl, doc) + + def docserver(self, server_name, package_documentation, methods): + """Produce HTML documentation for an XML-RPC server.""" + + fdict = {} + for key, value in methods.items(): + fdict[key] = '#-' + key + fdict[value] = fdict[key] + + server_name = self.escape(server_name) + head = '%s' % server_name + result = self.heading(head, '#ffffff', '#7799ee') + + doc = self.markup(package_documentation, self.preformat, fdict) + doc = doc and '%s' % doc + result = result + '

%s

\n' % doc + + contents = [] + method_items = sorted(methods.items()) + for key, value in method_items: + contents.append(self.docroutine(value, key, funcs=fdict)) + result = result + self.bigsection( + 'Methods', '#ffffff', '#eeaa77', ''.join(contents)) + + return result + +class XMLRPCDocGenerator(object): + """Generates documentation for an XML-RPC server. + + This class is designed as mix-in and should not + be constructed directly. + """ + + def __init__(self): + # setup variables used for HTML documentation + self.server_name = 'XML-RPC Server Documentation' + self.server_documentation = \ + "This server exports the following methods through the XML-RPC "\ + "protocol." + self.server_title = 'XML-RPC Server Documentation' + + def set_server_title(self, server_title): + """Set the HTML title of the generated server documentation""" + + self.server_title = server_title + + def set_server_name(self, server_name): + """Set the name of the generated HTML server documentation""" + + self.server_name = server_name + + def set_server_documentation(self, server_documentation): + """Set the documentation string for the entire server.""" + + self.server_documentation = server_documentation + + def generate_html_documentation(self): + """generate_html_documentation() => html documentation for the server + + Generates HTML documentation for the server using introspection for + installed functions and instances that do not implement the + _dispatch method. Alternatively, instances can choose to implement + the _get_method_argstring(method_name) method to provide the + argument string used in the documentation and the + _methodHelp(method_name) method to provide the help text used + in the documentation.""" + + methods = {} + + for method_name in self.system_listMethods(): + if method_name in self.funcs: + method = self.funcs[method_name] + elif self.instance is not None: + method_info = [None, None] # argspec, documentation + if hasattr(self.instance, '_get_method_argstring'): + method_info[0] = self.instance._get_method_argstring(method_name) + if hasattr(self.instance, '_methodHelp'): + method_info[1] = self.instance._methodHelp(method_name) + + method_info = tuple(method_info) + if method_info != (None, None): + method = method_info + elif not hasattr(self.instance, '_dispatch'): + try: + method = resolve_dotted_attribute( + self.instance, + method_name + ) + except AttributeError: + method = method_info + else: + method = method_info + else: + assert 0, "Could not find method in self.functions and no "\ + "instance installed" + + methods[method_name] = method + + documenter = ServerHTMLDoc() + documentation = documenter.docserver( + self.server_name, + self.server_documentation, + methods + ) + + return documenter.page(self.server_title, documentation) + +class DocXMLRPCRequestHandler(SimpleXMLRPCRequestHandler): + """XML-RPC and documentation request handler class. + + Handles all HTTP POST requests and attempts to decode them as + XML-RPC requests. + + Handles all HTTP GET requests and interprets them as requests + for documentation. + """ + + def do_GET(self): + """Handles the HTTP GET request. + + Interpret all HTTP GET requests as requests for server + documentation. + """ + # Check that the path is legal + if not self.is_rpc_path_valid(): + self.report_404() + return + + response = self.server.generate_html_documentation().encode('utf-8') + self.send_response(200) + self.send_header("Content-type", "text/html") + self.send_header("Content-length", str(len(response))) + self.end_headers() + self.wfile.write(response) + +class DocXMLRPCServer( SimpleXMLRPCServer, + XMLRPCDocGenerator): + """XML-RPC and HTML documentation server. + + Adds the ability to serve server documentation to the capabilities + of SimpleXMLRPCServer. + """ + + def __init__(self, addr, requestHandler=DocXMLRPCRequestHandler, + logRequests=True, allow_none=False, encoding=None, + bind_and_activate=True, use_builtin_types=False): + SimpleXMLRPCServer.__init__(self, addr, requestHandler, logRequests, + allow_none, encoding, bind_and_activate, + use_builtin_types) + XMLRPCDocGenerator.__init__(self) + +class DocCGIXMLRPCRequestHandler( CGIXMLRPCRequestHandler, + XMLRPCDocGenerator): + """Handler for XML-RPC data and documentation requests passed through + CGI""" + + def handle_get(self): + """Handles the HTTP GET request. + + Interpret all HTTP GET requests as requests for server + documentation. + """ + + response = self.generate_html_documentation().encode('utf-8') + + print('Content-Type: text/html') + print('Content-Length: %d' % len(response)) + print() + sys.stdout.flush() + sys.stdout.buffer.write(response) + sys.stdout.buffer.flush() + + def __init__(self): + CGIXMLRPCRequestHandler.__init__(self) + XMLRPCDocGenerator.__init__(self) + + +if __name__ == '__main__': + import datetime + + class ExampleService: + def getData(self): + return '42' + + class currentTime: + @staticmethod + def getCurrentTime(): + return datetime.datetime.now() + + server = SimpleXMLRPCServer(("localhost", 8000)) + server.register_function(pow) + server.register_function(lambda x,y: x+y, 'add') + server.register_instance(ExampleService(), allow_dotted_names=True) + server.register_multicall_functions() + print('Serving XML-RPC on localhost port 8000') + print('It is advisable to run this example server within a secure, closed network.') + try: + server.serve_forever() + except KeyboardInterrupt: + print("\nKeyboard interrupt received, exiting.") + server.server_close() + sys.exit(0) diff --git a/venv/lib/python3.10/site-packages/future/builtins/__init__.py b/venv/lib/python3.10/site-packages/future/builtins/__init__.py new file mode 100644 index 0000000..1734cd4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/builtins/__init__.py @@ -0,0 +1,51 @@ +""" +A module that brings in equivalents of the new and modified Python 3 +builtins into Py2. Has no effect on Py3. + +See the docs `here `_ +(``docs/what-else.rst``) for more information. + +""" + +from future.builtins.iterators import (filter, map, zip) +# The isinstance import is no longer needed. We provide it only for +# backward-compatibility with future v0.8.2. It will be removed in future v1.0. +from future.builtins.misc import (ascii, chr, hex, input, isinstance, next, + oct, open, pow, round, super, max, min) +from future.utils import PY3 + +if PY3: + import builtins + bytes = builtins.bytes + dict = builtins.dict + int = builtins.int + list = builtins.list + object = builtins.object + range = builtins.range + str = builtins.str + __all__ = [] +else: + from future.types import (newbytes as bytes, + newdict as dict, + newint as int, + newlist as list, + newobject as object, + newrange as range, + newstr as str) +from future import utils + + +if not utils.PY3: + # We only import names that shadow the builtins on Py2. No other namespace + # pollution on Py2. + + # Only shadow builtins on Py2; no new names + __all__ = ['filter', 'map', 'zip', + 'ascii', 'chr', 'hex', 'input', 'next', 'oct', 'open', 'pow', + 'round', 'super', + 'bytes', 'dict', 'int', 'list', 'object', 'range', 'str', 'max', 'min' + ] + +else: + # No namespace pollution on Py3 + __all__ = [] diff --git a/venv/lib/python3.10/site-packages/future/builtins/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..4737101 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/builtins/__pycache__/disabled.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/disabled.cpython-310.pyc new file mode 100644 index 0000000..c2cadef Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/disabled.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/builtins/__pycache__/iterators.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/iterators.cpython-310.pyc new file mode 100644 index 0000000..aa95575 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/iterators.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/builtins/__pycache__/misc.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/misc.cpython-310.pyc new file mode 100644 index 0000000..fb2dc37 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/misc.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/builtins/__pycache__/new_min_max.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/new_min_max.cpython-310.pyc new file mode 100644 index 0000000..e268a83 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/new_min_max.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/builtins/__pycache__/newnext.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/newnext.cpython-310.pyc new file mode 100644 index 0000000..f81a6d1 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/newnext.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/builtins/__pycache__/newround.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/newround.cpython-310.pyc new file mode 100644 index 0000000..1a02e7e Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/newround.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/builtins/__pycache__/newsuper.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/newsuper.cpython-310.pyc new file mode 100644 index 0000000..0d30278 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/builtins/__pycache__/newsuper.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/builtins/disabled.py b/venv/lib/python3.10/site-packages/future/builtins/disabled.py new file mode 100644 index 0000000..f6d6ea9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/builtins/disabled.py @@ -0,0 +1,66 @@ +""" +This disables builtin functions (and one exception class) which are +removed from Python 3.3. + +This module is designed to be used like this:: + + from future.builtins.disabled import * + +This disables the following obsolete Py2 builtin functions:: + + apply, cmp, coerce, execfile, file, input, long, + raw_input, reduce, reload, unicode, xrange + +We don't hack __builtin__, which is very fragile because it contaminates +imported modules too. Instead, we just create new functions with +the same names as the obsolete builtins from Python 2 which raise +NameError exceptions when called. + +Note that both ``input()`` and ``raw_input()`` are among the disabled +functions (in this module). Although ``input()`` exists as a builtin in +Python 3, the Python 2 ``input()`` builtin is unsafe to use because it +can lead to shell injection. Therefore we shadow it by default upon ``from +future.builtins.disabled import *``, in case someone forgets to import our +replacement ``input()`` somehow and expects Python 3 semantics. + +See the ``future.builtins.misc`` module for a working version of +``input`` with Python 3 semantics. + +(Note that callable() is not among the functions disabled; this was +reintroduced into Python 3.2.) + +This exception class is also disabled: + + StandardError + +""" + +from __future__ import division, absolute_import, print_function + +from future import utils + + +OBSOLETE_BUILTINS = ['apply', 'chr', 'cmp', 'coerce', 'execfile', 'file', + 'input', 'long', 'raw_input', 'reduce', 'reload', + 'unicode', 'xrange', 'StandardError'] + + +def disabled_function(name): + ''' + Returns a function that cannot be called + ''' + def disabled(*args, **kwargs): + ''' + A function disabled by the ``future`` module. This function is + no longer a builtin in Python 3. + ''' + raise NameError('obsolete Python 2 builtin {0} is disabled'.format(name)) + return disabled + + +if not utils.PY3: + for fname in OBSOLETE_BUILTINS: + locals()[fname] = disabled_function(fname) + __all__ = OBSOLETE_BUILTINS +else: + __all__ = [] diff --git a/venv/lib/python3.10/site-packages/future/builtins/iterators.py b/venv/lib/python3.10/site-packages/future/builtins/iterators.py new file mode 100644 index 0000000..dff651e --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/builtins/iterators.py @@ -0,0 +1,52 @@ +""" +This module is designed to be used as follows:: + + from future.builtins.iterators import * + +And then, for example:: + + for i in range(10**15): + pass + + for (a, b) in zip(range(10**15), range(-10**15, 0)): + pass + +Note that this is standard Python 3 code, plus some imports that do +nothing on Python 3. + +The iterators this brings in are:: + +- ``range`` +- ``filter`` +- ``map`` +- ``zip`` + +On Python 2, ``range`` is a pure-Python backport of Python 3's ``range`` +iterator with slicing support. The other iterators (``filter``, ``map``, +``zip``) are from the ``itertools`` module on Python 2. On Python 3 these +are available in the module namespace but not exported for * imports via +__all__ (zero no namespace pollution). + +Note that these are also available in the standard library +``future_builtins`` module on Python 2 -- but not Python 3, so using +the standard library version is not portable, nor anywhere near complete. +""" + +from __future__ import division, absolute_import, print_function + +import itertools +from future import utils + +if not utils.PY3: + filter = itertools.ifilter + map = itertools.imap + from future.types import newrange as range + zip = itertools.izip + __all__ = ['filter', 'map', 'range', 'zip'] +else: + import builtins + filter = builtins.filter + map = builtins.map + range = builtins.range + zip = builtins.zip + __all__ = [] diff --git a/venv/lib/python3.10/site-packages/future/builtins/misc.py b/venv/lib/python3.10/site-packages/future/builtins/misc.py new file mode 100644 index 0000000..f86ce5f --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/builtins/misc.py @@ -0,0 +1,135 @@ +""" +A module that brings in equivalents of various modified Python 3 builtins +into Py2. Has no effect on Py3. + +The builtin functions are: + +- ``ascii`` (from Py2's future_builtins module) +- ``hex`` (from Py2's future_builtins module) +- ``oct`` (from Py2's future_builtins module) +- ``chr`` (equivalent to ``unichr`` on Py2) +- ``input`` (equivalent to ``raw_input`` on Py2) +- ``next`` (calls ``__next__`` if it exists, else ``next`` method) +- ``open`` (equivalent to io.open on Py2) +- ``super`` (backport of Py3's magic zero-argument super() function +- ``round`` (new "Banker's Rounding" behaviour from Py3) +- ``max`` (new default option from Py3.4) +- ``min`` (new default option from Py3.4) + +``isinstance`` is also currently exported for backwards compatibility +with v0.8.2, although this has been deprecated since v0.9. + + +input() +------- +Like the new ``input()`` function from Python 3 (without eval()), except +that it returns bytes. Equivalent to Python 2's ``raw_input()``. + +Warning: By default, importing this module *removes* the old Python 2 +input() function entirely from ``__builtin__`` for safety. This is +because forgetting to import the new ``input`` from ``future`` might +otherwise lead to a security vulnerability (shell injection) on Python 2. + +To restore it, you can retrieve it yourself from +``__builtin__._old_input``. + +Fortunately, ``input()`` seems to be seldom used in the wild in Python +2... + +""" + +from future import utils + + +if utils.PY2: + from io import open + from future_builtins import ascii, oct, hex + from __builtin__ import unichr as chr, pow as _builtin_pow + import __builtin__ + + # Only for backward compatibility with future v0.8.2: + isinstance = __builtin__.isinstance + + # Warning: Python 2's input() is unsafe and MUST not be able to be used + # accidentally by someone who expects Python 3 semantics but forgets + # to import it on Python 2. Versions of ``future`` prior to 0.11 + # deleted it from __builtin__. Now we keep in __builtin__ but shadow + # the name like all others. Just be sure to import ``input``. + + input = raw_input + + from future.builtins.newnext import newnext as next + from future.builtins.newround import newround as round + from future.builtins.newsuper import newsuper as super + from future.builtins.new_min_max import newmax as max + from future.builtins.new_min_max import newmin as min + from future.types.newint import newint + + _SENTINEL = object() + + def pow(x, y, z=_SENTINEL): + """ + pow(x, y[, z]) -> number + + With two arguments, equivalent to x**y. With three arguments, + equivalent to (x**y) % z, but may be more efficient (e.g. for ints). + """ + # Handle newints + if isinstance(x, newint): + x = long(x) + if isinstance(y, newint): + y = long(y) + if isinstance(z, newint): + z = long(z) + + try: + if z == _SENTINEL: + return _builtin_pow(x, y) + else: + return _builtin_pow(x, y, z) + except ValueError: + if z == _SENTINEL: + return _builtin_pow(x+0j, y) + else: + return _builtin_pow(x+0j, y, z) + + + # ``future`` doesn't support Py3.0/3.1. If we ever did, we'd add this: + # callable = __builtin__.callable + + __all__ = ['ascii', 'chr', 'hex', 'input', 'isinstance', 'next', 'oct', + 'open', 'pow', 'round', 'super', 'max', 'min'] + +else: + import builtins + ascii = builtins.ascii + chr = builtins.chr + hex = builtins.hex + input = builtins.input + next = builtins.next + # Only for backward compatibility with future v0.8.2: + isinstance = builtins.isinstance + oct = builtins.oct + open = builtins.open + pow = builtins.pow + round = builtins.round + super = builtins.super + if utils.PY34_PLUS: + max = builtins.max + min = builtins.min + __all__ = [] + else: + from future.builtins.new_min_max import newmax as max + from future.builtins.new_min_max import newmin as min + __all__ = ['min', 'max'] + + # The callable() function was removed from Py3.0 and 3.1 and + # reintroduced into Py3.2+. ``future`` doesn't support Py3.0/3.1. If we ever + # did, we'd add this: + # try: + # callable = builtins.callable + # except AttributeError: + # # Definition from Pandas + # def callable(obj): + # return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) + # __all__.append('callable') diff --git a/venv/lib/python3.10/site-packages/future/builtins/new_min_max.py b/venv/lib/python3.10/site-packages/future/builtins/new_min_max.py new file mode 100644 index 0000000..6f0c2a8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/builtins/new_min_max.py @@ -0,0 +1,59 @@ +import itertools + +from future import utils +if utils.PY2: + from __builtin__ import max as _builtin_max, min as _builtin_min +else: + from builtins import max as _builtin_max, min as _builtin_min + +_SENTINEL = object() + + +def newmin(*args, **kwargs): + return new_min_max(_builtin_min, *args, **kwargs) + + +def newmax(*args, **kwargs): + return new_min_max(_builtin_max, *args, **kwargs) + + +def new_min_max(_builtin_func, *args, **kwargs): + """ + To support the argument "default" introduced in python 3.4 for min and max + :param _builtin_func: builtin min or builtin max + :param args: + :param kwargs: + :return: returns the min or max based on the arguments passed + """ + + for key, _ in kwargs.items(): + if key not in set(['key', 'default']): + raise TypeError('Illegal argument %s', key) + + if len(args) == 0: + raise TypeError + + if len(args) != 1 and kwargs.get('default', _SENTINEL) is not _SENTINEL: + raise TypeError + + if len(args) == 1: + iterator = iter(args[0]) + try: + first = next(iterator) + except StopIteration: + if kwargs.get('default', _SENTINEL) is not _SENTINEL: + return kwargs.get('default') + else: + raise ValueError('{}() arg is an empty sequence'.format(_builtin_func.__name__)) + else: + iterator = itertools.chain([first], iterator) + if kwargs.get('key') is not None: + return _builtin_func(iterator, key=kwargs.get('key')) + else: + return _builtin_func(iterator) + + if len(args) > 1: + if kwargs.get('key') is not None: + return _builtin_func(args, key=kwargs.get('key')) + else: + return _builtin_func(args) diff --git a/venv/lib/python3.10/site-packages/future/builtins/newnext.py b/venv/lib/python3.10/site-packages/future/builtins/newnext.py new file mode 100644 index 0000000..097638a --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/builtins/newnext.py @@ -0,0 +1,70 @@ +''' +This module provides a newnext() function in Python 2 that mimics the +behaviour of ``next()`` in Python 3, falling back to Python 2's behaviour for +compatibility if this fails. + +``newnext(iterator)`` calls the iterator's ``__next__()`` method if it exists. If this +doesn't exist, it falls back to calling a ``next()`` method. + +For example: + + >>> class Odds(object): + ... def __init__(self, start=1): + ... self.value = start - 2 + ... def __next__(self): # note the Py3 interface + ... self.value += 2 + ... return self.value + ... def __iter__(self): + ... return self + ... + >>> iterator = Odds() + >>> next(iterator) + 1 + >>> next(iterator) + 3 + +If you are defining your own custom iterator class as above, it is preferable +to explicitly decorate the class with the @implements_iterator decorator from +``future.utils`` as follows: + + >>> @implements_iterator + ... class Odds(object): + ... # etc + ... pass + +This next() function is primarily for consuming iterators defined in Python 3 +code elsewhere that we would like to run on Python 2 or 3. +''' + +_builtin_next = next + +_SENTINEL = object() + +def newnext(iterator, default=_SENTINEL): + """ + next(iterator[, default]) + + Return the next item from the iterator. If default is given and the iterator + is exhausted, it is returned instead of raising StopIteration. + """ + + # args = [] + # if default is not _SENTINEL: + # args.append(default) + try: + try: + return iterator.__next__() + except AttributeError: + try: + return iterator.next() + except AttributeError: + raise TypeError("'{0}' object is not an iterator".format( + iterator.__class__.__name__)) + except StopIteration as e: + if default is _SENTINEL: + raise e + else: + return default + + +__all__ = ['newnext'] diff --git a/venv/lib/python3.10/site-packages/future/builtins/newround.py b/venv/lib/python3.10/site-packages/future/builtins/newround.py new file mode 100644 index 0000000..b06c116 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/builtins/newround.py @@ -0,0 +1,105 @@ +""" +``python-future``: pure Python implementation of Python 3 round(). +""" + +from __future__ import division +from future.utils import PYPY, PY26, bind_method + +# Use the decimal module for simplicity of implementation (and +# hopefully correctness). +from decimal import Decimal, ROUND_HALF_EVEN + + +def newround(number, ndigits=None): + """ + See Python 3 documentation: uses Banker's Rounding. + + Delegates to the __round__ method if for some reason this exists. + + If not, rounds a number to a given precision in decimal digits (default + 0 digits). This returns an int when called with one argument, + otherwise the same type as the number. ndigits may be negative. + + See the test_round method in future/tests/test_builtins.py for + examples. + """ + return_int = False + if ndigits is None: + return_int = True + ndigits = 0 + if hasattr(number, '__round__'): + return number.__round__(ndigits) + + exponent = Decimal('10') ** (-ndigits) + + # Work around issue #24: round() breaks on PyPy with NumPy's types + # Also breaks on CPython with NumPy's specialized int types like uint64 + if 'numpy' in repr(type(number)): + number = float(number) + + if isinstance(number, Decimal): + d = number + else: + if not PY26: + d = Decimal.from_float(number) + else: + d = from_float_26(number) + + if ndigits < 0: + result = newround(d / exponent) * exponent + else: + result = d.quantize(exponent, rounding=ROUND_HALF_EVEN) + + if return_int: + return int(result) + else: + return float(result) + + +### From Python 2.7's decimal.py. Only needed to support Py2.6: + +def from_float_26(f): + """Converts a float to a decimal number, exactly. + + Note that Decimal.from_float(0.1) is not the same as Decimal('0.1'). + Since 0.1 is not exactly representable in binary floating point, the + value is stored as the nearest representable value which is + 0x1.999999999999ap-4. The exact equivalent of the value in decimal + is 0.1000000000000000055511151231257827021181583404541015625. + + >>> Decimal.from_float(0.1) + Decimal('0.1000000000000000055511151231257827021181583404541015625') + >>> Decimal.from_float(float('nan')) + Decimal('NaN') + >>> Decimal.from_float(float('inf')) + Decimal('Infinity') + >>> Decimal.from_float(-float('inf')) + Decimal('-Infinity') + >>> Decimal.from_float(-0.0) + Decimal('-0') + + """ + import math as _math + from decimal import _dec_from_triple # only available on Py2.6 and Py2.7 (not 3.3) + + if isinstance(f, (int, long)): # handle integer inputs + return Decimal(f) + if _math.isinf(f) or _math.isnan(f): # raises TypeError if not a float + return Decimal(repr(f)) + if _math.copysign(1.0, f) == 1.0: + sign = 0 + else: + sign = 1 + n, d = abs(f).as_integer_ratio() + # int.bit_length() method doesn't exist on Py2.6: + def bit_length(d): + if d != 0: + return len(bin(abs(d))) - 2 + else: + return 0 + k = bit_length(d) - 1 + result = _dec_from_triple(sign, str(n*5**k), -k) + return result + + +__all__ = ['newround'] diff --git a/venv/lib/python3.10/site-packages/future/builtins/newsuper.py b/venv/lib/python3.10/site-packages/future/builtins/newsuper.py new file mode 100644 index 0000000..3e8cc80 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/builtins/newsuper.py @@ -0,0 +1,113 @@ +''' +This module provides a newsuper() function in Python 2 that mimics the +behaviour of super() in Python 3. It is designed to be used as follows: + + from __future__ import division, absolute_import, print_function + from future.builtins import super + +And then, for example: + + class VerboseList(list): + def append(self, item): + print('Adding an item') + super().append(item) # new simpler super() function + +Importing this module on Python 3 has no effect. + +This is based on (i.e. almost identical to) Ryan Kelly's magicsuper +module here: + + https://github.com/rfk/magicsuper.git + +Excerpts from Ryan's docstring: + + "Of course, you can still explicitly pass in the arguments if you want + to do something strange. Sometimes you really do want that, e.g. to + skip over some classes in the method resolution order. + + "How does it work? By inspecting the calling frame to determine the + function object being executed and the object on which it's being + called, and then walking the object's __mro__ chain to find out where + that function was defined. Yuck, but it seems to work..." +''' + +from __future__ import absolute_import +import sys +from types import FunctionType + +from future.utils import PY3, PY26 + + +_builtin_super = super + +_SENTINEL = object() + +def newsuper(typ=_SENTINEL, type_or_obj=_SENTINEL, framedepth=1): + '''Like builtin super(), but capable of magic. + + This acts just like the builtin super() function, but if called + without any arguments it attempts to infer them at runtime. + ''' + # Infer the correct call if used without arguments. + if typ is _SENTINEL: + # We'll need to do some frame hacking. + f = sys._getframe(framedepth) + + try: + # Get the function's first positional argument. + type_or_obj = f.f_locals[f.f_code.co_varnames[0]] + except (IndexError, KeyError,): + raise RuntimeError('super() used in a function with no args') + + try: + typ = find_owner(type_or_obj, f.f_code) + except (AttributeError, RuntimeError, TypeError): + # see issues #160, #267 + try: + typ = find_owner(type_or_obj.__class__, f.f_code) + except AttributeError: + raise RuntimeError('super() used with an old-style class') + except TypeError: + raise RuntimeError('super() called outside a method') + + # Dispatch to builtin super(). + if type_or_obj is not _SENTINEL: + return _builtin_super(typ, type_or_obj) + return _builtin_super(typ) + + +def find_owner(cls, code): + '''Find the class that owns the currently-executing method. + ''' + for typ in cls.__mro__: + for meth in typ.__dict__.values(): + # Drill down through any wrappers to the underlying func. + # This handles e.g. classmethod() and staticmethod(). + try: + while not isinstance(meth,FunctionType): + if isinstance(meth, property): + # Calling __get__ on the property will invoke + # user code which might throw exceptions or have + # side effects + meth = meth.fget + else: + try: + meth = meth.__func__ + except AttributeError: + meth = meth.__get__(cls, typ) + except (AttributeError, TypeError): + continue + if meth.func_code is code: + return typ # Aha! Found you. + # Not found! Move onto the next class in MRO. + + raise TypeError + + +def superm(*args, **kwds): + f = sys._getframe(1) + nm = f.f_code.co_name + return getattr(newsuper(framedepth=2),nm)(*args, **kwds) + + +__all__ = ['newsuper'] diff --git a/venv/lib/python3.10/site-packages/future/moves/__init__.py b/venv/lib/python3.10/site-packages/future/moves/__init__.py new file mode 100644 index 0000000..0cd60d3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/__init__.py @@ -0,0 +1,8 @@ +# future.moves package +from __future__ import absolute_import +import sys +__future_module__ = True +from future.standard_library import import_top_level_modules + +if sys.version_info[0] >= 3: + import_top_level_modules() diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..b60b48a Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/_dummy_thread.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/_dummy_thread.cpython-310.pyc new file mode 100644 index 0000000..0e71703 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/_dummy_thread.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/_markupbase.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/_markupbase.cpython-310.pyc new file mode 100644 index 0000000..64e3059 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/_markupbase.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/_thread.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/_thread.cpython-310.pyc new file mode 100644 index 0000000..60c13f7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/_thread.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/builtins.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/builtins.cpython-310.pyc new file mode 100644 index 0000000..ecc4ff3 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/builtins.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/collections.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/collections.cpython-310.pyc new file mode 100644 index 0000000..4e891e8 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/collections.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/configparser.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/configparser.cpython-310.pyc new file mode 100644 index 0000000..d20c6f5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/configparser.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/copyreg.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/copyreg.cpython-310.pyc new file mode 100644 index 0000000..ee9ef70 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/copyreg.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/itertools.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/itertools.cpython-310.pyc new file mode 100644 index 0000000..7657870 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/itertools.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/multiprocessing.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/multiprocessing.cpython-310.pyc new file mode 100644 index 0000000..eeb32b0 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/multiprocessing.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/pickle.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/pickle.cpython-310.pyc new file mode 100644 index 0000000..4c1db9a Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/pickle.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/queue.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/queue.cpython-310.pyc new file mode 100644 index 0000000..5a6e207 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/queue.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/reprlib.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/reprlib.cpython-310.pyc new file mode 100644 index 0000000..bbd8538 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/reprlib.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/socketserver.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/socketserver.cpython-310.pyc new file mode 100644 index 0000000..3949918 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/socketserver.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/subprocess.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/subprocess.cpython-310.pyc new file mode 100644 index 0000000..7233d2f Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/subprocess.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/sys.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/sys.cpython-310.pyc new file mode 100644 index 0000000..5b90110 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/sys.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/__pycache__/winreg.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/__pycache__/winreg.cpython-310.pyc new file mode 100644 index 0000000..0b64b87 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/__pycache__/winreg.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/_dummy_thread.py b/venv/lib/python3.10/site-packages/future/moves/_dummy_thread.py new file mode 100644 index 0000000..6633f42 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/_dummy_thread.py @@ -0,0 +1,13 @@ +from __future__ import absolute_import +from future.utils import PY3, PY39_PLUS + + +if PY39_PLUS: + # _dummy_thread and dummy_threading modules were both deprecated in + # Python 3.7 and removed in Python 3.9 + from _thread import * +elif PY3: + from _dummy_thread import * +else: + __future_module__ = True + from dummy_thread import * diff --git a/venv/lib/python3.10/site-packages/future/moves/_markupbase.py b/venv/lib/python3.10/site-packages/future/moves/_markupbase.py new file mode 100644 index 0000000..f9fb4bb --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/_markupbase.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from _markupbase import * +else: + __future_module__ = True + from markupbase import * diff --git a/venv/lib/python3.10/site-packages/future/moves/_thread.py b/venv/lib/python3.10/site-packages/future/moves/_thread.py new file mode 100644 index 0000000..c68018b --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/_thread.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from _thread import * +else: + __future_module__ = True + from thread import * diff --git a/venv/lib/python3.10/site-packages/future/moves/builtins.py b/venv/lib/python3.10/site-packages/future/moves/builtins.py new file mode 100644 index 0000000..e4b6221 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/builtins.py @@ -0,0 +1,10 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from builtins import * +else: + __future_module__ = True + from __builtin__ import * + # Overwrite any old definitions with the equivalent future.builtins ones: + from future.builtins import * diff --git a/venv/lib/python3.10/site-packages/future/moves/collections.py b/venv/lib/python3.10/site-packages/future/moves/collections.py new file mode 100644 index 0000000..664ee6a --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/collections.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import +import sys + +from future.utils import PY2, PY26 +__future_module__ = True + +from collections import * + +if PY2: + from UserDict import UserDict + from UserList import UserList + from UserString import UserString + +if PY26: + from future.backports.misc import OrderedDict, Counter + +if sys.version_info < (3, 3): + from future.backports.misc import ChainMap, _count_elements diff --git a/venv/lib/python3.10/site-packages/future/moves/configparser.py b/venv/lib/python3.10/site-packages/future/moves/configparser.py new file mode 100644 index 0000000..33d9cf9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/configparser.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import + +from future.utils import PY2 + +if PY2: + from ConfigParser import * +else: + from configparser import * diff --git a/venv/lib/python3.10/site-packages/future/moves/copyreg.py b/venv/lib/python3.10/site-packages/future/moves/copyreg.py new file mode 100644 index 0000000..9d08cdc --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/copyreg.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + import copyreg, sys + # A "*" import uses Python 3's copyreg.__all__ which does not include + # all public names in the API surface for copyreg, this avoids that + # problem by just making our module _be_ a reference to the actual module. + sys.modules['future.moves.copyreg'] = copyreg +else: + __future_module__ = True + from copy_reg import * diff --git a/venv/lib/python3.10/site-packages/future/moves/dbm/__init__.py b/venv/lib/python3.10/site-packages/future/moves/dbm/__init__.py new file mode 100644 index 0000000..626b406 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/dbm/__init__.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from dbm import * +else: + __future_module__ = True + from whichdb import * + from anydbm import * + +# Py3.3's dbm/__init__.py imports ndbm but doesn't expose it via __all__. +# In case some (badly written) code depends on dbm.ndbm after import dbm, +# we simulate this: +if PY3: + from dbm import ndbm +else: + try: + from future.moves.dbm import ndbm + except ImportError: + ndbm = None diff --git a/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..5a3690b Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/dumb.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/dumb.cpython-310.pyc new file mode 100644 index 0000000..0850d14 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/dumb.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/gnu.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/gnu.cpython-310.pyc new file mode 100644 index 0000000..45d4eaf Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/gnu.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/ndbm.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/ndbm.cpython-310.pyc new file mode 100644 index 0000000..06e21e1 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/dbm/__pycache__/ndbm.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/dbm/dumb.py b/venv/lib/python3.10/site-packages/future/moves/dbm/dumb.py new file mode 100644 index 0000000..528383f --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/dbm/dumb.py @@ -0,0 +1,9 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from dbm.dumb import * +else: + __future_module__ = True + from dumbdbm import * diff --git a/venv/lib/python3.10/site-packages/future/moves/dbm/gnu.py b/venv/lib/python3.10/site-packages/future/moves/dbm/gnu.py new file mode 100644 index 0000000..68ccf67 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/dbm/gnu.py @@ -0,0 +1,9 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from dbm.gnu import * +else: + __future_module__ = True + from gdbm import * diff --git a/venv/lib/python3.10/site-packages/future/moves/dbm/ndbm.py b/venv/lib/python3.10/site-packages/future/moves/dbm/ndbm.py new file mode 100644 index 0000000..8c6fff8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/dbm/ndbm.py @@ -0,0 +1,9 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from dbm.ndbm import * +else: + __future_module__ = True + from dbm import * diff --git a/venv/lib/python3.10/site-packages/future/moves/html/__init__.py b/venv/lib/python3.10/site-packages/future/moves/html/__init__.py new file mode 100644 index 0000000..22ed6e7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/html/__init__.py @@ -0,0 +1,31 @@ +from __future__ import absolute_import +from future.utils import PY3 +__future_module__ = True + +if PY3: + from html import * +else: + # cgi.escape isn't good enough for the single Py3.3 html test to pass. + # Define it inline here instead. From the Py3.4 stdlib. Note that the + # html.escape() function from the Py3.3 stdlib is not suitable for use on + # Py2.x. + """ + General functions for HTML manipulation. + """ + + def escape(s, quote=True): + """ + Replace special characters "&", "<" and ">" to HTML-safe sequences. + If the optional flag quote is true (the default), the quotation mark + characters, both double quote (") and single quote (') characters are also + translated. + """ + s = s.replace("&", "&") # Must be done first! + s = s.replace("<", "<") + s = s.replace(">", ">") + if quote: + s = s.replace('"', """) + s = s.replace('\'', "'") + return s + + __all__ = ['escape'] diff --git a/venv/lib/python3.10/site-packages/future/moves/html/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/html/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..eb5b5db Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/html/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/html/__pycache__/entities.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/html/__pycache__/entities.cpython-310.pyc new file mode 100644 index 0000000..1f5c9d1 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/html/__pycache__/entities.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/html/__pycache__/parser.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/html/__pycache__/parser.cpython-310.pyc new file mode 100644 index 0000000..9ce0e98 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/html/__pycache__/parser.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/html/entities.py b/venv/lib/python3.10/site-packages/future/moves/html/entities.py new file mode 100644 index 0000000..56a8860 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/html/entities.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from html.entities import * +else: + __future_module__ = True + from htmlentitydefs import * diff --git a/venv/lib/python3.10/site-packages/future/moves/html/parser.py b/venv/lib/python3.10/site-packages/future/moves/html/parser.py new file mode 100644 index 0000000..a6115b5 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/html/parser.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 +__future_module__ = True + +if PY3: + from html.parser import * +else: + from HTMLParser import * diff --git a/venv/lib/python3.10/site-packages/future/moves/http/__init__.py b/venv/lib/python3.10/site-packages/future/moves/http/__init__.py new file mode 100644 index 0000000..917b3d7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/http/__init__.py @@ -0,0 +1,4 @@ +from future.utils import PY3 + +if not PY3: + __future_module__ = True diff --git a/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..cda6ad4 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/client.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/client.cpython-310.pyc new file mode 100644 index 0000000..8875438 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/client.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/cookiejar.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/cookiejar.cpython-310.pyc new file mode 100644 index 0000000..8937c41 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/cookiejar.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/cookies.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/cookies.cpython-310.pyc new file mode 100644 index 0000000..a433779 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/cookies.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/server.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/server.cpython-310.pyc new file mode 100644 index 0000000..dfe6bb3 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/http/__pycache__/server.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/http/client.py b/venv/lib/python3.10/site-packages/future/moves/http/client.py new file mode 100644 index 0000000..55f9c9c --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/http/client.py @@ -0,0 +1,8 @@ +from future.utils import PY3 + +if PY3: + from http.client import * +else: + from httplib import * + from httplib import HTTPMessage + __future_module__ = True diff --git a/venv/lib/python3.10/site-packages/future/moves/http/cookiejar.py b/venv/lib/python3.10/site-packages/future/moves/http/cookiejar.py new file mode 100644 index 0000000..ea00df7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/http/cookiejar.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from http.cookiejar import * +else: + __future_module__ = True + from cookielib import * diff --git a/venv/lib/python3.10/site-packages/future/moves/http/cookies.py b/venv/lib/python3.10/site-packages/future/moves/http/cookies.py new file mode 100644 index 0000000..1b74fe2 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/http/cookies.py @@ -0,0 +1,9 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from http.cookies import * +else: + __future_module__ = True + from Cookie import * + from Cookie import Morsel # left out of __all__ on Py2.7! diff --git a/venv/lib/python3.10/site-packages/future/moves/http/server.py b/venv/lib/python3.10/site-packages/future/moves/http/server.py new file mode 100644 index 0000000..4e75cc1 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/http/server.py @@ -0,0 +1,20 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from http.server import * +else: + __future_module__ = True + from BaseHTTPServer import * + from CGIHTTPServer import * + from SimpleHTTPServer import * + try: + from CGIHTTPServer import _url_collapse_path # needed for a test + except ImportError: + try: + # Python 2.7.0 to 2.7.3 + from CGIHTTPServer import ( + _url_collapse_path_split as _url_collapse_path) + except ImportError: + # Doesn't exist on Python 2.6.x. Ignore it. + pass diff --git a/venv/lib/python3.10/site-packages/future/moves/itertools.py b/venv/lib/python3.10/site-packages/future/moves/itertools.py new file mode 100644 index 0000000..e5eb20d --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/itertools.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import + +from itertools import * +try: + zip_longest = izip_longest + filterfalse = ifilterfalse +except NameError: + pass diff --git a/venv/lib/python3.10/site-packages/future/moves/multiprocessing.py b/venv/lib/python3.10/site-packages/future/moves/multiprocessing.py new file mode 100644 index 0000000..a871b67 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/multiprocessing.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import +from future.utils import PY3 + +from multiprocessing import * +if not PY3: + __future_module__ = True + from multiprocessing.queues import SimpleQueue diff --git a/venv/lib/python3.10/site-packages/future/moves/pickle.py b/venv/lib/python3.10/site-packages/future/moves/pickle.py new file mode 100644 index 0000000..c53d693 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/pickle.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from pickle import * +else: + __future_module__ = True + try: + from cPickle import * + except ImportError: + from pickle import * diff --git a/venv/lib/python3.10/site-packages/future/moves/queue.py b/venv/lib/python3.10/site-packages/future/moves/queue.py new file mode 100644 index 0000000..1cb1437 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/queue.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from queue import * +else: + __future_module__ = True + from Queue import * diff --git a/venv/lib/python3.10/site-packages/future/moves/reprlib.py b/venv/lib/python3.10/site-packages/future/moves/reprlib.py new file mode 100644 index 0000000..a313a13 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/reprlib.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from reprlib import * +else: + __future_module__ = True + from repr import * diff --git a/venv/lib/python3.10/site-packages/future/moves/socketserver.py b/venv/lib/python3.10/site-packages/future/moves/socketserver.py new file mode 100644 index 0000000..062e084 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/socketserver.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from socketserver import * +else: + __future_module__ = True + from SocketServer import * diff --git a/venv/lib/python3.10/site-packages/future/moves/subprocess.py b/venv/lib/python3.10/site-packages/future/moves/subprocess.py new file mode 100644 index 0000000..43ffd2a --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/subprocess.py @@ -0,0 +1,11 @@ +from __future__ import absolute_import +from future.utils import PY2, PY26 + +from subprocess import * + +if PY2: + __future_module__ = True + from commands import getoutput, getstatusoutput + +if PY26: + from future.backports.misc import check_output diff --git a/venv/lib/python3.10/site-packages/future/moves/sys.py b/venv/lib/python3.10/site-packages/future/moves/sys.py new file mode 100644 index 0000000..1293bcb --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/sys.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import + +from future.utils import PY2 + +from sys import * + +if PY2: + from __builtin__ import intern diff --git a/venv/lib/python3.10/site-packages/future/moves/test/__init__.py b/venv/lib/python3.10/site-packages/future/moves/test/__init__.py new file mode 100644 index 0000000..5cf428b --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/test/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if not PY3: + __future_module__ = True diff --git a/venv/lib/python3.10/site-packages/future/moves/test/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/test/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..dbaa3c8 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/test/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/test/__pycache__/support.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/test/__pycache__/support.cpython-310.pyc new file mode 100644 index 0000000..94f7427 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/test/__pycache__/support.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/test/support.py b/venv/lib/python3.10/site-packages/future/moves/test/support.py new file mode 100644 index 0000000..f70c9d7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/test/support.py @@ -0,0 +1,19 @@ +from __future__ import absolute_import + +import sys + +from future.standard_library import suspend_hooks +from future.utils import PY3 + +if PY3: + from test.support import * + if sys.version_info[:2] >= (3, 10): + from test.support.os_helper import ( + EnvironmentVarGuard, + TESTFN, + ) + from test.support.warnings_helper import check_warnings +else: + __future_module__ = True + with suspend_hooks(): + from test.test_support import * diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__init__.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/__init__.py new file mode 100644 index 0000000..e408296 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/__init__.py @@ -0,0 +1,27 @@ +from __future__ import absolute_import +from future.utils import PY3 +__future_module__ = True + +if not PY3: + from Tkinter import * + from Tkinter import (_cnfmerge, _default_root, _flatten, + _support_default_root, _test, + _tkinter, _setit) + + try: # >= 2.7.4 + from Tkinter import (_join) + except ImportError: + pass + + try: # >= 2.7.4 + from Tkinter import (_stringify) + except ImportError: + pass + + try: # >= 2.7.9 + from Tkinter import (_splitdict) + except ImportError: + pass + +else: + from tkinter import * diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..42a1649 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/colorchooser.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/colorchooser.cpython-310.pyc new file mode 100644 index 0000000..5ac772f Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/colorchooser.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/commondialog.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/commondialog.cpython-310.pyc new file mode 100644 index 0000000..2c49954 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/commondialog.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/constants.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/constants.cpython-310.pyc new file mode 100644 index 0000000..45ffa58 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/constants.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/dialog.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/dialog.cpython-310.pyc new file mode 100644 index 0000000..c4342ca Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/dialog.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/dnd.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/dnd.cpython-310.pyc new file mode 100644 index 0000000..08d830f Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/dnd.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/filedialog.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/filedialog.cpython-310.pyc new file mode 100644 index 0000000..103cddf Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/filedialog.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/font.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/font.cpython-310.pyc new file mode 100644 index 0000000..c8a0ecb Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/font.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/messagebox.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/messagebox.cpython-310.pyc new file mode 100644 index 0000000..b1aa7fc Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/messagebox.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/scrolledtext.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/scrolledtext.cpython-310.pyc new file mode 100644 index 0000000..b1c625a Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/scrolledtext.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/simpledialog.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/simpledialog.cpython-310.pyc new file mode 100644 index 0000000..620d0f6 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/simpledialog.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/tix.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/tix.cpython-310.pyc new file mode 100644 index 0000000..ffdb744 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/tix.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/ttk.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/ttk.cpython-310.pyc new file mode 100644 index 0000000..1e53811 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/tkinter/__pycache__/ttk.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/colorchooser.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/colorchooser.py new file mode 100644 index 0000000..6dde6e8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/colorchooser.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.colorchooser import * +else: + try: + from tkColorChooser import * + except ImportError: + raise ImportError('The tkColorChooser module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/commondialog.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/commondialog.py new file mode 100644 index 0000000..eb7ae8d --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/commondialog.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.commondialog import * +else: + try: + from tkCommonDialog import * + except ImportError: + raise ImportError('The tkCommonDialog module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/constants.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/constants.py new file mode 100644 index 0000000..ffe0981 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/constants.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.constants import * +else: + try: + from Tkconstants import * + except ImportError: + raise ImportError('The Tkconstants module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/dialog.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/dialog.py new file mode 100644 index 0000000..113370c --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/dialog.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.dialog import * +else: + try: + from Dialog import * + except ImportError: + raise ImportError('The Dialog module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/dnd.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/dnd.py new file mode 100644 index 0000000..1ab4379 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/dnd.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.dnd import * +else: + try: + from Tkdnd import * + except ImportError: + raise ImportError('The Tkdnd module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/filedialog.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/filedialog.py new file mode 100644 index 0000000..6a6f03c --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/filedialog.py @@ -0,0 +1,18 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.filedialog import * +else: + try: + from FileDialog import * + except ImportError: + raise ImportError('The FileDialog module is missing. Does your Py2 ' + 'installation include tkinter?') + + try: + from tkFileDialog import * + except ImportError: + raise ImportError('The tkFileDialog module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/font.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/font.py new file mode 100644 index 0000000..628f399 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/font.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.font import * +else: + try: + from tkFont import * + except ImportError: + raise ImportError('The tkFont module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/messagebox.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/messagebox.py new file mode 100644 index 0000000..b43d870 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/messagebox.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.messagebox import * +else: + try: + from tkMessageBox import * + except ImportError: + raise ImportError('The tkMessageBox module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/scrolledtext.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/scrolledtext.py new file mode 100644 index 0000000..1c69db6 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/scrolledtext.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.scrolledtext import * +else: + try: + from ScrolledText import * + except ImportError: + raise ImportError('The ScrolledText module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/simpledialog.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/simpledialog.py new file mode 100644 index 0000000..dba93fb --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/simpledialog.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.simpledialog import * +else: + try: + from SimpleDialog import * + except ImportError: + raise ImportError('The SimpleDialog module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/tix.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/tix.py new file mode 100644 index 0000000..8d1718a --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/tix.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.tix import * +else: + try: + from Tix import * + except ImportError: + raise ImportError('The Tix module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/tkinter/ttk.py b/venv/lib/python3.10/site-packages/future/moves/tkinter/ttk.py new file mode 100644 index 0000000..081c1b4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/tkinter/ttk.py @@ -0,0 +1,12 @@ +from __future__ import absolute_import + +from future.utils import PY3 + +if PY3: + from tkinter.ttk import * +else: + try: + from ttk import * + except ImportError: + raise ImportError('The ttk module is missing. Does your Py2 ' + 'installation include tkinter?') diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/__init__.py b/venv/lib/python3.10/site-packages/future/moves/urllib/__init__.py new file mode 100644 index 0000000..5cf428b --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/urllib/__init__.py @@ -0,0 +1,5 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if not PY3: + __future_module__ = True diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..52117b7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/error.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/error.cpython-310.pyc new file mode 100644 index 0000000..451b919 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/error.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/parse.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/parse.cpython-310.pyc new file mode 100644 index 0000000..1a581f0 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/parse.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/request.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/request.cpython-310.pyc new file mode 100644 index 0000000..d2cc0c9 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/request.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/response.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/response.cpython-310.pyc new file mode 100644 index 0000000..b22bcce Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/response.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/robotparser.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/robotparser.cpython-310.pyc new file mode 100644 index 0000000..1d828ca Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/urllib/__pycache__/robotparser.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/error.py b/venv/lib/python3.10/site-packages/future/moves/urllib/error.py new file mode 100644 index 0000000..7d8ada7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/urllib/error.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import +from future.standard_library import suspend_hooks + +from future.utils import PY3 + +if PY3: + from urllib.error import * +else: + __future_module__ = True + + # We use this method to get at the original Py2 urllib before any renaming magic + # ContentTooShortError = sys.py2_modules['urllib'].ContentTooShortError + + with suspend_hooks(): + from urllib import ContentTooShortError + from urllib2 import URLError, HTTPError diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/parse.py b/venv/lib/python3.10/site-packages/future/moves/urllib/parse.py new file mode 100644 index 0000000..9074b81 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/urllib/parse.py @@ -0,0 +1,28 @@ +from __future__ import absolute_import +from future.standard_library import suspend_hooks + +from future.utils import PY3 + +if PY3: + from urllib.parse import * +else: + __future_module__ = True + from urlparse import (ParseResult, SplitResult, parse_qs, parse_qsl, + urldefrag, urljoin, urlparse, urlsplit, + urlunparse, urlunsplit) + + # we use this method to get at the original py2 urllib before any renaming + # quote = sys.py2_modules['urllib'].quote + # quote_plus = sys.py2_modules['urllib'].quote_plus + # unquote = sys.py2_modules['urllib'].unquote + # unquote_plus = sys.py2_modules['urllib'].unquote_plus + # urlencode = sys.py2_modules['urllib'].urlencode + # splitquery = sys.py2_modules['urllib'].splitquery + + with suspend_hooks(): + from urllib import (quote, + quote_plus, + unquote, + unquote_plus, + urlencode, + splitquery) diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/request.py b/venv/lib/python3.10/site-packages/future/moves/urllib/request.py new file mode 100644 index 0000000..972aa4a --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/urllib/request.py @@ -0,0 +1,94 @@ +from __future__ import absolute_import + +from future.standard_library import suspend_hooks +from future.utils import PY3 + +if PY3: + from urllib.request import * + # This aren't in __all__: + from urllib.request import (getproxies, + pathname2url, + proxy_bypass, + quote, + request_host, + thishost, + unquote, + url2pathname, + urlcleanup, + urljoin, + urlopen, + urlparse, + urlretrieve, + urlsplit, + urlunparse) + + from urllib.parse import (splitattr, + splithost, + splitpasswd, + splitport, + splitquery, + splittag, + splittype, + splituser, + splitvalue, + to_bytes, + unwrap) +else: + __future_module__ = True + with suspend_hooks(): + from urllib import * + from urllib2 import * + from urlparse import * + + # Rename: + from urllib import toBytes # missing from __all__ on Py2.6 + to_bytes = toBytes + + # from urllib import (pathname2url, + # url2pathname, + # getproxies, + # urlretrieve, + # urlcleanup, + # URLopener, + # FancyURLopener, + # proxy_bypass) + + # from urllib2 import ( + # AbstractBasicAuthHandler, + # AbstractDigestAuthHandler, + # BaseHandler, + # CacheFTPHandler, + # FileHandler, + # FTPHandler, + # HTTPBasicAuthHandler, + # HTTPCookieProcessor, + # HTTPDefaultErrorHandler, + # HTTPDigestAuthHandler, + # HTTPErrorProcessor, + # HTTPHandler, + # HTTPPasswordMgr, + # HTTPPasswordMgrWithDefaultRealm, + # HTTPRedirectHandler, + # HTTPSHandler, + # URLError, + # build_opener, + # install_opener, + # OpenerDirector, + # ProxyBasicAuthHandler, + # ProxyDigestAuthHandler, + # ProxyHandler, + # Request, + # UnknownHandler, + # urlopen, + # ) + + # from urlparse import ( + # urldefrag + # urljoin, + # urlparse, + # urlunparse, + # urlsplit, + # urlunsplit, + # parse_qs, + # parse_q" + # ) diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/response.py b/venv/lib/python3.10/site-packages/future/moves/urllib/response.py new file mode 100644 index 0000000..a287ae2 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/urllib/response.py @@ -0,0 +1,12 @@ +from future import standard_library +from future.utils import PY3 + +if PY3: + from urllib.response import * +else: + __future_module__ = True + with standard_library.suspend_hooks(): + from urllib import (addbase, + addclosehook, + addinfo, + addinfourl) diff --git a/venv/lib/python3.10/site-packages/future/moves/urllib/robotparser.py b/venv/lib/python3.10/site-packages/future/moves/urllib/robotparser.py new file mode 100644 index 0000000..0dc8f57 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/urllib/robotparser.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from urllib.robotparser import * +else: + __future_module__ = True + from robotparser import * diff --git a/venv/lib/python3.10/site-packages/future/moves/winreg.py b/venv/lib/python3.10/site-packages/future/moves/winreg.py new file mode 100644 index 0000000..c8b1475 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/winreg.py @@ -0,0 +1,8 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from winreg import * +else: + __future_module__ = True + from _winreg import * diff --git a/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__init__.py b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..3f22d90 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__pycache__/client.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__pycache__/client.cpython-310.pyc new file mode 100644 index 0000000..d2d6fdd Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__pycache__/client.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__pycache__/server.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__pycache__/server.cpython-310.pyc new file mode 100644 index 0000000..eb9fbb0 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/__pycache__/server.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/moves/xmlrpc/client.py b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/client.py new file mode 100644 index 0000000..4708cf8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/client.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from xmlrpc.client import * +else: + from xmlrpclib import * diff --git a/venv/lib/python3.10/site-packages/future/moves/xmlrpc/server.py b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/server.py new file mode 100644 index 0000000..1a8af34 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/moves/xmlrpc/server.py @@ -0,0 +1,7 @@ +from __future__ import absolute_import +from future.utils import PY3 + +if PY3: + from xmlrpc.server import * +else: + from xmlrpclib import * diff --git a/venv/lib/python3.10/site-packages/future/standard_library/__init__.py b/venv/lib/python3.10/site-packages/future/standard_library/__init__.py new file mode 100644 index 0000000..d467aaf --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/standard_library/__init__.py @@ -0,0 +1,821 @@ +""" +Python 3 reorganized the standard library (PEP 3108). This module exposes +several standard library modules to Python 2 under their new Python 3 +names. + +It is designed to be used as follows:: + + from future import standard_library + standard_library.install_aliases() + +And then these normal Py3 imports work on both Py3 and Py2:: + + import builtins + import copyreg + import queue + import reprlib + import socketserver + import winreg # on Windows only + import test.support + import html, html.parser, html.entities + import http, http.client, http.server + import http.cookies, http.cookiejar + import urllib.parse, urllib.request, urllib.response, urllib.error, urllib.robotparser + import xmlrpc.client, xmlrpc.server + + import _thread + import _dummy_thread + import _markupbase + + from itertools import filterfalse, zip_longest + from sys import intern + from collections import UserDict, UserList, UserString + from collections import OrderedDict, Counter, ChainMap # even on Py2.6 + from subprocess import getoutput, getstatusoutput + from subprocess import check_output # even on Py2.6 + from multiprocessing import SimpleQueue + +(The renamed modules and functions are still available under their old +names on Python 2.) + +This is a cleaner alternative to this idiom (see +http://docs.pythonsprints.com/python3_porting/py-porting.html):: + + try: + import queue + except ImportError: + import Queue as queue + + +Limitations +----------- +We don't currently support these modules, but would like to:: + + import dbm + import dbm.dumb + import dbm.gnu + import collections.abc # on Py33 + import pickle # should (optionally) bring in cPickle on Python 2 + +""" + +from __future__ import absolute_import, division, print_function + +import sys +import logging +# imp was deprecated in python 3.6 +if sys.version_info >= (3, 6): + import importlib as imp +else: + import imp +import contextlib +import copy +import os + +# Make a dedicated logger; leave the root logger to be configured +# by the application. +flog = logging.getLogger('future_stdlib') +_formatter = logging.Formatter(logging.BASIC_FORMAT) +_handler = logging.StreamHandler() +_handler.setFormatter(_formatter) +flog.addHandler(_handler) +flog.setLevel(logging.WARN) + +from future.utils import PY2, PY3 + +# The modules that are defined under the same names on Py3 but with +# different contents in a significant way (e.g. submodules) are: +# pickle (fast one) +# dbm +# urllib +# test +# email + +REPLACED_MODULES = set(['test', 'urllib', 'pickle', 'dbm']) # add email and dbm when we support it + +# The following module names are not present in Python 2.x, so they cause no +# potential clashes between the old and new names: +# http +# html +# tkinter +# xmlrpc +# Keys: Py2 / real module names +# Values: Py3 / simulated module names +RENAMES = { + # 'cStringIO': 'io', # there's a new io module in Python 2.6 + # that provides StringIO and BytesIO + # 'StringIO': 'io', # ditto + # 'cPickle': 'pickle', + '__builtin__': 'builtins', + 'copy_reg': 'copyreg', + 'Queue': 'queue', + 'future.moves.socketserver': 'socketserver', + 'ConfigParser': 'configparser', + 'repr': 'reprlib', + 'multiprocessing.queues': 'multiprocessing', + # 'FileDialog': 'tkinter.filedialog', + # 'tkFileDialog': 'tkinter.filedialog', + # 'SimpleDialog': 'tkinter.simpledialog', + # 'tkSimpleDialog': 'tkinter.simpledialog', + # 'tkColorChooser': 'tkinter.colorchooser', + # 'tkCommonDialog': 'tkinter.commondialog', + # 'Dialog': 'tkinter.dialog', + # 'Tkdnd': 'tkinter.dnd', + # 'tkFont': 'tkinter.font', + # 'tkMessageBox': 'tkinter.messagebox', + # 'ScrolledText': 'tkinter.scrolledtext', + # 'Tkconstants': 'tkinter.constants', + # 'Tix': 'tkinter.tix', + # 'ttk': 'tkinter.ttk', + # 'Tkinter': 'tkinter', + '_winreg': 'winreg', + 'thread': '_thread', + 'dummy_thread': '_dummy_thread' if sys.version_info < (3, 9) else '_thread', + # 'anydbm': 'dbm', # causes infinite import loop + # 'whichdb': 'dbm', # causes infinite import loop + # anydbm and whichdb are handled by fix_imports2 + # 'dbhash': 'dbm.bsd', + # 'dumbdbm': 'dbm.dumb', + # 'dbm': 'dbm.ndbm', + # 'gdbm': 'dbm.gnu', + 'future.moves.xmlrpc': 'xmlrpc', + # 'future.backports.email': 'email', # for use by urllib + # 'DocXMLRPCServer': 'xmlrpc.server', + # 'SimpleXMLRPCServer': 'xmlrpc.server', + # 'httplib': 'http.client', + # 'htmlentitydefs' : 'html.entities', + # 'HTMLParser' : 'html.parser', + # 'Cookie': 'http.cookies', + # 'cookielib': 'http.cookiejar', + # 'BaseHTTPServer': 'http.server', + # 'SimpleHTTPServer': 'http.server', + # 'CGIHTTPServer': 'http.server', + # 'future.backports.test': 'test', # primarily for renaming test_support to support + # 'commands': 'subprocess', + # 'urlparse' : 'urllib.parse', + # 'robotparser' : 'urllib.robotparser', + # 'abc': 'collections.abc', # for Py33 + # 'future.utils.six.moves.html': 'html', + # 'future.utils.six.moves.http': 'http', + 'future.moves.html': 'html', + 'future.moves.http': 'http', + # 'future.backports.urllib': 'urllib', + # 'future.utils.six.moves.urllib': 'urllib', + 'future.moves._markupbase': '_markupbase', + } + + +# It is complicated and apparently brittle to mess around with the +# ``sys.modules`` cache in order to support "import urllib" meaning two +# different things (Py2.7 urllib and backported Py3.3-like urllib) in different +# contexts. So we require explicit imports for these modules. +assert len(set(RENAMES.values()) & set(REPLACED_MODULES)) == 0 + + +# Harmless renames that we can insert. +# These modules need names from elsewhere being added to them: +# subprocess: should provide getoutput and other fns from commands +# module but these fns are missing: getstatus, mk2arg, +# mkarg +# re: needs an ASCII constant that works compatibly with Py3 + +# etc: see lib2to3/fixes/fix_imports.py + +# (New module name, new object name, old module name, old object name) +MOVES = [('collections', 'UserList', 'UserList', 'UserList'), + ('collections', 'UserDict', 'UserDict', 'UserDict'), + ('collections', 'UserString','UserString', 'UserString'), + ('collections', 'ChainMap', 'future.backports.misc', 'ChainMap'), + ('itertools', 'filterfalse','itertools', 'ifilterfalse'), + ('itertools', 'zip_longest','itertools', 'izip_longest'), + ('sys', 'intern','__builtin__', 'intern'), + ('multiprocessing', 'SimpleQueue', 'multiprocessing.queues', 'SimpleQueue'), + # The re module has no ASCII flag in Py2, but this is the default. + # Set re.ASCII to a zero constant. stat.ST_MODE just happens to be one + # (and it exists on Py2.6+). + ('re', 'ASCII','stat', 'ST_MODE'), + ('base64', 'encodebytes','base64', 'encodestring'), + ('base64', 'decodebytes','base64', 'decodestring'), + ('subprocess', 'getoutput', 'commands', 'getoutput'), + ('subprocess', 'getstatusoutput', 'commands', 'getstatusoutput'), + ('subprocess', 'check_output', 'future.backports.misc', 'check_output'), + ('math', 'ceil', 'future.backports.misc', 'ceil'), + ('collections', 'OrderedDict', 'future.backports.misc', 'OrderedDict'), + ('collections', 'Counter', 'future.backports.misc', 'Counter'), + ('collections', 'ChainMap', 'future.backports.misc', 'ChainMap'), + ('itertools', 'count', 'future.backports.misc', 'count'), + ('reprlib', 'recursive_repr', 'future.backports.misc', 'recursive_repr'), + ('functools', 'cmp_to_key', 'future.backports.misc', 'cmp_to_key'), + +# This is no use, since "import urllib.request" etc. still fails: +# ('urllib', 'error', 'future.moves.urllib', 'error'), +# ('urllib', 'parse', 'future.moves.urllib', 'parse'), +# ('urllib', 'request', 'future.moves.urllib', 'request'), +# ('urllib', 'response', 'future.moves.urllib', 'response'), +# ('urllib', 'robotparser', 'future.moves.urllib', 'robotparser'), + ] + + +# A minimal example of an import hook: +# class WarnOnImport(object): +# def __init__(self, *args): +# self.module_names = args +# +# def find_module(self, fullname, path=None): +# if fullname in self.module_names: +# self.path = path +# return self +# return None +# +# def load_module(self, name): +# if name in sys.modules: +# return sys.modules[name] +# module_info = imp.find_module(name, self.path) +# module = imp.load_module(name, *module_info) +# sys.modules[name] = module +# flog.warning("Imported deprecated module %s", name) +# return module + + +class RenameImport(object): + """ + A class for import hooks mapping Py3 module names etc. to the Py2 equivalents. + """ + # Different RenameImport classes are created when importing this module from + # different source files. This causes isinstance(hook, RenameImport) checks + # to produce inconsistent results. We add this RENAMER attribute here so + # remove_hooks() and install_hooks() can find instances of these classes + # easily: + RENAMER = True + + def __init__(self, old_to_new): + ''' + Pass in a dictionary-like object mapping from old names to new + names. E.g. {'ConfigParser': 'configparser', 'cPickle': 'pickle'} + ''' + self.old_to_new = old_to_new + both = set(old_to_new.keys()) & set(old_to_new.values()) + assert (len(both) == 0 and + len(set(old_to_new.values())) == len(old_to_new.values())), \ + 'Ambiguity in renaming (handler not implemented)' + self.new_to_old = dict((new, old) for (old, new) in old_to_new.items()) + + def find_module(self, fullname, path=None): + # Handles hierarchical importing: package.module.module2 + new_base_names = set([s.split('.')[0] for s in self.new_to_old]) + # Before v0.12: Was: if fullname in set(self.old_to_new) | new_base_names: + if fullname in new_base_names: + return self + return None + + def load_module(self, name): + path = None + if name in sys.modules: + return sys.modules[name] + elif name in self.new_to_old: + # New name. Look up the corresponding old (Py2) name: + oldname = self.new_to_old[name] + module = self._find_and_load_module(oldname) + # module.__future_module__ = True + else: + module = self._find_and_load_module(name) + # In any case, make it available under the requested (Py3) name + sys.modules[name] = module + return module + + def _find_and_load_module(self, name, path=None): + """ + Finds and loads it. But if there's a . in the name, handles it + properly. + """ + bits = name.split('.') + while len(bits) > 1: + # Treat the first bit as a package + packagename = bits.pop(0) + package = self._find_and_load_module(packagename, path) + try: + path = package.__path__ + except AttributeError: + # This could be e.g. moves. + flog.debug('Package {0} has no __path__.'.format(package)) + if name in sys.modules: + return sys.modules[name] + flog.debug('What to do here?') + + name = bits[0] + module_info = imp.find_module(name, path) + return imp.load_module(name, *module_info) + + +class hooks(object): + """ + Acts as a context manager. Saves the state of sys.modules and restores it + after the 'with' block. + + Use like this: + + >>> from future import standard_library + >>> with standard_library.hooks(): + ... import http.client + >>> import requests + + For this to work, http.client will be scrubbed from sys.modules after the + 'with' block. That way the modules imported in the 'with' block will + continue to be accessible in the current namespace but not from any + imported modules (like requests). + """ + def __enter__(self): + # flog.debug('Entering hooks context manager') + self.old_sys_modules = copy.copy(sys.modules) + self.hooks_were_installed = detect_hooks() + # self.scrubbed = scrub_py2_sys_modules() + install_hooks() + return self + + def __exit__(self, *args): + # flog.debug('Exiting hooks context manager') + # restore_sys_modules(self.scrubbed) + if not self.hooks_were_installed: + remove_hooks() + # scrub_future_sys_modules() + +# Sanity check for is_py2_stdlib_module(): We aren't replacing any +# builtin modules names: +if PY2: + assert len(set(RENAMES.values()) & set(sys.builtin_module_names)) == 0 + + +def is_py2_stdlib_module(m): + """ + Tries to infer whether the module m is from the Python 2 standard library. + This may not be reliable on all systems. + """ + if PY3: + return False + if not 'stdlib_path' in is_py2_stdlib_module.__dict__: + stdlib_files = [contextlib.__file__, os.__file__, copy.__file__] + stdlib_paths = [os.path.split(f)[0] for f in stdlib_files] + if not len(set(stdlib_paths)) == 1: + # This seems to happen on travis-ci.org. Very strange. We'll try to + # ignore it. + flog.warn('Multiple locations found for the Python standard ' + 'library: %s' % stdlib_paths) + # Choose the first one arbitrarily + is_py2_stdlib_module.stdlib_path = stdlib_paths[0] + + if m.__name__ in sys.builtin_module_names: + return True + + if hasattr(m, '__file__'): + modpath = os.path.split(m.__file__) + if (modpath[0].startswith(is_py2_stdlib_module.stdlib_path) and + 'site-packages' not in modpath[0]): + return True + + return False + + +def scrub_py2_sys_modules(): + """ + Removes any Python 2 standard library modules from ``sys.modules`` that + would interfere with Py3-style imports using import hooks. Examples are + modules with the same names (like urllib or email). + + (Note that currently import hooks are disabled for modules like these + with ambiguous names anyway ...) + """ + if PY3: + return {} + scrubbed = {} + for modulename in REPLACED_MODULES & set(RENAMES.keys()): + if not modulename in sys.modules: + continue + + module = sys.modules[modulename] + + if is_py2_stdlib_module(module): + flog.debug('Deleting (Py2) {} from sys.modules'.format(modulename)) + scrubbed[modulename] = sys.modules[modulename] + del sys.modules[modulename] + return scrubbed + + +def scrub_future_sys_modules(): + """ + Deprecated. + """ + return {} + +class suspend_hooks(object): + """ + Acts as a context manager. Use like this: + + >>> from future import standard_library + >>> standard_library.install_hooks() + >>> import http.client + >>> # ... + >>> with standard_library.suspend_hooks(): + >>> import requests # incompatible with ``future``'s standard library hooks + + If the hooks were disabled before the context, they are not installed when + the context is left. + """ + def __enter__(self): + self.hooks_were_installed = detect_hooks() + remove_hooks() + # self.scrubbed = scrub_future_sys_modules() + return self + + def __exit__(self, *args): + if self.hooks_were_installed: + install_hooks() + # restore_sys_modules(self.scrubbed) + + +def restore_sys_modules(scrubbed): + """ + Add any previously scrubbed modules back to the sys.modules cache, + but only if it's safe to do so. + """ + clash = set(sys.modules) & set(scrubbed) + if len(clash) != 0: + # If several, choose one arbitrarily to raise an exception about + first = list(clash)[0] + raise ImportError('future module {} clashes with Py2 module' + .format(first)) + sys.modules.update(scrubbed) + + +def install_aliases(): + """ + Monkey-patches the standard library in Py2.6/7 to provide + aliases for better Py3 compatibility. + """ + if PY3: + return + # if hasattr(install_aliases, 'run_already'): + # return + for (newmodname, newobjname, oldmodname, oldobjname) in MOVES: + __import__(newmodname) + # We look up the module in sys.modules because __import__ just returns the + # top-level package: + newmod = sys.modules[newmodname] + # newmod.__future_module__ = True + + __import__(oldmodname) + oldmod = sys.modules[oldmodname] + + obj = getattr(oldmod, oldobjname) + setattr(newmod, newobjname, obj) + + # Hack for urllib so it appears to have the same structure on Py2 as on Py3 + import urllib + from future.backports.urllib import request + from future.backports.urllib import response + from future.backports.urllib import parse + from future.backports.urllib import error + from future.backports.urllib import robotparser + urllib.request = request + urllib.response = response + urllib.parse = parse + urllib.error = error + urllib.robotparser = robotparser + sys.modules['urllib.request'] = request + sys.modules['urllib.response'] = response + sys.modules['urllib.parse'] = parse + sys.modules['urllib.error'] = error + sys.modules['urllib.robotparser'] = robotparser + + # Patch the test module so it appears to have the same structure on Py2 as on Py3 + try: + import test + except ImportError: + pass + try: + from future.moves.test import support + except ImportError: + pass + else: + test.support = support + sys.modules['test.support'] = support + + # Patch the dbm module so it appears to have the same structure on Py2 as on Py3 + try: + import dbm + except ImportError: + pass + else: + from future.moves.dbm import dumb + dbm.dumb = dumb + sys.modules['dbm.dumb'] = dumb + try: + from future.moves.dbm import gnu + except ImportError: + pass + else: + dbm.gnu = gnu + sys.modules['dbm.gnu'] = gnu + try: + from future.moves.dbm import ndbm + except ImportError: + pass + else: + dbm.ndbm = ndbm + sys.modules['dbm.ndbm'] = ndbm + + # install_aliases.run_already = True + + +def install_hooks(): + """ + This function installs the future.standard_library import hook into + sys.meta_path. + """ + if PY3: + return + + install_aliases() + + flog.debug('sys.meta_path was: {0}'.format(sys.meta_path)) + flog.debug('Installing hooks ...') + + # Add it unless it's there already + newhook = RenameImport(RENAMES) + if not detect_hooks(): + sys.meta_path.append(newhook) + flog.debug('sys.meta_path is now: {0}'.format(sys.meta_path)) + + +def enable_hooks(): + """ + Deprecated. Use install_hooks() instead. This will be removed by + ``future`` v1.0. + """ + install_hooks() + + +def remove_hooks(scrub_sys_modules=False): + """ + This function removes the import hook from sys.meta_path. + """ + if PY3: + return + flog.debug('Uninstalling hooks ...') + # Loop backwards, so deleting items keeps the ordering: + for i, hook in list(enumerate(sys.meta_path))[::-1]: + if hasattr(hook, 'RENAMER'): + del sys.meta_path[i] + + # Explicit is better than implicit. In the future the interface should + # probably change so that scrubbing the import hooks requires a separate + # function call. Left as is for now for backward compatibility with + # v0.11.x. + if scrub_sys_modules: + scrub_future_sys_modules() + + +def disable_hooks(): + """ + Deprecated. Use remove_hooks() instead. This will be removed by + ``future`` v1.0. + """ + remove_hooks() + + +def detect_hooks(): + """ + Returns True if the import hooks are installed, False if not. + """ + flog.debug('Detecting hooks ...') + present = any([hasattr(hook, 'RENAMER') for hook in sys.meta_path]) + if present: + flog.debug('Detected.') + else: + flog.debug('Not detected.') + return present + + +# As of v0.12, this no longer happens implicitly: +# if not PY3: +# install_hooks() + + +if not hasattr(sys, 'py2_modules'): + sys.py2_modules = {} + +def cache_py2_modules(): + """ + Currently this function is unneeded, as we are not attempting to provide import hooks + for modules with ambiguous names: email, urllib, pickle. + """ + if len(sys.py2_modules) != 0: + return + assert not detect_hooks() + import urllib + sys.py2_modules['urllib'] = urllib + + import email + sys.py2_modules['email'] = email + + import pickle + sys.py2_modules['pickle'] = pickle + + # Not all Python installations have test module. (Anaconda doesn't, for example.) + # try: + # import test + # except ImportError: + # sys.py2_modules['test'] = None + # sys.py2_modules['test'] = test + + # import dbm + # sys.py2_modules['dbm'] = dbm + + +def import_(module_name, backport=False): + """ + Pass a (potentially dotted) module name of a Python 3 standard library + module. This function imports the module compatibly on Py2 and Py3 and + returns the top-level module. + + Example use: + >>> http = import_('http.client') + >>> http = import_('http.server') + >>> urllib = import_('urllib.request') + + Then: + >>> conn = http.client.HTTPConnection(...) + >>> response = urllib.request.urlopen('http://mywebsite.com') + >>> # etc. + + Use as follows: + >>> package_name = import_(module_name) + + On Py3, equivalent to this: + + >>> import module_name + + On Py2, equivalent to this if backport=False: + + >>> from future.moves import module_name + + or to this if backport=True: + + >>> from future.backports import module_name + + except that it also handles dotted module names such as ``http.client`` + The effect then is like this: + + >>> from future.backports import module + >>> from future.backports.module import submodule + >>> module.submodule = submodule + + Note that this would be a SyntaxError in Python: + + >>> from future.backports import http.client + + """ + # Python 2.6 doesn't have importlib in the stdlib, so it requires + # the backported ``importlib`` package from PyPI as a dependency to use + # this function: + import importlib + + if PY3: + return __import__(module_name) + else: + # client.blah = blah + # Then http.client = client + # etc. + if backport: + prefix = 'future.backports' + else: + prefix = 'future.moves' + parts = prefix.split('.') + module_name.split('.') + + modules = [] + for i, part in enumerate(parts): + sofar = '.'.join(parts[:i+1]) + modules.append(importlib.import_module(sofar)) + for i, part in reversed(list(enumerate(parts))): + if i == 0: + break + setattr(modules[i-1], part, modules[i]) + + # Return the next-most top-level module after future.backports / future.moves: + return modules[2] + + +def from_import(module_name, *symbol_names, **kwargs): + """ + Example use: + >>> HTTPConnection = from_import('http.client', 'HTTPConnection') + >>> HTTPServer = from_import('http.server', 'HTTPServer') + >>> urlopen, urlparse = from_import('urllib.request', 'urlopen', 'urlparse') + + Equivalent to this on Py3: + + >>> from module_name import symbol_names[0], symbol_names[1], ... + + and this on Py2: + + >>> from future.moves.module_name import symbol_names[0], ... + + or: + + >>> from future.backports.module_name import symbol_names[0], ... + + except that it also handles dotted module names such as ``http.client``. + """ + + if PY3: + return __import__(module_name) + else: + if 'backport' in kwargs and bool(kwargs['backport']): + prefix = 'future.backports' + else: + prefix = 'future.moves' + parts = prefix.split('.') + module_name.split('.') + module = importlib.import_module(prefix + '.' + module_name) + output = [getattr(module, name) for name in symbol_names] + if len(output) == 1: + return output[0] + else: + return output + + +class exclude_local_folder_imports(object): + """ + A context-manager that prevents standard library modules like configparser + from being imported from the local python-future source folder on Py3. + + (This was need prior to v0.16.0 because the presence of a configparser + folder would otherwise have prevented setuptools from running on Py3. Maybe + it's not needed any more?) + """ + def __init__(self, *args): + assert len(args) > 0 + self.module_names = args + # Disallow dotted module names like http.client: + if any(['.' in m for m in self.module_names]): + raise NotImplementedError('Dotted module names are not supported') + + def __enter__(self): + self.old_sys_path = copy.copy(sys.path) + self.old_sys_modules = copy.copy(sys.modules) + if sys.version_info[0] < 3: + return + # The presence of all these indicates we've found our source folder, + # because `builtins` won't have been installed in site-packages by setup.py: + FUTURE_SOURCE_SUBFOLDERS = ['future', 'past', 'libfuturize', 'libpasteurize', 'builtins'] + + # Look for the future source folder: + for folder in self.old_sys_path: + if all([os.path.exists(os.path.join(folder, subfolder)) + for subfolder in FUTURE_SOURCE_SUBFOLDERS]): + # Found it. Remove it. + sys.path.remove(folder) + + # Ensure we import the system module: + for m in self.module_names: + # Delete the module and any submodules from sys.modules: + # for key in list(sys.modules): + # if key == m or key.startswith(m + '.'): + # try: + # del sys.modules[key] + # except KeyError: + # pass + try: + module = __import__(m, level=0) + except ImportError: + # There's a problem importing the system module. E.g. the + # winreg module is not available except on Windows. + pass + + def __exit__(self, *args): + # Restore sys.path and sys.modules: + sys.path = self.old_sys_path + for m in set(self.old_sys_modules.keys()) - set(sys.modules.keys()): + sys.modules[m] = self.old_sys_modules[m] + +TOP_LEVEL_MODULES = ['builtins', + 'copyreg', + 'html', + 'http', + 'queue', + 'reprlib', + 'socketserver', + 'test', + 'tkinter', + 'winreg', + 'xmlrpc', + '_dummy_thread', + '_markupbase', + '_thread', + ] + +def import_top_level_modules(): + with exclude_local_folder_imports(*TOP_LEVEL_MODULES): + for m in TOP_LEVEL_MODULES: + try: + __import__(m) + except ImportError: # e.g. winreg + pass diff --git a/venv/lib/python3.10/site-packages/future/standard_library/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/standard_library/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f186161 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/standard_library/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/tests/__init__.py b/venv/lib/python3.10/site-packages/future/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/future/tests/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/tests/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..9df5e5b Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/tests/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/tests/__pycache__/base.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/tests/__pycache__/base.cpython-310.pyc new file mode 100644 index 0000000..e155cb5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/tests/__pycache__/base.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/tests/base.py b/venv/lib/python3.10/site-packages/future/tests/base.py new file mode 100644 index 0000000..4ef437b --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/tests/base.py @@ -0,0 +1,539 @@ +from __future__ import print_function, absolute_import +import os +import tempfile +import unittest +import sys +import re +import warnings +import io +from textwrap import dedent + +from future.utils import bind_method, PY26, PY3, PY2, PY27 +from future.moves.subprocess import check_output, STDOUT, CalledProcessError + +if PY26: + import unittest2 as unittest + + +def reformat_code(code): + """ + Removes any leading \n and dedents. + """ + if code.startswith('\n'): + code = code[1:] + return dedent(code) + + +def order_future_lines(code): + """ + Returns the code block with any ``__future__`` import lines sorted, and + then any ``future`` import lines sorted, then any ``builtins`` import lines + sorted. + + This only sorts the lines within the expected blocks. + + See test_order_future_lines() for an example. + """ + + # We need .splitlines(keepends=True), which doesn't exist on Py2, + # so we use this instead: + lines = code.split('\n') + + uufuture_line_numbers = [i for i, line in enumerate(lines) + if line.startswith('from __future__ import ')] + + future_line_numbers = [i for i, line in enumerate(lines) + if line.startswith('from future') + or line.startswith('from past')] + + builtins_line_numbers = [i for i, line in enumerate(lines) + if line.startswith('from builtins')] + + assert code.lstrip() == code, ('internal usage error: ' + 'dedent the code before calling order_future_lines()') + + def mymax(numbers): + return max(numbers) if len(numbers) > 0 else 0 + + def mymin(numbers): + return min(numbers) if len(numbers) > 0 else float('inf') + + assert mymax(uufuture_line_numbers) <= mymin(future_line_numbers), \ + 'the __future__ and future imports are out of order' + + # assert mymax(future_line_numbers) <= mymin(builtins_line_numbers), \ + # 'the future and builtins imports are out of order' + + uul = sorted([lines[i] for i in uufuture_line_numbers]) + sorted_uufuture_lines = dict(zip(uufuture_line_numbers, uul)) + + fl = sorted([lines[i] for i in future_line_numbers]) + sorted_future_lines = dict(zip(future_line_numbers, fl)) + + bl = sorted([lines[i] for i in builtins_line_numbers]) + sorted_builtins_lines = dict(zip(builtins_line_numbers, bl)) + + # Replace the old unsorted "from __future__ import ..." lines with the + # new sorted ones: + new_lines = [] + for i in range(len(lines)): + if i in uufuture_line_numbers: + new_lines.append(sorted_uufuture_lines[i]) + elif i in future_line_numbers: + new_lines.append(sorted_future_lines[i]) + elif i in builtins_line_numbers: + new_lines.append(sorted_builtins_lines[i]) + else: + new_lines.append(lines[i]) + return '\n'.join(new_lines) + + +class VerboseCalledProcessError(CalledProcessError): + """ + Like CalledProcessError, but it displays more information (message and + script output) for diagnosing test failures etc. + """ + def __init__(self, msg, returncode, cmd, output=None): + self.msg = msg + self.returncode = returncode + self.cmd = cmd + self.output = output + + def __str__(self): + return ("Command '%s' failed with exit status %d\nMessage: %s\nOutput: %s" + % (self.cmd, self.returncode, self.msg, self.output)) + +class FuturizeError(VerboseCalledProcessError): + pass + +class PasteurizeError(VerboseCalledProcessError): + pass + + +class CodeHandler(unittest.TestCase): + """ + Handy mixin for test classes for writing / reading / futurizing / + running .py files in the test suite. + """ + def setUp(self): + """ + The outputs from the various futurize stages should have the + following headers: + """ + # After stage1: + # TODO: use this form after implementing a fixer to consolidate + # __future__ imports into a single line: + # self.headers1 = """ + # from __future__ import absolute_import, division, print_function + # """ + self.headers1 = reformat_code(""" + from __future__ import absolute_import + from __future__ import division + from __future__ import print_function + """) + + # After stage2 --all-imports: + # TODO: use this form after implementing a fixer to consolidate + # __future__ imports into a single line: + # self.headers2 = """ + # from __future__ import (absolute_import, division, + # print_function, unicode_literals) + # from future import standard_library + # from future.builtins import * + # """ + self.headers2 = reformat_code(""" + from __future__ import absolute_import + from __future__ import division + from __future__ import print_function + from __future__ import unicode_literals + from future import standard_library + standard_library.install_aliases() + from builtins import * + """) + self.interpreters = [sys.executable] + self.tempdir = tempfile.mkdtemp() + os.path.sep + pypath = os.getenv('PYTHONPATH') + if pypath: + self.env = {'PYTHONPATH': os.getcwd() + os.pathsep + pypath} + else: + self.env = {'PYTHONPATH': os.getcwd()} + + def convert(self, code, stages=(1, 2), all_imports=False, from3=False, + reformat=True, run=True, conservative=False): + """ + Converts the code block using ``futurize`` and returns the + resulting code. + + Passing stages=[1] or stages=[2] passes the flag ``--stage1`` or + ``stage2`` to ``futurize``. Passing both stages runs ``futurize`` + with both stages by default. + + If from3 is False, runs ``futurize``, converting from Python 2 to + both 2 and 3. If from3 is True, runs ``pasteurize`` to convert + from Python 3 to both 2 and 3. + + Optionally reformats the code block first using the reformat() function. + + If run is True, runs the resulting code under all Python + interpreters in self.interpreters. + """ + if reformat: + code = reformat_code(code) + self._write_test_script(code) + self._futurize_test_script(stages=stages, all_imports=all_imports, + from3=from3, conservative=conservative) + output = self._read_test_script() + if run: + for interpreter in self.interpreters: + _ = self._run_test_script(interpreter=interpreter) + return output + + def compare(self, output, expected, ignore_imports=True): + """ + Compares whether the code blocks are equal. If not, raises an + exception so the test fails. Ignores any trailing whitespace like + blank lines. + + If ignore_imports is True, passes the code blocks into the + strip_future_imports method. + + If one code block is a unicode string and the other a + byte-string, it assumes the byte-string is encoded as utf-8. + """ + if ignore_imports: + output = self.strip_future_imports(output) + expected = self.strip_future_imports(expected) + if isinstance(output, bytes) and not isinstance(expected, bytes): + output = output.decode('utf-8') + if isinstance(expected, bytes) and not isinstance(output, bytes): + expected = expected.decode('utf-8') + self.assertEqual(order_future_lines(output.rstrip()), + expected.rstrip()) + + def strip_future_imports(self, code): + """ + Strips any of these import lines: + + from __future__ import + from future + from future. + from builtins + + or any line containing: + install_hooks() + or: + install_aliases() + + Limitation: doesn't handle imports split across multiple lines like + this: + + from __future__ import (absolute_import, division, print_function, + unicode_literals) + """ + output = [] + # We need .splitlines(keepends=True), which doesn't exist on Py2, + # so we use this instead: + for line in code.split('\n'): + if not (line.startswith('from __future__ import ') + or line.startswith('from future ') + or line.startswith('from builtins ') + or 'install_hooks()' in line + or 'install_aliases()' in line + # but don't match "from future_builtins" :) + or line.startswith('from future.')): + output.append(line) + return '\n'.join(output) + + def convert_check(self, before, expected, stages=(1, 2), all_imports=False, + ignore_imports=True, from3=False, run=True, + conservative=False): + """ + Convenience method that calls convert() and compare(). + + Reformats the code blocks automatically using the reformat_code() + function. + + If all_imports is passed, we add the appropriate import headers + for the stage(s) selected to the ``expected`` code-block, so they + needn't appear repeatedly in the test code. + + If ignore_imports is True, ignores the presence of any lines + beginning: + + from __future__ import ... + from future import ... + + for the purpose of the comparison. + """ + output = self.convert(before, stages=stages, all_imports=all_imports, + from3=from3, run=run, conservative=conservative) + if all_imports: + headers = self.headers2 if 2 in stages else self.headers1 + else: + headers = '' + + reformatted = reformat_code(expected) + if headers in reformatted: + headers = '' + + self.compare(output, headers + reformatted, + ignore_imports=ignore_imports) + + def unchanged(self, code, **kwargs): + """ + Convenience method to ensure the code is unchanged by the + futurize process. + """ + self.convert_check(code, code, **kwargs) + + def _write_test_script(self, code, filename='mytestscript.py'): + """ + Dedents the given code (a multiline string) and writes it out to + a file in a temporary folder like /tmp/tmpUDCn7x/mytestscript.py. + """ + if isinstance(code, bytes): + code = code.decode('utf-8') + # Be explicit about encoding the temp file as UTF-8 (issue #63): + with io.open(self.tempdir + filename, 'wt', encoding='utf-8') as f: + f.write(dedent(code)) + + def _read_test_script(self, filename='mytestscript.py'): + with io.open(self.tempdir + filename, 'rt', encoding='utf-8') as f: + newsource = f.read() + return newsource + + def _futurize_test_script(self, filename='mytestscript.py', stages=(1, 2), + all_imports=False, from3=False, + conservative=False): + params = [] + stages = list(stages) + if all_imports: + params.append('--all-imports') + if from3: + script = 'pasteurize.py' + else: + script = 'futurize.py' + if stages == [1]: + params.append('--stage1') + elif stages == [2]: + params.append('--stage2') + else: + assert stages == [1, 2] + if conservative: + params.append('--conservative') + # No extra params needed + + # Absolute file path: + fn = self.tempdir + filename + call_args = [sys.executable, script] + params + ['-w', fn] + try: + output = check_output(call_args, stderr=STDOUT, env=self.env) + except CalledProcessError as e: + with open(fn) as f: + msg = ( + 'Error running the command %s\n' + '%s\n' + 'Contents of file %s:\n' + '\n' + '%s') % ( + ' '.join(call_args), + 'env=%s' % self.env, + fn, + '----\n%s\n----' % f.read(), + ) + ErrorClass = (FuturizeError if 'futurize' in script else PasteurizeError) + + if not hasattr(e, 'output'): + # The attribute CalledProcessError.output doesn't exist on Py2.6 + e.output = None + raise ErrorClass(msg, e.returncode, e.cmd, output=e.output) + return output + + def _run_test_script(self, filename='mytestscript.py', + interpreter=sys.executable): + # Absolute file path: + fn = self.tempdir + filename + try: + output = check_output([interpreter, fn], + env=self.env, stderr=STDOUT) + except CalledProcessError as e: + with open(fn) as f: + msg = ( + 'Error running the command %s\n' + '%s\n' + 'Contents of file %s:\n' + '\n' + '%s') % ( + ' '.join([interpreter, fn]), + 'env=%s' % self.env, + fn, + '----\n%s\n----' % f.read(), + ) + if not hasattr(e, 'output'): + # The attribute CalledProcessError.output doesn't exist on Py2.6 + e.output = None + raise VerboseCalledProcessError(msg, e.returncode, e.cmd, output=e.output) + return output + + +# Decorator to skip some tests on Python 2.6 ... +skip26 = unittest.skipIf(PY26, "this test is known to fail on Py2.6") + + +def expectedFailurePY3(func): + if not PY3: + return func + return unittest.expectedFailure(func) + +def expectedFailurePY26(func): + if not PY26: + return func + return unittest.expectedFailure(func) + + +def expectedFailurePY27(func): + if not PY27: + return func + return unittest.expectedFailure(func) + + +def expectedFailurePY2(func): + if not PY2: + return func + return unittest.expectedFailure(func) + + +# Renamed in Py3.3: +if not hasattr(unittest.TestCase, 'assertRaisesRegex'): + unittest.TestCase.assertRaisesRegex = unittest.TestCase.assertRaisesRegexp + +# From Py3.3: +def assertRegex(self, text, expected_regex, msg=None): + """Fail the test unless the text matches the regular expression.""" + if isinstance(expected_regex, (str, unicode)): + assert expected_regex, "expected_regex must not be empty." + expected_regex = re.compile(expected_regex) + if not expected_regex.search(text): + msg = msg or "Regex didn't match" + msg = '%s: %r not found in %r' % (msg, expected_regex.pattern, text) + raise self.failureException(msg) + +if not hasattr(unittest.TestCase, 'assertRegex'): + bind_method(unittest.TestCase, 'assertRegex', assertRegex) + +class _AssertRaisesBaseContext(object): + + def __init__(self, expected, test_case, callable_obj=None, + expected_regex=None): + self.expected = expected + self.test_case = test_case + if callable_obj is not None: + try: + self.obj_name = callable_obj.__name__ + except AttributeError: + self.obj_name = str(callable_obj) + else: + self.obj_name = None + if isinstance(expected_regex, (bytes, str)): + expected_regex = re.compile(expected_regex) + self.expected_regex = expected_regex + self.msg = None + + def _raiseFailure(self, standardMsg): + msg = self.test_case._formatMessage(self.msg, standardMsg) + raise self.test_case.failureException(msg) + + def handle(self, name, callable_obj, args, kwargs): + """ + If callable_obj is None, assertRaises/Warns is being used as a + context manager, so check for a 'msg' kwarg and return self. + If callable_obj is not None, call it passing args and kwargs. + """ + if callable_obj is None: + self.msg = kwargs.pop('msg', None) + return self + with self: + callable_obj(*args, **kwargs) + +class _AssertWarnsContext(_AssertRaisesBaseContext): + """A context manager used to implement TestCase.assertWarns* methods.""" + + def __enter__(self): + # The __warningregistry__'s need to be in a pristine state for tests + # to work properly. + for v in sys.modules.values(): + if getattr(v, '__warningregistry__', None): + v.__warningregistry__ = {} + self.warnings_manager = warnings.catch_warnings(record=True) + self.warnings = self.warnings_manager.__enter__() + warnings.simplefilter("always", self.expected) + return self + + def __exit__(self, exc_type, exc_value, tb): + self.warnings_manager.__exit__(exc_type, exc_value, tb) + if exc_type is not None: + # let unexpected exceptions pass through + return + try: + exc_name = self.expected.__name__ + except AttributeError: + exc_name = str(self.expected) + first_matching = None + for m in self.warnings: + w = m.message + if not isinstance(w, self.expected): + continue + if first_matching is None: + first_matching = w + if (self.expected_regex is not None and + not self.expected_regex.search(str(w))): + continue + # store warning for later retrieval + self.warning = w + self.filename = m.filename + self.lineno = m.lineno + return + # Now we simply try to choose a helpful failure message + if first_matching is not None: + self._raiseFailure('"{}" does not match "{}"'.format( + self.expected_regex.pattern, str(first_matching))) + if self.obj_name: + self._raiseFailure("{} not triggered by {}".format(exc_name, + self.obj_name)) + else: + self._raiseFailure("{} not triggered".format(exc_name)) + + +def assertWarns(self, expected_warning, callable_obj=None, *args, **kwargs): + """Fail unless a warning of class warnClass is triggered + by callable_obj when invoked with arguments args and keyword + arguments kwargs. If a different type of warning is + triggered, it will not be handled: depending on the other + warning filtering rules in effect, it might be silenced, printed + out, or raised as an exception. + + If called with callable_obj omitted or None, will return a + context object used like this:: + + with self.assertWarns(SomeWarning): + do_something() + + An optional keyword argument 'msg' can be provided when assertWarns + is used as a context object. + + The context manager keeps a reference to the first matching + warning as the 'warning' attribute; similarly, the 'filename' + and 'lineno' attributes give you information about the line + of Python code from which the warning was triggered. + This allows you to inspect the warning after the assertion:: + + with self.assertWarns(SomeWarning) as cm: + do_something() + the_warning = cm.warning + self.assertEqual(the_warning.some_attribute, 147) + """ + context = _AssertWarnsContext(expected_warning, self, callable_obj) + return context.handle('assertWarns', callable_obj, args, kwargs) + +if not hasattr(unittest.TestCase, 'assertWarns'): + bind_method(unittest.TestCase, 'assertWarns', assertWarns) diff --git a/venv/lib/python3.10/site-packages/future/types/__init__.py b/venv/lib/python3.10/site-packages/future/types/__init__.py new file mode 100644 index 0000000..0625077 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/types/__init__.py @@ -0,0 +1,257 @@ +""" +This module contains backports the data types that were significantly changed +in the transition from Python 2 to Python 3. + +- an implementation of Python 3's bytes object (pure Python subclass of + Python 2's builtin 8-bit str type) +- an implementation of Python 3's str object (pure Python subclass of + Python 2's builtin unicode type) +- a backport of the range iterator from Py3 with slicing support + +It is used as follows:: + + from __future__ import division, absolute_import, print_function + from builtins import bytes, dict, int, range, str + +to bring in the new semantics for these functions from Python 3. And +then, for example:: + + b = bytes(b'ABCD') + assert list(b) == [65, 66, 67, 68] + assert repr(b) == "b'ABCD'" + assert [65, 66] in b + + # These raise TypeErrors: + # b + u'EFGH' + # b.split(u'B') + # bytes(b',').join([u'Fred', u'Bill']) + + + s = str(u'ABCD') + + # These raise TypeErrors: + # s.join([b'Fred', b'Bill']) + # s.startswith(b'A') + # b'B' in s + # s.find(b'A') + # s.replace(u'A', b'a') + + # This raises an AttributeError: + # s.decode('utf-8') + + assert repr(s) == 'ABCD' # consistent repr with Py3 (no u prefix) + + + for i in range(10**11)[:10]: + pass + +and:: + + class VerboseList(list): + def append(self, item): + print('Adding an item') + super().append(item) # new simpler super() function + +For more information: +--------------------- + +- future.types.newbytes +- future.types.newdict +- future.types.newint +- future.types.newobject +- future.types.newrange +- future.types.newstr + + +Notes +===== + +range() +------- +``range`` is a custom class that backports the slicing behaviour from +Python 3 (based on the ``xrange`` module by Dan Crosta). See the +``newrange`` module docstring for more details. + + +super() +------- +``super()`` is based on Ryan Kelly's ``magicsuper`` module. See the +``newsuper`` module docstring for more details. + + +round() +------- +Python 3 modifies the behaviour of ``round()`` to use "Banker's Rounding". +See http://stackoverflow.com/a/10825998. See the ``newround`` module +docstring for more details. + +""" + +from __future__ import absolute_import, division, print_function + +import functools +from numbers import Integral + +from future import utils + + +# Some utility functions to enforce strict type-separation of unicode str and +# bytes: +def disallow_types(argnums, disallowed_types): + """ + A decorator that raises a TypeError if any of the given numbered + arguments is of the corresponding given type (e.g. bytes or unicode + string). + + For example: + + @disallow_types([0, 1], [unicode, bytes]) + def f(a, b): + pass + + raises a TypeError when f is called if a unicode object is passed as + `a` or a bytes object is passed as `b`. + + This also skips over keyword arguments, so + + @disallow_types([0, 1], [unicode, bytes]) + def g(a, b=None): + pass + + doesn't raise an exception if g is called with only one argument a, + e.g.: + + g(b'Byte string') + + Example use: + + >>> class newbytes(object): + ... @disallow_types([1], [unicode]) + ... def __add__(self, other): + ... pass + + >>> newbytes('1234') + u'1234' #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError: can't concat 'bytes' to (unicode) str + """ + + def decorator(function): + + @functools.wraps(function) + def wrapper(*args, **kwargs): + # These imports are just for this decorator, and are defined here + # to prevent circular imports: + from .newbytes import newbytes + from .newint import newint + from .newstr import newstr + + errmsg = "argument can't be {0}" + for (argnum, mytype) in zip(argnums, disallowed_types): + # Handle the case where the type is passed as a string like 'newbytes'. + if isinstance(mytype, str) or isinstance(mytype, bytes): + mytype = locals()[mytype] + + # Only restrict kw args only if they are passed: + if len(args) <= argnum: + break + + # Here we use type() rather than isinstance() because + # __instancecheck__ is being overridden. E.g. + # isinstance(b'abc', newbytes) is True on Py2. + if type(args[argnum]) == mytype: + raise TypeError(errmsg.format(mytype)) + + return function(*args, **kwargs) + return wrapper + return decorator + + +def no(mytype, argnums=(1,)): + """ + A shortcut for the disallow_types decorator that disallows only one type + (in any position in argnums). + + Example use: + + >>> class newstr(object): + ... @no('bytes') + ... def __add__(self, other): + ... pass + + >>> newstr(u'1234') + b'1234' #doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + TypeError: argument can't be bytes + + The object can also be passed directly, but passing the string helps + to prevent circular import problems. + """ + if isinstance(argnums, Integral): + argnums = (argnums,) + disallowed_types = [mytype] * len(argnums) + return disallow_types(argnums, disallowed_types) + + +def issubset(list1, list2): + """ + Examples: + + >>> issubset([], [65, 66, 67]) + True + >>> issubset([65], [65, 66, 67]) + True + >>> issubset([65, 66], [65, 66, 67]) + True + >>> issubset([65, 67], [65, 66, 67]) + False + """ + n = len(list1) + for startpos in range(len(list2) - n + 1): + if list2[startpos:startpos+n] == list1: + return True + return False + + +if utils.PY3: + import builtins + bytes = builtins.bytes + dict = builtins.dict + int = builtins.int + list = builtins.list + object = builtins.object + range = builtins.range + str = builtins.str + + # The identity mapping + newtypes = {bytes: bytes, + dict: dict, + int: int, + list: list, + object: object, + range: range, + str: str} + + __all__ = ['newtypes'] + +else: + + from .newbytes import newbytes + from .newdict import newdict + from .newint import newint + from .newlist import newlist + from .newrange import newrange + from .newobject import newobject + from .newstr import newstr + + newtypes = {bytes: newbytes, + dict: newdict, + int: newint, + long: newint, + list: newlist, + object: newobject, + range: newrange, + str: newbytes, + unicode: newstr} + + __all__ = ['newbytes', 'newdict', 'newint', 'newlist', 'newrange', 'newstr', 'newtypes'] diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..326faa9 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/newbytes.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/newbytes.cpython-310.pyc new file mode 100644 index 0000000..ac88cf0 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/newbytes.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/newdict.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/newdict.cpython-310.pyc new file mode 100644 index 0000000..7abf1dd Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/newdict.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/newint.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/newint.cpython-310.pyc new file mode 100644 index 0000000..7e0f47a Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/newint.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/newlist.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/newlist.cpython-310.pyc new file mode 100644 index 0000000..3b93535 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/newlist.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/newmemoryview.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/newmemoryview.cpython-310.pyc new file mode 100644 index 0000000..32baa06 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/newmemoryview.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/newobject.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/newobject.cpython-310.pyc new file mode 100644 index 0000000..2b75c75 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/newobject.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/newopen.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/newopen.cpython-310.pyc new file mode 100644 index 0000000..80c67c7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/newopen.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/newrange.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/newrange.cpython-310.pyc new file mode 100644 index 0000000..aa19a30 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/newrange.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/__pycache__/newstr.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/types/__pycache__/newstr.cpython-310.pyc new file mode 100644 index 0000000..0ea1c59 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/types/__pycache__/newstr.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/types/newbytes.py b/venv/lib/python3.10/site-packages/future/types/newbytes.py new file mode 100644 index 0000000..c9d584a --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/types/newbytes.py @@ -0,0 +1,460 @@ +""" +Pure-Python implementation of a Python 3-like bytes object for Python 2. + +Why do this? Without it, the Python 2 bytes object is a very, very +different beast to the Python 3 bytes object. +""" + +from numbers import Integral +import string +import copy + +from future.utils import istext, isbytes, PY2, PY3, with_metaclass +from future.types import no, issubset +from future.types.newobject import newobject + +if PY2: + from collections import Iterable +else: + from collections.abc import Iterable + + +_builtin_bytes = bytes + +if PY3: + # We'll probably never use newstr on Py3 anyway... + unicode = str + + +class BaseNewBytes(type): + def __instancecheck__(cls, instance): + if cls == newbytes: + return isinstance(instance, _builtin_bytes) + else: + return issubclass(instance.__class__, cls) + + +def _newchr(x): + if isinstance(x, str): # this happens on pypy + return x.encode('ascii') + else: + return chr(x) + + +class newbytes(with_metaclass(BaseNewBytes, _builtin_bytes)): + """ + A backport of the Python 3 bytes object to Py2 + """ + def __new__(cls, *args, **kwargs): + """ + From the Py3 bytes docstring: + + bytes(iterable_of_ints) -> bytes + bytes(string, encoding[, errors]) -> bytes + bytes(bytes_or_buffer) -> immutable copy of bytes_or_buffer + bytes(int) -> bytes object of size given by the parameter initialized with null bytes + bytes() -> empty bytes object + + Construct an immutable array of bytes from: + - an iterable yielding integers in range(256) + - a text string encoded using the specified encoding + - any object implementing the buffer API. + - an integer + """ + + encoding = None + errors = None + + if len(args) == 0: + return super(newbytes, cls).__new__(cls) + elif len(args) >= 2: + args = list(args) + if len(args) == 3: + errors = args.pop() + encoding=args.pop() + # Was: elif isinstance(args[0], newbytes): + # We use type() instead of the above because we're redefining + # this to be True for all unicode string subclasses. Warning: + # This may render newstr un-subclassable. + if type(args[0]) == newbytes: + # Special-case: for consistency with Py3.3, we return the same object + # (with the same id) if a newbytes object is passed into the + # newbytes constructor. + return args[0] + elif isinstance(args[0], _builtin_bytes): + value = args[0] + elif isinstance(args[0], unicode): + try: + if 'encoding' in kwargs: + assert encoding is None + encoding = kwargs['encoding'] + if 'errors' in kwargs: + assert errors is None + errors = kwargs['errors'] + except AssertionError: + raise TypeError('Argument given by name and position') + if encoding is None: + raise TypeError('unicode string argument without an encoding') + ### + # Was: value = args[0].encode(**kwargs) + # Python 2.6 string encode() method doesn't take kwargs: + # Use this instead: + newargs = [encoding] + if errors is not None: + newargs.append(errors) + value = args[0].encode(*newargs) + ### + elif hasattr(args[0], '__bytes__'): + value = args[0].__bytes__() + elif isinstance(args[0], Iterable): + if len(args[0]) == 0: + # This could be an empty list or tuple. Return b'' as on Py3. + value = b'' + else: + # Was: elif len(args[0])>0 and isinstance(args[0][0], Integral): + # # It's a list of integers + # But then we can't index into e.g. frozensets. Try to proceed + # anyway. + try: + value = bytearray([_newchr(x) for x in args[0]]) + except: + raise ValueError('bytes must be in range(0, 256)') + elif isinstance(args[0], Integral): + if args[0] < 0: + raise ValueError('negative count') + value = b'\x00' * args[0] + else: + value = args[0] + if type(value) == newbytes: + # Above we use type(...) rather than isinstance(...) because the + # newbytes metaclass overrides __instancecheck__. + # oldbytes(value) gives the wrong thing on Py2: the same + # result as str(value) on Py3, e.g. "b'abc'". (Issue #193). + # So we handle this case separately: + return copy.copy(value) + else: + return super(newbytes, cls).__new__(cls, value) + + def __repr__(self): + return 'b' + super(newbytes, self).__repr__() + + def __str__(self): + return 'b' + "'{0}'".format(super(newbytes, self).__str__()) + + def __getitem__(self, y): + value = super(newbytes, self).__getitem__(y) + if isinstance(y, Integral): + return ord(value) + else: + return newbytes(value) + + def __getslice__(self, *args): + return self.__getitem__(slice(*args)) + + def __contains__(self, key): + if isinstance(key, int): + newbyteskey = newbytes([key]) + # Don't use isinstance() here because we only want to catch + # newbytes, not Python 2 str: + elif type(key) == newbytes: + newbyteskey = key + else: + newbyteskey = newbytes(key) + return issubset(list(newbyteskey), list(self)) + + @no(unicode) + def __add__(self, other): + return newbytes(super(newbytes, self).__add__(other)) + + @no(unicode) + def __radd__(self, left): + return newbytes(left) + self + + @no(unicode) + def __mul__(self, other): + return newbytes(super(newbytes, self).__mul__(other)) + + @no(unicode) + def __rmul__(self, other): + return newbytes(super(newbytes, self).__rmul__(other)) + + def __mod__(self, vals): + if isinstance(vals, newbytes): + vals = _builtin_bytes.__str__(vals) + + elif isinstance(vals, tuple): + newvals = [] + for v in vals: + if isinstance(v, newbytes): + v = _builtin_bytes.__str__(v) + newvals.append(v) + vals = tuple(newvals) + + elif (hasattr(vals.__class__, '__getitem__') and + hasattr(vals.__class__, 'iteritems')): + for k, v in vals.iteritems(): + if isinstance(v, newbytes): + vals[k] = _builtin_bytes.__str__(v) + + return _builtin_bytes.__mod__(self, vals) + + def __imod__(self, other): + return self.__mod__(other) + + def join(self, iterable_of_bytes): + errmsg = 'sequence item {0}: expected bytes, {1} found' + if isbytes(iterable_of_bytes) or istext(iterable_of_bytes): + raise TypeError(errmsg.format(0, type(iterable_of_bytes))) + for i, item in enumerate(iterable_of_bytes): + if istext(item): + raise TypeError(errmsg.format(i, type(item))) + return newbytes(super(newbytes, self).join(iterable_of_bytes)) + + @classmethod + def fromhex(cls, string): + # Only on Py2: + return cls(string.replace(' ', '').decode('hex')) + + @no(unicode) + def find(self, sub, *args): + return super(newbytes, self).find(sub, *args) + + @no(unicode) + def rfind(self, sub, *args): + return super(newbytes, self).rfind(sub, *args) + + @no(unicode, (1, 2)) + def replace(self, old, new, *args): + return newbytes(super(newbytes, self).replace(old, new, *args)) + + def encode(self, *args): + raise AttributeError("encode method has been disabled in newbytes") + + def decode(self, encoding='utf-8', errors='strict'): + """ + Returns a newstr (i.e. unicode subclass) + + Decode B using the codec registered for encoding. Default encoding + is 'utf-8'. errors may be given to set a different error + handling scheme. Default is 'strict' meaning that encoding errors raise + a UnicodeDecodeError. Other possible values are 'ignore' and 'replace' + as well as any other name registered with codecs.register_error that is + able to handle UnicodeDecodeErrors. + """ + # Py2 str.encode() takes encoding and errors as optional parameter, + # not keyword arguments as in Python 3 str. + + from future.types.newstr import newstr + + if errors == 'surrogateescape': + from future.utils.surrogateescape import register_surrogateescape + register_surrogateescape() + + return newstr(super(newbytes, self).decode(encoding, errors)) + + # This is currently broken: + # # We implement surrogateescape error handling here in addition rather + # # than relying on the custom error handler from + # # future.utils.surrogateescape to be registered globally, even though + # # that is fine in the case of decoding. (But not encoding: see the + # # comments in newstr.encode()``.) + # + # if errors == 'surrogateescape': + # # Decode char by char + # mybytes = [] + # for code in self: + # # Code is an int + # if 0x80 <= code <= 0xFF: + # b = 0xDC00 + code + # elif code <= 0x7F: + # b = _unichr(c).decode(encoding=encoding) + # else: + # # # It may be a bad byte + # # FIXME: What to do in this case? See the Py3 docs / tests. + # # # Try swallowing it. + # # continue + # # print("RAISE!") + # raise NotASurrogateError + # mybytes.append(b) + # return newbytes(mybytes) + # return newbytes(super(newstr, self).decode(encoding, errors)) + + @no(unicode) + def startswith(self, prefix, *args): + return super(newbytes, self).startswith(prefix, *args) + + @no(unicode) + def endswith(self, prefix, *args): + return super(newbytes, self).endswith(prefix, *args) + + @no(unicode) + def split(self, sep=None, maxsplit=-1): + # Py2 str.split() takes maxsplit as an optional parameter, not as a + # keyword argument as in Python 3 bytes. + parts = super(newbytes, self).split(sep, maxsplit) + return [newbytes(part) for part in parts] + + def splitlines(self, keepends=False): + """ + B.splitlines([keepends]) -> list of lines + + Return a list of the lines in B, breaking at line boundaries. + Line breaks are not included in the resulting list unless keepends + is given and true. + """ + # Py2 str.splitlines() takes keepends as an optional parameter, + # not as a keyword argument as in Python 3 bytes. + parts = super(newbytes, self).splitlines(keepends) + return [newbytes(part) for part in parts] + + @no(unicode) + def rsplit(self, sep=None, maxsplit=-1): + # Py2 str.rsplit() takes maxsplit as an optional parameter, not as a + # keyword argument as in Python 3 bytes. + parts = super(newbytes, self).rsplit(sep, maxsplit) + return [newbytes(part) for part in parts] + + @no(unicode) + def partition(self, sep): + parts = super(newbytes, self).partition(sep) + return tuple(newbytes(part) for part in parts) + + @no(unicode) + def rpartition(self, sep): + parts = super(newbytes, self).rpartition(sep) + return tuple(newbytes(part) for part in parts) + + @no(unicode, (1,)) + def rindex(self, sub, *args): + ''' + S.rindex(sub [,start [,end]]) -> int + + Like S.rfind() but raise ValueError when the substring is not found. + ''' + pos = self.rfind(sub, *args) + if pos == -1: + raise ValueError('substring not found') + + @no(unicode) + def index(self, sub, *args): + ''' + Returns index of sub in bytes. + Raises ValueError if byte is not in bytes and TypeError if can't + be converted bytes or its length is not 1. + ''' + if isinstance(sub, int): + if len(args) == 0: + start, end = 0, len(self) + elif len(args) == 1: + start = args[0] + elif len(args) == 2: + start, end = args + else: + raise TypeError('takes at most 3 arguments') + return list(self)[start:end].index(sub) + if not isinstance(sub, bytes): + try: + sub = self.__class__(sub) + except (TypeError, ValueError): + raise TypeError("can't convert sub to bytes") + try: + return super(newbytes, self).index(sub, *args) + except ValueError: + raise ValueError('substring not found') + + def __eq__(self, other): + if isinstance(other, (_builtin_bytes, bytearray)): + return super(newbytes, self).__eq__(other) + else: + return False + + def __ne__(self, other): + if isinstance(other, _builtin_bytes): + return super(newbytes, self).__ne__(other) + else: + return True + + unorderable_err = 'unorderable types: bytes() and {0}' + + def __lt__(self, other): + if isinstance(other, _builtin_bytes): + return super(newbytes, self).__lt__(other) + raise TypeError(self.unorderable_err.format(type(other))) + + def __le__(self, other): + if isinstance(other, _builtin_bytes): + return super(newbytes, self).__le__(other) + raise TypeError(self.unorderable_err.format(type(other))) + + def __gt__(self, other): + if isinstance(other, _builtin_bytes): + return super(newbytes, self).__gt__(other) + raise TypeError(self.unorderable_err.format(type(other))) + + def __ge__(self, other): + if isinstance(other, _builtin_bytes): + return super(newbytes, self).__ge__(other) + raise TypeError(self.unorderable_err.format(type(other))) + + def __native__(self): + # We can't just feed a newbytes object into str(), because + # newbytes.__str__() returns e.g. "b'blah'", consistent with Py3 bytes. + return super(newbytes, self).__str__() + + def __getattribute__(self, name): + """ + A trick to cause the ``hasattr`` builtin-fn to return False for + the 'encode' method on Py2. + """ + if name in ['encode', u'encode']: + raise AttributeError("encode method has been disabled in newbytes") + return super(newbytes, self).__getattribute__(name) + + @no(unicode) + def rstrip(self, bytes_to_strip=None): + """ + Strip trailing bytes contained in the argument. + If the argument is omitted, strip trailing ASCII whitespace. + """ + return newbytes(super(newbytes, self).rstrip(bytes_to_strip)) + + @no(unicode) + def strip(self, bytes_to_strip=None): + """ + Strip leading and trailing bytes contained in the argument. + If the argument is omitted, strip trailing ASCII whitespace. + """ + return newbytes(super(newbytes, self).strip(bytes_to_strip)) + + def lower(self): + """ + b.lower() -> copy of b + + Return a copy of b with all ASCII characters converted to lowercase. + """ + return newbytes(super(newbytes, self).lower()) + + @no(unicode) + def upper(self): + """ + b.upper() -> copy of b + + Return a copy of b with all ASCII characters converted to uppercase. + """ + return newbytes(super(newbytes, self).upper()) + + @classmethod + @no(unicode) + def maketrans(cls, frm, to): + """ + B.maketrans(frm, to) -> translation table + + Return a translation table (a bytes object of length 256) suitable + for use in the bytes or bytearray translate method where each byte + in frm is mapped to the byte at the same position in to. + The bytes objects frm and to must be of the same length. + """ + return newbytes(string.maketrans(frm, to)) + + +__all__ = ['newbytes'] diff --git a/venv/lib/python3.10/site-packages/future/types/newdict.py b/venv/lib/python3.10/site-packages/future/types/newdict.py new file mode 100644 index 0000000..d90316c --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/types/newdict.py @@ -0,0 +1,76 @@ +""" +A dict subclass for Python 2 that behaves like Python 3's dict + +Example use: + +>>> from builtins import dict +>>> d1 = dict() # instead of {} for an empty dict +>>> d2 = dict(key1='value1', key2='value2') + +The keys, values and items methods now return iterators on Python 2.x +(with set-like behaviour on Python 2.7). + +>>> for d in (d1, d2): +... assert not isinstance(d.keys(), list) +... assert not isinstance(d.values(), list) +... assert not isinstance(d.items(), list) +""" + +import sys + +from future.utils import with_metaclass +from future.types.newobject import newobject + + +_builtin_dict = dict +ver = sys.version_info + + +class BaseNewDict(type): + def __instancecheck__(cls, instance): + if cls == newdict: + return isinstance(instance, _builtin_dict) + else: + return issubclass(instance.__class__, cls) + + +class newdict(with_metaclass(BaseNewDict, _builtin_dict)): + """ + A backport of the Python 3 dict object to Py2 + """ + + if ver >= (3,): + # Inherit items, keys and values from `dict` in 3.x + pass + elif ver >= (2, 7): + items = dict.viewitems + keys = dict.viewkeys + values = dict.viewvalues + else: + items = dict.iteritems + keys = dict.iterkeys + values = dict.itervalues + + def __new__(cls, *args, **kwargs): + """ + dict() -> new empty dictionary + dict(mapping) -> new dictionary initialized from a mapping object's + (key, value) pairs + dict(iterable) -> new dictionary initialized as if via: + d = {} + for k, v in iterable: + d[k] = v + dict(**kwargs) -> new dictionary initialized with the name=value pairs + in the keyword argument list. For example: dict(one=1, two=2) + """ + + return super(newdict, cls).__new__(cls, *args) + + def __native__(self): + """ + Hook for the future.utils.native() function + """ + return dict(self) + + +__all__ = ['newdict'] diff --git a/venv/lib/python3.10/site-packages/future/types/newint.py b/venv/lib/python3.10/site-packages/future/types/newint.py new file mode 100644 index 0000000..ebc5715 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/types/newint.py @@ -0,0 +1,386 @@ +""" +Backport of Python 3's int, based on Py2's long. + +They are very similar. The most notable difference is: + +- representation: trailing L in Python 2 removed in Python 3 +""" +from __future__ import division + +import struct + +from future.types.newbytes import newbytes +from future.types.newobject import newobject +from future.utils import PY3, isint, istext, isbytes, with_metaclass, native + + +if PY3: + long = int + from collections.abc import Iterable +else: + from collections import Iterable + + +class BaseNewInt(type): + def __instancecheck__(cls, instance): + if cls == newint: + # Special case for Py2 short or long int + return isinstance(instance, (int, long)) + else: + return issubclass(instance.__class__, cls) + + +class newint(with_metaclass(BaseNewInt, long)): + """ + A backport of the Python 3 int object to Py2 + """ + def __new__(cls, x=0, base=10): + """ + From the Py3 int docstring: + + | int(x=0) -> integer + | int(x, base=10) -> integer + | + | Convert a number or string to an integer, or return 0 if no + | arguments are given. If x is a number, return x.__int__(). For + | floating point numbers, this truncates towards zero. + | + | If x is not a number or if base is given, then x must be a string, + | bytes, or bytearray instance representing an integer literal in the + | given base. The literal can be preceded by '+' or '-' and be + | surrounded by whitespace. The base defaults to 10. Valid bases are + | 0 and 2-36. Base 0 means to interpret the base from the string as an + | integer literal. + | >>> int('0b100', base=0) + | 4 + + """ + try: + val = x.__int__() + except AttributeError: + val = x + else: + if not isint(val): + raise TypeError('__int__ returned non-int ({0})'.format( + type(val))) + + if base != 10: + # Explicit base + if not (istext(val) or isbytes(val) or isinstance(val, bytearray)): + raise TypeError( + "int() can't convert non-string with explicit base") + try: + return super(newint, cls).__new__(cls, val, base) + except TypeError: + return super(newint, cls).__new__(cls, newbytes(val), base) + # After here, base is 10 + try: + return super(newint, cls).__new__(cls, val) + except TypeError: + # Py2 long doesn't handle bytearray input with an explicit base, so + # handle this here. + # Py3: int(bytearray(b'10'), 2) == 2 + # Py2: int(bytearray(b'10'), 2) == 2 raises TypeError + # Py2: long(bytearray(b'10'), 2) == 2 raises TypeError + try: + return super(newint, cls).__new__(cls, newbytes(val)) + except: + raise TypeError("newint argument must be a string or a number," + "not '{0}'".format(type(val))) + + def __repr__(self): + """ + Without the L suffix + """ + value = super(newint, self).__repr__() + assert value[-1] == 'L' + return value[:-1] + + def __add__(self, other): + value = super(newint, self).__add__(other) + if value is NotImplemented: + return long(self) + other + return newint(value) + + def __radd__(self, other): + value = super(newint, self).__radd__(other) + if value is NotImplemented: + return other + long(self) + return newint(value) + + def __sub__(self, other): + value = super(newint, self).__sub__(other) + if value is NotImplemented: + return long(self) - other + return newint(value) + + def __rsub__(self, other): + value = super(newint, self).__rsub__(other) + if value is NotImplemented: + return other - long(self) + return newint(value) + + def __mul__(self, other): + value = super(newint, self).__mul__(other) + if isint(value): + return newint(value) + elif value is NotImplemented: + return long(self) * other + return value + + def __rmul__(self, other): + value = super(newint, self).__rmul__(other) + if isint(value): + return newint(value) + elif value is NotImplemented: + return other * long(self) + return value + + def __div__(self, other): + # We override this rather than e.g. relying on object.__div__ or + # long.__div__ because we want to wrap the value in a newint() + # call if other is another int + value = long(self) / other + if isinstance(other, (int, long)): + return newint(value) + else: + return value + + def __rdiv__(self, other): + value = other / long(self) + if isinstance(other, (int, long)): + return newint(value) + else: + return value + + def __idiv__(self, other): + # long has no __idiv__ method. Use __itruediv__ and cast back to + # newint: + value = self.__itruediv__(other) + if isinstance(other, (int, long)): + return newint(value) + else: + return value + + def __truediv__(self, other): + value = super(newint, self).__truediv__(other) + if value is NotImplemented: + value = long(self) / other + return value + + def __rtruediv__(self, other): + return super(newint, self).__rtruediv__(other) + + def __itruediv__(self, other): + # long has no __itruediv__ method + mylong = long(self) + mylong /= other + return mylong + + def __floordiv__(self, other): + return newint(super(newint, self).__floordiv__(other)) + + def __rfloordiv__(self, other): + return newint(super(newint, self).__rfloordiv__(other)) + + def __ifloordiv__(self, other): + # long has no __ifloordiv__ method + mylong = long(self) + mylong //= other + return newint(mylong) + + def __mod__(self, other): + value = super(newint, self).__mod__(other) + if value is NotImplemented: + return long(self) % other + return newint(value) + + def __rmod__(self, other): + value = super(newint, self).__rmod__(other) + if value is NotImplemented: + return other % long(self) + return newint(value) + + def __divmod__(self, other): + value = super(newint, self).__divmod__(other) + if value is NotImplemented: + mylong = long(self) + return (mylong // other, mylong % other) + return (newint(value[0]), newint(value[1])) + + def __rdivmod__(self, other): + value = super(newint, self).__rdivmod__(other) + if value is NotImplemented: + mylong = long(self) + return (other // mylong, other % mylong) + return (newint(value[0]), newint(value[1])) + + def __pow__(self, other): + value = super(newint, self).__pow__(other) + if value is NotImplemented: + return long(self) ** other + return newint(value) + + def __rpow__(self, other): + value = super(newint, self).__rpow__(other) + if isint(value): + return newint(value) + elif value is NotImplemented: + return other ** long(self) + return value + + def __lshift__(self, other): + if not isint(other): + raise TypeError( + "unsupported operand type(s) for <<: '%s' and '%s'" % + (type(self).__name__, type(other).__name__)) + return newint(super(newint, self).__lshift__(other)) + + def __rshift__(self, other): + if not isint(other): + raise TypeError( + "unsupported operand type(s) for >>: '%s' and '%s'" % + (type(self).__name__, type(other).__name__)) + return newint(super(newint, self).__rshift__(other)) + + def __and__(self, other): + if not isint(other): + raise TypeError( + "unsupported operand type(s) for &: '%s' and '%s'" % + (type(self).__name__, type(other).__name__)) + return newint(super(newint, self).__and__(other)) + + def __or__(self, other): + if not isint(other): + raise TypeError( + "unsupported operand type(s) for |: '%s' and '%s'" % + (type(self).__name__, type(other).__name__)) + return newint(super(newint, self).__or__(other)) + + def __xor__(self, other): + if not isint(other): + raise TypeError( + "unsupported operand type(s) for ^: '%s' and '%s'" % + (type(self).__name__, type(other).__name__)) + return newint(super(newint, self).__xor__(other)) + + def __neg__(self): + return newint(super(newint, self).__neg__()) + + def __pos__(self): + return newint(super(newint, self).__pos__()) + + def __abs__(self): + return newint(super(newint, self).__abs__()) + + def __invert__(self): + return newint(super(newint, self).__invert__()) + + def __int__(self): + return self + + def __nonzero__(self): + return self.__bool__() + + def __bool__(self): + """ + So subclasses can override this, Py3-style + """ + if PY3: + return super(newint, self).__bool__() + + return super(newint, self).__nonzero__() + + def __native__(self): + return long(self) + + def to_bytes(self, length, byteorder='big', signed=False): + """ + Return an array of bytes representing an integer. + + The integer is represented using length bytes. An OverflowError is + raised if the integer is not representable with the given number of + bytes. + + The byteorder argument determines the byte order used to represent the + integer. If byteorder is 'big', the most significant byte is at the + beginning of the byte array. If byteorder is 'little', the most + significant byte is at the end of the byte array. To request the native + byte order of the host system, use `sys.byteorder' as the byte order value. + + The signed keyword-only argument determines whether two's complement is + used to represent the integer. If signed is False and a negative integer + is given, an OverflowError is raised. + """ + if length < 0: + raise ValueError("length argument must be non-negative") + if length == 0 and self == 0: + return newbytes() + if signed and self < 0: + bits = length * 8 + num = (2**bits) + self + if num <= 0: + raise OverflowError("int too small to convert") + else: + if self < 0: + raise OverflowError("can't convert negative int to unsigned") + num = self + if byteorder not in ('little', 'big'): + raise ValueError("byteorder must be either 'little' or 'big'") + h = b'%x' % num + s = newbytes((b'0'*(len(h) % 2) + h).zfill(length*2).decode('hex')) + if signed: + high_set = s[0] & 0x80 + if self > 0 and high_set: + raise OverflowError("int too big to convert") + if self < 0 and not high_set: + raise OverflowError("int too small to convert") + if len(s) > length: + raise OverflowError("int too big to convert") + return s if byteorder == 'big' else s[::-1] + + @classmethod + def from_bytes(cls, mybytes, byteorder='big', signed=False): + """ + Return the integer represented by the given array of bytes. + + The mybytes argument must either support the buffer protocol or be an + iterable object producing bytes. Bytes and bytearray are examples of + built-in objects that support the buffer protocol. + + The byteorder argument determines the byte order used to represent the + integer. If byteorder is 'big', the most significant byte is at the + beginning of the byte array. If byteorder is 'little', the most + significant byte is at the end of the byte array. To request the native + byte order of the host system, use `sys.byteorder' as the byte order value. + + The signed keyword-only argument indicates whether two's complement is + used to represent the integer. + """ + if byteorder not in ('little', 'big'): + raise ValueError("byteorder must be either 'little' or 'big'") + if isinstance(mybytes, unicode): + raise TypeError("cannot convert unicode objects to bytes") + # mybytes can also be passed as a sequence of integers on Py3. + # Test for this: + elif isinstance(mybytes, Iterable): + mybytes = newbytes(mybytes) + b = mybytes if byteorder == 'big' else mybytes[::-1] + if len(b) == 0: + b = b'\x00' + # The encode() method has been disabled by newbytes, but Py2's + # str has it: + num = int(native(b).encode('hex'), 16) + if signed and (b[0] & 0x80): + num = num - (2 ** (len(b)*8)) + return cls(num) + + +# def _twos_comp(val, bits): +# """compute the 2's compliment of int value val""" +# if( (val&(1<<(bits-1))) != 0 ): +# val = val - (1<>> from builtins import list +>>> l1 = list() # instead of {} for an empty list +>>> l1.append('hello') +>>> l2 = l1.copy() + +""" + +import sys +import copy + +from future.utils import with_metaclass +from future.types.newobject import newobject + + +_builtin_list = list +ver = sys.version_info[:2] + + +class BaseNewList(type): + def __instancecheck__(cls, instance): + if cls == newlist: + return isinstance(instance, _builtin_list) + else: + return issubclass(instance.__class__, cls) + + +class newlist(with_metaclass(BaseNewList, _builtin_list)): + """ + A backport of the Python 3 list object to Py2 + """ + def copy(self): + """ + L.copy() -> list -- a shallow copy of L + """ + return copy.copy(self) + + def clear(self): + """L.clear() -> None -- remove all items from L""" + for i in range(len(self)): + self.pop() + + def __new__(cls, *args, **kwargs): + """ + list() -> new empty list + list(iterable) -> new list initialized from iterable's items + """ + + if len(args) == 0: + return super(newlist, cls).__new__(cls) + elif type(args[0]) == newlist: + value = args[0] + else: + value = args[0] + return super(newlist, cls).__new__(cls, value) + + def __add__(self, value): + return newlist(super(newlist, self).__add__(value)) + + def __radd__(self, left): + " left + self " + try: + return newlist(left) + self + except: + return NotImplemented + + def __getitem__(self, y): + """ + x.__getitem__(y) <==> x[y] + + Warning: a bug in Python 2.x prevents indexing via a slice from + returning a newlist object. + """ + if isinstance(y, slice): + return newlist(super(newlist, self).__getitem__(y)) + else: + return super(newlist, self).__getitem__(y) + + def __native__(self): + """ + Hook for the future.utils.native() function + """ + return list(self) + + def __nonzero__(self): + return len(self) > 0 + + +__all__ = ['newlist'] diff --git a/venv/lib/python3.10/site-packages/future/types/newmemoryview.py b/venv/lib/python3.10/site-packages/future/types/newmemoryview.py new file mode 100644 index 0000000..09f804d --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/types/newmemoryview.py @@ -0,0 +1,29 @@ +""" +A pretty lame implementation of a memoryview object for Python 2.6. +""" +from numbers import Integral +import string + +from future.utils import istext, isbytes, PY2, with_metaclass +from future.types import no, issubset + +if PY2: + from collections import Iterable +else: + from collections.abc import Iterable + +# class BaseNewBytes(type): +# def __instancecheck__(cls, instance): +# return isinstance(instance, _builtin_bytes) + + +class newmemoryview(object): # with_metaclass(BaseNewBytes, _builtin_bytes)): + """ + A pretty lame backport of the Python 2.7 and Python 3.x + memoryviewview object to Py2.6. + """ + def __init__(self, obj): + return obj + + +__all__ = ['newmemoryview'] diff --git a/venv/lib/python3.10/site-packages/future/types/newobject.py b/venv/lib/python3.10/site-packages/future/types/newobject.py new file mode 100644 index 0000000..31b84fc --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/types/newobject.py @@ -0,0 +1,117 @@ +""" +An object subclass for Python 2 that gives new-style classes written in the +style of Python 3 (with ``__next__`` and unicode-returning ``__str__`` methods) +the appropriate Python 2-style ``next`` and ``__unicode__`` methods for compatible. + +Example use:: + + from builtins import object + + my_unicode_str = u'Unicode string: \u5b54\u5b50' + + class A(object): + def __str__(self): + return my_unicode_str + + a = A() + print(str(a)) + + # On Python 2, these relations hold: + assert unicode(a) == my_unicode_string + assert str(a) == my_unicode_string.encode('utf-8') + + +Another example:: + + from builtins import object + + class Upper(object): + def __init__(self, iterable): + self._iter = iter(iterable) + def __next__(self): # note the Py3 interface + return next(self._iter).upper() + def __iter__(self): + return self + + assert list(Upper('hello')) == list('HELLO') + +""" + + +class newobject(object): + """ + A magical object class that provides Python 2 compatibility methods:: + next + __unicode__ + __nonzero__ + + Subclasses of this class can merely define the Python 3 methods (__next__, + __str__, and __bool__). + """ + def next(self): + if hasattr(self, '__next__'): + return type(self).__next__(self) + raise TypeError('newobject is not an iterator') + + def __unicode__(self): + # All subclasses of the builtin object should have __str__ defined. + # Note that old-style classes do not have __str__ defined. + if hasattr(self, '__str__'): + s = type(self).__str__(self) + else: + s = str(self) + if isinstance(s, unicode): + return s + else: + return s.decode('utf-8') + + def __nonzero__(self): + if hasattr(self, '__bool__'): + return type(self).__bool__(self) + if hasattr(self, '__len__'): + return type(self).__len__(self) + # object has no __nonzero__ method + return True + + # Are these ever needed? + # def __div__(self): + # return self.__truediv__() + + # def __idiv__(self, other): + # return self.__itruediv__(other) + + def __long__(self): + if not hasattr(self, '__int__'): + return NotImplemented + return self.__int__() # not type(self).__int__(self) + + # def __new__(cls, *args, **kwargs): + # """ + # dict() -> new empty dictionary + # dict(mapping) -> new dictionary initialized from a mapping object's + # (key, value) pairs + # dict(iterable) -> new dictionary initialized as if via: + # d = {} + # for k, v in iterable: + # d[k] = v + # dict(**kwargs) -> new dictionary initialized with the name=value pairs + # in the keyword argument list. For example: dict(one=1, two=2) + # """ + + # if len(args) == 0: + # return super(newdict, cls).__new__(cls) + # elif type(args[0]) == newdict: + # return args[0] + # else: + # value = args[0] + # return super(newdict, cls).__new__(cls, value) + + def __native__(self): + """ + Hook for the future.utils.native() function + """ + return object(self) + + __slots__ = [] + +__all__ = ['newobject'] diff --git a/venv/lib/python3.10/site-packages/future/types/newopen.py b/venv/lib/python3.10/site-packages/future/types/newopen.py new file mode 100644 index 0000000..b75d45a --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/types/newopen.py @@ -0,0 +1,32 @@ +""" +A substitute for the Python 3 open() function. + +Note that io.open() is more complete but maybe slower. Even so, the +completeness may be a better default. TODO: compare these +""" + +_builtin_open = open + +class newopen(object): + """Wrapper providing key part of Python 3 open() interface. + + From IPython's py3compat.py module. License: BSD. + """ + def __init__(self, fname, mode="r", encoding="utf-8"): + self.f = _builtin_open(fname, mode) + self.enc = encoding + + def write(self, s): + return self.f.write(s.encode(self.enc)) + + def read(self, size=-1): + return self.f.read(size).decode(self.enc) + + def close(self): + return self.f.close() + + def __enter__(self): + return self + + def __exit__(self, etype, value, traceback): + self.f.close() diff --git a/venv/lib/python3.10/site-packages/future/types/newrange.py b/venv/lib/python3.10/site-packages/future/types/newrange.py new file mode 100644 index 0000000..dc5eb80 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/types/newrange.py @@ -0,0 +1,170 @@ +""" +Nearly identical to xrange.py, by Dan Crosta, from + + https://github.com/dcrosta/xrange.git + +This is included here in the ``future`` package rather than pointed to as +a dependency because there is no package for ``xrange`` on PyPI. It is +also tweaked to appear like a regular Python 3 ``range`` object rather +than a Python 2 xrange. + +From Dan Crosta's README: + + "A pure-Python implementation of Python 2.7's xrange built-in, with + some features backported from the Python 3.x range built-in (which + replaced xrange) in that version." + + Read more at + https://late.am/post/2012/06/18/what-the-heck-is-an-xrange +""" +from __future__ import absolute_import + +from future.utils import PY2 + +if PY2: + from collections import Sequence, Iterator +else: + from collections.abc import Sequence, Iterator +from itertools import islice + +from future.backports.misc import count # with step parameter on Py2.6 +# For backward compatibility with python-future versions < 0.14.4: +_count = count + + +class newrange(Sequence): + """ + Pure-Python backport of Python 3's range object. See `the CPython + documentation for details: + `_ + """ + + def __init__(self, *args): + if len(args) == 1: + start, stop, step = 0, args[0], 1 + elif len(args) == 2: + start, stop, step = args[0], args[1], 1 + elif len(args) == 3: + start, stop, step = args + else: + raise TypeError('range() requires 1-3 int arguments') + + try: + start, stop, step = int(start), int(stop), int(step) + except ValueError: + raise TypeError('an integer is required') + + if step == 0: + raise ValueError('range() arg 3 must not be zero') + elif step < 0: + stop = min(stop, start) + else: + stop = max(stop, start) + + self._start = start + self._stop = stop + self._step = step + self._len = (stop - start) // step + bool((stop - start) % step) + + @property + def start(self): + return self._start + + @property + def stop(self): + return self._stop + + @property + def step(self): + return self._step + + def __repr__(self): + if self._step == 1: + return 'range(%d, %d)' % (self._start, self._stop) + return 'range(%d, %d, %d)' % (self._start, self._stop, self._step) + + def __eq__(self, other): + return (isinstance(other, newrange) and + (self._len == 0 == other._len or + (self._start, self._step, self._len) == + (other._start, other._step, other._len))) + + def __len__(self): + return self._len + + def index(self, value): + """Return the 0-based position of integer `value` in + the sequence this range represents.""" + try: + diff = value - self._start + except TypeError: + raise ValueError('%r is not in range' % value) + quotient, remainder = divmod(diff, self._step) + if remainder == 0 and 0 <= quotient < self._len: + return abs(quotient) + raise ValueError('%r is not in range' % value) + + def count(self, value): + """Return the number of occurrences of integer `value` + in the sequence this range represents.""" + # a value can occur exactly zero or one times + return int(value in self) + + def __contains__(self, value): + """Return ``True`` if the integer `value` occurs in + the sequence this range represents.""" + try: + self.index(value) + return True + except ValueError: + return False + + def __reversed__(self): + return iter(self[::-1]) + + def __getitem__(self, index): + """Return the element at position ``index`` in the sequence + this range represents, or raise :class:`IndexError` if the + position is out of range.""" + if isinstance(index, slice): + return self.__getitem_slice(index) + if index < 0: + # negative indexes access from the end + index = self._len + index + if index < 0 or index >= self._len: + raise IndexError('range object index out of range') + return self._start + index * self._step + + def __getitem_slice(self, slce): + """Return a range which represents the requested slce + of the sequence represented by this range. + """ + scaled_indices = (self._step * n for n in slce.indices(self._len)) + start_offset, stop_offset, new_step = scaled_indices + return newrange(self._start + start_offset, + self._start + stop_offset, + new_step) + + def __iter__(self): + """Return an iterator which enumerates the elements of the + sequence this range represents.""" + return range_iterator(self) + + +class range_iterator(Iterator): + """An iterator for a :class:`range`. + """ + def __init__(self, range_): + self._stepper = islice(count(range_.start, range_.step), len(range_)) + + def __iter__(self): + return self + + def __next__(self): + return next(self._stepper) + + def next(self): + return next(self._stepper) + + +__all__ = ['newrange'] diff --git a/venv/lib/python3.10/site-packages/future/types/newstr.py b/venv/lib/python3.10/site-packages/future/types/newstr.py new file mode 100644 index 0000000..8ca191f --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/types/newstr.py @@ -0,0 +1,426 @@ +""" +This module redefines ``str`` on Python 2.x to be a subclass of the Py2 +``unicode`` type that behaves like the Python 3.x ``str``. + +The main differences between ``newstr`` and Python 2.x's ``unicode`` type are +the stricter type-checking and absence of a `u''` prefix in the representation. + +It is designed to be used together with the ``unicode_literals`` import +as follows: + + >>> from __future__ import unicode_literals + >>> from builtins import str, isinstance + +On Python 3.x and normally on Python 2.x, these expressions hold + + >>> str('blah') is 'blah' + True + >>> isinstance('blah', str) + True + +However, on Python 2.x, with this import: + + >>> from __future__ import unicode_literals + +the same expressions are False: + + >>> str('blah') is 'blah' + False + >>> isinstance('blah', str) + False + +This module is designed to be imported together with ``unicode_literals`` on +Python 2 to bring the meaning of ``str`` back into alignment with unprefixed +string literals (i.e. ``unicode`` subclasses). + +Note that ``str()`` (and ``print()``) would then normally call the +``__unicode__`` method on objects in Python 2. To define string +representations of your objects portably across Py3 and Py2, use the +:func:`python_2_unicode_compatible` decorator in :mod:`future.utils`. + +""" + +from numbers import Number + +from future.utils import PY3, istext, with_metaclass, isnewbytes +from future.types import no, issubset +from future.types.newobject import newobject + + +if PY3: + # We'll probably never use newstr on Py3 anyway... + unicode = str + from collections.abc import Iterable +else: + from collections import Iterable + + +class BaseNewStr(type): + def __instancecheck__(cls, instance): + if cls == newstr: + return isinstance(instance, unicode) + else: + return issubclass(instance.__class__, cls) + + +class newstr(with_metaclass(BaseNewStr, unicode)): + """ + A backport of the Python 3 str object to Py2 + """ + no_convert_msg = "Can't convert '{0}' object to str implicitly" + + def __new__(cls, *args, **kwargs): + """ + From the Py3 str docstring: + + str(object='') -> str + str(bytes_or_buffer[, encoding[, errors]]) -> str + + Create a new string object from the given object. If encoding or + errors is specified, then the object must expose a data buffer + that will be decoded using the given encoding and error handler. + Otherwise, returns the result of object.__str__() (if defined) + or repr(object). + encoding defaults to sys.getdefaultencoding(). + errors defaults to 'strict'. + + """ + if len(args) == 0: + return super(newstr, cls).__new__(cls) + # Special case: If someone requests str(str(u'abc')), return the same + # object (same id) for consistency with Py3.3. This is not true for + # other objects like list or dict. + elif type(args[0]) == newstr and cls == newstr: + return args[0] + elif isinstance(args[0], unicode): + value = args[0] + elif isinstance(args[0], bytes): # i.e. Py2 bytes or newbytes + if 'encoding' in kwargs or len(args) > 1: + value = args[0].decode(*args[1:], **kwargs) + else: + value = args[0].__str__() + else: + value = args[0] + return super(newstr, cls).__new__(cls, value) + + def __repr__(self): + """ + Without the u prefix + """ + + value = super(newstr, self).__repr__() + # assert value[0] == u'u' + return value[1:] + + def __getitem__(self, y): + """ + Warning: Python <= 2.7.6 has a bug that causes this method never to be called + when y is a slice object. Therefore the type of newstr()[:2] is wrong + (unicode instead of newstr). + """ + return newstr(super(newstr, self).__getitem__(y)) + + def __contains__(self, key): + errmsg = "'in ' requires string as left operand, not {0}" + # Don't use isinstance() here because we only want to catch + # newstr, not Python 2 unicode: + if type(key) == newstr: + newkey = key + elif isinstance(key, unicode) or isinstance(key, bytes) and not isnewbytes(key): + newkey = newstr(key) + else: + raise TypeError(errmsg.format(type(key))) + return issubset(list(newkey), list(self)) + + @no('newbytes') + def __add__(self, other): + return newstr(super(newstr, self).__add__(other)) + + @no('newbytes') + def __radd__(self, left): + " left + self " + try: + return newstr(left) + self + except: + return NotImplemented + + def __mul__(self, other): + return newstr(super(newstr, self).__mul__(other)) + + def __rmul__(self, other): + return newstr(super(newstr, self).__rmul__(other)) + + def join(self, iterable): + errmsg = 'sequence item {0}: expected unicode string, found bytes' + for i, item in enumerate(iterable): + # Here we use type() rather than isinstance() because + # __instancecheck__ is being overridden. E.g. + # isinstance(b'abc', newbytes) is True on Py2. + if isnewbytes(item): + raise TypeError(errmsg.format(i)) + # Support use as a staticmethod: str.join('-', ['a', 'b']) + if type(self) == newstr: + return newstr(super(newstr, self).join(iterable)) + else: + return newstr(super(newstr, newstr(self)).join(iterable)) + + @no('newbytes') + def find(self, sub, *args): + return super(newstr, self).find(sub, *args) + + @no('newbytes') + def rfind(self, sub, *args): + return super(newstr, self).rfind(sub, *args) + + @no('newbytes', (1, 2)) + def replace(self, old, new, *args): + return newstr(super(newstr, self).replace(old, new, *args)) + + def decode(self, *args): + raise AttributeError("decode method has been disabled in newstr") + + def encode(self, encoding='utf-8', errors='strict'): + """ + Returns bytes + + Encode S using the codec registered for encoding. Default encoding + is 'utf-8'. errors may be given to set a different error + handling scheme. Default is 'strict' meaning that encoding errors raise + a UnicodeEncodeError. Other possible values are 'ignore', 'replace' and + 'xmlcharrefreplace' as well as any other name registered with + codecs.register_error that can handle UnicodeEncodeErrors. + """ + from future.types.newbytes import newbytes + # Py2 unicode.encode() takes encoding and errors as optional parameter, + # not keyword arguments as in Python 3 str. + + # For the surrogateescape error handling mechanism, the + # codecs.register_error() function seems to be inadequate for an + # implementation of it when encoding. (Decoding seems fine, however.) + # For example, in the case of + # u'\udcc3'.encode('ascii', 'surrogateescape_handler') + # after registering the ``surrogateescape_handler`` function in + # future.utils.surrogateescape, both Python 2.x and 3.x raise an + # exception anyway after the function is called because the unicode + # string it has to return isn't encodable strictly as ASCII. + + if errors == 'surrogateescape': + if encoding == 'utf-16': + # Known to fail here. See test_encoding_works_normally() + raise NotImplementedError('FIXME: surrogateescape handling is ' + 'not yet implemented properly') + # Encode char by char, building up list of byte-strings + mybytes = [] + for c in self: + code = ord(c) + if 0xD800 <= code <= 0xDCFF: + mybytes.append(newbytes([code - 0xDC00])) + else: + mybytes.append(c.encode(encoding=encoding)) + return newbytes(b'').join(mybytes) + return newbytes(super(newstr, self).encode(encoding, errors)) + + @no('newbytes', 1) + def startswith(self, prefix, *args): + if isinstance(prefix, Iterable): + for thing in prefix: + if isnewbytes(thing): + raise TypeError(self.no_convert_msg.format(type(thing))) + return super(newstr, self).startswith(prefix, *args) + + @no('newbytes', 1) + def endswith(self, prefix, *args): + # Note we need the decorator above as well as the isnewbytes() + # check because prefix can be either a bytes object or e.g. a + # tuple of possible prefixes. (If it's a bytes object, each item + # in it is an int.) + if isinstance(prefix, Iterable): + for thing in prefix: + if isnewbytes(thing): + raise TypeError(self.no_convert_msg.format(type(thing))) + return super(newstr, self).endswith(prefix, *args) + + @no('newbytes', 1) + def split(self, sep=None, maxsplit=-1): + # Py2 unicode.split() takes maxsplit as an optional parameter, + # not as a keyword argument as in Python 3 str. + parts = super(newstr, self).split(sep, maxsplit) + return [newstr(part) for part in parts] + + @no('newbytes', 1) + def rsplit(self, sep=None, maxsplit=-1): + # Py2 unicode.rsplit() takes maxsplit as an optional parameter, + # not as a keyword argument as in Python 3 str. + parts = super(newstr, self).rsplit(sep, maxsplit) + return [newstr(part) for part in parts] + + @no('newbytes', 1) + def partition(self, sep): + parts = super(newstr, self).partition(sep) + return tuple(newstr(part) for part in parts) + + @no('newbytes', 1) + def rpartition(self, sep): + parts = super(newstr, self).rpartition(sep) + return tuple(newstr(part) for part in parts) + + @no('newbytes', 1) + def index(self, sub, *args): + """ + Like newstr.find() but raise ValueError when the substring is not + found. + """ + pos = self.find(sub, *args) + if pos == -1: + raise ValueError('substring not found') + return pos + + def splitlines(self, keepends=False): + """ + S.splitlines(keepends=False) -> list of strings + + Return a list of the lines in S, breaking at line boundaries. + Line breaks are not included in the resulting list unless keepends + is given and true. + """ + # Py2 unicode.splitlines() takes keepends as an optional parameter, + # not as a keyword argument as in Python 3 str. + parts = super(newstr, self).splitlines(keepends) + return [newstr(part) for part in parts] + + def __eq__(self, other): + if (isinstance(other, unicode) or + isinstance(other, bytes) and not isnewbytes(other)): + return super(newstr, self).__eq__(other) + else: + return NotImplemented + + def __hash__(self): + if (isinstance(self, unicode) or + isinstance(self, bytes) and not isnewbytes(self)): + return super(newstr, self).__hash__() + else: + raise NotImplementedError() + + def __ne__(self, other): + if (isinstance(other, unicode) or + isinstance(other, bytes) and not isnewbytes(other)): + return super(newstr, self).__ne__(other) + else: + return True + + unorderable_err = 'unorderable types: str() and {0}' + + def __lt__(self, other): + if (isinstance(other, unicode) or + isinstance(other, bytes) and not isnewbytes(other)): + return super(newstr, self).__lt__(other) + raise TypeError(self.unorderable_err.format(type(other))) + + def __le__(self, other): + if (isinstance(other, unicode) or + isinstance(other, bytes) and not isnewbytes(other)): + return super(newstr, self).__le__(other) + raise TypeError(self.unorderable_err.format(type(other))) + + def __gt__(self, other): + if (isinstance(other, unicode) or + isinstance(other, bytes) and not isnewbytes(other)): + return super(newstr, self).__gt__(other) + raise TypeError(self.unorderable_err.format(type(other))) + + def __ge__(self, other): + if (isinstance(other, unicode) or + isinstance(other, bytes) and not isnewbytes(other)): + return super(newstr, self).__ge__(other) + raise TypeError(self.unorderable_err.format(type(other))) + + def __getattribute__(self, name): + """ + A trick to cause the ``hasattr`` builtin-fn to return False for + the 'decode' method on Py2. + """ + if name in ['decode', u'decode']: + raise AttributeError("decode method has been disabled in newstr") + return super(newstr, self).__getattribute__(name) + + def __native__(self): + """ + A hook for the future.utils.native() function. + """ + return unicode(self) + + @staticmethod + def maketrans(x, y=None, z=None): + """ + Return a translation table usable for str.translate(). + + If there is only one argument, it must be a dictionary mapping Unicode + ordinals (integers) or characters to Unicode ordinals, strings or None. + Character keys will be then converted to ordinals. + If there are two arguments, they must be strings of equal length, and + in the resulting dictionary, each character in x will be mapped to the + character at the same position in y. If there is a third argument, it + must be a string, whose characters will be mapped to None in the result. + """ + + if y is None: + assert z is None + if not isinstance(x, dict): + raise TypeError('if you give only one argument to maketrans it must be a dict') + result = {} + for (key, value) in x.items(): + if len(key) > 1: + raise ValueError('keys in translate table must be strings or integers') + result[ord(key)] = value + else: + if not isinstance(x, unicode) and isinstance(y, unicode): + raise TypeError('x and y must be unicode strings') + if not len(x) == len(y): + raise ValueError('the first two maketrans arguments must have equal length') + result = {} + for (xi, yi) in zip(x, y): + if len(xi) > 1: + raise ValueError('keys in translate table must be strings or integers') + result[ord(xi)] = ord(yi) + + if z is not None: + for char in z: + result[ord(char)] = None + return result + + def translate(self, table): + """ + S.translate(table) -> str + + Return a copy of the string S, where all characters have been mapped + through the given translation table, which must be a mapping of + Unicode ordinals to Unicode ordinals, strings, or None. + Unmapped characters are left untouched. Characters mapped to None + are deleted. + """ + l = [] + for c in self: + if ord(c) in table: + val = table[ord(c)] + if val is None: + continue + elif isinstance(val, unicode): + l.append(val) + else: + l.append(chr(val)) + else: + l.append(c) + return ''.join(l) + + def isprintable(self): + raise NotImplementedError('fixme') + + def isidentifier(self): + raise NotImplementedError('fixme') + + def format_map(self): + raise NotImplementedError('fixme') + + +__all__ = ['newstr'] diff --git a/venv/lib/python3.10/site-packages/future/utils/__init__.py b/venv/lib/python3.10/site-packages/future/utils/__init__.py new file mode 100644 index 0000000..ec1b102 --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/utils/__init__.py @@ -0,0 +1,770 @@ +""" +A selection of cross-compatible functions for Python 2 and 3. + +This module exports useful functions for 2/3 compatible code: + + * bind_method: binds functions to classes + * ``native_str_to_bytes`` and ``bytes_to_native_str`` + * ``native_str``: always equal to the native platform string object (because + this may be shadowed by imports from future.builtins) + * lists: lrange(), lmap(), lzip(), lfilter() + * iterable method compatibility: + - iteritems, iterkeys, itervalues + - viewitems, viewkeys, viewvalues + + These use the original method if available, otherwise they use items, + keys, values. + + * types: + + * text_type: unicode in Python 2, str in Python 3 + * string_types: basestring in Python 2, str in Python 3 + * binary_type: str in Python 2, bytes in Python 3 + * integer_types: (int, long) in Python 2, int in Python 3 + * class_types: (type, types.ClassType) in Python 2, type in Python 3 + + * bchr(c): + Take an integer and make a 1-character byte string + * bord(c) + Take the result of indexing on a byte string and make an integer + * tobytes(s) + Take a text string, a byte string, or a sequence of characters taken + from a byte string, and make a byte string. + + * raise_from() + * raise_with_traceback() + +This module also defines these decorators: + + * ``python_2_unicode_compatible`` + * ``with_metaclass`` + * ``implements_iterator`` + +Some of the functions in this module come from the following sources: + + * Jinja2 (BSD licensed: see + https://github.com/mitsuhiko/jinja2/blob/master/LICENSE) + * Pandas compatibility module pandas.compat + * six.py by Benjamin Peterson + * Django +""" + +import types +import sys +import numbers +import functools +import copy +import inspect + + +PY3 = sys.version_info[0] >= 3 +PY34_PLUS = sys.version_info[0:2] >= (3, 4) +PY35_PLUS = sys.version_info[0:2] >= (3, 5) +PY36_PLUS = sys.version_info[0:2] >= (3, 6) +PY37_PLUS = sys.version_info[0:2] >= (3, 7) +PY38_PLUS = sys.version_info[0:2] >= (3, 8) +PY39_PLUS = sys.version_info[0:2] >= (3, 9) +PY2 = sys.version_info[0] == 2 +PY26 = sys.version_info[0:2] == (2, 6) +PY27 = sys.version_info[0:2] == (2, 7) +PYPY = hasattr(sys, 'pypy_translation_info') + + +def python_2_unicode_compatible(cls): + """ + A decorator that defines __unicode__ and __str__ methods under Python + 2. Under Python 3, this decorator is a no-op. + + To support Python 2 and 3 with a single code base, define a __str__ + method returning unicode text and apply this decorator to the class, like + this:: + + >>> from future.utils import python_2_unicode_compatible + + >>> @python_2_unicode_compatible + ... class MyClass(object): + ... def __str__(self): + ... return u'Unicode string: \u5b54\u5b50' + + >>> a = MyClass() + + Then, after this import: + + >>> from future.builtins import str + + the following is ``True`` on both Python 3 and 2:: + + >>> str(a) == a.encode('utf-8').decode('utf-8') + True + + and, on a Unicode-enabled terminal with the right fonts, these both print the + Chinese characters for Confucius:: + + >>> print(a) + >>> print(str(a)) + + The implementation comes from django.utils.encoding. + """ + if not PY3: + cls.__unicode__ = cls.__str__ + cls.__str__ = lambda self: self.__unicode__().encode('utf-8') + return cls + + +def with_metaclass(meta, *bases): + """ + Function from jinja2/_compat.py. License: BSD. + + Use it like this:: + + class BaseForm(object): + pass + + class FormType(type): + pass + + class Form(with_metaclass(FormType, BaseForm)): + pass + + This requires a bit of explanation: the basic idea is to make a + dummy metaclass for one level of class instantiation that replaces + itself with the actual metaclass. Because of internal type checks + we also need to make sure that we downgrade the custom metaclass + for one level to something closer to type (that's why __call__ and + __init__ comes back from type etc.). + + This has the advantage over six.with_metaclass of not introducing + dummy classes into the final MRO. + """ + class metaclass(meta): + __call__ = type.__call__ + __init__ = type.__init__ + def __new__(cls, name, this_bases, d): + if this_bases is None: + return type.__new__(cls, name, (), d) + return meta(name, bases, d) + return metaclass('temporary_class', None, {}) + + +# Definitions from pandas.compat and six.py follow: +if PY3: + def bchr(s): + return bytes([s]) + def bstr(s): + if isinstance(s, str): + return bytes(s, 'latin-1') + else: + return bytes(s) + def bord(s): + return s + + string_types = str, + integer_types = int, + class_types = type, + text_type = str + binary_type = bytes + +else: + # Python 2 + def bchr(s): + return chr(s) + def bstr(s): + return str(s) + def bord(s): + return ord(s) + + string_types = basestring, + integer_types = (int, long) + class_types = (type, types.ClassType) + text_type = unicode + binary_type = str + +### + +if PY3: + def tobytes(s): + if isinstance(s, bytes): + return s + else: + if isinstance(s, str): + return s.encode('latin-1') + else: + return bytes(s) +else: + # Python 2 + def tobytes(s): + if isinstance(s, unicode): + return s.encode('latin-1') + else: + return ''.join(s) + +tobytes.__doc__ = """ + Encodes to latin-1 (where the first 256 chars are the same as + ASCII.) + """ + +if PY3: + def native_str_to_bytes(s, encoding='utf-8'): + return s.encode(encoding) + + def bytes_to_native_str(b, encoding='utf-8'): + return b.decode(encoding) + + def text_to_native_str(t, encoding=None): + return t +else: + # Python 2 + def native_str_to_bytes(s, encoding=None): + from future.types import newbytes # to avoid a circular import + return newbytes(s) + + def bytes_to_native_str(b, encoding=None): + return native(b) + + def text_to_native_str(t, encoding='ascii'): + """ + Use this to create a Py2 native string when "from __future__ import + unicode_literals" is in effect. + """ + return unicode(t).encode(encoding) + +native_str_to_bytes.__doc__ = """ + On Py3, returns an encoded string. + On Py2, returns a newbytes type, ignoring the ``encoding`` argument. + """ + +if PY3: + # list-producing versions of the major Python iterating functions + def lrange(*args, **kwargs): + return list(range(*args, **kwargs)) + + def lzip(*args, **kwargs): + return list(zip(*args, **kwargs)) + + def lmap(*args, **kwargs): + return list(map(*args, **kwargs)) + + def lfilter(*args, **kwargs): + return list(filter(*args, **kwargs)) +else: + import __builtin__ + # Python 2-builtin ranges produce lists + lrange = __builtin__.range + lzip = __builtin__.zip + lmap = __builtin__.map + lfilter = __builtin__.filter + + +def isidentifier(s, dotted=False): + ''' + A function equivalent to the str.isidentifier method on Py3 + ''' + if dotted: + return all(isidentifier(a) for a in s.split('.')) + if PY3: + return s.isidentifier() + else: + import re + _name_re = re.compile(r"[a-zA-Z_][a-zA-Z0-9_]*$") + return bool(_name_re.match(s)) + + +def viewitems(obj, **kwargs): + """ + Function for iterating over dictionary items with the same set-like + behaviour on Py2.7 as on Py3. + + Passes kwargs to method.""" + func = getattr(obj, "viewitems", None) + if not func: + func = obj.items + return func(**kwargs) + + +def viewkeys(obj, **kwargs): + """ + Function for iterating over dictionary keys with the same set-like + behaviour on Py2.7 as on Py3. + + Passes kwargs to method.""" + func = getattr(obj, "viewkeys", None) + if not func: + func = obj.keys + return func(**kwargs) + + +def viewvalues(obj, **kwargs): + """ + Function for iterating over dictionary values with the same set-like + behaviour on Py2.7 as on Py3. + + Passes kwargs to method.""" + func = getattr(obj, "viewvalues", None) + if not func: + func = obj.values + return func(**kwargs) + + +def iteritems(obj, **kwargs): + """Use this only if compatibility with Python versions before 2.7 is + required. Otherwise, prefer viewitems(). + """ + func = getattr(obj, "iteritems", None) + if not func: + func = obj.items + return func(**kwargs) + + +def iterkeys(obj, **kwargs): + """Use this only if compatibility with Python versions before 2.7 is + required. Otherwise, prefer viewkeys(). + """ + func = getattr(obj, "iterkeys", None) + if not func: + func = obj.keys + return func(**kwargs) + + +def itervalues(obj, **kwargs): + """Use this only if compatibility with Python versions before 2.7 is + required. Otherwise, prefer viewvalues(). + """ + func = getattr(obj, "itervalues", None) + if not func: + func = obj.values + return func(**kwargs) + + +def bind_method(cls, name, func): + """Bind a method to class, python 2 and python 3 compatible. + + Parameters + ---------- + + cls : type + class to receive bound method + name : basestring + name of method on class instance + func : function + function to be bound as method + + Returns + ------- + None + """ + # only python 2 has an issue with bound/unbound methods + if not PY3: + setattr(cls, name, types.MethodType(func, None, cls)) + else: + setattr(cls, name, func) + + +def getexception(): + return sys.exc_info()[1] + + +def _get_caller_globals_and_locals(): + """ + Returns the globals and locals of the calling frame. + + Is there an alternative to frame hacking here? + """ + caller_frame = inspect.stack()[2] + myglobals = caller_frame[0].f_globals + mylocals = caller_frame[0].f_locals + return myglobals, mylocals + + +def _repr_strip(mystring): + """ + Returns the string without any initial or final quotes. + """ + r = repr(mystring) + if r.startswith("'") and r.endswith("'"): + return r[1:-1] + else: + return r + + +if PY3: + def raise_from(exc, cause): + """ + Equivalent to: + + raise EXCEPTION from CAUSE + + on Python 3. (See PEP 3134). + """ + myglobals, mylocals = _get_caller_globals_and_locals() + + # We pass the exception and cause along with other globals + # when we exec(): + myglobals = myglobals.copy() + myglobals['__python_future_raise_from_exc'] = exc + myglobals['__python_future_raise_from_cause'] = cause + execstr = "raise __python_future_raise_from_exc from __python_future_raise_from_cause" + exec(execstr, myglobals, mylocals) + + def raise_(tp, value=None, tb=None): + """ + A function that matches the Python 2.x ``raise`` statement. This + allows re-raising exceptions with the cls value and traceback on + Python 2 and 3. + """ + if isinstance(tp, BaseException): + # If the first object is an instance, the type of the exception + # is the class of the instance, the instance itself is the value, + # and the second object must be None. + if value is not None: + raise TypeError("instance exception may not have a separate value") + exc = tp + elif isinstance(tp, type) and not issubclass(tp, BaseException): + # If the first object is a class, it becomes the type of the + # exception. + raise TypeError("class must derive from BaseException, not %s" % tp.__name__) + else: + # The second object is used to determine the exception value: If it + # is an instance of the class, the instance becomes the exception + # value. If the second object is a tuple, it is used as the argument + # list for the class constructor; if it is None, an empty argument + # list is used, and any other object is treated as a single argument + # to the constructor. The instance so created by calling the + # constructor is used as the exception value. + if isinstance(value, tp): + exc = value + elif isinstance(value, tuple): + exc = tp(*value) + elif value is None: + exc = tp() + else: + exc = tp(value) + + if exc.__traceback__ is not tb: + raise exc.with_traceback(tb) + raise exc + + def raise_with_traceback(exc, traceback=Ellipsis): + if traceback == Ellipsis: + _, _, traceback = sys.exc_info() + raise exc.with_traceback(traceback) + +else: + def raise_from(exc, cause): + """ + Equivalent to: + + raise EXCEPTION from CAUSE + + on Python 3. (See PEP 3134). + """ + # Is either arg an exception class (e.g. IndexError) rather than + # instance (e.g. IndexError('my message here')? If so, pass the + # name of the class undisturbed through to "raise ... from ...". + if isinstance(exc, type) and issubclass(exc, Exception): + e = exc() + # exc = exc.__name__ + # execstr = "e = " + _repr_strip(exc) + "()" + # myglobals, mylocals = _get_caller_globals_and_locals() + # exec(execstr, myglobals, mylocals) + else: + e = exc + e.__suppress_context__ = False + if isinstance(cause, type) and issubclass(cause, Exception): + e.__cause__ = cause() + e.__cause__.__traceback__ = sys.exc_info()[2] + e.__suppress_context__ = True + elif cause is None: + e.__cause__ = None + e.__suppress_context__ = True + elif isinstance(cause, BaseException): + e.__cause__ = cause + object.__setattr__(e.__cause__, '__traceback__', sys.exc_info()[2]) + e.__suppress_context__ = True + else: + raise TypeError("exception causes must derive from BaseException") + e.__context__ = sys.exc_info()[1] + raise e + + exec(''' +def raise_(tp, value=None, tb=None): + raise tp, value, tb + +def raise_with_traceback(exc, traceback=Ellipsis): + if traceback == Ellipsis: + _, _, traceback = sys.exc_info() + raise exc, None, traceback +'''.strip()) + + +raise_with_traceback.__doc__ = ( +"""Raise exception with existing traceback. +If traceback is not passed, uses sys.exc_info() to get traceback.""" +) + + +# Deprecated alias for backward compatibility with ``future`` versions < 0.11: +reraise = raise_ + + +def implements_iterator(cls): + ''' + From jinja2/_compat.py. License: BSD. + + Use as a decorator like this:: + + @implements_iterator + class UppercasingIterator(object): + def __init__(self, iterable): + self._iter = iter(iterable) + def __iter__(self): + return self + def __next__(self): + return next(self._iter).upper() + + ''' + if PY3: + return cls + else: + cls.next = cls.__next__ + del cls.__next__ + return cls + +if PY3: + get_next = lambda x: x.__next__ +else: + get_next = lambda x: x.next + + +def encode_filename(filename): + if PY3: + return filename + else: + if isinstance(filename, unicode): + return filename.encode('utf-8') + return filename + + +def is_new_style(cls): + """ + Python 2.7 has both new-style and old-style classes. Old-style classes can + be pesky in some circumstances, such as when using inheritance. Use this + function to test for whether a class is new-style. (Python 3 only has + new-style classes.) + """ + return hasattr(cls, '__class__') and ('__dict__' in dir(cls) + or hasattr(cls, '__slots__')) + +# The native platform string and bytes types. Useful because ``str`` and +# ``bytes`` are redefined on Py2 by ``from future.builtins import *``. +native_str = str +native_bytes = bytes + + +def istext(obj): + """ + Deprecated. Use:: + >>> isinstance(obj, str) + after this import: + >>> from future.builtins import str + """ + return isinstance(obj, type(u'')) + + +def isbytes(obj): + """ + Deprecated. Use:: + >>> isinstance(obj, bytes) + after this import: + >>> from future.builtins import bytes + """ + return isinstance(obj, type(b'')) + + +def isnewbytes(obj): + """ + Equivalent to the result of ``type(obj) == type(newbytes)`` + in other words, it is REALLY a newbytes instance, not a Py2 native str + object? + + Note that this does not cover subclasses of newbytes, and it is not + equivalent to ininstance(obj, newbytes) + """ + return type(obj).__name__ == 'newbytes' + + +def isint(obj): + """ + Deprecated. Tests whether an object is a Py3 ``int`` or either a Py2 ``int`` or + ``long``. + + Instead of using this function, you can use: + + >>> from future.builtins import int + >>> isinstance(obj, int) + + The following idiom is equivalent: + + >>> from numbers import Integral + >>> isinstance(obj, Integral) + """ + + return isinstance(obj, numbers.Integral) + + +def native(obj): + """ + On Py3, this is a no-op: native(obj) -> obj + + On Py2, returns the corresponding native Py2 types that are + superclasses for backported objects from Py3: + + >>> from builtins import str, bytes, int + + >>> native(str(u'ABC')) + u'ABC' + >>> type(native(str(u'ABC'))) + unicode + + >>> native(bytes(b'ABC')) + b'ABC' + >>> type(native(bytes(b'ABC'))) + bytes + + >>> native(int(10**20)) + 100000000000000000000L + >>> type(native(int(10**20))) + long + + Existing native types on Py2 will be returned unchanged: + + >>> type(native(u'ABC')) + unicode + """ + if hasattr(obj, '__native__'): + return obj.__native__() + else: + return obj + + +# Implementation of exec_ is from ``six``: +if PY3: + import builtins + exec_ = getattr(builtins, "exec") +else: + def exec_(code, globs=None, locs=None): + """Execute code in a namespace.""" + if globs is None: + frame = sys._getframe(1) + globs = frame.f_globals + if locs is None: + locs = frame.f_locals + del frame + elif locs is None: + locs = globs + exec("""exec code in globs, locs""") + + +# Defined here for backward compatibility: +def old_div(a, b): + """ + DEPRECATED: import ``old_div`` from ``past.utils`` instead. + + Equivalent to ``a / b`` on Python 2 without ``from __future__ import + division``. + + TODO: generalize this to other objects (like arrays etc.) + """ + if isinstance(a, numbers.Integral) and isinstance(b, numbers.Integral): + return a // b + else: + return a / b + + +def as_native_str(encoding='utf-8'): + ''' + A decorator to turn a function or method call that returns text, i.e. + unicode, into one that returns a native platform str. + + Use it as a decorator like this:: + + from __future__ import unicode_literals + + class MyClass(object): + @as_native_str(encoding='ascii') + def __repr__(self): + return next(self._iter).upper() + ''' + if PY3: + return lambda f: f + else: + def encoder(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + return f(*args, **kwargs).encode(encoding=encoding) + return wrapper + return encoder + +# listvalues and listitems definitions from Nick Coghlan's (withdrawn) +# PEP 496: +try: + dict.iteritems +except AttributeError: + # Python 3 + def listvalues(d): + return list(d.values()) + def listitems(d): + return list(d.items()) +else: + # Python 2 + def listvalues(d): + return d.values() + def listitems(d): + return d.items() + +if PY3: + def ensure_new_type(obj): + return obj +else: + def ensure_new_type(obj): + from future.types.newbytes import newbytes + from future.types.newstr import newstr + from future.types.newint import newint + from future.types.newdict import newdict + + native_type = type(native(obj)) + + # Upcast only if the type is already a native (non-future) type + if issubclass(native_type, type(obj)): + # Upcast + if native_type == str: # i.e. Py2 8-bit str + return newbytes(obj) + elif native_type == unicode: + return newstr(obj) + elif native_type == int: + return newint(obj) + elif native_type == long: + return newint(obj) + elif native_type == dict: + return newdict(obj) + else: + return obj + else: + # Already a new type + assert type(obj) in [newbytes, newstr] + return obj + + +__all__ = ['PY2', 'PY26', 'PY3', 'PYPY', + 'as_native_str', 'binary_type', 'bind_method', 'bord', 'bstr', + 'bytes_to_native_str', 'class_types', 'encode_filename', + 'ensure_new_type', 'exec_', 'get_next', 'getexception', + 'implements_iterator', 'integer_types', 'is_new_style', 'isbytes', + 'isidentifier', 'isint', 'isnewbytes', 'istext', 'iteritems', + 'iterkeys', 'itervalues', 'lfilter', 'listitems', 'listvalues', + 'lmap', 'lrange', 'lzip', 'native', 'native_bytes', 'native_str', + 'native_str_to_bytes', 'old_div', + 'python_2_unicode_compatible', 'raise_', + 'raise_with_traceback', 'reraise', 'string_types', + 'text_to_native_str', 'text_type', 'tobytes', 'viewitems', + 'viewkeys', 'viewvalues', 'with_metaclass' + ] diff --git a/venv/lib/python3.10/site-packages/future/utils/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/utils/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..2065d65 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/utils/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/utils/__pycache__/surrogateescape.cpython-310.pyc b/venv/lib/python3.10/site-packages/future/utils/__pycache__/surrogateescape.cpython-310.pyc new file mode 100644 index 0000000..96ac328 Binary files /dev/null and b/venv/lib/python3.10/site-packages/future/utils/__pycache__/surrogateescape.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/future/utils/surrogateescape.py b/venv/lib/python3.10/site-packages/future/utils/surrogateescape.py new file mode 100644 index 0000000..0dcc9fa --- /dev/null +++ b/venv/lib/python3.10/site-packages/future/utils/surrogateescape.py @@ -0,0 +1,198 @@ +""" +This is Victor Stinner's pure-Python implementation of PEP 383: the "surrogateescape" error +handler of Python 3. + +Source: misc/python/surrogateescape.py in https://bitbucket.org/haypo/misc +""" + +# This code is released under the Python license and the BSD 2-clause license + +import codecs +import sys + +from future import utils + + +FS_ERRORS = 'surrogateescape' + +# # -- Python 2/3 compatibility ------------------------------------- +# FS_ERRORS = 'my_surrogateescape' + +def u(text): + if utils.PY3: + return text + else: + return text.decode('unicode_escape') + +def b(data): + if utils.PY3: + return data.encode('latin1') + else: + return data + +if utils.PY3: + _unichr = chr + bytes_chr = lambda code: bytes((code,)) +else: + _unichr = unichr + bytes_chr = chr + +def surrogateescape_handler(exc): + """ + Pure Python implementation of the PEP 383: the "surrogateescape" error + handler of Python 3. Undecodable bytes will be replaced by a Unicode + character U+DCxx on decoding, and these are translated into the + original bytes on encoding. + """ + mystring = exc.object[exc.start:exc.end] + + try: + if isinstance(exc, UnicodeDecodeError): + # mystring is a byte-string in this case + decoded = replace_surrogate_decode(mystring) + elif isinstance(exc, UnicodeEncodeError): + # In the case of u'\udcc3'.encode('ascii', + # 'this_surrogateescape_handler'), both Python 2.x and 3.x raise an + # exception anyway after this function is called, even though I think + # it's doing what it should. It seems that the strict encoder is called + # to encode the unicode string that this function returns ... + decoded = replace_surrogate_encode(mystring) + else: + raise exc + except NotASurrogateError: + raise exc + return (decoded, exc.end) + + +class NotASurrogateError(Exception): + pass + + +def replace_surrogate_encode(mystring): + """ + Returns a (unicode) string, not the more logical bytes, because the codecs + register_error functionality expects this. + """ + decoded = [] + for ch in mystring: + # if utils.PY3: + # code = ch + # else: + code = ord(ch) + + # The following magic comes from Py3.3's Python/codecs.c file: + if not 0xD800 <= code <= 0xDCFF: + # Not a surrogate. Fail with the original exception. + raise NotASurrogateError + # mybytes = [0xe0 | (code >> 12), + # 0x80 | ((code >> 6) & 0x3f), + # 0x80 | (code & 0x3f)] + # Is this a good idea? + if 0xDC00 <= code <= 0xDC7F: + decoded.append(_unichr(code - 0xDC00)) + elif code <= 0xDCFF: + decoded.append(_unichr(code - 0xDC00)) + else: + raise NotASurrogateError + return str().join(decoded) + + +def replace_surrogate_decode(mybytes): + """ + Returns a (unicode) string + """ + decoded = [] + for ch in mybytes: + # We may be parsing newbytes (in which case ch is an int) or a native + # str on Py2 + if isinstance(ch, int): + code = ch + else: + code = ord(ch) + if 0x80 <= code <= 0xFF: + decoded.append(_unichr(0xDC00 + code)) + elif code <= 0x7F: + decoded.append(_unichr(code)) + else: + # # It may be a bad byte + # # Try swallowing it. + # continue + # print("RAISE!") + raise NotASurrogateError + return str().join(decoded) + + +def encodefilename(fn): + if FS_ENCODING == 'ascii': + # ASCII encoder of Python 2 expects that the error handler returns a + # Unicode string encodable to ASCII, whereas our surrogateescape error + # handler has to return bytes in 0x80-0xFF range. + encoded = [] + for index, ch in enumerate(fn): + code = ord(ch) + if code < 128: + ch = bytes_chr(code) + elif 0xDC80 <= code <= 0xDCFF: + ch = bytes_chr(code - 0xDC00) + else: + raise UnicodeEncodeError(FS_ENCODING, + fn, index, index+1, + 'ordinal not in range(128)') + encoded.append(ch) + return bytes().join(encoded) + elif FS_ENCODING == 'utf-8': + # UTF-8 encoder of Python 2 encodes surrogates, so U+DC80-U+DCFF + # doesn't go through our error handler + encoded = [] + for index, ch in enumerate(fn): + code = ord(ch) + if 0xD800 <= code <= 0xDFFF: + if 0xDC80 <= code <= 0xDCFF: + ch = bytes_chr(code - 0xDC00) + encoded.append(ch) + else: + raise UnicodeEncodeError( + FS_ENCODING, + fn, index, index+1, 'surrogates not allowed') + else: + ch_utf8 = ch.encode('utf-8') + encoded.append(ch_utf8) + return bytes().join(encoded) + else: + return fn.encode(FS_ENCODING, FS_ERRORS) + +def decodefilename(fn): + return fn.decode(FS_ENCODING, FS_ERRORS) + +FS_ENCODING = 'ascii'; fn = b('[abc\xff]'); encoded = u('[abc\udcff]') +# FS_ENCODING = 'cp932'; fn = b('[abc\x81\x00]'); encoded = u('[abc\udc81\x00]') +# FS_ENCODING = 'UTF-8'; fn = b('[abc\xff]'); encoded = u('[abc\udcff]') + + +# normalize the filesystem encoding name. +# For example, we expect "utf-8", not "UTF8". +FS_ENCODING = codecs.lookup(FS_ENCODING).name + + +def register_surrogateescape(): + """ + Registers the surrogateescape error handler on Python 2 (only) + """ + if utils.PY3: + return + try: + codecs.lookup_error(FS_ERRORS) + except LookupError: + codecs.register_error(FS_ERRORS, surrogateescape_handler) + + +if __name__ == '__main__': + pass + # # Tests: + # register_surrogateescape() + + # b = decodefilename(fn) + # assert b == encoded, "%r != %r" % (b, encoded) + # c = encodefilename(b) + # assert c == fn, '%r != %r' % (c, fn) + # # print("ok") diff --git a/venv/lib/python3.10/site-packages/idna-3.11.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/idna-3.11.dist-info/METADATA b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/METADATA new file mode 100644 index 0000000..7a4a4b7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/METADATA @@ -0,0 +1,209 @@ +Metadata-Version: 2.4 +Name: idna +Version: 3.11 +Summary: Internationalized Domain Names in Applications (IDNA) +Author-email: Kim Davies +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +License-Expression: BSD-3-Clause +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Programming Language :: Python :: 3.14 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet :: Name Service (DNS) +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Utilities +License-File: LICENSE.md +Requires-Dist: ruff >= 0.6.2 ; extra == "all" +Requires-Dist: mypy >= 1.11.2 ; extra == "all" +Requires-Dist: pytest >= 8.3.2 ; extra == "all" +Requires-Dist: flake8 >= 7.1.1 ; extra == "all" +Project-URL: Changelog, https://github.com/kjd/idna/blob/master/HISTORY.rst +Project-URL: Issue tracker, https://github.com/kjd/idna/issues +Project-URL: Source, https://github.com/kjd/idna +Provides-Extra: all + +Internationalized Domain Names in Applications (IDNA) +===================================================== + +Support for `Internationalized Domain Names in +Applications (IDNA) `_ +and `Unicode IDNA Compatibility Processing +`_. + +The latest versions of these standards supplied here provide +more comprehensive language coverage and reduce the potential of +allowing domains with known security vulnerabilities. This library +is a suitable replacement for the “encodings.idna” +module that comes with the Python standard library, but which +only supports an older superseded IDNA specification from 2003. + +Basic functions are simply executed: + +.. code-block:: pycon + + >>> import idna + >>> idna.encode('ドメイン.テスト') + b'xn--eckwd4c7c.xn--zckzah' + >>> print(idna.decode('xn--eckwd4c7c.xn--zckzah')) + ドメイン.テスト + + +Installation +------------ + +This package is available for installation from PyPI via the +typical mechanisms, such as: + +.. code-block:: bash + + $ python3 -m pip install idna + + +Usage +----- + +For typical usage, the ``encode`` and ``decode`` functions will take a +domain name argument and perform a conversion to ASCII compatible encoding +(known as A-labels), or to Unicode strings (known as U-labels) +respectively. + +.. code-block:: pycon + + >>> import idna + >>> idna.encode('ドメイン.テスト') + b'xn--eckwd4c7c.xn--zckzah' + >>> print(idna.decode('xn--eckwd4c7c.xn--zckzah')) + ドメイン.テスト + +Conversions can be applied at a per-label basis using the ``ulabel`` or +``alabel`` functions if necessary: + +.. code-block:: pycon + + >>> idna.alabel('测试') + b'xn--0zwm56d' + + +Compatibility Mapping (UTS #46) ++++++++++++++++++++++++++++++++ + +This library provides support for `Unicode IDNA Compatibility +Processing `_ which normalizes input from +different potential ways a user may input a domain prior to performing the IDNA +conversion operations. This functionality, known as a +`mapping `_, is considered by the +specification to be a local user-interface issue distinct from IDNA +conversion functionality. + +For example, “Königsgäßchen” is not a permissible label as *LATIN +CAPITAL LETTER K* is not allowed (nor are capital letters in general). +UTS 46 will convert this into lower case prior to applying the IDNA +conversion. + +.. code-block:: pycon + + >>> import idna + >>> idna.encode('Königsgäßchen') + ... + idna.core.InvalidCodepoint: Codepoint U+004B at position 1 of 'Königsgäßchen' not allowed + >>> idna.encode('Königsgäßchen', uts46=True) + b'xn--knigsgchen-b4a3dun' + >>> print(idna.decode('xn--knigsgchen-b4a3dun')) + königsgäßchen + + +Exceptions +---------- + +All errors raised during the conversion following the specification +should raise an exception derived from the ``idna.IDNAError`` base +class. + +More specific exceptions that may be generated as ``idna.IDNABidiError`` +when the error reflects an illegal combination of left-to-right and +right-to-left characters in a label; ``idna.InvalidCodepoint`` when +a specific codepoint is an illegal character in an IDN label (i.e. +INVALID); and ``idna.InvalidCodepointContext`` when the codepoint is +illegal based on its position in the string (i.e. it is CONTEXTO or CONTEXTJ +but the contextual requirements are not satisfied.) + +Building and Diagnostics +------------------------ + +The IDNA and UTS 46 functionality relies upon pre-calculated lookup +tables for performance. These tables are derived from computing against +eligibility criteria in the respective standards using the command-line +script ``tools/idna-data``. + +This tool will fetch relevant codepoint data from the Unicode repository +and perform the required calculations to identify eligibility. There are +three main modes: + +* ``idna-data make-libdata``. Generates ``idnadata.py`` and + ``uts46data.py``, the pre-calculated lookup tables used for IDNA and + UTS 46 conversions. Implementers who wish to track this library against + a different Unicode version may use this tool to manually generate a + different version of the ``idnadata.py`` and ``uts46data.py`` files. + +* ``idna-data make-table``. Generate a table of the IDNA disposition + (e.g. PVALID, CONTEXTJ, CONTEXTO) in the format found in Appendix + B.1 of RFC 5892 and the pre-computed tables published by `IANA + `_. + +* ``idna-data U+0061``. Prints debugging output on the various + properties associated with an individual Unicode codepoint (in this + case, U+0061), that are used to assess the IDNA and UTS 46 status of a + codepoint. This is helpful in debugging or analysis. + +The tool accepts a number of arguments, described using ``idna-data +-h``. Most notably, the ``--version`` argument allows the specification +of the version of Unicode to be used in computing the table data. For +example, ``idna-data --version 9.0.0 make-libdata`` will generate +library data against Unicode 9.0.0. + + +Additional Notes +---------------- + +* **Packages**. The latest tagged release version is published in the + `Python Package Index `_. + +* **Version support**. This library supports Python 3.8 and higher. + As this library serves as a low-level toolkit for a variety of + applications, many of which strive for broad compatibility with older + Python versions, there is no rush to remove older interpreter support. + Support for older versions are likely to be removed from new releases + as automated tests can no longer easily be run, i.e. once the Python + version is officially end-of-life. + +* **Testing**. The library has a test suite based on each rule of the + IDNA specification, as well as tests that are provided as part of the + Unicode Technical Standard 46, `Unicode IDNA Compatibility Processing + `_. + +* **Emoji**. It is an occasional request to support emoji domains in + this library. Encoding of symbols like emoji is expressly prohibited by + the technical standard IDNA 2008 and emoji domains are broadly phased + out across the domain industry due to associated security risks. For + now, applications that need to support these non-compliant labels + may wish to consider trying the encode/decode operation in this library + first, and then falling back to using `encodings.idna`. See `the Github + project `_ for more discussion. + +* **Transitional processing**. Unicode 16.0.0 removed transitional + processing so the `transitional` argument for the encode() method + no longer has any effect and will be removed at a later date. + diff --git a/venv/lib/python3.10/site-packages/idna-3.11.dist-info/RECORD b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/RECORD new file mode 100644 index 0000000..8995942 --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/RECORD @@ -0,0 +1,22 @@ +idna-3.11.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +idna-3.11.dist-info/METADATA,sha256=fCwSww9SuiN8TIHllFSASUQCW55hAs8dzKnr9RaEEbA,8378 +idna-3.11.dist-info/RECORD,, +idna-3.11.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +idna-3.11.dist-info/licenses/LICENSE.md,sha256=t6M2q_OwThgOwGXN0W5wXQeeHMehT5EKpukYfza5zYc,1541 +idna/__init__.py,sha256=MPqNDLZbXqGaNdXxAFhiqFPKEQXju2jNQhCey6-5eJM,868 +idna/__pycache__/__init__.cpython-310.pyc,, +idna/__pycache__/codec.cpython-310.pyc,, +idna/__pycache__/compat.cpython-310.pyc,, +idna/__pycache__/core.cpython-310.pyc,, +idna/__pycache__/idnadata.cpython-310.pyc,, +idna/__pycache__/intranges.cpython-310.pyc,, +idna/__pycache__/package_data.cpython-310.pyc,, +idna/__pycache__/uts46data.cpython-310.pyc,, +idna/codec.py,sha256=M2SGWN7cs_6B32QmKTyTN6xQGZeYQgQ2wiX3_DR6loE,3438 +idna/compat.py,sha256=RzLy6QQCdl9784aFhb2EX9EKGCJjg0P3PilGdeXXcx8,316 +idna/core.py,sha256=P26_XVycuMTZ1R2mNK1ZREVzM5mvTzdabBXfyZVU1Lc,13246 +idna/idnadata.py,sha256=SG8jhaGE53iiD6B49pt2pwTv_UvClciWE-N54oR2p4U,79623 +idna/intranges.py,sha256=amUtkdhYcQG8Zr-CoMM_kVRacxkivC1WgxN1b63KKdU,1898 +idna/package_data.py,sha256=_CUavOxobnbyNG2FLyHoN8QHP3QM9W1tKuw7eq9QwBk,21 +idna/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +idna/uts46data.py,sha256=H9J35VkD0F9L9mKOqjeNGd2A-Va6FlPoz6Jz4K7h-ps,243725 diff --git a/venv/lib/python3.10/site-packages/idna-3.11.dist-info/WHEEL b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.10/site-packages/idna-3.11.dist-info/licenses/LICENSE.md b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/licenses/LICENSE.md new file mode 100644 index 0000000..256ba90 --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna-3.11.dist-info/licenses/LICENSE.md @@ -0,0 +1,31 @@ +BSD 3-Clause License + +Copyright (c) 2013-2025, Kim Davies and contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/venv/lib/python3.10/site-packages/idna/__init__.py b/venv/lib/python3.10/site-packages/idna/__init__.py new file mode 100644 index 0000000..cfdc030 --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna/__init__.py @@ -0,0 +1,45 @@ +from .core import ( + IDNABidiError, + IDNAError, + InvalidCodepoint, + InvalidCodepointContext, + alabel, + check_bidi, + check_hyphen_ok, + check_initial_combiner, + check_label, + check_nfc, + decode, + encode, + ulabel, + uts46_remap, + valid_contextj, + valid_contexto, + valid_label_length, + valid_string_length, +) +from .intranges import intranges_contain +from .package_data import __version__ + +__all__ = [ + "__version__", + "IDNABidiError", + "IDNAError", + "InvalidCodepoint", + "InvalidCodepointContext", + "alabel", + "check_bidi", + "check_hyphen_ok", + "check_initial_combiner", + "check_label", + "check_nfc", + "decode", + "encode", + "intranges_contain", + "ulabel", + "uts46_remap", + "valid_contextj", + "valid_contexto", + "valid_label_length", + "valid_string_length", +] diff --git a/venv/lib/python3.10/site-packages/idna/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/idna/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..9c505b7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/idna/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/idna/__pycache__/codec.cpython-310.pyc b/venv/lib/python3.10/site-packages/idna/__pycache__/codec.cpython-310.pyc new file mode 100644 index 0000000..4cd0935 Binary files /dev/null and b/venv/lib/python3.10/site-packages/idna/__pycache__/codec.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/idna/__pycache__/compat.cpython-310.pyc b/venv/lib/python3.10/site-packages/idna/__pycache__/compat.cpython-310.pyc new file mode 100644 index 0000000..255cdb8 Binary files /dev/null and b/venv/lib/python3.10/site-packages/idna/__pycache__/compat.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/idna/__pycache__/core.cpython-310.pyc b/venv/lib/python3.10/site-packages/idna/__pycache__/core.cpython-310.pyc new file mode 100644 index 0000000..9418220 Binary files /dev/null and b/venv/lib/python3.10/site-packages/idna/__pycache__/core.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/idna/__pycache__/idnadata.cpython-310.pyc b/venv/lib/python3.10/site-packages/idna/__pycache__/idnadata.cpython-310.pyc new file mode 100644 index 0000000..1a435be Binary files /dev/null and b/venv/lib/python3.10/site-packages/idna/__pycache__/idnadata.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/idna/__pycache__/intranges.cpython-310.pyc b/venv/lib/python3.10/site-packages/idna/__pycache__/intranges.cpython-310.pyc new file mode 100644 index 0000000..db5f558 Binary files /dev/null and b/venv/lib/python3.10/site-packages/idna/__pycache__/intranges.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/idna/__pycache__/package_data.cpython-310.pyc b/venv/lib/python3.10/site-packages/idna/__pycache__/package_data.cpython-310.pyc new file mode 100644 index 0000000..542d452 Binary files /dev/null and b/venv/lib/python3.10/site-packages/idna/__pycache__/package_data.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/idna/__pycache__/uts46data.cpython-310.pyc b/venv/lib/python3.10/site-packages/idna/__pycache__/uts46data.cpython-310.pyc new file mode 100644 index 0000000..50f9af2 Binary files /dev/null and b/venv/lib/python3.10/site-packages/idna/__pycache__/uts46data.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/idna/codec.py b/venv/lib/python3.10/site-packages/idna/codec.py new file mode 100644 index 0000000..cbc2e4f --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna/codec.py @@ -0,0 +1,122 @@ +import codecs +import re +from typing import Any, Optional, Tuple + +from .core import IDNAError, alabel, decode, encode, ulabel + +_unicode_dots_re = re.compile("[\u002e\u3002\uff0e\uff61]") + + +class Codec(codecs.Codec): + def encode(self, data: str, errors: str = "strict") -> Tuple[bytes, int]: + if errors != "strict": + raise IDNAError('Unsupported error handling "{}"'.format(errors)) + + if not data: + return b"", 0 + + return encode(data), len(data) + + def decode(self, data: bytes, errors: str = "strict") -> Tuple[str, int]: + if errors != "strict": + raise IDNAError('Unsupported error handling "{}"'.format(errors)) + + if not data: + return "", 0 + + return decode(data), len(data) + + +class IncrementalEncoder(codecs.BufferedIncrementalEncoder): + def _buffer_encode(self, data: str, errors: str, final: bool) -> Tuple[bytes, int]: + if errors != "strict": + raise IDNAError('Unsupported error handling "{}"'.format(errors)) + + if not data: + return b"", 0 + + labels = _unicode_dots_re.split(data) + trailing_dot = b"" + if labels: + if not labels[-1]: + trailing_dot = b"." + del labels[-1] + elif not final: + # Keep potentially unfinished label until the next call + del labels[-1] + if labels: + trailing_dot = b"." + + result = [] + size = 0 + for label in labels: + result.append(alabel(label)) + if size: + size += 1 + size += len(label) + + # Join with U+002E + result_bytes = b".".join(result) + trailing_dot + size += len(trailing_dot) + return result_bytes, size + + +class IncrementalDecoder(codecs.BufferedIncrementalDecoder): + def _buffer_decode(self, data: Any, errors: str, final: bool) -> Tuple[str, int]: + if errors != "strict": + raise IDNAError('Unsupported error handling "{}"'.format(errors)) + + if not data: + return ("", 0) + + if not isinstance(data, str): + data = str(data, "ascii") + + labels = _unicode_dots_re.split(data) + trailing_dot = "" + if labels: + if not labels[-1]: + trailing_dot = "." + del labels[-1] + elif not final: + # Keep potentially unfinished label until the next call + del labels[-1] + if labels: + trailing_dot = "." + + result = [] + size = 0 + for label in labels: + result.append(ulabel(label)) + if size: + size += 1 + size += len(label) + + result_str = ".".join(result) + trailing_dot + size += len(trailing_dot) + return (result_str, size) + + +class StreamWriter(Codec, codecs.StreamWriter): + pass + + +class StreamReader(Codec, codecs.StreamReader): + pass + + +def search_function(name: str) -> Optional[codecs.CodecInfo]: + if name != "idna2008": + return None + return codecs.CodecInfo( + name=name, + encode=Codec().encode, + decode=Codec().decode, # type: ignore + incrementalencoder=IncrementalEncoder, + incrementaldecoder=IncrementalDecoder, + streamwriter=StreamWriter, + streamreader=StreamReader, + ) + + +codecs.register(search_function) diff --git a/venv/lib/python3.10/site-packages/idna/compat.py b/venv/lib/python3.10/site-packages/idna/compat.py new file mode 100644 index 0000000..1df9f2a --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna/compat.py @@ -0,0 +1,15 @@ +from typing import Any, Union + +from .core import decode, encode + + +def ToASCII(label: str) -> bytes: + return encode(label) + + +def ToUnicode(label: Union[bytes, bytearray]) -> str: + return decode(label) + + +def nameprep(s: Any) -> None: + raise NotImplementedError("IDNA 2008 does not utilise nameprep protocol") diff --git a/venv/lib/python3.10/site-packages/idna/core.py b/venv/lib/python3.10/site-packages/idna/core.py new file mode 100644 index 0000000..8177bf7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna/core.py @@ -0,0 +1,437 @@ +import bisect +import re +import unicodedata +from typing import Optional, Union + +from . import idnadata +from .intranges import intranges_contain + +_virama_combining_class = 9 +_alabel_prefix = b"xn--" +_unicode_dots_re = re.compile("[\u002e\u3002\uff0e\uff61]") + + +class IDNAError(UnicodeError): + """Base exception for all IDNA-encoding related problems""" + + pass + + +class IDNABidiError(IDNAError): + """Exception when bidirectional requirements are not satisfied""" + + pass + + +class InvalidCodepoint(IDNAError): + """Exception when a disallowed or unallocated codepoint is used""" + + pass + + +class InvalidCodepointContext(IDNAError): + """Exception when the codepoint is not valid in the context it is used""" + + pass + + +def _combining_class(cp: int) -> int: + v = unicodedata.combining(chr(cp)) + if v == 0: + if not unicodedata.name(chr(cp)): + raise ValueError("Unknown character in unicodedata") + return v + + +def _is_script(cp: str, script: str) -> bool: + return intranges_contain(ord(cp), idnadata.scripts[script]) + + +def _punycode(s: str) -> bytes: + return s.encode("punycode") + + +def _unot(s: int) -> str: + return "U+{:04X}".format(s) + + +def valid_label_length(label: Union[bytes, str]) -> bool: + if len(label) > 63: + return False + return True + + +def valid_string_length(label: Union[bytes, str], trailing_dot: bool) -> bool: + if len(label) > (254 if trailing_dot else 253): + return False + return True + + +def check_bidi(label: str, check_ltr: bool = False) -> bool: + # Bidi rules should only be applied if string contains RTL characters + bidi_label = False + for idx, cp in enumerate(label, 1): + direction = unicodedata.bidirectional(cp) + if direction == "": + # String likely comes from a newer version of Unicode + raise IDNABidiError("Unknown directionality in label {} at position {}".format(repr(label), idx)) + if direction in ["R", "AL", "AN"]: + bidi_label = True + if not bidi_label and not check_ltr: + return True + + # Bidi rule 1 + direction = unicodedata.bidirectional(label[0]) + if direction in ["R", "AL"]: + rtl = True + elif direction == "L": + rtl = False + else: + raise IDNABidiError("First codepoint in label {} must be directionality L, R or AL".format(repr(label))) + + valid_ending = False + number_type: Optional[str] = None + for idx, cp in enumerate(label, 1): + direction = unicodedata.bidirectional(cp) + + if rtl: + # Bidi rule 2 + if direction not in [ + "R", + "AL", + "AN", + "EN", + "ES", + "CS", + "ET", + "ON", + "BN", + "NSM", + ]: + raise IDNABidiError("Invalid direction for codepoint at position {} in a right-to-left label".format(idx)) + # Bidi rule 3 + if direction in ["R", "AL", "EN", "AN"]: + valid_ending = True + elif direction != "NSM": + valid_ending = False + # Bidi rule 4 + if direction in ["AN", "EN"]: + if not number_type: + number_type = direction + else: + if number_type != direction: + raise IDNABidiError("Can not mix numeral types in a right-to-left label") + else: + # Bidi rule 5 + if direction not in ["L", "EN", "ES", "CS", "ET", "ON", "BN", "NSM"]: + raise IDNABidiError("Invalid direction for codepoint at position {} in a left-to-right label".format(idx)) + # Bidi rule 6 + if direction in ["L", "EN"]: + valid_ending = True + elif direction != "NSM": + valid_ending = False + + if not valid_ending: + raise IDNABidiError("Label ends with illegal codepoint directionality") + + return True + + +def check_initial_combiner(label: str) -> bool: + if unicodedata.category(label[0])[0] == "M": + raise IDNAError("Label begins with an illegal combining character") + return True + + +def check_hyphen_ok(label: str) -> bool: + if label[2:4] == "--": + raise IDNAError("Label has disallowed hyphens in 3rd and 4th position") + if label[0] == "-" or label[-1] == "-": + raise IDNAError("Label must not start or end with a hyphen") + return True + + +def check_nfc(label: str) -> None: + if unicodedata.normalize("NFC", label) != label: + raise IDNAError("Label must be in Normalization Form C") + + +def valid_contextj(label: str, pos: int) -> bool: + cp_value = ord(label[pos]) + + if cp_value == 0x200C: + if pos > 0: + if _combining_class(ord(label[pos - 1])) == _virama_combining_class: + return True + + ok = False + for i in range(pos - 1, -1, -1): + joining_type = idnadata.joining_types.get(ord(label[i])) + if joining_type == ord("T"): + continue + elif joining_type in [ord("L"), ord("D")]: + ok = True + break + else: + break + + if not ok: + return False + + ok = False + for i in range(pos + 1, len(label)): + joining_type = idnadata.joining_types.get(ord(label[i])) + if joining_type == ord("T"): + continue + elif joining_type in [ord("R"), ord("D")]: + ok = True + break + else: + break + return ok + + if cp_value == 0x200D: + if pos > 0: + if _combining_class(ord(label[pos - 1])) == _virama_combining_class: + return True + return False + + else: + return False + + +def valid_contexto(label: str, pos: int, exception: bool = False) -> bool: + cp_value = ord(label[pos]) + + if cp_value == 0x00B7: + if 0 < pos < len(label) - 1: + if ord(label[pos - 1]) == 0x006C and ord(label[pos + 1]) == 0x006C: + return True + return False + + elif cp_value == 0x0375: + if pos < len(label) - 1 and len(label) > 1: + return _is_script(label[pos + 1], "Greek") + return False + + elif cp_value == 0x05F3 or cp_value == 0x05F4: + if pos > 0: + return _is_script(label[pos - 1], "Hebrew") + return False + + elif cp_value == 0x30FB: + for cp in label: + if cp == "\u30fb": + continue + if _is_script(cp, "Hiragana") or _is_script(cp, "Katakana") or _is_script(cp, "Han"): + return True + return False + + elif 0x660 <= cp_value <= 0x669: + for cp in label: + if 0x6F0 <= ord(cp) <= 0x06F9: + return False + return True + + elif 0x6F0 <= cp_value <= 0x6F9: + for cp in label: + if 0x660 <= ord(cp) <= 0x0669: + return False + return True + + return False + + +def check_label(label: Union[str, bytes, bytearray]) -> None: + if isinstance(label, (bytes, bytearray)): + label = label.decode("utf-8") + if len(label) == 0: + raise IDNAError("Empty Label") + + check_nfc(label) + check_hyphen_ok(label) + check_initial_combiner(label) + + for pos, cp in enumerate(label): + cp_value = ord(cp) + if intranges_contain(cp_value, idnadata.codepoint_classes["PVALID"]): + continue + elif intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTJ"]): + try: + if not valid_contextj(label, pos): + raise InvalidCodepointContext( + "Joiner {} not allowed at position {} in {}".format(_unot(cp_value), pos + 1, repr(label)) + ) + except ValueError: + raise IDNAError( + "Unknown codepoint adjacent to joiner {} at position {} in {}".format( + _unot(cp_value), pos + 1, repr(label) + ) + ) + elif intranges_contain(cp_value, idnadata.codepoint_classes["CONTEXTO"]): + if not valid_contexto(label, pos): + raise InvalidCodepointContext( + "Codepoint {} not allowed at position {} in {}".format(_unot(cp_value), pos + 1, repr(label)) + ) + else: + raise InvalidCodepoint( + "Codepoint {} at position {} of {} not allowed".format(_unot(cp_value), pos + 1, repr(label)) + ) + + check_bidi(label) + + +def alabel(label: str) -> bytes: + try: + label_bytes = label.encode("ascii") + ulabel(label_bytes) + if not valid_label_length(label_bytes): + raise IDNAError("Label too long") + return label_bytes + except UnicodeEncodeError: + pass + + check_label(label) + label_bytes = _alabel_prefix + _punycode(label) + + if not valid_label_length(label_bytes): + raise IDNAError("Label too long") + + return label_bytes + + +def ulabel(label: Union[str, bytes, bytearray]) -> str: + if not isinstance(label, (bytes, bytearray)): + try: + label_bytes = label.encode("ascii") + except UnicodeEncodeError: + check_label(label) + return label + else: + label_bytes = bytes(label) + + label_bytes = label_bytes.lower() + if label_bytes.startswith(_alabel_prefix): + label_bytes = label_bytes[len(_alabel_prefix) :] + if not label_bytes: + raise IDNAError("Malformed A-label, no Punycode eligible content found") + if label_bytes.decode("ascii")[-1] == "-": + raise IDNAError("A-label must not end with a hyphen") + else: + check_label(label_bytes) + return label_bytes.decode("ascii") + + try: + label = label_bytes.decode("punycode") + except UnicodeError: + raise IDNAError("Invalid A-label") + check_label(label) + return label + + +def uts46_remap(domain: str, std3_rules: bool = True, transitional: bool = False) -> str: + """Re-map the characters in the string according to UTS46 processing.""" + from .uts46data import uts46data + + output = "" + + for pos, char in enumerate(domain): + code_point = ord(char) + try: + uts46row = uts46data[code_point if code_point < 256 else bisect.bisect_left(uts46data, (code_point, "Z")) - 1] + status = uts46row[1] + replacement: Optional[str] = None + if len(uts46row) == 3: + replacement = uts46row[2] + if ( + status == "V" + or (status == "D" and not transitional) + or (status == "3" and not std3_rules and replacement is None) + ): + output += char + elif replacement is not None and ( + status == "M" or (status == "3" and not std3_rules) or (status == "D" and transitional) + ): + output += replacement + elif status != "I": + raise IndexError() + except IndexError: + raise InvalidCodepoint( + "Codepoint {} not allowed at position {} in {}".format(_unot(code_point), pos + 1, repr(domain)) + ) + + return unicodedata.normalize("NFC", output) + + +def encode( + s: Union[str, bytes, bytearray], + strict: bool = False, + uts46: bool = False, + std3_rules: bool = False, + transitional: bool = False, +) -> bytes: + if not isinstance(s, str): + try: + s = str(s, "ascii") + except UnicodeDecodeError: + raise IDNAError("should pass a unicode string to the function rather than a byte string.") + if uts46: + s = uts46_remap(s, std3_rules, transitional) + trailing_dot = False + result = [] + if strict: + labels = s.split(".") + else: + labels = _unicode_dots_re.split(s) + if not labels or labels == [""]: + raise IDNAError("Empty domain") + if labels[-1] == "": + del labels[-1] + trailing_dot = True + for label in labels: + s = alabel(label) + if s: + result.append(s) + else: + raise IDNAError("Empty label") + if trailing_dot: + result.append(b"") + s = b".".join(result) + if not valid_string_length(s, trailing_dot): + raise IDNAError("Domain too long") + return s + + +def decode( + s: Union[str, bytes, bytearray], + strict: bool = False, + uts46: bool = False, + std3_rules: bool = False, +) -> str: + try: + if not isinstance(s, str): + s = str(s, "ascii") + except UnicodeDecodeError: + raise IDNAError("Invalid ASCII in A-label") + if uts46: + s = uts46_remap(s, std3_rules, False) + trailing_dot = False + result = [] + if not strict: + labels = _unicode_dots_re.split(s) + else: + labels = s.split(".") + if not labels or labels == [""]: + raise IDNAError("Empty domain") + if not labels[-1]: + del labels[-1] + trailing_dot = True + for label in labels: + s = ulabel(label) + if s: + result.append(s) + else: + raise IDNAError("Empty label") + if trailing_dot: + result.append("") + return ".".join(result) diff --git a/venv/lib/python3.10/site-packages/idna/idnadata.py b/venv/lib/python3.10/site-packages/idna/idnadata.py new file mode 100644 index 0000000..ded47ca --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna/idnadata.py @@ -0,0 +1,4309 @@ +# This file is automatically generated by tools/idna-data + +__version__ = "16.0.0" + +scripts = { + "Greek": ( + 0x37000000374, + 0x37500000378, + 0x37A0000037E, + 0x37F00000380, + 0x38400000385, + 0x38600000387, + 0x3880000038B, + 0x38C0000038D, + 0x38E000003A2, + 0x3A3000003E2, + 0x3F000000400, + 0x1D2600001D2B, + 0x1D5D00001D62, + 0x1D6600001D6B, + 0x1DBF00001DC0, + 0x1F0000001F16, + 0x1F1800001F1E, + 0x1F2000001F46, + 0x1F4800001F4E, + 0x1F5000001F58, + 0x1F5900001F5A, + 0x1F5B00001F5C, + 0x1F5D00001F5E, + 0x1F5F00001F7E, + 0x1F8000001FB5, + 0x1FB600001FC5, + 0x1FC600001FD4, + 0x1FD600001FDC, + 0x1FDD00001FF0, + 0x1FF200001FF5, + 0x1FF600001FFF, + 0x212600002127, + 0xAB650000AB66, + 0x101400001018F, + 0x101A0000101A1, + 0x1D2000001D246, + ), + "Han": ( + 0x2E8000002E9A, + 0x2E9B00002EF4, + 0x2F0000002FD6, + 0x300500003006, + 0x300700003008, + 0x30210000302A, + 0x30380000303C, + 0x340000004DC0, + 0x4E000000A000, + 0xF9000000FA6E, + 0xFA700000FADA, + 0x16FE200016FE4, + 0x16FF000016FF2, + 0x200000002A6E0, + 0x2A7000002B73A, + 0x2B7400002B81E, + 0x2B8200002CEA2, + 0x2CEB00002EBE1, + 0x2EBF00002EE5E, + 0x2F8000002FA1E, + 0x300000003134B, + 0x31350000323B0, + ), + "Hebrew": ( + 0x591000005C8, + 0x5D0000005EB, + 0x5EF000005F5, + 0xFB1D0000FB37, + 0xFB380000FB3D, + 0xFB3E0000FB3F, + 0xFB400000FB42, + 0xFB430000FB45, + 0xFB460000FB50, + ), + "Hiragana": ( + 0x304100003097, + 0x309D000030A0, + 0x1B0010001B120, + 0x1B1320001B133, + 0x1B1500001B153, + 0x1F2000001F201, + ), + "Katakana": ( + 0x30A1000030FB, + 0x30FD00003100, + 0x31F000003200, + 0x32D0000032FF, + 0x330000003358, + 0xFF660000FF70, + 0xFF710000FF9E, + 0x1AFF00001AFF4, + 0x1AFF50001AFFC, + 0x1AFFD0001AFFF, + 0x1B0000001B001, + 0x1B1200001B123, + 0x1B1550001B156, + 0x1B1640001B168, + ), +} +joining_types = { + 0xAD: 84, + 0x300: 84, + 0x301: 84, + 0x302: 84, + 0x303: 84, + 0x304: 84, + 0x305: 84, + 0x306: 84, + 0x307: 84, + 0x308: 84, + 0x309: 84, + 0x30A: 84, + 0x30B: 84, + 0x30C: 84, + 0x30D: 84, + 0x30E: 84, + 0x30F: 84, + 0x310: 84, + 0x311: 84, + 0x312: 84, + 0x313: 84, + 0x314: 84, + 0x315: 84, + 0x316: 84, + 0x317: 84, + 0x318: 84, + 0x319: 84, + 0x31A: 84, + 0x31B: 84, + 0x31C: 84, + 0x31D: 84, + 0x31E: 84, + 0x31F: 84, + 0x320: 84, + 0x321: 84, + 0x322: 84, + 0x323: 84, + 0x324: 84, + 0x325: 84, + 0x326: 84, + 0x327: 84, + 0x328: 84, + 0x329: 84, + 0x32A: 84, + 0x32B: 84, + 0x32C: 84, + 0x32D: 84, + 0x32E: 84, + 0x32F: 84, + 0x330: 84, + 0x331: 84, + 0x332: 84, + 0x333: 84, + 0x334: 84, + 0x335: 84, + 0x336: 84, + 0x337: 84, + 0x338: 84, + 0x339: 84, + 0x33A: 84, + 0x33B: 84, + 0x33C: 84, + 0x33D: 84, + 0x33E: 84, + 0x33F: 84, + 0x340: 84, + 0x341: 84, + 0x342: 84, + 0x343: 84, + 0x344: 84, + 0x345: 84, + 0x346: 84, + 0x347: 84, + 0x348: 84, + 0x349: 84, + 0x34A: 84, + 0x34B: 84, + 0x34C: 84, + 0x34D: 84, + 0x34E: 84, + 0x34F: 84, + 0x350: 84, + 0x351: 84, + 0x352: 84, + 0x353: 84, + 0x354: 84, + 0x355: 84, + 0x356: 84, + 0x357: 84, + 0x358: 84, + 0x359: 84, + 0x35A: 84, + 0x35B: 84, + 0x35C: 84, + 0x35D: 84, + 0x35E: 84, + 0x35F: 84, + 0x360: 84, + 0x361: 84, + 0x362: 84, + 0x363: 84, + 0x364: 84, + 0x365: 84, + 0x366: 84, + 0x367: 84, + 0x368: 84, + 0x369: 84, + 0x36A: 84, + 0x36B: 84, + 0x36C: 84, + 0x36D: 84, + 0x36E: 84, + 0x36F: 84, + 0x483: 84, + 0x484: 84, + 0x485: 84, + 0x486: 84, + 0x487: 84, + 0x488: 84, + 0x489: 84, + 0x591: 84, + 0x592: 84, + 0x593: 84, + 0x594: 84, + 0x595: 84, + 0x596: 84, + 0x597: 84, + 0x598: 84, + 0x599: 84, + 0x59A: 84, + 0x59B: 84, + 0x59C: 84, + 0x59D: 84, + 0x59E: 84, + 0x59F: 84, + 0x5A0: 84, + 0x5A1: 84, + 0x5A2: 84, + 0x5A3: 84, + 0x5A4: 84, + 0x5A5: 84, + 0x5A6: 84, + 0x5A7: 84, + 0x5A8: 84, + 0x5A9: 84, + 0x5AA: 84, + 0x5AB: 84, + 0x5AC: 84, + 0x5AD: 84, + 0x5AE: 84, + 0x5AF: 84, + 0x5B0: 84, + 0x5B1: 84, + 0x5B2: 84, + 0x5B3: 84, + 0x5B4: 84, + 0x5B5: 84, + 0x5B6: 84, + 0x5B7: 84, + 0x5B8: 84, + 0x5B9: 84, + 0x5BA: 84, + 0x5BB: 84, + 0x5BC: 84, + 0x5BD: 84, + 0x5BF: 84, + 0x5C1: 84, + 0x5C2: 84, + 0x5C4: 84, + 0x5C5: 84, + 0x5C7: 84, + 0x610: 84, + 0x611: 84, + 0x612: 84, + 0x613: 84, + 0x614: 84, + 0x615: 84, + 0x616: 84, + 0x617: 84, + 0x618: 84, + 0x619: 84, + 0x61A: 84, + 0x61C: 84, + 0x620: 68, + 0x622: 82, + 0x623: 82, + 0x624: 82, + 0x625: 82, + 0x626: 68, + 0x627: 82, + 0x628: 68, + 0x629: 82, + 0x62A: 68, + 0x62B: 68, + 0x62C: 68, + 0x62D: 68, + 0x62E: 68, + 0x62F: 82, + 0x630: 82, + 0x631: 82, + 0x632: 82, + 0x633: 68, + 0x634: 68, + 0x635: 68, + 0x636: 68, + 0x637: 68, + 0x638: 68, + 0x639: 68, + 0x63A: 68, + 0x63B: 68, + 0x63C: 68, + 0x63D: 68, + 0x63E: 68, + 0x63F: 68, + 0x640: 67, + 0x641: 68, + 0x642: 68, + 0x643: 68, + 0x644: 68, + 0x645: 68, + 0x646: 68, + 0x647: 68, + 0x648: 82, + 0x649: 68, + 0x64A: 68, + 0x64B: 84, + 0x64C: 84, + 0x64D: 84, + 0x64E: 84, + 0x64F: 84, + 0x650: 84, + 0x651: 84, + 0x652: 84, + 0x653: 84, + 0x654: 84, + 0x655: 84, + 0x656: 84, + 0x657: 84, + 0x658: 84, + 0x659: 84, + 0x65A: 84, + 0x65B: 84, + 0x65C: 84, + 0x65D: 84, + 0x65E: 84, + 0x65F: 84, + 0x66E: 68, + 0x66F: 68, + 0x670: 84, + 0x671: 82, + 0x672: 82, + 0x673: 82, + 0x675: 82, + 0x676: 82, + 0x677: 82, + 0x678: 68, + 0x679: 68, + 0x67A: 68, + 0x67B: 68, + 0x67C: 68, + 0x67D: 68, + 0x67E: 68, + 0x67F: 68, + 0x680: 68, + 0x681: 68, + 0x682: 68, + 0x683: 68, + 0x684: 68, + 0x685: 68, + 0x686: 68, + 0x687: 68, + 0x688: 82, + 0x689: 82, + 0x68A: 82, + 0x68B: 82, + 0x68C: 82, + 0x68D: 82, + 0x68E: 82, + 0x68F: 82, + 0x690: 82, + 0x691: 82, + 0x692: 82, + 0x693: 82, + 0x694: 82, + 0x695: 82, + 0x696: 82, + 0x697: 82, + 0x698: 82, + 0x699: 82, + 0x69A: 68, + 0x69B: 68, + 0x69C: 68, + 0x69D: 68, + 0x69E: 68, + 0x69F: 68, + 0x6A0: 68, + 0x6A1: 68, + 0x6A2: 68, + 0x6A3: 68, + 0x6A4: 68, + 0x6A5: 68, + 0x6A6: 68, + 0x6A7: 68, + 0x6A8: 68, + 0x6A9: 68, + 0x6AA: 68, + 0x6AB: 68, + 0x6AC: 68, + 0x6AD: 68, + 0x6AE: 68, + 0x6AF: 68, + 0x6B0: 68, + 0x6B1: 68, + 0x6B2: 68, + 0x6B3: 68, + 0x6B4: 68, + 0x6B5: 68, + 0x6B6: 68, + 0x6B7: 68, + 0x6B8: 68, + 0x6B9: 68, + 0x6BA: 68, + 0x6BB: 68, + 0x6BC: 68, + 0x6BD: 68, + 0x6BE: 68, + 0x6BF: 68, + 0x6C0: 82, + 0x6C1: 68, + 0x6C2: 68, + 0x6C3: 82, + 0x6C4: 82, + 0x6C5: 82, + 0x6C6: 82, + 0x6C7: 82, + 0x6C8: 82, + 0x6C9: 82, + 0x6CA: 82, + 0x6CB: 82, + 0x6CC: 68, + 0x6CD: 82, + 0x6CE: 68, + 0x6CF: 82, + 0x6D0: 68, + 0x6D1: 68, + 0x6D2: 82, + 0x6D3: 82, + 0x6D5: 82, + 0x6D6: 84, + 0x6D7: 84, + 0x6D8: 84, + 0x6D9: 84, + 0x6DA: 84, + 0x6DB: 84, + 0x6DC: 84, + 0x6DF: 84, + 0x6E0: 84, + 0x6E1: 84, + 0x6E2: 84, + 0x6E3: 84, + 0x6E4: 84, + 0x6E7: 84, + 0x6E8: 84, + 0x6EA: 84, + 0x6EB: 84, + 0x6EC: 84, + 0x6ED: 84, + 0x6EE: 82, + 0x6EF: 82, + 0x6FA: 68, + 0x6FB: 68, + 0x6FC: 68, + 0x6FF: 68, + 0x70F: 84, + 0x710: 82, + 0x711: 84, + 0x712: 68, + 0x713: 68, + 0x714: 68, + 0x715: 82, + 0x716: 82, + 0x717: 82, + 0x718: 82, + 0x719: 82, + 0x71A: 68, + 0x71B: 68, + 0x71C: 68, + 0x71D: 68, + 0x71E: 82, + 0x71F: 68, + 0x720: 68, + 0x721: 68, + 0x722: 68, + 0x723: 68, + 0x724: 68, + 0x725: 68, + 0x726: 68, + 0x727: 68, + 0x728: 82, + 0x729: 68, + 0x72A: 82, + 0x72B: 68, + 0x72C: 82, + 0x72D: 68, + 0x72E: 68, + 0x72F: 82, + 0x730: 84, + 0x731: 84, + 0x732: 84, + 0x733: 84, + 0x734: 84, + 0x735: 84, + 0x736: 84, + 0x737: 84, + 0x738: 84, + 0x739: 84, + 0x73A: 84, + 0x73B: 84, + 0x73C: 84, + 0x73D: 84, + 0x73E: 84, + 0x73F: 84, + 0x740: 84, + 0x741: 84, + 0x742: 84, + 0x743: 84, + 0x744: 84, + 0x745: 84, + 0x746: 84, + 0x747: 84, + 0x748: 84, + 0x749: 84, + 0x74A: 84, + 0x74D: 82, + 0x74E: 68, + 0x74F: 68, + 0x750: 68, + 0x751: 68, + 0x752: 68, + 0x753: 68, + 0x754: 68, + 0x755: 68, + 0x756: 68, + 0x757: 68, + 0x758: 68, + 0x759: 82, + 0x75A: 82, + 0x75B: 82, + 0x75C: 68, + 0x75D: 68, + 0x75E: 68, + 0x75F: 68, + 0x760: 68, + 0x761: 68, + 0x762: 68, + 0x763: 68, + 0x764: 68, + 0x765: 68, + 0x766: 68, + 0x767: 68, + 0x768: 68, + 0x769: 68, + 0x76A: 68, + 0x76B: 82, + 0x76C: 82, + 0x76D: 68, + 0x76E: 68, + 0x76F: 68, + 0x770: 68, + 0x771: 82, + 0x772: 68, + 0x773: 82, + 0x774: 82, + 0x775: 68, + 0x776: 68, + 0x777: 68, + 0x778: 82, + 0x779: 82, + 0x77A: 68, + 0x77B: 68, + 0x77C: 68, + 0x77D: 68, + 0x77E: 68, + 0x77F: 68, + 0x7A6: 84, + 0x7A7: 84, + 0x7A8: 84, + 0x7A9: 84, + 0x7AA: 84, + 0x7AB: 84, + 0x7AC: 84, + 0x7AD: 84, + 0x7AE: 84, + 0x7AF: 84, + 0x7B0: 84, + 0x7CA: 68, + 0x7CB: 68, + 0x7CC: 68, + 0x7CD: 68, + 0x7CE: 68, + 0x7CF: 68, + 0x7D0: 68, + 0x7D1: 68, + 0x7D2: 68, + 0x7D3: 68, + 0x7D4: 68, + 0x7D5: 68, + 0x7D6: 68, + 0x7D7: 68, + 0x7D8: 68, + 0x7D9: 68, + 0x7DA: 68, + 0x7DB: 68, + 0x7DC: 68, + 0x7DD: 68, + 0x7DE: 68, + 0x7DF: 68, + 0x7E0: 68, + 0x7E1: 68, + 0x7E2: 68, + 0x7E3: 68, + 0x7E4: 68, + 0x7E5: 68, + 0x7E6: 68, + 0x7E7: 68, + 0x7E8: 68, + 0x7E9: 68, + 0x7EA: 68, + 0x7EB: 84, + 0x7EC: 84, + 0x7ED: 84, + 0x7EE: 84, + 0x7EF: 84, + 0x7F0: 84, + 0x7F1: 84, + 0x7F2: 84, + 0x7F3: 84, + 0x7FA: 67, + 0x7FD: 84, + 0x816: 84, + 0x817: 84, + 0x818: 84, + 0x819: 84, + 0x81B: 84, + 0x81C: 84, + 0x81D: 84, + 0x81E: 84, + 0x81F: 84, + 0x820: 84, + 0x821: 84, + 0x822: 84, + 0x823: 84, + 0x825: 84, + 0x826: 84, + 0x827: 84, + 0x829: 84, + 0x82A: 84, + 0x82B: 84, + 0x82C: 84, + 0x82D: 84, + 0x840: 82, + 0x841: 68, + 0x842: 68, + 0x843: 68, + 0x844: 68, + 0x845: 68, + 0x846: 82, + 0x847: 82, + 0x848: 68, + 0x849: 82, + 0x84A: 68, + 0x84B: 68, + 0x84C: 68, + 0x84D: 68, + 0x84E: 68, + 0x84F: 68, + 0x850: 68, + 0x851: 68, + 0x852: 68, + 0x853: 68, + 0x854: 82, + 0x855: 68, + 0x856: 82, + 0x857: 82, + 0x858: 82, + 0x859: 84, + 0x85A: 84, + 0x85B: 84, + 0x860: 68, + 0x862: 68, + 0x863: 68, + 0x864: 68, + 0x865: 68, + 0x867: 82, + 0x868: 68, + 0x869: 82, + 0x86A: 82, + 0x870: 82, + 0x871: 82, + 0x872: 82, + 0x873: 82, + 0x874: 82, + 0x875: 82, + 0x876: 82, + 0x877: 82, + 0x878: 82, + 0x879: 82, + 0x87A: 82, + 0x87B: 82, + 0x87C: 82, + 0x87D: 82, + 0x87E: 82, + 0x87F: 82, + 0x880: 82, + 0x881: 82, + 0x882: 82, + 0x883: 67, + 0x884: 67, + 0x885: 67, + 0x886: 68, + 0x889: 68, + 0x88A: 68, + 0x88B: 68, + 0x88C: 68, + 0x88D: 68, + 0x88E: 82, + 0x897: 84, + 0x898: 84, + 0x899: 84, + 0x89A: 84, + 0x89B: 84, + 0x89C: 84, + 0x89D: 84, + 0x89E: 84, + 0x89F: 84, + 0x8A0: 68, + 0x8A1: 68, + 0x8A2: 68, + 0x8A3: 68, + 0x8A4: 68, + 0x8A5: 68, + 0x8A6: 68, + 0x8A7: 68, + 0x8A8: 68, + 0x8A9: 68, + 0x8AA: 82, + 0x8AB: 82, + 0x8AC: 82, + 0x8AE: 82, + 0x8AF: 68, + 0x8B0: 68, + 0x8B1: 82, + 0x8B2: 82, + 0x8B3: 68, + 0x8B4: 68, + 0x8B5: 68, + 0x8B6: 68, + 0x8B7: 68, + 0x8B8: 68, + 0x8B9: 82, + 0x8BA: 68, + 0x8BB: 68, + 0x8BC: 68, + 0x8BD: 68, + 0x8BE: 68, + 0x8BF: 68, + 0x8C0: 68, + 0x8C1: 68, + 0x8C2: 68, + 0x8C3: 68, + 0x8C4: 68, + 0x8C5: 68, + 0x8C6: 68, + 0x8C7: 68, + 0x8C8: 68, + 0x8CA: 84, + 0x8CB: 84, + 0x8CC: 84, + 0x8CD: 84, + 0x8CE: 84, + 0x8CF: 84, + 0x8D0: 84, + 0x8D1: 84, + 0x8D2: 84, + 0x8D3: 84, + 0x8D4: 84, + 0x8D5: 84, + 0x8D6: 84, + 0x8D7: 84, + 0x8D8: 84, + 0x8D9: 84, + 0x8DA: 84, + 0x8DB: 84, + 0x8DC: 84, + 0x8DD: 84, + 0x8DE: 84, + 0x8DF: 84, + 0x8E0: 84, + 0x8E1: 84, + 0x8E3: 84, + 0x8E4: 84, + 0x8E5: 84, + 0x8E6: 84, + 0x8E7: 84, + 0x8E8: 84, + 0x8E9: 84, + 0x8EA: 84, + 0x8EB: 84, + 0x8EC: 84, + 0x8ED: 84, + 0x8EE: 84, + 0x8EF: 84, + 0x8F0: 84, + 0x8F1: 84, + 0x8F2: 84, + 0x8F3: 84, + 0x8F4: 84, + 0x8F5: 84, + 0x8F6: 84, + 0x8F7: 84, + 0x8F8: 84, + 0x8F9: 84, + 0x8FA: 84, + 0x8FB: 84, + 0x8FC: 84, + 0x8FD: 84, + 0x8FE: 84, + 0x8FF: 84, + 0x900: 84, + 0x901: 84, + 0x902: 84, + 0x93A: 84, + 0x93C: 84, + 0x941: 84, + 0x942: 84, + 0x943: 84, + 0x944: 84, + 0x945: 84, + 0x946: 84, + 0x947: 84, + 0x948: 84, + 0x94D: 84, + 0x951: 84, + 0x952: 84, + 0x953: 84, + 0x954: 84, + 0x955: 84, + 0x956: 84, + 0x957: 84, + 0x962: 84, + 0x963: 84, + 0x981: 84, + 0x9BC: 84, + 0x9C1: 84, + 0x9C2: 84, + 0x9C3: 84, + 0x9C4: 84, + 0x9CD: 84, + 0x9E2: 84, + 0x9E3: 84, + 0x9FE: 84, + 0xA01: 84, + 0xA02: 84, + 0xA3C: 84, + 0xA41: 84, + 0xA42: 84, + 0xA47: 84, + 0xA48: 84, + 0xA4B: 84, + 0xA4C: 84, + 0xA4D: 84, + 0xA51: 84, + 0xA70: 84, + 0xA71: 84, + 0xA75: 84, + 0xA81: 84, + 0xA82: 84, + 0xABC: 84, + 0xAC1: 84, + 0xAC2: 84, + 0xAC3: 84, + 0xAC4: 84, + 0xAC5: 84, + 0xAC7: 84, + 0xAC8: 84, + 0xACD: 84, + 0xAE2: 84, + 0xAE3: 84, + 0xAFA: 84, + 0xAFB: 84, + 0xAFC: 84, + 0xAFD: 84, + 0xAFE: 84, + 0xAFF: 84, + 0xB01: 84, + 0xB3C: 84, + 0xB3F: 84, + 0xB41: 84, + 0xB42: 84, + 0xB43: 84, + 0xB44: 84, + 0xB4D: 84, + 0xB55: 84, + 0xB56: 84, + 0xB62: 84, + 0xB63: 84, + 0xB82: 84, + 0xBC0: 84, + 0xBCD: 84, + 0xC00: 84, + 0xC04: 84, + 0xC3C: 84, + 0xC3E: 84, + 0xC3F: 84, + 0xC40: 84, + 0xC46: 84, + 0xC47: 84, + 0xC48: 84, + 0xC4A: 84, + 0xC4B: 84, + 0xC4C: 84, + 0xC4D: 84, + 0xC55: 84, + 0xC56: 84, + 0xC62: 84, + 0xC63: 84, + 0xC81: 84, + 0xCBC: 84, + 0xCBF: 84, + 0xCC6: 84, + 0xCCC: 84, + 0xCCD: 84, + 0xCE2: 84, + 0xCE3: 84, + 0xD00: 84, + 0xD01: 84, + 0xD3B: 84, + 0xD3C: 84, + 0xD41: 84, + 0xD42: 84, + 0xD43: 84, + 0xD44: 84, + 0xD4D: 84, + 0xD62: 84, + 0xD63: 84, + 0xD81: 84, + 0xDCA: 84, + 0xDD2: 84, + 0xDD3: 84, + 0xDD4: 84, + 0xDD6: 84, + 0xE31: 84, + 0xE34: 84, + 0xE35: 84, + 0xE36: 84, + 0xE37: 84, + 0xE38: 84, + 0xE39: 84, + 0xE3A: 84, + 0xE47: 84, + 0xE48: 84, + 0xE49: 84, + 0xE4A: 84, + 0xE4B: 84, + 0xE4C: 84, + 0xE4D: 84, + 0xE4E: 84, + 0xEB1: 84, + 0xEB4: 84, + 0xEB5: 84, + 0xEB6: 84, + 0xEB7: 84, + 0xEB8: 84, + 0xEB9: 84, + 0xEBA: 84, + 0xEBB: 84, + 0xEBC: 84, + 0xEC8: 84, + 0xEC9: 84, + 0xECA: 84, + 0xECB: 84, + 0xECC: 84, + 0xECD: 84, + 0xECE: 84, + 0xF18: 84, + 0xF19: 84, + 0xF35: 84, + 0xF37: 84, + 0xF39: 84, + 0xF71: 84, + 0xF72: 84, + 0xF73: 84, + 0xF74: 84, + 0xF75: 84, + 0xF76: 84, + 0xF77: 84, + 0xF78: 84, + 0xF79: 84, + 0xF7A: 84, + 0xF7B: 84, + 0xF7C: 84, + 0xF7D: 84, + 0xF7E: 84, + 0xF80: 84, + 0xF81: 84, + 0xF82: 84, + 0xF83: 84, + 0xF84: 84, + 0xF86: 84, + 0xF87: 84, + 0xF8D: 84, + 0xF8E: 84, + 0xF8F: 84, + 0xF90: 84, + 0xF91: 84, + 0xF92: 84, + 0xF93: 84, + 0xF94: 84, + 0xF95: 84, + 0xF96: 84, + 0xF97: 84, + 0xF99: 84, + 0xF9A: 84, + 0xF9B: 84, + 0xF9C: 84, + 0xF9D: 84, + 0xF9E: 84, + 0xF9F: 84, + 0xFA0: 84, + 0xFA1: 84, + 0xFA2: 84, + 0xFA3: 84, + 0xFA4: 84, + 0xFA5: 84, + 0xFA6: 84, + 0xFA7: 84, + 0xFA8: 84, + 0xFA9: 84, + 0xFAA: 84, + 0xFAB: 84, + 0xFAC: 84, + 0xFAD: 84, + 0xFAE: 84, + 0xFAF: 84, + 0xFB0: 84, + 0xFB1: 84, + 0xFB2: 84, + 0xFB3: 84, + 0xFB4: 84, + 0xFB5: 84, + 0xFB6: 84, + 0xFB7: 84, + 0xFB8: 84, + 0xFB9: 84, + 0xFBA: 84, + 0xFBB: 84, + 0xFBC: 84, + 0xFC6: 84, + 0x102D: 84, + 0x102E: 84, + 0x102F: 84, + 0x1030: 84, + 0x1032: 84, + 0x1033: 84, + 0x1034: 84, + 0x1035: 84, + 0x1036: 84, + 0x1037: 84, + 0x1039: 84, + 0x103A: 84, + 0x103D: 84, + 0x103E: 84, + 0x1058: 84, + 0x1059: 84, + 0x105E: 84, + 0x105F: 84, + 0x1060: 84, + 0x1071: 84, + 0x1072: 84, + 0x1073: 84, + 0x1074: 84, + 0x1082: 84, + 0x1085: 84, + 0x1086: 84, + 0x108D: 84, + 0x109D: 84, + 0x135D: 84, + 0x135E: 84, + 0x135F: 84, + 0x1712: 84, + 0x1713: 84, + 0x1714: 84, + 0x1732: 84, + 0x1733: 84, + 0x1752: 84, + 0x1753: 84, + 0x1772: 84, + 0x1773: 84, + 0x17B4: 84, + 0x17B5: 84, + 0x17B7: 84, + 0x17B8: 84, + 0x17B9: 84, + 0x17BA: 84, + 0x17BB: 84, + 0x17BC: 84, + 0x17BD: 84, + 0x17C6: 84, + 0x17C9: 84, + 0x17CA: 84, + 0x17CB: 84, + 0x17CC: 84, + 0x17CD: 84, + 0x17CE: 84, + 0x17CF: 84, + 0x17D0: 84, + 0x17D1: 84, + 0x17D2: 84, + 0x17D3: 84, + 0x17DD: 84, + 0x1807: 68, + 0x180A: 67, + 0x180B: 84, + 0x180C: 84, + 0x180D: 84, + 0x180F: 84, + 0x1820: 68, + 0x1821: 68, + 0x1822: 68, + 0x1823: 68, + 0x1824: 68, + 0x1825: 68, + 0x1826: 68, + 0x1827: 68, + 0x1828: 68, + 0x1829: 68, + 0x182A: 68, + 0x182B: 68, + 0x182C: 68, + 0x182D: 68, + 0x182E: 68, + 0x182F: 68, + 0x1830: 68, + 0x1831: 68, + 0x1832: 68, + 0x1833: 68, + 0x1834: 68, + 0x1835: 68, + 0x1836: 68, + 0x1837: 68, + 0x1838: 68, + 0x1839: 68, + 0x183A: 68, + 0x183B: 68, + 0x183C: 68, + 0x183D: 68, + 0x183E: 68, + 0x183F: 68, + 0x1840: 68, + 0x1841: 68, + 0x1842: 68, + 0x1843: 68, + 0x1844: 68, + 0x1845: 68, + 0x1846: 68, + 0x1847: 68, + 0x1848: 68, + 0x1849: 68, + 0x184A: 68, + 0x184B: 68, + 0x184C: 68, + 0x184D: 68, + 0x184E: 68, + 0x184F: 68, + 0x1850: 68, + 0x1851: 68, + 0x1852: 68, + 0x1853: 68, + 0x1854: 68, + 0x1855: 68, + 0x1856: 68, + 0x1857: 68, + 0x1858: 68, + 0x1859: 68, + 0x185A: 68, + 0x185B: 68, + 0x185C: 68, + 0x185D: 68, + 0x185E: 68, + 0x185F: 68, + 0x1860: 68, + 0x1861: 68, + 0x1862: 68, + 0x1863: 68, + 0x1864: 68, + 0x1865: 68, + 0x1866: 68, + 0x1867: 68, + 0x1868: 68, + 0x1869: 68, + 0x186A: 68, + 0x186B: 68, + 0x186C: 68, + 0x186D: 68, + 0x186E: 68, + 0x186F: 68, + 0x1870: 68, + 0x1871: 68, + 0x1872: 68, + 0x1873: 68, + 0x1874: 68, + 0x1875: 68, + 0x1876: 68, + 0x1877: 68, + 0x1878: 68, + 0x1885: 84, + 0x1886: 84, + 0x1887: 68, + 0x1888: 68, + 0x1889: 68, + 0x188A: 68, + 0x188B: 68, + 0x188C: 68, + 0x188D: 68, + 0x188E: 68, + 0x188F: 68, + 0x1890: 68, + 0x1891: 68, + 0x1892: 68, + 0x1893: 68, + 0x1894: 68, + 0x1895: 68, + 0x1896: 68, + 0x1897: 68, + 0x1898: 68, + 0x1899: 68, + 0x189A: 68, + 0x189B: 68, + 0x189C: 68, + 0x189D: 68, + 0x189E: 68, + 0x189F: 68, + 0x18A0: 68, + 0x18A1: 68, + 0x18A2: 68, + 0x18A3: 68, + 0x18A4: 68, + 0x18A5: 68, + 0x18A6: 68, + 0x18A7: 68, + 0x18A8: 68, + 0x18A9: 84, + 0x18AA: 68, + 0x1920: 84, + 0x1921: 84, + 0x1922: 84, + 0x1927: 84, + 0x1928: 84, + 0x1932: 84, + 0x1939: 84, + 0x193A: 84, + 0x193B: 84, + 0x1A17: 84, + 0x1A18: 84, + 0x1A1B: 84, + 0x1A56: 84, + 0x1A58: 84, + 0x1A59: 84, + 0x1A5A: 84, + 0x1A5B: 84, + 0x1A5C: 84, + 0x1A5D: 84, + 0x1A5E: 84, + 0x1A60: 84, + 0x1A62: 84, + 0x1A65: 84, + 0x1A66: 84, + 0x1A67: 84, + 0x1A68: 84, + 0x1A69: 84, + 0x1A6A: 84, + 0x1A6B: 84, + 0x1A6C: 84, + 0x1A73: 84, + 0x1A74: 84, + 0x1A75: 84, + 0x1A76: 84, + 0x1A77: 84, + 0x1A78: 84, + 0x1A79: 84, + 0x1A7A: 84, + 0x1A7B: 84, + 0x1A7C: 84, + 0x1A7F: 84, + 0x1AB0: 84, + 0x1AB1: 84, + 0x1AB2: 84, + 0x1AB3: 84, + 0x1AB4: 84, + 0x1AB5: 84, + 0x1AB6: 84, + 0x1AB7: 84, + 0x1AB8: 84, + 0x1AB9: 84, + 0x1ABA: 84, + 0x1ABB: 84, + 0x1ABC: 84, + 0x1ABD: 84, + 0x1ABE: 84, + 0x1ABF: 84, + 0x1AC0: 84, + 0x1AC1: 84, + 0x1AC2: 84, + 0x1AC3: 84, + 0x1AC4: 84, + 0x1AC5: 84, + 0x1AC6: 84, + 0x1AC7: 84, + 0x1AC8: 84, + 0x1AC9: 84, + 0x1ACA: 84, + 0x1ACB: 84, + 0x1ACC: 84, + 0x1ACD: 84, + 0x1ACE: 84, + 0x1B00: 84, + 0x1B01: 84, + 0x1B02: 84, + 0x1B03: 84, + 0x1B34: 84, + 0x1B36: 84, + 0x1B37: 84, + 0x1B38: 84, + 0x1B39: 84, + 0x1B3A: 84, + 0x1B3C: 84, + 0x1B42: 84, + 0x1B6B: 84, + 0x1B6C: 84, + 0x1B6D: 84, + 0x1B6E: 84, + 0x1B6F: 84, + 0x1B70: 84, + 0x1B71: 84, + 0x1B72: 84, + 0x1B73: 84, + 0x1B80: 84, + 0x1B81: 84, + 0x1BA2: 84, + 0x1BA3: 84, + 0x1BA4: 84, + 0x1BA5: 84, + 0x1BA8: 84, + 0x1BA9: 84, + 0x1BAB: 84, + 0x1BAC: 84, + 0x1BAD: 84, + 0x1BE6: 84, + 0x1BE8: 84, + 0x1BE9: 84, + 0x1BED: 84, + 0x1BEF: 84, + 0x1BF0: 84, + 0x1BF1: 84, + 0x1C2C: 84, + 0x1C2D: 84, + 0x1C2E: 84, + 0x1C2F: 84, + 0x1C30: 84, + 0x1C31: 84, + 0x1C32: 84, + 0x1C33: 84, + 0x1C36: 84, + 0x1C37: 84, + 0x1CD0: 84, + 0x1CD1: 84, + 0x1CD2: 84, + 0x1CD4: 84, + 0x1CD5: 84, + 0x1CD6: 84, + 0x1CD7: 84, + 0x1CD8: 84, + 0x1CD9: 84, + 0x1CDA: 84, + 0x1CDB: 84, + 0x1CDC: 84, + 0x1CDD: 84, + 0x1CDE: 84, + 0x1CDF: 84, + 0x1CE0: 84, + 0x1CE2: 84, + 0x1CE3: 84, + 0x1CE4: 84, + 0x1CE5: 84, + 0x1CE6: 84, + 0x1CE7: 84, + 0x1CE8: 84, + 0x1CED: 84, + 0x1CF4: 84, + 0x1CF8: 84, + 0x1CF9: 84, + 0x1DC0: 84, + 0x1DC1: 84, + 0x1DC2: 84, + 0x1DC3: 84, + 0x1DC4: 84, + 0x1DC5: 84, + 0x1DC6: 84, + 0x1DC7: 84, + 0x1DC8: 84, + 0x1DC9: 84, + 0x1DCA: 84, + 0x1DCB: 84, + 0x1DCC: 84, + 0x1DCD: 84, + 0x1DCE: 84, + 0x1DCF: 84, + 0x1DD0: 84, + 0x1DD1: 84, + 0x1DD2: 84, + 0x1DD3: 84, + 0x1DD4: 84, + 0x1DD5: 84, + 0x1DD6: 84, + 0x1DD7: 84, + 0x1DD8: 84, + 0x1DD9: 84, + 0x1DDA: 84, + 0x1DDB: 84, + 0x1DDC: 84, + 0x1DDD: 84, + 0x1DDE: 84, + 0x1DDF: 84, + 0x1DE0: 84, + 0x1DE1: 84, + 0x1DE2: 84, + 0x1DE3: 84, + 0x1DE4: 84, + 0x1DE5: 84, + 0x1DE6: 84, + 0x1DE7: 84, + 0x1DE8: 84, + 0x1DE9: 84, + 0x1DEA: 84, + 0x1DEB: 84, + 0x1DEC: 84, + 0x1DED: 84, + 0x1DEE: 84, + 0x1DEF: 84, + 0x1DF0: 84, + 0x1DF1: 84, + 0x1DF2: 84, + 0x1DF3: 84, + 0x1DF4: 84, + 0x1DF5: 84, + 0x1DF6: 84, + 0x1DF7: 84, + 0x1DF8: 84, + 0x1DF9: 84, + 0x1DFA: 84, + 0x1DFB: 84, + 0x1DFC: 84, + 0x1DFD: 84, + 0x1DFE: 84, + 0x1DFF: 84, + 0x200B: 84, + 0x200D: 67, + 0x200E: 84, + 0x200F: 84, + 0x202A: 84, + 0x202B: 84, + 0x202C: 84, + 0x202D: 84, + 0x202E: 84, + 0x2060: 84, + 0x2061: 84, + 0x2062: 84, + 0x2063: 84, + 0x2064: 84, + 0x206A: 84, + 0x206B: 84, + 0x206C: 84, + 0x206D: 84, + 0x206E: 84, + 0x206F: 84, + 0x20D0: 84, + 0x20D1: 84, + 0x20D2: 84, + 0x20D3: 84, + 0x20D4: 84, + 0x20D5: 84, + 0x20D6: 84, + 0x20D7: 84, + 0x20D8: 84, + 0x20D9: 84, + 0x20DA: 84, + 0x20DB: 84, + 0x20DC: 84, + 0x20DD: 84, + 0x20DE: 84, + 0x20DF: 84, + 0x20E0: 84, + 0x20E1: 84, + 0x20E2: 84, + 0x20E3: 84, + 0x20E4: 84, + 0x20E5: 84, + 0x20E6: 84, + 0x20E7: 84, + 0x20E8: 84, + 0x20E9: 84, + 0x20EA: 84, + 0x20EB: 84, + 0x20EC: 84, + 0x20ED: 84, + 0x20EE: 84, + 0x20EF: 84, + 0x20F0: 84, + 0x2CEF: 84, + 0x2CF0: 84, + 0x2CF1: 84, + 0x2D7F: 84, + 0x2DE0: 84, + 0x2DE1: 84, + 0x2DE2: 84, + 0x2DE3: 84, + 0x2DE4: 84, + 0x2DE5: 84, + 0x2DE6: 84, + 0x2DE7: 84, + 0x2DE8: 84, + 0x2DE9: 84, + 0x2DEA: 84, + 0x2DEB: 84, + 0x2DEC: 84, + 0x2DED: 84, + 0x2DEE: 84, + 0x2DEF: 84, + 0x2DF0: 84, + 0x2DF1: 84, + 0x2DF2: 84, + 0x2DF3: 84, + 0x2DF4: 84, + 0x2DF5: 84, + 0x2DF6: 84, + 0x2DF7: 84, + 0x2DF8: 84, + 0x2DF9: 84, + 0x2DFA: 84, + 0x2DFB: 84, + 0x2DFC: 84, + 0x2DFD: 84, + 0x2DFE: 84, + 0x2DFF: 84, + 0x302A: 84, + 0x302B: 84, + 0x302C: 84, + 0x302D: 84, + 0x3099: 84, + 0x309A: 84, + 0xA66F: 84, + 0xA670: 84, + 0xA671: 84, + 0xA672: 84, + 0xA674: 84, + 0xA675: 84, + 0xA676: 84, + 0xA677: 84, + 0xA678: 84, + 0xA679: 84, + 0xA67A: 84, + 0xA67B: 84, + 0xA67C: 84, + 0xA67D: 84, + 0xA69E: 84, + 0xA69F: 84, + 0xA6F0: 84, + 0xA6F1: 84, + 0xA802: 84, + 0xA806: 84, + 0xA80B: 84, + 0xA825: 84, + 0xA826: 84, + 0xA82C: 84, + 0xA840: 68, + 0xA841: 68, + 0xA842: 68, + 0xA843: 68, + 0xA844: 68, + 0xA845: 68, + 0xA846: 68, + 0xA847: 68, + 0xA848: 68, + 0xA849: 68, + 0xA84A: 68, + 0xA84B: 68, + 0xA84C: 68, + 0xA84D: 68, + 0xA84E: 68, + 0xA84F: 68, + 0xA850: 68, + 0xA851: 68, + 0xA852: 68, + 0xA853: 68, + 0xA854: 68, + 0xA855: 68, + 0xA856: 68, + 0xA857: 68, + 0xA858: 68, + 0xA859: 68, + 0xA85A: 68, + 0xA85B: 68, + 0xA85C: 68, + 0xA85D: 68, + 0xA85E: 68, + 0xA85F: 68, + 0xA860: 68, + 0xA861: 68, + 0xA862: 68, + 0xA863: 68, + 0xA864: 68, + 0xA865: 68, + 0xA866: 68, + 0xA867: 68, + 0xA868: 68, + 0xA869: 68, + 0xA86A: 68, + 0xA86B: 68, + 0xA86C: 68, + 0xA86D: 68, + 0xA86E: 68, + 0xA86F: 68, + 0xA870: 68, + 0xA871: 68, + 0xA872: 76, + 0xA8C4: 84, + 0xA8C5: 84, + 0xA8E0: 84, + 0xA8E1: 84, + 0xA8E2: 84, + 0xA8E3: 84, + 0xA8E4: 84, + 0xA8E5: 84, + 0xA8E6: 84, + 0xA8E7: 84, + 0xA8E8: 84, + 0xA8E9: 84, + 0xA8EA: 84, + 0xA8EB: 84, + 0xA8EC: 84, + 0xA8ED: 84, + 0xA8EE: 84, + 0xA8EF: 84, + 0xA8F0: 84, + 0xA8F1: 84, + 0xA8FF: 84, + 0xA926: 84, + 0xA927: 84, + 0xA928: 84, + 0xA929: 84, + 0xA92A: 84, + 0xA92B: 84, + 0xA92C: 84, + 0xA92D: 84, + 0xA947: 84, + 0xA948: 84, + 0xA949: 84, + 0xA94A: 84, + 0xA94B: 84, + 0xA94C: 84, + 0xA94D: 84, + 0xA94E: 84, + 0xA94F: 84, + 0xA950: 84, + 0xA951: 84, + 0xA980: 84, + 0xA981: 84, + 0xA982: 84, + 0xA9B3: 84, + 0xA9B6: 84, + 0xA9B7: 84, + 0xA9B8: 84, + 0xA9B9: 84, + 0xA9BC: 84, + 0xA9BD: 84, + 0xA9E5: 84, + 0xAA29: 84, + 0xAA2A: 84, + 0xAA2B: 84, + 0xAA2C: 84, + 0xAA2D: 84, + 0xAA2E: 84, + 0xAA31: 84, + 0xAA32: 84, + 0xAA35: 84, + 0xAA36: 84, + 0xAA43: 84, + 0xAA4C: 84, + 0xAA7C: 84, + 0xAAB0: 84, + 0xAAB2: 84, + 0xAAB3: 84, + 0xAAB4: 84, + 0xAAB7: 84, + 0xAAB8: 84, + 0xAABE: 84, + 0xAABF: 84, + 0xAAC1: 84, + 0xAAEC: 84, + 0xAAED: 84, + 0xAAF6: 84, + 0xABE5: 84, + 0xABE8: 84, + 0xABED: 84, + 0xFB1E: 84, + 0xFE00: 84, + 0xFE01: 84, + 0xFE02: 84, + 0xFE03: 84, + 0xFE04: 84, + 0xFE05: 84, + 0xFE06: 84, + 0xFE07: 84, + 0xFE08: 84, + 0xFE09: 84, + 0xFE0A: 84, + 0xFE0B: 84, + 0xFE0C: 84, + 0xFE0D: 84, + 0xFE0E: 84, + 0xFE0F: 84, + 0xFE20: 84, + 0xFE21: 84, + 0xFE22: 84, + 0xFE23: 84, + 0xFE24: 84, + 0xFE25: 84, + 0xFE26: 84, + 0xFE27: 84, + 0xFE28: 84, + 0xFE29: 84, + 0xFE2A: 84, + 0xFE2B: 84, + 0xFE2C: 84, + 0xFE2D: 84, + 0xFE2E: 84, + 0xFE2F: 84, + 0xFEFF: 84, + 0xFFF9: 84, + 0xFFFA: 84, + 0xFFFB: 84, + 0x101FD: 84, + 0x102E0: 84, + 0x10376: 84, + 0x10377: 84, + 0x10378: 84, + 0x10379: 84, + 0x1037A: 84, + 0x10A01: 84, + 0x10A02: 84, + 0x10A03: 84, + 0x10A05: 84, + 0x10A06: 84, + 0x10A0C: 84, + 0x10A0D: 84, + 0x10A0E: 84, + 0x10A0F: 84, + 0x10A38: 84, + 0x10A39: 84, + 0x10A3A: 84, + 0x10A3F: 84, + 0x10AC0: 68, + 0x10AC1: 68, + 0x10AC2: 68, + 0x10AC3: 68, + 0x10AC4: 68, + 0x10AC5: 82, + 0x10AC7: 82, + 0x10AC9: 82, + 0x10ACA: 82, + 0x10ACD: 76, + 0x10ACE: 82, + 0x10ACF: 82, + 0x10AD0: 82, + 0x10AD1: 82, + 0x10AD2: 82, + 0x10AD3: 68, + 0x10AD4: 68, + 0x10AD5: 68, + 0x10AD6: 68, + 0x10AD7: 76, + 0x10AD8: 68, + 0x10AD9: 68, + 0x10ADA: 68, + 0x10ADB: 68, + 0x10ADC: 68, + 0x10ADD: 82, + 0x10ADE: 68, + 0x10ADF: 68, + 0x10AE0: 68, + 0x10AE1: 82, + 0x10AE4: 82, + 0x10AE5: 84, + 0x10AE6: 84, + 0x10AEB: 68, + 0x10AEC: 68, + 0x10AED: 68, + 0x10AEE: 68, + 0x10AEF: 82, + 0x10B80: 68, + 0x10B81: 82, + 0x10B82: 68, + 0x10B83: 82, + 0x10B84: 82, + 0x10B85: 82, + 0x10B86: 68, + 0x10B87: 68, + 0x10B88: 68, + 0x10B89: 82, + 0x10B8A: 68, + 0x10B8B: 68, + 0x10B8C: 82, + 0x10B8D: 68, + 0x10B8E: 82, + 0x10B8F: 82, + 0x10B90: 68, + 0x10B91: 82, + 0x10BA9: 82, + 0x10BAA: 82, + 0x10BAB: 82, + 0x10BAC: 82, + 0x10BAD: 68, + 0x10BAE: 68, + 0x10D00: 76, + 0x10D01: 68, + 0x10D02: 68, + 0x10D03: 68, + 0x10D04: 68, + 0x10D05: 68, + 0x10D06: 68, + 0x10D07: 68, + 0x10D08: 68, + 0x10D09: 68, + 0x10D0A: 68, + 0x10D0B: 68, + 0x10D0C: 68, + 0x10D0D: 68, + 0x10D0E: 68, + 0x10D0F: 68, + 0x10D10: 68, + 0x10D11: 68, + 0x10D12: 68, + 0x10D13: 68, + 0x10D14: 68, + 0x10D15: 68, + 0x10D16: 68, + 0x10D17: 68, + 0x10D18: 68, + 0x10D19: 68, + 0x10D1A: 68, + 0x10D1B: 68, + 0x10D1C: 68, + 0x10D1D: 68, + 0x10D1E: 68, + 0x10D1F: 68, + 0x10D20: 68, + 0x10D21: 68, + 0x10D22: 82, + 0x10D23: 68, + 0x10D24: 84, + 0x10D25: 84, + 0x10D26: 84, + 0x10D27: 84, + 0x10D69: 84, + 0x10D6A: 84, + 0x10D6B: 84, + 0x10D6C: 84, + 0x10D6D: 84, + 0x10EAB: 84, + 0x10EAC: 84, + 0x10EC2: 82, + 0x10EC3: 68, + 0x10EC4: 68, + 0x10EFC: 84, + 0x10EFD: 84, + 0x10EFE: 84, + 0x10EFF: 84, + 0x10F30: 68, + 0x10F31: 68, + 0x10F32: 68, + 0x10F33: 82, + 0x10F34: 68, + 0x10F35: 68, + 0x10F36: 68, + 0x10F37: 68, + 0x10F38: 68, + 0x10F39: 68, + 0x10F3A: 68, + 0x10F3B: 68, + 0x10F3C: 68, + 0x10F3D: 68, + 0x10F3E: 68, + 0x10F3F: 68, + 0x10F40: 68, + 0x10F41: 68, + 0x10F42: 68, + 0x10F43: 68, + 0x10F44: 68, + 0x10F46: 84, + 0x10F47: 84, + 0x10F48: 84, + 0x10F49: 84, + 0x10F4A: 84, + 0x10F4B: 84, + 0x10F4C: 84, + 0x10F4D: 84, + 0x10F4E: 84, + 0x10F4F: 84, + 0x10F50: 84, + 0x10F51: 68, + 0x10F52: 68, + 0x10F53: 68, + 0x10F54: 82, + 0x10F70: 68, + 0x10F71: 68, + 0x10F72: 68, + 0x10F73: 68, + 0x10F74: 82, + 0x10F75: 82, + 0x10F76: 68, + 0x10F77: 68, + 0x10F78: 68, + 0x10F79: 68, + 0x10F7A: 68, + 0x10F7B: 68, + 0x10F7C: 68, + 0x10F7D: 68, + 0x10F7E: 68, + 0x10F7F: 68, + 0x10F80: 68, + 0x10F81: 68, + 0x10F82: 84, + 0x10F83: 84, + 0x10F84: 84, + 0x10F85: 84, + 0x10FB0: 68, + 0x10FB2: 68, + 0x10FB3: 68, + 0x10FB4: 82, + 0x10FB5: 82, + 0x10FB6: 82, + 0x10FB8: 68, + 0x10FB9: 82, + 0x10FBA: 82, + 0x10FBB: 68, + 0x10FBC: 68, + 0x10FBD: 82, + 0x10FBE: 68, + 0x10FBF: 68, + 0x10FC1: 68, + 0x10FC2: 82, + 0x10FC3: 82, + 0x10FC4: 68, + 0x10FC9: 82, + 0x10FCA: 68, + 0x10FCB: 76, + 0x11001: 84, + 0x11038: 84, + 0x11039: 84, + 0x1103A: 84, + 0x1103B: 84, + 0x1103C: 84, + 0x1103D: 84, + 0x1103E: 84, + 0x1103F: 84, + 0x11040: 84, + 0x11041: 84, + 0x11042: 84, + 0x11043: 84, + 0x11044: 84, + 0x11045: 84, + 0x11046: 84, + 0x11070: 84, + 0x11073: 84, + 0x11074: 84, + 0x1107F: 84, + 0x11080: 84, + 0x11081: 84, + 0x110B3: 84, + 0x110B4: 84, + 0x110B5: 84, + 0x110B6: 84, + 0x110B9: 84, + 0x110BA: 84, + 0x110C2: 84, + 0x11100: 84, + 0x11101: 84, + 0x11102: 84, + 0x11127: 84, + 0x11128: 84, + 0x11129: 84, + 0x1112A: 84, + 0x1112B: 84, + 0x1112D: 84, + 0x1112E: 84, + 0x1112F: 84, + 0x11130: 84, + 0x11131: 84, + 0x11132: 84, + 0x11133: 84, + 0x11134: 84, + 0x11173: 84, + 0x11180: 84, + 0x11181: 84, + 0x111B6: 84, + 0x111B7: 84, + 0x111B8: 84, + 0x111B9: 84, + 0x111BA: 84, + 0x111BB: 84, + 0x111BC: 84, + 0x111BD: 84, + 0x111BE: 84, + 0x111C9: 84, + 0x111CA: 84, + 0x111CB: 84, + 0x111CC: 84, + 0x111CF: 84, + 0x1122F: 84, + 0x11230: 84, + 0x11231: 84, + 0x11234: 84, + 0x11236: 84, + 0x11237: 84, + 0x1123E: 84, + 0x11241: 84, + 0x112DF: 84, + 0x112E3: 84, + 0x112E4: 84, + 0x112E5: 84, + 0x112E6: 84, + 0x112E7: 84, + 0x112E8: 84, + 0x112E9: 84, + 0x112EA: 84, + 0x11300: 84, + 0x11301: 84, + 0x1133B: 84, + 0x1133C: 84, + 0x11340: 84, + 0x11366: 84, + 0x11367: 84, + 0x11368: 84, + 0x11369: 84, + 0x1136A: 84, + 0x1136B: 84, + 0x1136C: 84, + 0x11370: 84, + 0x11371: 84, + 0x11372: 84, + 0x11373: 84, + 0x11374: 84, + 0x113BB: 84, + 0x113BC: 84, + 0x113BD: 84, + 0x113BE: 84, + 0x113BF: 84, + 0x113C0: 84, + 0x113CE: 84, + 0x113D0: 84, + 0x113D2: 84, + 0x113E1: 84, + 0x113E2: 84, + 0x11438: 84, + 0x11439: 84, + 0x1143A: 84, + 0x1143B: 84, + 0x1143C: 84, + 0x1143D: 84, + 0x1143E: 84, + 0x1143F: 84, + 0x11442: 84, + 0x11443: 84, + 0x11444: 84, + 0x11446: 84, + 0x1145E: 84, + 0x114B3: 84, + 0x114B4: 84, + 0x114B5: 84, + 0x114B6: 84, + 0x114B7: 84, + 0x114B8: 84, + 0x114BA: 84, + 0x114BF: 84, + 0x114C0: 84, + 0x114C2: 84, + 0x114C3: 84, + 0x115B2: 84, + 0x115B3: 84, + 0x115B4: 84, + 0x115B5: 84, + 0x115BC: 84, + 0x115BD: 84, + 0x115BF: 84, + 0x115C0: 84, + 0x115DC: 84, + 0x115DD: 84, + 0x11633: 84, + 0x11634: 84, + 0x11635: 84, + 0x11636: 84, + 0x11637: 84, + 0x11638: 84, + 0x11639: 84, + 0x1163A: 84, + 0x1163D: 84, + 0x1163F: 84, + 0x11640: 84, + 0x116AB: 84, + 0x116AD: 84, + 0x116B0: 84, + 0x116B1: 84, + 0x116B2: 84, + 0x116B3: 84, + 0x116B4: 84, + 0x116B5: 84, + 0x116B7: 84, + 0x1171D: 84, + 0x1171F: 84, + 0x11722: 84, + 0x11723: 84, + 0x11724: 84, + 0x11725: 84, + 0x11727: 84, + 0x11728: 84, + 0x11729: 84, + 0x1172A: 84, + 0x1172B: 84, + 0x1182F: 84, + 0x11830: 84, + 0x11831: 84, + 0x11832: 84, + 0x11833: 84, + 0x11834: 84, + 0x11835: 84, + 0x11836: 84, + 0x11837: 84, + 0x11839: 84, + 0x1183A: 84, + 0x1193B: 84, + 0x1193C: 84, + 0x1193E: 84, + 0x11943: 84, + 0x119D4: 84, + 0x119D5: 84, + 0x119D6: 84, + 0x119D7: 84, + 0x119DA: 84, + 0x119DB: 84, + 0x119E0: 84, + 0x11A01: 84, + 0x11A02: 84, + 0x11A03: 84, + 0x11A04: 84, + 0x11A05: 84, + 0x11A06: 84, + 0x11A07: 84, + 0x11A08: 84, + 0x11A09: 84, + 0x11A0A: 84, + 0x11A33: 84, + 0x11A34: 84, + 0x11A35: 84, + 0x11A36: 84, + 0x11A37: 84, + 0x11A38: 84, + 0x11A3B: 84, + 0x11A3C: 84, + 0x11A3D: 84, + 0x11A3E: 84, + 0x11A47: 84, + 0x11A51: 84, + 0x11A52: 84, + 0x11A53: 84, + 0x11A54: 84, + 0x11A55: 84, + 0x11A56: 84, + 0x11A59: 84, + 0x11A5A: 84, + 0x11A5B: 84, + 0x11A8A: 84, + 0x11A8B: 84, + 0x11A8C: 84, + 0x11A8D: 84, + 0x11A8E: 84, + 0x11A8F: 84, + 0x11A90: 84, + 0x11A91: 84, + 0x11A92: 84, + 0x11A93: 84, + 0x11A94: 84, + 0x11A95: 84, + 0x11A96: 84, + 0x11A98: 84, + 0x11A99: 84, + 0x11C30: 84, + 0x11C31: 84, + 0x11C32: 84, + 0x11C33: 84, + 0x11C34: 84, + 0x11C35: 84, + 0x11C36: 84, + 0x11C38: 84, + 0x11C39: 84, + 0x11C3A: 84, + 0x11C3B: 84, + 0x11C3C: 84, + 0x11C3D: 84, + 0x11C3F: 84, + 0x11C92: 84, + 0x11C93: 84, + 0x11C94: 84, + 0x11C95: 84, + 0x11C96: 84, + 0x11C97: 84, + 0x11C98: 84, + 0x11C99: 84, + 0x11C9A: 84, + 0x11C9B: 84, + 0x11C9C: 84, + 0x11C9D: 84, + 0x11C9E: 84, + 0x11C9F: 84, + 0x11CA0: 84, + 0x11CA1: 84, + 0x11CA2: 84, + 0x11CA3: 84, + 0x11CA4: 84, + 0x11CA5: 84, + 0x11CA6: 84, + 0x11CA7: 84, + 0x11CAA: 84, + 0x11CAB: 84, + 0x11CAC: 84, + 0x11CAD: 84, + 0x11CAE: 84, + 0x11CAF: 84, + 0x11CB0: 84, + 0x11CB2: 84, + 0x11CB3: 84, + 0x11CB5: 84, + 0x11CB6: 84, + 0x11D31: 84, + 0x11D32: 84, + 0x11D33: 84, + 0x11D34: 84, + 0x11D35: 84, + 0x11D36: 84, + 0x11D3A: 84, + 0x11D3C: 84, + 0x11D3D: 84, + 0x11D3F: 84, + 0x11D40: 84, + 0x11D41: 84, + 0x11D42: 84, + 0x11D43: 84, + 0x11D44: 84, + 0x11D45: 84, + 0x11D47: 84, + 0x11D90: 84, + 0x11D91: 84, + 0x11D95: 84, + 0x11D97: 84, + 0x11EF3: 84, + 0x11EF4: 84, + 0x11F00: 84, + 0x11F01: 84, + 0x11F36: 84, + 0x11F37: 84, + 0x11F38: 84, + 0x11F39: 84, + 0x11F3A: 84, + 0x11F40: 84, + 0x11F42: 84, + 0x11F5A: 84, + 0x13430: 84, + 0x13431: 84, + 0x13432: 84, + 0x13433: 84, + 0x13434: 84, + 0x13435: 84, + 0x13436: 84, + 0x13437: 84, + 0x13438: 84, + 0x13439: 84, + 0x1343A: 84, + 0x1343B: 84, + 0x1343C: 84, + 0x1343D: 84, + 0x1343E: 84, + 0x1343F: 84, + 0x13440: 84, + 0x13447: 84, + 0x13448: 84, + 0x13449: 84, + 0x1344A: 84, + 0x1344B: 84, + 0x1344C: 84, + 0x1344D: 84, + 0x1344E: 84, + 0x1344F: 84, + 0x13450: 84, + 0x13451: 84, + 0x13452: 84, + 0x13453: 84, + 0x13454: 84, + 0x13455: 84, + 0x1611E: 84, + 0x1611F: 84, + 0x16120: 84, + 0x16121: 84, + 0x16122: 84, + 0x16123: 84, + 0x16124: 84, + 0x16125: 84, + 0x16126: 84, + 0x16127: 84, + 0x16128: 84, + 0x16129: 84, + 0x1612D: 84, + 0x1612E: 84, + 0x1612F: 84, + 0x16AF0: 84, + 0x16AF1: 84, + 0x16AF2: 84, + 0x16AF3: 84, + 0x16AF4: 84, + 0x16B30: 84, + 0x16B31: 84, + 0x16B32: 84, + 0x16B33: 84, + 0x16B34: 84, + 0x16B35: 84, + 0x16B36: 84, + 0x16F4F: 84, + 0x16F8F: 84, + 0x16F90: 84, + 0x16F91: 84, + 0x16F92: 84, + 0x16FE4: 84, + 0x1BC9D: 84, + 0x1BC9E: 84, + 0x1BCA0: 84, + 0x1BCA1: 84, + 0x1BCA2: 84, + 0x1BCA3: 84, + 0x1CF00: 84, + 0x1CF01: 84, + 0x1CF02: 84, + 0x1CF03: 84, + 0x1CF04: 84, + 0x1CF05: 84, + 0x1CF06: 84, + 0x1CF07: 84, + 0x1CF08: 84, + 0x1CF09: 84, + 0x1CF0A: 84, + 0x1CF0B: 84, + 0x1CF0C: 84, + 0x1CF0D: 84, + 0x1CF0E: 84, + 0x1CF0F: 84, + 0x1CF10: 84, + 0x1CF11: 84, + 0x1CF12: 84, + 0x1CF13: 84, + 0x1CF14: 84, + 0x1CF15: 84, + 0x1CF16: 84, + 0x1CF17: 84, + 0x1CF18: 84, + 0x1CF19: 84, + 0x1CF1A: 84, + 0x1CF1B: 84, + 0x1CF1C: 84, + 0x1CF1D: 84, + 0x1CF1E: 84, + 0x1CF1F: 84, + 0x1CF20: 84, + 0x1CF21: 84, + 0x1CF22: 84, + 0x1CF23: 84, + 0x1CF24: 84, + 0x1CF25: 84, + 0x1CF26: 84, + 0x1CF27: 84, + 0x1CF28: 84, + 0x1CF29: 84, + 0x1CF2A: 84, + 0x1CF2B: 84, + 0x1CF2C: 84, + 0x1CF2D: 84, + 0x1CF30: 84, + 0x1CF31: 84, + 0x1CF32: 84, + 0x1CF33: 84, + 0x1CF34: 84, + 0x1CF35: 84, + 0x1CF36: 84, + 0x1CF37: 84, + 0x1CF38: 84, + 0x1CF39: 84, + 0x1CF3A: 84, + 0x1CF3B: 84, + 0x1CF3C: 84, + 0x1CF3D: 84, + 0x1CF3E: 84, + 0x1CF3F: 84, + 0x1CF40: 84, + 0x1CF41: 84, + 0x1CF42: 84, + 0x1CF43: 84, + 0x1CF44: 84, + 0x1CF45: 84, + 0x1CF46: 84, + 0x1D167: 84, + 0x1D168: 84, + 0x1D169: 84, + 0x1D173: 84, + 0x1D174: 84, + 0x1D175: 84, + 0x1D176: 84, + 0x1D177: 84, + 0x1D178: 84, + 0x1D179: 84, + 0x1D17A: 84, + 0x1D17B: 84, + 0x1D17C: 84, + 0x1D17D: 84, + 0x1D17E: 84, + 0x1D17F: 84, + 0x1D180: 84, + 0x1D181: 84, + 0x1D182: 84, + 0x1D185: 84, + 0x1D186: 84, + 0x1D187: 84, + 0x1D188: 84, + 0x1D189: 84, + 0x1D18A: 84, + 0x1D18B: 84, + 0x1D1AA: 84, + 0x1D1AB: 84, + 0x1D1AC: 84, + 0x1D1AD: 84, + 0x1D242: 84, + 0x1D243: 84, + 0x1D244: 84, + 0x1DA00: 84, + 0x1DA01: 84, + 0x1DA02: 84, + 0x1DA03: 84, + 0x1DA04: 84, + 0x1DA05: 84, + 0x1DA06: 84, + 0x1DA07: 84, + 0x1DA08: 84, + 0x1DA09: 84, + 0x1DA0A: 84, + 0x1DA0B: 84, + 0x1DA0C: 84, + 0x1DA0D: 84, + 0x1DA0E: 84, + 0x1DA0F: 84, + 0x1DA10: 84, + 0x1DA11: 84, + 0x1DA12: 84, + 0x1DA13: 84, + 0x1DA14: 84, + 0x1DA15: 84, + 0x1DA16: 84, + 0x1DA17: 84, + 0x1DA18: 84, + 0x1DA19: 84, + 0x1DA1A: 84, + 0x1DA1B: 84, + 0x1DA1C: 84, + 0x1DA1D: 84, + 0x1DA1E: 84, + 0x1DA1F: 84, + 0x1DA20: 84, + 0x1DA21: 84, + 0x1DA22: 84, + 0x1DA23: 84, + 0x1DA24: 84, + 0x1DA25: 84, + 0x1DA26: 84, + 0x1DA27: 84, + 0x1DA28: 84, + 0x1DA29: 84, + 0x1DA2A: 84, + 0x1DA2B: 84, + 0x1DA2C: 84, + 0x1DA2D: 84, + 0x1DA2E: 84, + 0x1DA2F: 84, + 0x1DA30: 84, + 0x1DA31: 84, + 0x1DA32: 84, + 0x1DA33: 84, + 0x1DA34: 84, + 0x1DA35: 84, + 0x1DA36: 84, + 0x1DA3B: 84, + 0x1DA3C: 84, + 0x1DA3D: 84, + 0x1DA3E: 84, + 0x1DA3F: 84, + 0x1DA40: 84, + 0x1DA41: 84, + 0x1DA42: 84, + 0x1DA43: 84, + 0x1DA44: 84, + 0x1DA45: 84, + 0x1DA46: 84, + 0x1DA47: 84, + 0x1DA48: 84, + 0x1DA49: 84, + 0x1DA4A: 84, + 0x1DA4B: 84, + 0x1DA4C: 84, + 0x1DA4D: 84, + 0x1DA4E: 84, + 0x1DA4F: 84, + 0x1DA50: 84, + 0x1DA51: 84, + 0x1DA52: 84, + 0x1DA53: 84, + 0x1DA54: 84, + 0x1DA55: 84, + 0x1DA56: 84, + 0x1DA57: 84, + 0x1DA58: 84, + 0x1DA59: 84, + 0x1DA5A: 84, + 0x1DA5B: 84, + 0x1DA5C: 84, + 0x1DA5D: 84, + 0x1DA5E: 84, + 0x1DA5F: 84, + 0x1DA60: 84, + 0x1DA61: 84, + 0x1DA62: 84, + 0x1DA63: 84, + 0x1DA64: 84, + 0x1DA65: 84, + 0x1DA66: 84, + 0x1DA67: 84, + 0x1DA68: 84, + 0x1DA69: 84, + 0x1DA6A: 84, + 0x1DA6B: 84, + 0x1DA6C: 84, + 0x1DA75: 84, + 0x1DA84: 84, + 0x1DA9B: 84, + 0x1DA9C: 84, + 0x1DA9D: 84, + 0x1DA9E: 84, + 0x1DA9F: 84, + 0x1DAA1: 84, + 0x1DAA2: 84, + 0x1DAA3: 84, + 0x1DAA4: 84, + 0x1DAA5: 84, + 0x1DAA6: 84, + 0x1DAA7: 84, + 0x1DAA8: 84, + 0x1DAA9: 84, + 0x1DAAA: 84, + 0x1DAAB: 84, + 0x1DAAC: 84, + 0x1DAAD: 84, + 0x1DAAE: 84, + 0x1DAAF: 84, + 0x1E000: 84, + 0x1E001: 84, + 0x1E002: 84, + 0x1E003: 84, + 0x1E004: 84, + 0x1E005: 84, + 0x1E006: 84, + 0x1E008: 84, + 0x1E009: 84, + 0x1E00A: 84, + 0x1E00B: 84, + 0x1E00C: 84, + 0x1E00D: 84, + 0x1E00E: 84, + 0x1E00F: 84, + 0x1E010: 84, + 0x1E011: 84, + 0x1E012: 84, + 0x1E013: 84, + 0x1E014: 84, + 0x1E015: 84, + 0x1E016: 84, + 0x1E017: 84, + 0x1E018: 84, + 0x1E01B: 84, + 0x1E01C: 84, + 0x1E01D: 84, + 0x1E01E: 84, + 0x1E01F: 84, + 0x1E020: 84, + 0x1E021: 84, + 0x1E023: 84, + 0x1E024: 84, + 0x1E026: 84, + 0x1E027: 84, + 0x1E028: 84, + 0x1E029: 84, + 0x1E02A: 84, + 0x1E08F: 84, + 0x1E130: 84, + 0x1E131: 84, + 0x1E132: 84, + 0x1E133: 84, + 0x1E134: 84, + 0x1E135: 84, + 0x1E136: 84, + 0x1E2AE: 84, + 0x1E2EC: 84, + 0x1E2ED: 84, + 0x1E2EE: 84, + 0x1E2EF: 84, + 0x1E4EC: 84, + 0x1E4ED: 84, + 0x1E4EE: 84, + 0x1E4EF: 84, + 0x1E5EE: 84, + 0x1E5EF: 84, + 0x1E8D0: 84, + 0x1E8D1: 84, + 0x1E8D2: 84, + 0x1E8D3: 84, + 0x1E8D4: 84, + 0x1E8D5: 84, + 0x1E8D6: 84, + 0x1E900: 68, + 0x1E901: 68, + 0x1E902: 68, + 0x1E903: 68, + 0x1E904: 68, + 0x1E905: 68, + 0x1E906: 68, + 0x1E907: 68, + 0x1E908: 68, + 0x1E909: 68, + 0x1E90A: 68, + 0x1E90B: 68, + 0x1E90C: 68, + 0x1E90D: 68, + 0x1E90E: 68, + 0x1E90F: 68, + 0x1E910: 68, + 0x1E911: 68, + 0x1E912: 68, + 0x1E913: 68, + 0x1E914: 68, + 0x1E915: 68, + 0x1E916: 68, + 0x1E917: 68, + 0x1E918: 68, + 0x1E919: 68, + 0x1E91A: 68, + 0x1E91B: 68, + 0x1E91C: 68, + 0x1E91D: 68, + 0x1E91E: 68, + 0x1E91F: 68, + 0x1E920: 68, + 0x1E921: 68, + 0x1E922: 68, + 0x1E923: 68, + 0x1E924: 68, + 0x1E925: 68, + 0x1E926: 68, + 0x1E927: 68, + 0x1E928: 68, + 0x1E929: 68, + 0x1E92A: 68, + 0x1E92B: 68, + 0x1E92C: 68, + 0x1E92D: 68, + 0x1E92E: 68, + 0x1E92F: 68, + 0x1E930: 68, + 0x1E931: 68, + 0x1E932: 68, + 0x1E933: 68, + 0x1E934: 68, + 0x1E935: 68, + 0x1E936: 68, + 0x1E937: 68, + 0x1E938: 68, + 0x1E939: 68, + 0x1E93A: 68, + 0x1E93B: 68, + 0x1E93C: 68, + 0x1E93D: 68, + 0x1E93E: 68, + 0x1E93F: 68, + 0x1E940: 68, + 0x1E941: 68, + 0x1E942: 68, + 0x1E943: 68, + 0x1E944: 84, + 0x1E945: 84, + 0x1E946: 84, + 0x1E947: 84, + 0x1E948: 84, + 0x1E949: 84, + 0x1E94A: 84, + 0x1E94B: 84, + 0xE0001: 84, + 0xE0020: 84, + 0xE0021: 84, + 0xE0022: 84, + 0xE0023: 84, + 0xE0024: 84, + 0xE0025: 84, + 0xE0026: 84, + 0xE0027: 84, + 0xE0028: 84, + 0xE0029: 84, + 0xE002A: 84, + 0xE002B: 84, + 0xE002C: 84, + 0xE002D: 84, + 0xE002E: 84, + 0xE002F: 84, + 0xE0030: 84, + 0xE0031: 84, + 0xE0032: 84, + 0xE0033: 84, + 0xE0034: 84, + 0xE0035: 84, + 0xE0036: 84, + 0xE0037: 84, + 0xE0038: 84, + 0xE0039: 84, + 0xE003A: 84, + 0xE003B: 84, + 0xE003C: 84, + 0xE003D: 84, + 0xE003E: 84, + 0xE003F: 84, + 0xE0040: 84, + 0xE0041: 84, + 0xE0042: 84, + 0xE0043: 84, + 0xE0044: 84, + 0xE0045: 84, + 0xE0046: 84, + 0xE0047: 84, + 0xE0048: 84, + 0xE0049: 84, + 0xE004A: 84, + 0xE004B: 84, + 0xE004C: 84, + 0xE004D: 84, + 0xE004E: 84, + 0xE004F: 84, + 0xE0050: 84, + 0xE0051: 84, + 0xE0052: 84, + 0xE0053: 84, + 0xE0054: 84, + 0xE0055: 84, + 0xE0056: 84, + 0xE0057: 84, + 0xE0058: 84, + 0xE0059: 84, + 0xE005A: 84, + 0xE005B: 84, + 0xE005C: 84, + 0xE005D: 84, + 0xE005E: 84, + 0xE005F: 84, + 0xE0060: 84, + 0xE0061: 84, + 0xE0062: 84, + 0xE0063: 84, + 0xE0064: 84, + 0xE0065: 84, + 0xE0066: 84, + 0xE0067: 84, + 0xE0068: 84, + 0xE0069: 84, + 0xE006A: 84, + 0xE006B: 84, + 0xE006C: 84, + 0xE006D: 84, + 0xE006E: 84, + 0xE006F: 84, + 0xE0070: 84, + 0xE0071: 84, + 0xE0072: 84, + 0xE0073: 84, + 0xE0074: 84, + 0xE0075: 84, + 0xE0076: 84, + 0xE0077: 84, + 0xE0078: 84, + 0xE0079: 84, + 0xE007A: 84, + 0xE007B: 84, + 0xE007C: 84, + 0xE007D: 84, + 0xE007E: 84, + 0xE007F: 84, + 0xE0100: 84, + 0xE0101: 84, + 0xE0102: 84, + 0xE0103: 84, + 0xE0104: 84, + 0xE0105: 84, + 0xE0106: 84, + 0xE0107: 84, + 0xE0108: 84, + 0xE0109: 84, + 0xE010A: 84, + 0xE010B: 84, + 0xE010C: 84, + 0xE010D: 84, + 0xE010E: 84, + 0xE010F: 84, + 0xE0110: 84, + 0xE0111: 84, + 0xE0112: 84, + 0xE0113: 84, + 0xE0114: 84, + 0xE0115: 84, + 0xE0116: 84, + 0xE0117: 84, + 0xE0118: 84, + 0xE0119: 84, + 0xE011A: 84, + 0xE011B: 84, + 0xE011C: 84, + 0xE011D: 84, + 0xE011E: 84, + 0xE011F: 84, + 0xE0120: 84, + 0xE0121: 84, + 0xE0122: 84, + 0xE0123: 84, + 0xE0124: 84, + 0xE0125: 84, + 0xE0126: 84, + 0xE0127: 84, + 0xE0128: 84, + 0xE0129: 84, + 0xE012A: 84, + 0xE012B: 84, + 0xE012C: 84, + 0xE012D: 84, + 0xE012E: 84, + 0xE012F: 84, + 0xE0130: 84, + 0xE0131: 84, + 0xE0132: 84, + 0xE0133: 84, + 0xE0134: 84, + 0xE0135: 84, + 0xE0136: 84, + 0xE0137: 84, + 0xE0138: 84, + 0xE0139: 84, + 0xE013A: 84, + 0xE013B: 84, + 0xE013C: 84, + 0xE013D: 84, + 0xE013E: 84, + 0xE013F: 84, + 0xE0140: 84, + 0xE0141: 84, + 0xE0142: 84, + 0xE0143: 84, + 0xE0144: 84, + 0xE0145: 84, + 0xE0146: 84, + 0xE0147: 84, + 0xE0148: 84, + 0xE0149: 84, + 0xE014A: 84, + 0xE014B: 84, + 0xE014C: 84, + 0xE014D: 84, + 0xE014E: 84, + 0xE014F: 84, + 0xE0150: 84, + 0xE0151: 84, + 0xE0152: 84, + 0xE0153: 84, + 0xE0154: 84, + 0xE0155: 84, + 0xE0156: 84, + 0xE0157: 84, + 0xE0158: 84, + 0xE0159: 84, + 0xE015A: 84, + 0xE015B: 84, + 0xE015C: 84, + 0xE015D: 84, + 0xE015E: 84, + 0xE015F: 84, + 0xE0160: 84, + 0xE0161: 84, + 0xE0162: 84, + 0xE0163: 84, + 0xE0164: 84, + 0xE0165: 84, + 0xE0166: 84, + 0xE0167: 84, + 0xE0168: 84, + 0xE0169: 84, + 0xE016A: 84, + 0xE016B: 84, + 0xE016C: 84, + 0xE016D: 84, + 0xE016E: 84, + 0xE016F: 84, + 0xE0170: 84, + 0xE0171: 84, + 0xE0172: 84, + 0xE0173: 84, + 0xE0174: 84, + 0xE0175: 84, + 0xE0176: 84, + 0xE0177: 84, + 0xE0178: 84, + 0xE0179: 84, + 0xE017A: 84, + 0xE017B: 84, + 0xE017C: 84, + 0xE017D: 84, + 0xE017E: 84, + 0xE017F: 84, + 0xE0180: 84, + 0xE0181: 84, + 0xE0182: 84, + 0xE0183: 84, + 0xE0184: 84, + 0xE0185: 84, + 0xE0186: 84, + 0xE0187: 84, + 0xE0188: 84, + 0xE0189: 84, + 0xE018A: 84, + 0xE018B: 84, + 0xE018C: 84, + 0xE018D: 84, + 0xE018E: 84, + 0xE018F: 84, + 0xE0190: 84, + 0xE0191: 84, + 0xE0192: 84, + 0xE0193: 84, + 0xE0194: 84, + 0xE0195: 84, + 0xE0196: 84, + 0xE0197: 84, + 0xE0198: 84, + 0xE0199: 84, + 0xE019A: 84, + 0xE019B: 84, + 0xE019C: 84, + 0xE019D: 84, + 0xE019E: 84, + 0xE019F: 84, + 0xE01A0: 84, + 0xE01A1: 84, + 0xE01A2: 84, + 0xE01A3: 84, + 0xE01A4: 84, + 0xE01A5: 84, + 0xE01A6: 84, + 0xE01A7: 84, + 0xE01A8: 84, + 0xE01A9: 84, + 0xE01AA: 84, + 0xE01AB: 84, + 0xE01AC: 84, + 0xE01AD: 84, + 0xE01AE: 84, + 0xE01AF: 84, + 0xE01B0: 84, + 0xE01B1: 84, + 0xE01B2: 84, + 0xE01B3: 84, + 0xE01B4: 84, + 0xE01B5: 84, + 0xE01B6: 84, + 0xE01B7: 84, + 0xE01B8: 84, + 0xE01B9: 84, + 0xE01BA: 84, + 0xE01BB: 84, + 0xE01BC: 84, + 0xE01BD: 84, + 0xE01BE: 84, + 0xE01BF: 84, + 0xE01C0: 84, + 0xE01C1: 84, + 0xE01C2: 84, + 0xE01C3: 84, + 0xE01C4: 84, + 0xE01C5: 84, + 0xE01C6: 84, + 0xE01C7: 84, + 0xE01C8: 84, + 0xE01C9: 84, + 0xE01CA: 84, + 0xE01CB: 84, + 0xE01CC: 84, + 0xE01CD: 84, + 0xE01CE: 84, + 0xE01CF: 84, + 0xE01D0: 84, + 0xE01D1: 84, + 0xE01D2: 84, + 0xE01D3: 84, + 0xE01D4: 84, + 0xE01D5: 84, + 0xE01D6: 84, + 0xE01D7: 84, + 0xE01D8: 84, + 0xE01D9: 84, + 0xE01DA: 84, + 0xE01DB: 84, + 0xE01DC: 84, + 0xE01DD: 84, + 0xE01DE: 84, + 0xE01DF: 84, + 0xE01E0: 84, + 0xE01E1: 84, + 0xE01E2: 84, + 0xE01E3: 84, + 0xE01E4: 84, + 0xE01E5: 84, + 0xE01E6: 84, + 0xE01E7: 84, + 0xE01E8: 84, + 0xE01E9: 84, + 0xE01EA: 84, + 0xE01EB: 84, + 0xE01EC: 84, + 0xE01ED: 84, + 0xE01EE: 84, + 0xE01EF: 84, +} +codepoint_classes = { + "PVALID": ( + 0x2D0000002E, + 0x300000003A, + 0x610000007B, + 0xDF000000F7, + 0xF800000100, + 0x10100000102, + 0x10300000104, + 0x10500000106, + 0x10700000108, + 0x1090000010A, + 0x10B0000010C, + 0x10D0000010E, + 0x10F00000110, + 0x11100000112, + 0x11300000114, + 0x11500000116, + 0x11700000118, + 0x1190000011A, + 0x11B0000011C, + 0x11D0000011E, + 0x11F00000120, + 0x12100000122, + 0x12300000124, + 0x12500000126, + 0x12700000128, + 0x1290000012A, + 0x12B0000012C, + 0x12D0000012E, + 0x12F00000130, + 0x13100000132, + 0x13500000136, + 0x13700000139, + 0x13A0000013B, + 0x13C0000013D, + 0x13E0000013F, + 0x14200000143, + 0x14400000145, + 0x14600000147, + 0x14800000149, + 0x14B0000014C, + 0x14D0000014E, + 0x14F00000150, + 0x15100000152, + 0x15300000154, + 0x15500000156, + 0x15700000158, + 0x1590000015A, + 0x15B0000015C, + 0x15D0000015E, + 0x15F00000160, + 0x16100000162, + 0x16300000164, + 0x16500000166, + 0x16700000168, + 0x1690000016A, + 0x16B0000016C, + 0x16D0000016E, + 0x16F00000170, + 0x17100000172, + 0x17300000174, + 0x17500000176, + 0x17700000178, + 0x17A0000017B, + 0x17C0000017D, + 0x17E0000017F, + 0x18000000181, + 0x18300000184, + 0x18500000186, + 0x18800000189, + 0x18C0000018E, + 0x19200000193, + 0x19500000196, + 0x1990000019C, + 0x19E0000019F, + 0x1A1000001A2, + 0x1A3000001A4, + 0x1A5000001A6, + 0x1A8000001A9, + 0x1AA000001AC, + 0x1AD000001AE, + 0x1B0000001B1, + 0x1B4000001B5, + 0x1B6000001B7, + 0x1B9000001BC, + 0x1BD000001C4, + 0x1CE000001CF, + 0x1D0000001D1, + 0x1D2000001D3, + 0x1D4000001D5, + 0x1D6000001D7, + 0x1D8000001D9, + 0x1DA000001DB, + 0x1DC000001DE, + 0x1DF000001E0, + 0x1E1000001E2, + 0x1E3000001E4, + 0x1E5000001E6, + 0x1E7000001E8, + 0x1E9000001EA, + 0x1EB000001EC, + 0x1ED000001EE, + 0x1EF000001F1, + 0x1F5000001F6, + 0x1F9000001FA, + 0x1FB000001FC, + 0x1FD000001FE, + 0x1FF00000200, + 0x20100000202, + 0x20300000204, + 0x20500000206, + 0x20700000208, + 0x2090000020A, + 0x20B0000020C, + 0x20D0000020E, + 0x20F00000210, + 0x21100000212, + 0x21300000214, + 0x21500000216, + 0x21700000218, + 0x2190000021A, + 0x21B0000021C, + 0x21D0000021E, + 0x21F00000220, + 0x22100000222, + 0x22300000224, + 0x22500000226, + 0x22700000228, + 0x2290000022A, + 0x22B0000022C, + 0x22D0000022E, + 0x22F00000230, + 0x23100000232, + 0x2330000023A, + 0x23C0000023D, + 0x23F00000241, + 0x24200000243, + 0x24700000248, + 0x2490000024A, + 0x24B0000024C, + 0x24D0000024E, + 0x24F000002B0, + 0x2B9000002C2, + 0x2C6000002D2, + 0x2EC000002ED, + 0x2EE000002EF, + 0x30000000340, + 0x34200000343, + 0x3460000034F, + 0x35000000370, + 0x37100000372, + 0x37300000374, + 0x37700000378, + 0x37B0000037E, + 0x39000000391, + 0x3AC000003CF, + 0x3D7000003D8, + 0x3D9000003DA, + 0x3DB000003DC, + 0x3DD000003DE, + 0x3DF000003E0, + 0x3E1000003E2, + 0x3E3000003E4, + 0x3E5000003E6, + 0x3E7000003E8, + 0x3E9000003EA, + 0x3EB000003EC, + 0x3ED000003EE, + 0x3EF000003F0, + 0x3F3000003F4, + 0x3F8000003F9, + 0x3FB000003FD, + 0x43000000460, + 0x46100000462, + 0x46300000464, + 0x46500000466, + 0x46700000468, + 0x4690000046A, + 0x46B0000046C, + 0x46D0000046E, + 0x46F00000470, + 0x47100000472, + 0x47300000474, + 0x47500000476, + 0x47700000478, + 0x4790000047A, + 0x47B0000047C, + 0x47D0000047E, + 0x47F00000480, + 0x48100000482, + 0x48300000488, + 0x48B0000048C, + 0x48D0000048E, + 0x48F00000490, + 0x49100000492, + 0x49300000494, + 0x49500000496, + 0x49700000498, + 0x4990000049A, + 0x49B0000049C, + 0x49D0000049E, + 0x49F000004A0, + 0x4A1000004A2, + 0x4A3000004A4, + 0x4A5000004A6, + 0x4A7000004A8, + 0x4A9000004AA, + 0x4AB000004AC, + 0x4AD000004AE, + 0x4AF000004B0, + 0x4B1000004B2, + 0x4B3000004B4, + 0x4B5000004B6, + 0x4B7000004B8, + 0x4B9000004BA, + 0x4BB000004BC, + 0x4BD000004BE, + 0x4BF000004C0, + 0x4C2000004C3, + 0x4C4000004C5, + 0x4C6000004C7, + 0x4C8000004C9, + 0x4CA000004CB, + 0x4CC000004CD, + 0x4CE000004D0, + 0x4D1000004D2, + 0x4D3000004D4, + 0x4D5000004D6, + 0x4D7000004D8, + 0x4D9000004DA, + 0x4DB000004DC, + 0x4DD000004DE, + 0x4DF000004E0, + 0x4E1000004E2, + 0x4E3000004E4, + 0x4E5000004E6, + 0x4E7000004E8, + 0x4E9000004EA, + 0x4EB000004EC, + 0x4ED000004EE, + 0x4EF000004F0, + 0x4F1000004F2, + 0x4F3000004F4, + 0x4F5000004F6, + 0x4F7000004F8, + 0x4F9000004FA, + 0x4FB000004FC, + 0x4FD000004FE, + 0x4FF00000500, + 0x50100000502, + 0x50300000504, + 0x50500000506, + 0x50700000508, + 0x5090000050A, + 0x50B0000050C, + 0x50D0000050E, + 0x50F00000510, + 0x51100000512, + 0x51300000514, + 0x51500000516, + 0x51700000518, + 0x5190000051A, + 0x51B0000051C, + 0x51D0000051E, + 0x51F00000520, + 0x52100000522, + 0x52300000524, + 0x52500000526, + 0x52700000528, + 0x5290000052A, + 0x52B0000052C, + 0x52D0000052E, + 0x52F00000530, + 0x5590000055A, + 0x56000000587, + 0x58800000589, + 0x591000005BE, + 0x5BF000005C0, + 0x5C1000005C3, + 0x5C4000005C6, + 0x5C7000005C8, + 0x5D0000005EB, + 0x5EF000005F3, + 0x6100000061B, + 0x62000000640, + 0x64100000660, + 0x66E00000675, + 0x679000006D4, + 0x6D5000006DD, + 0x6DF000006E9, + 0x6EA000006F0, + 0x6FA00000700, + 0x7100000074B, + 0x74D000007B2, + 0x7C0000007F6, + 0x7FD000007FE, + 0x8000000082E, + 0x8400000085C, + 0x8600000086B, + 0x87000000888, + 0x8890000088F, + 0x897000008E2, + 0x8E300000958, + 0x96000000964, + 0x96600000970, + 0x97100000984, + 0x9850000098D, + 0x98F00000991, + 0x993000009A9, + 0x9AA000009B1, + 0x9B2000009B3, + 0x9B6000009BA, + 0x9BC000009C5, + 0x9C7000009C9, + 0x9CB000009CF, + 0x9D7000009D8, + 0x9E0000009E4, + 0x9E6000009F2, + 0x9FC000009FD, + 0x9FE000009FF, + 0xA0100000A04, + 0xA0500000A0B, + 0xA0F00000A11, + 0xA1300000A29, + 0xA2A00000A31, + 0xA3200000A33, + 0xA3500000A36, + 0xA3800000A3A, + 0xA3C00000A3D, + 0xA3E00000A43, + 0xA4700000A49, + 0xA4B00000A4E, + 0xA5100000A52, + 0xA5C00000A5D, + 0xA6600000A76, + 0xA8100000A84, + 0xA8500000A8E, + 0xA8F00000A92, + 0xA9300000AA9, + 0xAAA00000AB1, + 0xAB200000AB4, + 0xAB500000ABA, + 0xABC00000AC6, + 0xAC700000ACA, + 0xACB00000ACE, + 0xAD000000AD1, + 0xAE000000AE4, + 0xAE600000AF0, + 0xAF900000B00, + 0xB0100000B04, + 0xB0500000B0D, + 0xB0F00000B11, + 0xB1300000B29, + 0xB2A00000B31, + 0xB3200000B34, + 0xB3500000B3A, + 0xB3C00000B45, + 0xB4700000B49, + 0xB4B00000B4E, + 0xB5500000B58, + 0xB5F00000B64, + 0xB6600000B70, + 0xB7100000B72, + 0xB8200000B84, + 0xB8500000B8B, + 0xB8E00000B91, + 0xB9200000B96, + 0xB9900000B9B, + 0xB9C00000B9D, + 0xB9E00000BA0, + 0xBA300000BA5, + 0xBA800000BAB, + 0xBAE00000BBA, + 0xBBE00000BC3, + 0xBC600000BC9, + 0xBCA00000BCE, + 0xBD000000BD1, + 0xBD700000BD8, + 0xBE600000BF0, + 0xC0000000C0D, + 0xC0E00000C11, + 0xC1200000C29, + 0xC2A00000C3A, + 0xC3C00000C45, + 0xC4600000C49, + 0xC4A00000C4E, + 0xC5500000C57, + 0xC5800000C5B, + 0xC5D00000C5E, + 0xC6000000C64, + 0xC6600000C70, + 0xC8000000C84, + 0xC8500000C8D, + 0xC8E00000C91, + 0xC9200000CA9, + 0xCAA00000CB4, + 0xCB500000CBA, + 0xCBC00000CC5, + 0xCC600000CC9, + 0xCCA00000CCE, + 0xCD500000CD7, + 0xCDD00000CDF, + 0xCE000000CE4, + 0xCE600000CF0, + 0xCF100000CF4, + 0xD0000000D0D, + 0xD0E00000D11, + 0xD1200000D45, + 0xD4600000D49, + 0xD4A00000D4F, + 0xD5400000D58, + 0xD5F00000D64, + 0xD6600000D70, + 0xD7A00000D80, + 0xD8100000D84, + 0xD8500000D97, + 0xD9A00000DB2, + 0xDB300000DBC, + 0xDBD00000DBE, + 0xDC000000DC7, + 0xDCA00000DCB, + 0xDCF00000DD5, + 0xDD600000DD7, + 0xDD800000DE0, + 0xDE600000DF0, + 0xDF200000DF4, + 0xE0100000E33, + 0xE3400000E3B, + 0xE4000000E4F, + 0xE5000000E5A, + 0xE8100000E83, + 0xE8400000E85, + 0xE8600000E8B, + 0xE8C00000EA4, + 0xEA500000EA6, + 0xEA700000EB3, + 0xEB400000EBE, + 0xEC000000EC5, + 0xEC600000EC7, + 0xEC800000ECF, + 0xED000000EDA, + 0xEDE00000EE0, + 0xF0000000F01, + 0xF0B00000F0C, + 0xF1800000F1A, + 0xF2000000F2A, + 0xF3500000F36, + 0xF3700000F38, + 0xF3900000F3A, + 0xF3E00000F43, + 0xF4400000F48, + 0xF4900000F4D, + 0xF4E00000F52, + 0xF5300000F57, + 0xF5800000F5C, + 0xF5D00000F69, + 0xF6A00000F6D, + 0xF7100000F73, + 0xF7400000F75, + 0xF7A00000F81, + 0xF8200000F85, + 0xF8600000F93, + 0xF9400000F98, + 0xF9900000F9D, + 0xF9E00000FA2, + 0xFA300000FA7, + 0xFA800000FAC, + 0xFAD00000FB9, + 0xFBA00000FBD, + 0xFC600000FC7, + 0x10000000104A, + 0x10500000109E, + 0x10D0000010FB, + 0x10FD00001100, + 0x120000001249, + 0x124A0000124E, + 0x125000001257, + 0x125800001259, + 0x125A0000125E, + 0x126000001289, + 0x128A0000128E, + 0x1290000012B1, + 0x12B2000012B6, + 0x12B8000012BF, + 0x12C0000012C1, + 0x12C2000012C6, + 0x12C8000012D7, + 0x12D800001311, + 0x131200001316, + 0x13180000135B, + 0x135D00001360, + 0x138000001390, + 0x13A0000013F6, + 0x14010000166D, + 0x166F00001680, + 0x16810000169B, + 0x16A0000016EB, + 0x16F1000016F9, + 0x170000001716, + 0x171F00001735, + 0x174000001754, + 0x17600000176D, + 0x176E00001771, + 0x177200001774, + 0x1780000017B4, + 0x17B6000017D4, + 0x17D7000017D8, + 0x17DC000017DE, + 0x17E0000017EA, + 0x18100000181A, + 0x182000001879, + 0x1880000018AB, + 0x18B0000018F6, + 0x19000000191F, + 0x19200000192C, + 0x19300000193C, + 0x19460000196E, + 0x197000001975, + 0x1980000019AC, + 0x19B0000019CA, + 0x19D0000019DA, + 0x1A0000001A1C, + 0x1A2000001A5F, + 0x1A6000001A7D, + 0x1A7F00001A8A, + 0x1A9000001A9A, + 0x1AA700001AA8, + 0x1AB000001ABE, + 0x1ABF00001ACF, + 0x1B0000001B4D, + 0x1B5000001B5A, + 0x1B6B00001B74, + 0x1B8000001BF4, + 0x1C0000001C38, + 0x1C4000001C4A, + 0x1C4D00001C7E, + 0x1C8A00001C8B, + 0x1CD000001CD3, + 0x1CD400001CFB, + 0x1D0000001D2C, + 0x1D2F00001D30, + 0x1D3B00001D3C, + 0x1D4E00001D4F, + 0x1D6B00001D78, + 0x1D7900001D9B, + 0x1DC000001E00, + 0x1E0100001E02, + 0x1E0300001E04, + 0x1E0500001E06, + 0x1E0700001E08, + 0x1E0900001E0A, + 0x1E0B00001E0C, + 0x1E0D00001E0E, + 0x1E0F00001E10, + 0x1E1100001E12, + 0x1E1300001E14, + 0x1E1500001E16, + 0x1E1700001E18, + 0x1E1900001E1A, + 0x1E1B00001E1C, + 0x1E1D00001E1E, + 0x1E1F00001E20, + 0x1E2100001E22, + 0x1E2300001E24, + 0x1E2500001E26, + 0x1E2700001E28, + 0x1E2900001E2A, + 0x1E2B00001E2C, + 0x1E2D00001E2E, + 0x1E2F00001E30, + 0x1E3100001E32, + 0x1E3300001E34, + 0x1E3500001E36, + 0x1E3700001E38, + 0x1E3900001E3A, + 0x1E3B00001E3C, + 0x1E3D00001E3E, + 0x1E3F00001E40, + 0x1E4100001E42, + 0x1E4300001E44, + 0x1E4500001E46, + 0x1E4700001E48, + 0x1E4900001E4A, + 0x1E4B00001E4C, + 0x1E4D00001E4E, + 0x1E4F00001E50, + 0x1E5100001E52, + 0x1E5300001E54, + 0x1E5500001E56, + 0x1E5700001E58, + 0x1E5900001E5A, + 0x1E5B00001E5C, + 0x1E5D00001E5E, + 0x1E5F00001E60, + 0x1E6100001E62, + 0x1E6300001E64, + 0x1E6500001E66, + 0x1E6700001E68, + 0x1E6900001E6A, + 0x1E6B00001E6C, + 0x1E6D00001E6E, + 0x1E6F00001E70, + 0x1E7100001E72, + 0x1E7300001E74, + 0x1E7500001E76, + 0x1E7700001E78, + 0x1E7900001E7A, + 0x1E7B00001E7C, + 0x1E7D00001E7E, + 0x1E7F00001E80, + 0x1E8100001E82, + 0x1E8300001E84, + 0x1E8500001E86, + 0x1E8700001E88, + 0x1E8900001E8A, + 0x1E8B00001E8C, + 0x1E8D00001E8E, + 0x1E8F00001E90, + 0x1E9100001E92, + 0x1E9300001E94, + 0x1E9500001E9A, + 0x1E9C00001E9E, + 0x1E9F00001EA0, + 0x1EA100001EA2, + 0x1EA300001EA4, + 0x1EA500001EA6, + 0x1EA700001EA8, + 0x1EA900001EAA, + 0x1EAB00001EAC, + 0x1EAD00001EAE, + 0x1EAF00001EB0, + 0x1EB100001EB2, + 0x1EB300001EB4, + 0x1EB500001EB6, + 0x1EB700001EB8, + 0x1EB900001EBA, + 0x1EBB00001EBC, + 0x1EBD00001EBE, + 0x1EBF00001EC0, + 0x1EC100001EC2, + 0x1EC300001EC4, + 0x1EC500001EC6, + 0x1EC700001EC8, + 0x1EC900001ECA, + 0x1ECB00001ECC, + 0x1ECD00001ECE, + 0x1ECF00001ED0, + 0x1ED100001ED2, + 0x1ED300001ED4, + 0x1ED500001ED6, + 0x1ED700001ED8, + 0x1ED900001EDA, + 0x1EDB00001EDC, + 0x1EDD00001EDE, + 0x1EDF00001EE0, + 0x1EE100001EE2, + 0x1EE300001EE4, + 0x1EE500001EE6, + 0x1EE700001EE8, + 0x1EE900001EEA, + 0x1EEB00001EEC, + 0x1EED00001EEE, + 0x1EEF00001EF0, + 0x1EF100001EF2, + 0x1EF300001EF4, + 0x1EF500001EF6, + 0x1EF700001EF8, + 0x1EF900001EFA, + 0x1EFB00001EFC, + 0x1EFD00001EFE, + 0x1EFF00001F08, + 0x1F1000001F16, + 0x1F2000001F28, + 0x1F3000001F38, + 0x1F4000001F46, + 0x1F5000001F58, + 0x1F6000001F68, + 0x1F7000001F71, + 0x1F7200001F73, + 0x1F7400001F75, + 0x1F7600001F77, + 0x1F7800001F79, + 0x1F7A00001F7B, + 0x1F7C00001F7D, + 0x1FB000001FB2, + 0x1FB600001FB7, + 0x1FC600001FC7, + 0x1FD000001FD3, + 0x1FD600001FD8, + 0x1FE000001FE3, + 0x1FE400001FE8, + 0x1FF600001FF7, + 0x214E0000214F, + 0x218400002185, + 0x2C3000002C60, + 0x2C6100002C62, + 0x2C6500002C67, + 0x2C6800002C69, + 0x2C6A00002C6B, + 0x2C6C00002C6D, + 0x2C7100002C72, + 0x2C7300002C75, + 0x2C7600002C7C, + 0x2C8100002C82, + 0x2C8300002C84, + 0x2C8500002C86, + 0x2C8700002C88, + 0x2C8900002C8A, + 0x2C8B00002C8C, + 0x2C8D00002C8E, + 0x2C8F00002C90, + 0x2C9100002C92, + 0x2C9300002C94, + 0x2C9500002C96, + 0x2C9700002C98, + 0x2C9900002C9A, + 0x2C9B00002C9C, + 0x2C9D00002C9E, + 0x2C9F00002CA0, + 0x2CA100002CA2, + 0x2CA300002CA4, + 0x2CA500002CA6, + 0x2CA700002CA8, + 0x2CA900002CAA, + 0x2CAB00002CAC, + 0x2CAD00002CAE, + 0x2CAF00002CB0, + 0x2CB100002CB2, + 0x2CB300002CB4, + 0x2CB500002CB6, + 0x2CB700002CB8, + 0x2CB900002CBA, + 0x2CBB00002CBC, + 0x2CBD00002CBE, + 0x2CBF00002CC0, + 0x2CC100002CC2, + 0x2CC300002CC4, + 0x2CC500002CC6, + 0x2CC700002CC8, + 0x2CC900002CCA, + 0x2CCB00002CCC, + 0x2CCD00002CCE, + 0x2CCF00002CD0, + 0x2CD100002CD2, + 0x2CD300002CD4, + 0x2CD500002CD6, + 0x2CD700002CD8, + 0x2CD900002CDA, + 0x2CDB00002CDC, + 0x2CDD00002CDE, + 0x2CDF00002CE0, + 0x2CE100002CE2, + 0x2CE300002CE5, + 0x2CEC00002CED, + 0x2CEE00002CF2, + 0x2CF300002CF4, + 0x2D0000002D26, + 0x2D2700002D28, + 0x2D2D00002D2E, + 0x2D3000002D68, + 0x2D7F00002D97, + 0x2DA000002DA7, + 0x2DA800002DAF, + 0x2DB000002DB7, + 0x2DB800002DBF, + 0x2DC000002DC7, + 0x2DC800002DCF, + 0x2DD000002DD7, + 0x2DD800002DDF, + 0x2DE000002E00, + 0x2E2F00002E30, + 0x300500003008, + 0x302A0000302E, + 0x303C0000303D, + 0x304100003097, + 0x30990000309B, + 0x309D0000309F, + 0x30A1000030FB, + 0x30FC000030FF, + 0x310500003130, + 0x31A0000031C0, + 0x31F000003200, + 0x340000004DC0, + 0x4E000000A48D, + 0xA4D00000A4FE, + 0xA5000000A60D, + 0xA6100000A62C, + 0xA6410000A642, + 0xA6430000A644, + 0xA6450000A646, + 0xA6470000A648, + 0xA6490000A64A, + 0xA64B0000A64C, + 0xA64D0000A64E, + 0xA64F0000A650, + 0xA6510000A652, + 0xA6530000A654, + 0xA6550000A656, + 0xA6570000A658, + 0xA6590000A65A, + 0xA65B0000A65C, + 0xA65D0000A65E, + 0xA65F0000A660, + 0xA6610000A662, + 0xA6630000A664, + 0xA6650000A666, + 0xA6670000A668, + 0xA6690000A66A, + 0xA66B0000A66C, + 0xA66D0000A670, + 0xA6740000A67E, + 0xA67F0000A680, + 0xA6810000A682, + 0xA6830000A684, + 0xA6850000A686, + 0xA6870000A688, + 0xA6890000A68A, + 0xA68B0000A68C, + 0xA68D0000A68E, + 0xA68F0000A690, + 0xA6910000A692, + 0xA6930000A694, + 0xA6950000A696, + 0xA6970000A698, + 0xA6990000A69A, + 0xA69B0000A69C, + 0xA69E0000A6E6, + 0xA6F00000A6F2, + 0xA7170000A720, + 0xA7230000A724, + 0xA7250000A726, + 0xA7270000A728, + 0xA7290000A72A, + 0xA72B0000A72C, + 0xA72D0000A72E, + 0xA72F0000A732, + 0xA7330000A734, + 0xA7350000A736, + 0xA7370000A738, + 0xA7390000A73A, + 0xA73B0000A73C, + 0xA73D0000A73E, + 0xA73F0000A740, + 0xA7410000A742, + 0xA7430000A744, + 0xA7450000A746, + 0xA7470000A748, + 0xA7490000A74A, + 0xA74B0000A74C, + 0xA74D0000A74E, + 0xA74F0000A750, + 0xA7510000A752, + 0xA7530000A754, + 0xA7550000A756, + 0xA7570000A758, + 0xA7590000A75A, + 0xA75B0000A75C, + 0xA75D0000A75E, + 0xA75F0000A760, + 0xA7610000A762, + 0xA7630000A764, + 0xA7650000A766, + 0xA7670000A768, + 0xA7690000A76A, + 0xA76B0000A76C, + 0xA76D0000A76E, + 0xA76F0000A770, + 0xA7710000A779, + 0xA77A0000A77B, + 0xA77C0000A77D, + 0xA77F0000A780, + 0xA7810000A782, + 0xA7830000A784, + 0xA7850000A786, + 0xA7870000A789, + 0xA78C0000A78D, + 0xA78E0000A790, + 0xA7910000A792, + 0xA7930000A796, + 0xA7970000A798, + 0xA7990000A79A, + 0xA79B0000A79C, + 0xA79D0000A79E, + 0xA79F0000A7A0, + 0xA7A10000A7A2, + 0xA7A30000A7A4, + 0xA7A50000A7A6, + 0xA7A70000A7A8, + 0xA7A90000A7AA, + 0xA7AF0000A7B0, + 0xA7B50000A7B6, + 0xA7B70000A7B8, + 0xA7B90000A7BA, + 0xA7BB0000A7BC, + 0xA7BD0000A7BE, + 0xA7BF0000A7C0, + 0xA7C10000A7C2, + 0xA7C30000A7C4, + 0xA7C80000A7C9, + 0xA7CA0000A7CB, + 0xA7CD0000A7CE, + 0xA7D10000A7D2, + 0xA7D30000A7D4, + 0xA7D50000A7D6, + 0xA7D70000A7D8, + 0xA7D90000A7DA, + 0xA7DB0000A7DC, + 0xA7F60000A7F8, + 0xA7FA0000A828, + 0xA82C0000A82D, + 0xA8400000A874, + 0xA8800000A8C6, + 0xA8D00000A8DA, + 0xA8E00000A8F8, + 0xA8FB0000A8FC, + 0xA8FD0000A92E, + 0xA9300000A954, + 0xA9800000A9C1, + 0xA9CF0000A9DA, + 0xA9E00000A9FF, + 0xAA000000AA37, + 0xAA400000AA4E, + 0xAA500000AA5A, + 0xAA600000AA77, + 0xAA7A0000AAC3, + 0xAADB0000AADE, + 0xAAE00000AAF0, + 0xAAF20000AAF7, + 0xAB010000AB07, + 0xAB090000AB0F, + 0xAB110000AB17, + 0xAB200000AB27, + 0xAB280000AB2F, + 0xAB300000AB5B, + 0xAB600000AB69, + 0xABC00000ABEB, + 0xABEC0000ABEE, + 0xABF00000ABFA, + 0xAC000000D7A4, + 0xFA0E0000FA10, + 0xFA110000FA12, + 0xFA130000FA15, + 0xFA1F0000FA20, + 0xFA210000FA22, + 0xFA230000FA25, + 0xFA270000FA2A, + 0xFB1E0000FB1F, + 0xFE200000FE30, + 0xFE730000FE74, + 0x100000001000C, + 0x1000D00010027, + 0x100280001003B, + 0x1003C0001003E, + 0x1003F0001004E, + 0x100500001005E, + 0x10080000100FB, + 0x101FD000101FE, + 0x102800001029D, + 0x102A0000102D1, + 0x102E0000102E1, + 0x1030000010320, + 0x1032D00010341, + 0x103420001034A, + 0x103500001037B, + 0x103800001039E, + 0x103A0000103C4, + 0x103C8000103D0, + 0x104280001049E, + 0x104A0000104AA, + 0x104D8000104FC, + 0x1050000010528, + 0x1053000010564, + 0x10597000105A2, + 0x105A3000105B2, + 0x105B3000105BA, + 0x105BB000105BD, + 0x105C0000105F4, + 0x1060000010737, + 0x1074000010756, + 0x1076000010768, + 0x1078000010781, + 0x1080000010806, + 0x1080800010809, + 0x1080A00010836, + 0x1083700010839, + 0x1083C0001083D, + 0x1083F00010856, + 0x1086000010877, + 0x108800001089F, + 0x108E0000108F3, + 0x108F4000108F6, + 0x1090000010916, + 0x109200001093A, + 0x10980000109B8, + 0x109BE000109C0, + 0x10A0000010A04, + 0x10A0500010A07, + 0x10A0C00010A14, + 0x10A1500010A18, + 0x10A1900010A36, + 0x10A3800010A3B, + 0x10A3F00010A40, + 0x10A6000010A7D, + 0x10A8000010A9D, + 0x10AC000010AC8, + 0x10AC900010AE7, + 0x10B0000010B36, + 0x10B4000010B56, + 0x10B6000010B73, + 0x10B8000010B92, + 0x10C0000010C49, + 0x10CC000010CF3, + 0x10D0000010D28, + 0x10D3000010D3A, + 0x10D4000010D50, + 0x10D6900010D6E, + 0x10D6F00010D86, + 0x10E8000010EAA, + 0x10EAB00010EAD, + 0x10EB000010EB2, + 0x10EC200010EC5, + 0x10EFC00010F1D, + 0x10F2700010F28, + 0x10F3000010F51, + 0x10F7000010F86, + 0x10FB000010FC5, + 0x10FE000010FF7, + 0x1100000011047, + 0x1106600011076, + 0x1107F000110BB, + 0x110C2000110C3, + 0x110D0000110E9, + 0x110F0000110FA, + 0x1110000011135, + 0x1113600011140, + 0x1114400011148, + 0x1115000011174, + 0x1117600011177, + 0x11180000111C5, + 0x111C9000111CD, + 0x111CE000111DB, + 0x111DC000111DD, + 0x1120000011212, + 0x1121300011238, + 0x1123E00011242, + 0x1128000011287, + 0x1128800011289, + 0x1128A0001128E, + 0x1128F0001129E, + 0x1129F000112A9, + 0x112B0000112EB, + 0x112F0000112FA, + 0x1130000011304, + 0x113050001130D, + 0x1130F00011311, + 0x1131300011329, + 0x1132A00011331, + 0x1133200011334, + 0x113350001133A, + 0x1133B00011345, + 0x1134700011349, + 0x1134B0001134E, + 0x1135000011351, + 0x1135700011358, + 0x1135D00011364, + 0x113660001136D, + 0x1137000011375, + 0x113800001138A, + 0x1138B0001138C, + 0x1138E0001138F, + 0x11390000113B6, + 0x113B7000113C1, + 0x113C2000113C3, + 0x113C5000113C6, + 0x113C7000113CB, + 0x113CC000113D4, + 0x113E1000113E3, + 0x114000001144B, + 0x114500001145A, + 0x1145E00011462, + 0x11480000114C6, + 0x114C7000114C8, + 0x114D0000114DA, + 0x11580000115B6, + 0x115B8000115C1, + 0x115D8000115DE, + 0x1160000011641, + 0x1164400011645, + 0x116500001165A, + 0x11680000116B9, + 0x116C0000116CA, + 0x116D0000116E4, + 0x117000001171B, + 0x1171D0001172C, + 0x117300001173A, + 0x1174000011747, + 0x118000001183B, + 0x118C0000118EA, + 0x118FF00011907, + 0x119090001190A, + 0x1190C00011914, + 0x1191500011917, + 0x1191800011936, + 0x1193700011939, + 0x1193B00011944, + 0x119500001195A, + 0x119A0000119A8, + 0x119AA000119D8, + 0x119DA000119E2, + 0x119E3000119E5, + 0x11A0000011A3F, + 0x11A4700011A48, + 0x11A5000011A9A, + 0x11A9D00011A9E, + 0x11AB000011AF9, + 0x11BC000011BE1, + 0x11BF000011BFA, + 0x11C0000011C09, + 0x11C0A00011C37, + 0x11C3800011C41, + 0x11C5000011C5A, + 0x11C7200011C90, + 0x11C9200011CA8, + 0x11CA900011CB7, + 0x11D0000011D07, + 0x11D0800011D0A, + 0x11D0B00011D37, + 0x11D3A00011D3B, + 0x11D3C00011D3E, + 0x11D3F00011D48, + 0x11D5000011D5A, + 0x11D6000011D66, + 0x11D6700011D69, + 0x11D6A00011D8F, + 0x11D9000011D92, + 0x11D9300011D99, + 0x11DA000011DAA, + 0x11EE000011EF7, + 0x11F0000011F11, + 0x11F1200011F3B, + 0x11F3E00011F43, + 0x11F5000011F5B, + 0x11FB000011FB1, + 0x120000001239A, + 0x1248000012544, + 0x12F9000012FF1, + 0x1300000013430, + 0x1344000013456, + 0x13460000143FB, + 0x1440000014647, + 0x161000001613A, + 0x1680000016A39, + 0x16A4000016A5F, + 0x16A6000016A6A, + 0x16A7000016ABF, + 0x16AC000016ACA, + 0x16AD000016AEE, + 0x16AF000016AF5, + 0x16B0000016B37, + 0x16B4000016B44, + 0x16B5000016B5A, + 0x16B6300016B78, + 0x16B7D00016B90, + 0x16D4000016D6D, + 0x16D7000016D7A, + 0x16E6000016E80, + 0x16F0000016F4B, + 0x16F4F00016F88, + 0x16F8F00016FA0, + 0x16FE000016FE2, + 0x16FE300016FE5, + 0x16FF000016FF2, + 0x17000000187F8, + 0x1880000018CD6, + 0x18CFF00018D09, + 0x1AFF00001AFF4, + 0x1AFF50001AFFC, + 0x1AFFD0001AFFF, + 0x1B0000001B123, + 0x1B1320001B133, + 0x1B1500001B153, + 0x1B1550001B156, + 0x1B1640001B168, + 0x1B1700001B2FC, + 0x1BC000001BC6B, + 0x1BC700001BC7D, + 0x1BC800001BC89, + 0x1BC900001BC9A, + 0x1BC9D0001BC9F, + 0x1CCF00001CCFA, + 0x1CF000001CF2E, + 0x1CF300001CF47, + 0x1DA000001DA37, + 0x1DA3B0001DA6D, + 0x1DA750001DA76, + 0x1DA840001DA85, + 0x1DA9B0001DAA0, + 0x1DAA10001DAB0, + 0x1DF000001DF1F, + 0x1DF250001DF2B, + 0x1E0000001E007, + 0x1E0080001E019, + 0x1E01B0001E022, + 0x1E0230001E025, + 0x1E0260001E02B, + 0x1E08F0001E090, + 0x1E1000001E12D, + 0x1E1300001E13E, + 0x1E1400001E14A, + 0x1E14E0001E14F, + 0x1E2900001E2AF, + 0x1E2C00001E2FA, + 0x1E4D00001E4FA, + 0x1E5D00001E5FB, + 0x1E7E00001E7E7, + 0x1E7E80001E7EC, + 0x1E7ED0001E7EF, + 0x1E7F00001E7FF, + 0x1E8000001E8C5, + 0x1E8D00001E8D7, + 0x1E9220001E94C, + 0x1E9500001E95A, + 0x200000002A6E0, + 0x2A7000002B73A, + 0x2B7400002B81E, + 0x2B8200002CEA2, + 0x2CEB00002EBE1, + 0x2EBF00002EE5E, + 0x300000003134B, + 0x31350000323B0, + ), + "CONTEXTJ": (0x200C0000200E,), + "CONTEXTO": ( + 0xB7000000B8, + 0x37500000376, + 0x5F3000005F5, + 0x6600000066A, + 0x6F0000006FA, + 0x30FB000030FC, + ), +} diff --git a/venv/lib/python3.10/site-packages/idna/intranges.py b/venv/lib/python3.10/site-packages/idna/intranges.py new file mode 100644 index 0000000..7bfaa8d --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna/intranges.py @@ -0,0 +1,57 @@ +""" +Given a list of integers, made up of (hopefully) a small number of long runs +of consecutive integers, compute a representation of the form +((start1, end1), (start2, end2) ...). Then answer the question "was x present +in the original list?" in time O(log(# runs)). +""" + +import bisect +from typing import List, Tuple + + +def intranges_from_list(list_: List[int]) -> Tuple[int, ...]: + """Represent a list of integers as a sequence of ranges: + ((start_0, end_0), (start_1, end_1), ...), such that the original + integers are exactly those x such that start_i <= x < end_i for some i. + + Ranges are encoded as single integers (start << 32 | end), not as tuples. + """ + + sorted_list = sorted(list_) + ranges = [] + last_write = -1 + for i in range(len(sorted_list)): + if i + 1 < len(sorted_list): + if sorted_list[i] == sorted_list[i + 1] - 1: + continue + current_range = sorted_list[last_write + 1 : i + 1] + ranges.append(_encode_range(current_range[0], current_range[-1] + 1)) + last_write = i + + return tuple(ranges) + + +def _encode_range(start: int, end: int) -> int: + return (start << 32) | end + + +def _decode_range(r: int) -> Tuple[int, int]: + return (r >> 32), (r & ((1 << 32) - 1)) + + +def intranges_contain(int_: int, ranges: Tuple[int, ...]) -> bool: + """Determine if `int_` falls into one of the ranges in `ranges`.""" + tuple_ = _encode_range(int_, 0) + pos = bisect.bisect_left(ranges, tuple_) + # we could be immediately ahead of a tuple (start, end) + # with start < int_ <= end + if pos > 0: + left, right = _decode_range(ranges[pos - 1]) + if left <= int_ < right: + return True + # or we could be immediately behind a tuple (int_, end) + if pos < len(ranges): + left, _ = _decode_range(ranges[pos]) + if left == int_: + return True + return False diff --git a/venv/lib/python3.10/site-packages/idna/package_data.py b/venv/lib/python3.10/site-packages/idna/package_data.py new file mode 100644 index 0000000..7272c8d --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna/package_data.py @@ -0,0 +1 @@ +__version__ = "3.11" diff --git a/venv/lib/python3.10/site-packages/idna/py.typed b/venv/lib/python3.10/site-packages/idna/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/idna/uts46data.py b/venv/lib/python3.10/site-packages/idna/uts46data.py new file mode 100644 index 0000000..4610b71 --- /dev/null +++ b/venv/lib/python3.10/site-packages/idna/uts46data.py @@ -0,0 +1,8841 @@ +# This file is automatically generated by tools/idna-data +# vim: set fileencoding=utf-8 : + +from typing import List, Tuple, Union + +"""IDNA Mapping Table from UTS46.""" + + +__version__ = "16.0.0" + + +def _seg_0() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x0, "V"), + (0x1, "V"), + (0x2, "V"), + (0x3, "V"), + (0x4, "V"), + (0x5, "V"), + (0x6, "V"), + (0x7, "V"), + (0x8, "V"), + (0x9, "V"), + (0xA, "V"), + (0xB, "V"), + (0xC, "V"), + (0xD, "V"), + (0xE, "V"), + (0xF, "V"), + (0x10, "V"), + (0x11, "V"), + (0x12, "V"), + (0x13, "V"), + (0x14, "V"), + (0x15, "V"), + (0x16, "V"), + (0x17, "V"), + (0x18, "V"), + (0x19, "V"), + (0x1A, "V"), + (0x1B, "V"), + (0x1C, "V"), + (0x1D, "V"), + (0x1E, "V"), + (0x1F, "V"), + (0x20, "V"), + (0x21, "V"), + (0x22, "V"), + (0x23, "V"), + (0x24, "V"), + (0x25, "V"), + (0x26, "V"), + (0x27, "V"), + (0x28, "V"), + (0x29, "V"), + (0x2A, "V"), + (0x2B, "V"), + (0x2C, "V"), + (0x2D, "V"), + (0x2E, "V"), + (0x2F, "V"), + (0x30, "V"), + (0x31, "V"), + (0x32, "V"), + (0x33, "V"), + (0x34, "V"), + (0x35, "V"), + (0x36, "V"), + (0x37, "V"), + (0x38, "V"), + (0x39, "V"), + (0x3A, "V"), + (0x3B, "V"), + (0x3C, "V"), + (0x3D, "V"), + (0x3E, "V"), + (0x3F, "V"), + (0x40, "V"), + (0x41, "M", "a"), + (0x42, "M", "b"), + (0x43, "M", "c"), + (0x44, "M", "d"), + (0x45, "M", "e"), + (0x46, "M", "f"), + (0x47, "M", "g"), + (0x48, "M", "h"), + (0x49, "M", "i"), + (0x4A, "M", "j"), + (0x4B, "M", "k"), + (0x4C, "M", "l"), + (0x4D, "M", "m"), + (0x4E, "M", "n"), + (0x4F, "M", "o"), + (0x50, "M", "p"), + (0x51, "M", "q"), + (0x52, "M", "r"), + (0x53, "M", "s"), + (0x54, "M", "t"), + (0x55, "M", "u"), + (0x56, "M", "v"), + (0x57, "M", "w"), + (0x58, "M", "x"), + (0x59, "M", "y"), + (0x5A, "M", "z"), + (0x5B, "V"), + (0x5C, "V"), + (0x5D, "V"), + (0x5E, "V"), + (0x5F, "V"), + (0x60, "V"), + (0x61, "V"), + (0x62, "V"), + (0x63, "V"), + ] + + +def _seg_1() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x64, "V"), + (0x65, "V"), + (0x66, "V"), + (0x67, "V"), + (0x68, "V"), + (0x69, "V"), + (0x6A, "V"), + (0x6B, "V"), + (0x6C, "V"), + (0x6D, "V"), + (0x6E, "V"), + (0x6F, "V"), + (0x70, "V"), + (0x71, "V"), + (0x72, "V"), + (0x73, "V"), + (0x74, "V"), + (0x75, "V"), + (0x76, "V"), + (0x77, "V"), + (0x78, "V"), + (0x79, "V"), + (0x7A, "V"), + (0x7B, "V"), + (0x7C, "V"), + (0x7D, "V"), + (0x7E, "V"), + (0x7F, "V"), + (0x80, "X"), + (0x81, "X"), + (0x82, "X"), + (0x83, "X"), + (0x84, "X"), + (0x85, "X"), + (0x86, "X"), + (0x87, "X"), + (0x88, "X"), + (0x89, "X"), + (0x8A, "X"), + (0x8B, "X"), + (0x8C, "X"), + (0x8D, "X"), + (0x8E, "X"), + (0x8F, "X"), + (0x90, "X"), + (0x91, "X"), + (0x92, "X"), + (0x93, "X"), + (0x94, "X"), + (0x95, "X"), + (0x96, "X"), + (0x97, "X"), + (0x98, "X"), + (0x99, "X"), + (0x9A, "X"), + (0x9B, "X"), + (0x9C, "X"), + (0x9D, "X"), + (0x9E, "X"), + (0x9F, "X"), + (0xA0, "M", " "), + (0xA1, "V"), + (0xA2, "V"), + (0xA3, "V"), + (0xA4, "V"), + (0xA5, "V"), + (0xA6, "V"), + (0xA7, "V"), + (0xA8, "M", " ̈"), + (0xA9, "V"), + (0xAA, "M", "a"), + (0xAB, "V"), + (0xAC, "V"), + (0xAD, "I"), + (0xAE, "V"), + (0xAF, "M", " ̄"), + (0xB0, "V"), + (0xB1, "V"), + (0xB2, "M", "2"), + (0xB3, "M", "3"), + (0xB4, "M", " ́"), + (0xB5, "M", "μ"), + (0xB6, "V"), + (0xB7, "V"), + (0xB8, "M", " ̧"), + (0xB9, "M", "1"), + (0xBA, "M", "o"), + (0xBB, "V"), + (0xBC, "M", "1⁄4"), + (0xBD, "M", "1⁄2"), + (0xBE, "M", "3⁄4"), + (0xBF, "V"), + (0xC0, "M", "à"), + (0xC1, "M", "á"), + (0xC2, "M", "â"), + (0xC3, "M", "ã"), + (0xC4, "M", "ä"), + (0xC5, "M", "å"), + (0xC6, "M", "æ"), + (0xC7, "M", "ç"), + ] + + +def _seg_2() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xC8, "M", "è"), + (0xC9, "M", "é"), + (0xCA, "M", "ê"), + (0xCB, "M", "ë"), + (0xCC, "M", "ì"), + (0xCD, "M", "í"), + (0xCE, "M", "î"), + (0xCF, "M", "ï"), + (0xD0, "M", "ð"), + (0xD1, "M", "ñ"), + (0xD2, "M", "ò"), + (0xD3, "M", "ó"), + (0xD4, "M", "ô"), + (0xD5, "M", "õ"), + (0xD6, "M", "ö"), + (0xD7, "V"), + (0xD8, "M", "ø"), + (0xD9, "M", "ù"), + (0xDA, "M", "ú"), + (0xDB, "M", "û"), + (0xDC, "M", "ü"), + (0xDD, "M", "ý"), + (0xDE, "M", "þ"), + (0xDF, "D", "ss"), + (0xE0, "V"), + (0xE1, "V"), + (0xE2, "V"), + (0xE3, "V"), + (0xE4, "V"), + (0xE5, "V"), + (0xE6, "V"), + (0xE7, "V"), + (0xE8, "V"), + (0xE9, "V"), + (0xEA, "V"), + (0xEB, "V"), + (0xEC, "V"), + (0xED, "V"), + (0xEE, "V"), + (0xEF, "V"), + (0xF0, "V"), + (0xF1, "V"), + (0xF2, "V"), + (0xF3, "V"), + (0xF4, "V"), + (0xF5, "V"), + (0xF6, "V"), + (0xF7, "V"), + (0xF8, "V"), + (0xF9, "V"), + (0xFA, "V"), + (0xFB, "V"), + (0xFC, "V"), + (0xFD, "V"), + (0xFE, "V"), + (0xFF, "V"), + (0x100, "M", "ā"), + (0x101, "V"), + (0x102, "M", "ă"), + (0x103, "V"), + (0x104, "M", "ą"), + (0x105, "V"), + (0x106, "M", "ć"), + (0x107, "V"), + (0x108, "M", "ĉ"), + (0x109, "V"), + (0x10A, "M", "ċ"), + (0x10B, "V"), + (0x10C, "M", "č"), + (0x10D, "V"), + (0x10E, "M", "ď"), + (0x10F, "V"), + (0x110, "M", "đ"), + (0x111, "V"), + (0x112, "M", "ē"), + (0x113, "V"), + (0x114, "M", "ĕ"), + (0x115, "V"), + (0x116, "M", "ė"), + (0x117, "V"), + (0x118, "M", "ę"), + (0x119, "V"), + (0x11A, "M", "ě"), + (0x11B, "V"), + (0x11C, "M", "ĝ"), + (0x11D, "V"), + (0x11E, "M", "ğ"), + (0x11F, "V"), + (0x120, "M", "ġ"), + (0x121, "V"), + (0x122, "M", "ģ"), + (0x123, "V"), + (0x124, "M", "ĥ"), + (0x125, "V"), + (0x126, "M", "ħ"), + (0x127, "V"), + (0x128, "M", "ĩ"), + (0x129, "V"), + (0x12A, "M", "ī"), + (0x12B, "V"), + ] + + +def _seg_3() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x12C, "M", "ĭ"), + (0x12D, "V"), + (0x12E, "M", "į"), + (0x12F, "V"), + (0x130, "M", "i̇"), + (0x131, "V"), + (0x132, "M", "ij"), + (0x134, "M", "ĵ"), + (0x135, "V"), + (0x136, "M", "ķ"), + (0x137, "V"), + (0x139, "M", "ĺ"), + (0x13A, "V"), + (0x13B, "M", "ļ"), + (0x13C, "V"), + (0x13D, "M", "ľ"), + (0x13E, "V"), + (0x13F, "M", "l·"), + (0x141, "M", "ł"), + (0x142, "V"), + (0x143, "M", "ń"), + (0x144, "V"), + (0x145, "M", "ņ"), + (0x146, "V"), + (0x147, "M", "ň"), + (0x148, "V"), + (0x149, "M", "ʼn"), + (0x14A, "M", "ŋ"), + (0x14B, "V"), + (0x14C, "M", "ō"), + (0x14D, "V"), + (0x14E, "M", "ŏ"), + (0x14F, "V"), + (0x150, "M", "ő"), + (0x151, "V"), + (0x152, "M", "œ"), + (0x153, "V"), + (0x154, "M", "ŕ"), + (0x155, "V"), + (0x156, "M", "ŗ"), + (0x157, "V"), + (0x158, "M", "ř"), + (0x159, "V"), + (0x15A, "M", "ś"), + (0x15B, "V"), + (0x15C, "M", "ŝ"), + (0x15D, "V"), + (0x15E, "M", "ş"), + (0x15F, "V"), + (0x160, "M", "š"), + (0x161, "V"), + (0x162, "M", "ţ"), + (0x163, "V"), + (0x164, "M", "ť"), + (0x165, "V"), + (0x166, "M", "ŧ"), + (0x167, "V"), + (0x168, "M", "ũ"), + (0x169, "V"), + (0x16A, "M", "ū"), + (0x16B, "V"), + (0x16C, "M", "ŭ"), + (0x16D, "V"), + (0x16E, "M", "ů"), + (0x16F, "V"), + (0x170, "M", "ű"), + (0x171, "V"), + (0x172, "M", "ų"), + (0x173, "V"), + (0x174, "M", "ŵ"), + (0x175, "V"), + (0x176, "M", "ŷ"), + (0x177, "V"), + (0x178, "M", "ÿ"), + (0x179, "M", "ź"), + (0x17A, "V"), + (0x17B, "M", "ż"), + (0x17C, "V"), + (0x17D, "M", "ž"), + (0x17E, "V"), + (0x17F, "M", "s"), + (0x180, "V"), + (0x181, "M", "ɓ"), + (0x182, "M", "ƃ"), + (0x183, "V"), + (0x184, "M", "ƅ"), + (0x185, "V"), + (0x186, "M", "ɔ"), + (0x187, "M", "ƈ"), + (0x188, "V"), + (0x189, "M", "ɖ"), + (0x18A, "M", "ɗ"), + (0x18B, "M", "ƌ"), + (0x18C, "V"), + (0x18E, "M", "ǝ"), + (0x18F, "M", "ə"), + (0x190, "M", "ɛ"), + (0x191, "M", "ƒ"), + (0x192, "V"), + (0x193, "M", "ɠ"), + ] + + +def _seg_4() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x194, "M", "ɣ"), + (0x195, "V"), + (0x196, "M", "ɩ"), + (0x197, "M", "ɨ"), + (0x198, "M", "ƙ"), + (0x199, "V"), + (0x19C, "M", "ɯ"), + (0x19D, "M", "ɲ"), + (0x19E, "V"), + (0x19F, "M", "ɵ"), + (0x1A0, "M", "ơ"), + (0x1A1, "V"), + (0x1A2, "M", "ƣ"), + (0x1A3, "V"), + (0x1A4, "M", "ƥ"), + (0x1A5, "V"), + (0x1A6, "M", "ʀ"), + (0x1A7, "M", "ƨ"), + (0x1A8, "V"), + (0x1A9, "M", "ʃ"), + (0x1AA, "V"), + (0x1AC, "M", "ƭ"), + (0x1AD, "V"), + (0x1AE, "M", "ʈ"), + (0x1AF, "M", "ư"), + (0x1B0, "V"), + (0x1B1, "M", "ʊ"), + (0x1B2, "M", "ʋ"), + (0x1B3, "M", "ƴ"), + (0x1B4, "V"), + (0x1B5, "M", "ƶ"), + (0x1B6, "V"), + (0x1B7, "M", "ʒ"), + (0x1B8, "M", "ƹ"), + (0x1B9, "V"), + (0x1BC, "M", "ƽ"), + (0x1BD, "V"), + (0x1C4, "M", "dž"), + (0x1C7, "M", "lj"), + (0x1CA, "M", "nj"), + (0x1CD, "M", "ǎ"), + (0x1CE, "V"), + (0x1CF, "M", "ǐ"), + (0x1D0, "V"), + (0x1D1, "M", "ǒ"), + (0x1D2, "V"), + (0x1D3, "M", "ǔ"), + (0x1D4, "V"), + (0x1D5, "M", "ǖ"), + (0x1D6, "V"), + (0x1D7, "M", "ǘ"), + (0x1D8, "V"), + (0x1D9, "M", "ǚ"), + (0x1DA, "V"), + (0x1DB, "M", "ǜ"), + (0x1DC, "V"), + (0x1DE, "M", "ǟ"), + (0x1DF, "V"), + (0x1E0, "M", "ǡ"), + (0x1E1, "V"), + (0x1E2, "M", "ǣ"), + (0x1E3, "V"), + (0x1E4, "M", "ǥ"), + (0x1E5, "V"), + (0x1E6, "M", "ǧ"), + (0x1E7, "V"), + (0x1E8, "M", "ǩ"), + (0x1E9, "V"), + (0x1EA, "M", "ǫ"), + (0x1EB, "V"), + (0x1EC, "M", "ǭ"), + (0x1ED, "V"), + (0x1EE, "M", "ǯ"), + (0x1EF, "V"), + (0x1F1, "M", "dz"), + (0x1F4, "M", "ǵ"), + (0x1F5, "V"), + (0x1F6, "M", "ƕ"), + (0x1F7, "M", "ƿ"), + (0x1F8, "M", "ǹ"), + (0x1F9, "V"), + (0x1FA, "M", "ǻ"), + (0x1FB, "V"), + (0x1FC, "M", "ǽ"), + (0x1FD, "V"), + (0x1FE, "M", "ǿ"), + (0x1FF, "V"), + (0x200, "M", "ȁ"), + (0x201, "V"), + (0x202, "M", "ȃ"), + (0x203, "V"), + (0x204, "M", "ȅ"), + (0x205, "V"), + (0x206, "M", "ȇ"), + (0x207, "V"), + (0x208, "M", "ȉ"), + (0x209, "V"), + (0x20A, "M", "ȋ"), + (0x20B, "V"), + (0x20C, "M", "ȍ"), + ] + + +def _seg_5() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x20D, "V"), + (0x20E, "M", "ȏ"), + (0x20F, "V"), + (0x210, "M", "ȑ"), + (0x211, "V"), + (0x212, "M", "ȓ"), + (0x213, "V"), + (0x214, "M", "ȕ"), + (0x215, "V"), + (0x216, "M", "ȗ"), + (0x217, "V"), + (0x218, "M", "ș"), + (0x219, "V"), + (0x21A, "M", "ț"), + (0x21B, "V"), + (0x21C, "M", "ȝ"), + (0x21D, "V"), + (0x21E, "M", "ȟ"), + (0x21F, "V"), + (0x220, "M", "ƞ"), + (0x221, "V"), + (0x222, "M", "ȣ"), + (0x223, "V"), + (0x224, "M", "ȥ"), + (0x225, "V"), + (0x226, "M", "ȧ"), + (0x227, "V"), + (0x228, "M", "ȩ"), + (0x229, "V"), + (0x22A, "M", "ȫ"), + (0x22B, "V"), + (0x22C, "M", "ȭ"), + (0x22D, "V"), + (0x22E, "M", "ȯ"), + (0x22F, "V"), + (0x230, "M", "ȱ"), + (0x231, "V"), + (0x232, "M", "ȳ"), + (0x233, "V"), + (0x23A, "M", "ⱥ"), + (0x23B, "M", "ȼ"), + (0x23C, "V"), + (0x23D, "M", "ƚ"), + (0x23E, "M", "ⱦ"), + (0x23F, "V"), + (0x241, "M", "ɂ"), + (0x242, "V"), + (0x243, "M", "ƀ"), + (0x244, "M", "ʉ"), + (0x245, "M", "ʌ"), + (0x246, "M", "ɇ"), + (0x247, "V"), + (0x248, "M", "ɉ"), + (0x249, "V"), + (0x24A, "M", "ɋ"), + (0x24B, "V"), + (0x24C, "M", "ɍ"), + (0x24D, "V"), + (0x24E, "M", "ɏ"), + (0x24F, "V"), + (0x2B0, "M", "h"), + (0x2B1, "M", "ɦ"), + (0x2B2, "M", "j"), + (0x2B3, "M", "r"), + (0x2B4, "M", "ɹ"), + (0x2B5, "M", "ɻ"), + (0x2B6, "M", "ʁ"), + (0x2B7, "M", "w"), + (0x2B8, "M", "y"), + (0x2B9, "V"), + (0x2D8, "M", " ̆"), + (0x2D9, "M", " ̇"), + (0x2DA, "M", " ̊"), + (0x2DB, "M", " ̨"), + (0x2DC, "M", " ̃"), + (0x2DD, "M", " ̋"), + (0x2DE, "V"), + (0x2E0, "M", "ɣ"), + (0x2E1, "M", "l"), + (0x2E2, "M", "s"), + (0x2E3, "M", "x"), + (0x2E4, "M", "ʕ"), + (0x2E5, "V"), + (0x340, "M", "̀"), + (0x341, "M", "́"), + (0x342, "V"), + (0x343, "M", "̓"), + (0x344, "M", "̈́"), + (0x345, "M", "ι"), + (0x346, "V"), + (0x34F, "I"), + (0x350, "V"), + (0x370, "M", "ͱ"), + (0x371, "V"), + (0x372, "M", "ͳ"), + (0x373, "V"), + (0x374, "M", "ʹ"), + (0x375, "V"), + (0x376, "M", "ͷ"), + (0x377, "V"), + ] + + +def _seg_6() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x378, "X"), + (0x37A, "M", " ι"), + (0x37B, "V"), + (0x37E, "M", ";"), + (0x37F, "M", "ϳ"), + (0x380, "X"), + (0x384, "M", " ́"), + (0x385, "M", " ̈́"), + (0x386, "M", "ά"), + (0x387, "M", "·"), + (0x388, "M", "έ"), + (0x389, "M", "ή"), + (0x38A, "M", "ί"), + (0x38B, "X"), + (0x38C, "M", "ό"), + (0x38D, "X"), + (0x38E, "M", "ύ"), + (0x38F, "M", "ώ"), + (0x390, "V"), + (0x391, "M", "α"), + (0x392, "M", "β"), + (0x393, "M", "γ"), + (0x394, "M", "δ"), + (0x395, "M", "ε"), + (0x396, "M", "ζ"), + (0x397, "M", "η"), + (0x398, "M", "θ"), + (0x399, "M", "ι"), + (0x39A, "M", "κ"), + (0x39B, "M", "λ"), + (0x39C, "M", "μ"), + (0x39D, "M", "ν"), + (0x39E, "M", "ξ"), + (0x39F, "M", "ο"), + (0x3A0, "M", "π"), + (0x3A1, "M", "ρ"), + (0x3A2, "X"), + (0x3A3, "M", "σ"), + (0x3A4, "M", "τ"), + (0x3A5, "M", "υ"), + (0x3A6, "M", "φ"), + (0x3A7, "M", "χ"), + (0x3A8, "M", "ψ"), + (0x3A9, "M", "ω"), + (0x3AA, "M", "ϊ"), + (0x3AB, "M", "ϋ"), + (0x3AC, "V"), + (0x3C2, "D", "σ"), + (0x3C3, "V"), + (0x3CF, "M", "ϗ"), + (0x3D0, "M", "β"), + (0x3D1, "M", "θ"), + (0x3D2, "M", "υ"), + (0x3D3, "M", "ύ"), + (0x3D4, "M", "ϋ"), + (0x3D5, "M", "φ"), + (0x3D6, "M", "π"), + (0x3D7, "V"), + (0x3D8, "M", "ϙ"), + (0x3D9, "V"), + (0x3DA, "M", "ϛ"), + (0x3DB, "V"), + (0x3DC, "M", "ϝ"), + (0x3DD, "V"), + (0x3DE, "M", "ϟ"), + (0x3DF, "V"), + (0x3E0, "M", "ϡ"), + (0x3E1, "V"), + (0x3E2, "M", "ϣ"), + (0x3E3, "V"), + (0x3E4, "M", "ϥ"), + (0x3E5, "V"), + (0x3E6, "M", "ϧ"), + (0x3E7, "V"), + (0x3E8, "M", "ϩ"), + (0x3E9, "V"), + (0x3EA, "M", "ϫ"), + (0x3EB, "V"), + (0x3EC, "M", "ϭ"), + (0x3ED, "V"), + (0x3EE, "M", "ϯ"), + (0x3EF, "V"), + (0x3F0, "M", "κ"), + (0x3F1, "M", "ρ"), + (0x3F2, "M", "σ"), + (0x3F3, "V"), + (0x3F4, "M", "θ"), + (0x3F5, "M", "ε"), + (0x3F6, "V"), + (0x3F7, "M", "ϸ"), + (0x3F8, "V"), + (0x3F9, "M", "σ"), + (0x3FA, "M", "ϻ"), + (0x3FB, "V"), + (0x3FD, "M", "ͻ"), + (0x3FE, "M", "ͼ"), + (0x3FF, "M", "ͽ"), + (0x400, "M", "ѐ"), + (0x401, "M", "ё"), + (0x402, "M", "ђ"), + ] + + +def _seg_7() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x403, "M", "ѓ"), + (0x404, "M", "є"), + (0x405, "M", "ѕ"), + (0x406, "M", "і"), + (0x407, "M", "ї"), + (0x408, "M", "ј"), + (0x409, "M", "љ"), + (0x40A, "M", "њ"), + (0x40B, "M", "ћ"), + (0x40C, "M", "ќ"), + (0x40D, "M", "ѝ"), + (0x40E, "M", "ў"), + (0x40F, "M", "џ"), + (0x410, "M", "а"), + (0x411, "M", "б"), + (0x412, "M", "в"), + (0x413, "M", "г"), + (0x414, "M", "д"), + (0x415, "M", "е"), + (0x416, "M", "ж"), + (0x417, "M", "з"), + (0x418, "M", "и"), + (0x419, "M", "й"), + (0x41A, "M", "к"), + (0x41B, "M", "л"), + (0x41C, "M", "м"), + (0x41D, "M", "н"), + (0x41E, "M", "о"), + (0x41F, "M", "п"), + (0x420, "M", "р"), + (0x421, "M", "с"), + (0x422, "M", "т"), + (0x423, "M", "у"), + (0x424, "M", "ф"), + (0x425, "M", "х"), + (0x426, "M", "ц"), + (0x427, "M", "ч"), + (0x428, "M", "ш"), + (0x429, "M", "щ"), + (0x42A, "M", "ъ"), + (0x42B, "M", "ы"), + (0x42C, "M", "ь"), + (0x42D, "M", "э"), + (0x42E, "M", "ю"), + (0x42F, "M", "я"), + (0x430, "V"), + (0x460, "M", "ѡ"), + (0x461, "V"), + (0x462, "M", "ѣ"), + (0x463, "V"), + (0x464, "M", "ѥ"), + (0x465, "V"), + (0x466, "M", "ѧ"), + (0x467, "V"), + (0x468, "M", "ѩ"), + (0x469, "V"), + (0x46A, "M", "ѫ"), + (0x46B, "V"), + (0x46C, "M", "ѭ"), + (0x46D, "V"), + (0x46E, "M", "ѯ"), + (0x46F, "V"), + (0x470, "M", "ѱ"), + (0x471, "V"), + (0x472, "M", "ѳ"), + (0x473, "V"), + (0x474, "M", "ѵ"), + (0x475, "V"), + (0x476, "M", "ѷ"), + (0x477, "V"), + (0x478, "M", "ѹ"), + (0x479, "V"), + (0x47A, "M", "ѻ"), + (0x47B, "V"), + (0x47C, "M", "ѽ"), + (0x47D, "V"), + (0x47E, "M", "ѿ"), + (0x47F, "V"), + (0x480, "M", "ҁ"), + (0x481, "V"), + (0x48A, "M", "ҋ"), + (0x48B, "V"), + (0x48C, "M", "ҍ"), + (0x48D, "V"), + (0x48E, "M", "ҏ"), + (0x48F, "V"), + (0x490, "M", "ґ"), + (0x491, "V"), + (0x492, "M", "ғ"), + (0x493, "V"), + (0x494, "M", "ҕ"), + (0x495, "V"), + (0x496, "M", "җ"), + (0x497, "V"), + (0x498, "M", "ҙ"), + (0x499, "V"), + (0x49A, "M", "қ"), + (0x49B, "V"), + (0x49C, "M", "ҝ"), + (0x49D, "V"), + ] + + +def _seg_8() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x49E, "M", "ҟ"), + (0x49F, "V"), + (0x4A0, "M", "ҡ"), + (0x4A1, "V"), + (0x4A2, "M", "ң"), + (0x4A3, "V"), + (0x4A4, "M", "ҥ"), + (0x4A5, "V"), + (0x4A6, "M", "ҧ"), + (0x4A7, "V"), + (0x4A8, "M", "ҩ"), + (0x4A9, "V"), + (0x4AA, "M", "ҫ"), + (0x4AB, "V"), + (0x4AC, "M", "ҭ"), + (0x4AD, "V"), + (0x4AE, "M", "ү"), + (0x4AF, "V"), + (0x4B0, "M", "ұ"), + (0x4B1, "V"), + (0x4B2, "M", "ҳ"), + (0x4B3, "V"), + (0x4B4, "M", "ҵ"), + (0x4B5, "V"), + (0x4B6, "M", "ҷ"), + (0x4B7, "V"), + (0x4B8, "M", "ҹ"), + (0x4B9, "V"), + (0x4BA, "M", "һ"), + (0x4BB, "V"), + (0x4BC, "M", "ҽ"), + (0x4BD, "V"), + (0x4BE, "M", "ҿ"), + (0x4BF, "V"), + (0x4C0, "M", "ӏ"), + (0x4C1, "M", "ӂ"), + (0x4C2, "V"), + (0x4C3, "M", "ӄ"), + (0x4C4, "V"), + (0x4C5, "M", "ӆ"), + (0x4C6, "V"), + (0x4C7, "M", "ӈ"), + (0x4C8, "V"), + (0x4C9, "M", "ӊ"), + (0x4CA, "V"), + (0x4CB, "M", "ӌ"), + (0x4CC, "V"), + (0x4CD, "M", "ӎ"), + (0x4CE, "V"), + (0x4D0, "M", "ӑ"), + (0x4D1, "V"), + (0x4D2, "M", "ӓ"), + (0x4D3, "V"), + (0x4D4, "M", "ӕ"), + (0x4D5, "V"), + (0x4D6, "M", "ӗ"), + (0x4D7, "V"), + (0x4D8, "M", "ә"), + (0x4D9, "V"), + (0x4DA, "M", "ӛ"), + (0x4DB, "V"), + (0x4DC, "M", "ӝ"), + (0x4DD, "V"), + (0x4DE, "M", "ӟ"), + (0x4DF, "V"), + (0x4E0, "M", "ӡ"), + (0x4E1, "V"), + (0x4E2, "M", "ӣ"), + (0x4E3, "V"), + (0x4E4, "M", "ӥ"), + (0x4E5, "V"), + (0x4E6, "M", "ӧ"), + (0x4E7, "V"), + (0x4E8, "M", "ө"), + (0x4E9, "V"), + (0x4EA, "M", "ӫ"), + (0x4EB, "V"), + (0x4EC, "M", "ӭ"), + (0x4ED, "V"), + (0x4EE, "M", "ӯ"), + (0x4EF, "V"), + (0x4F0, "M", "ӱ"), + (0x4F1, "V"), + (0x4F2, "M", "ӳ"), + (0x4F3, "V"), + (0x4F4, "M", "ӵ"), + (0x4F5, "V"), + (0x4F6, "M", "ӷ"), + (0x4F7, "V"), + (0x4F8, "M", "ӹ"), + (0x4F9, "V"), + (0x4FA, "M", "ӻ"), + (0x4FB, "V"), + (0x4FC, "M", "ӽ"), + (0x4FD, "V"), + (0x4FE, "M", "ӿ"), + (0x4FF, "V"), + (0x500, "M", "ԁ"), + (0x501, "V"), + (0x502, "M", "ԃ"), + ] + + +def _seg_9() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x503, "V"), + (0x504, "M", "ԅ"), + (0x505, "V"), + (0x506, "M", "ԇ"), + (0x507, "V"), + (0x508, "M", "ԉ"), + (0x509, "V"), + (0x50A, "M", "ԋ"), + (0x50B, "V"), + (0x50C, "M", "ԍ"), + (0x50D, "V"), + (0x50E, "M", "ԏ"), + (0x50F, "V"), + (0x510, "M", "ԑ"), + (0x511, "V"), + (0x512, "M", "ԓ"), + (0x513, "V"), + (0x514, "M", "ԕ"), + (0x515, "V"), + (0x516, "M", "ԗ"), + (0x517, "V"), + (0x518, "M", "ԙ"), + (0x519, "V"), + (0x51A, "M", "ԛ"), + (0x51B, "V"), + (0x51C, "M", "ԝ"), + (0x51D, "V"), + (0x51E, "M", "ԟ"), + (0x51F, "V"), + (0x520, "M", "ԡ"), + (0x521, "V"), + (0x522, "M", "ԣ"), + (0x523, "V"), + (0x524, "M", "ԥ"), + (0x525, "V"), + (0x526, "M", "ԧ"), + (0x527, "V"), + (0x528, "M", "ԩ"), + (0x529, "V"), + (0x52A, "M", "ԫ"), + (0x52B, "V"), + (0x52C, "M", "ԭ"), + (0x52D, "V"), + (0x52E, "M", "ԯ"), + (0x52F, "V"), + (0x530, "X"), + (0x531, "M", "ա"), + (0x532, "M", "բ"), + (0x533, "M", "գ"), + (0x534, "M", "դ"), + (0x535, "M", "ե"), + (0x536, "M", "զ"), + (0x537, "M", "է"), + (0x538, "M", "ը"), + (0x539, "M", "թ"), + (0x53A, "M", "ժ"), + (0x53B, "M", "ի"), + (0x53C, "M", "լ"), + (0x53D, "M", "խ"), + (0x53E, "M", "ծ"), + (0x53F, "M", "կ"), + (0x540, "M", "հ"), + (0x541, "M", "ձ"), + (0x542, "M", "ղ"), + (0x543, "M", "ճ"), + (0x544, "M", "մ"), + (0x545, "M", "յ"), + (0x546, "M", "ն"), + (0x547, "M", "շ"), + (0x548, "M", "ո"), + (0x549, "M", "չ"), + (0x54A, "M", "պ"), + (0x54B, "M", "ջ"), + (0x54C, "M", "ռ"), + (0x54D, "M", "ս"), + (0x54E, "M", "վ"), + (0x54F, "M", "տ"), + (0x550, "M", "ր"), + (0x551, "M", "ց"), + (0x552, "M", "ւ"), + (0x553, "M", "փ"), + (0x554, "M", "ք"), + (0x555, "M", "օ"), + (0x556, "M", "ֆ"), + (0x557, "X"), + (0x559, "V"), + (0x587, "M", "եւ"), + (0x588, "V"), + (0x58B, "X"), + (0x58D, "V"), + (0x590, "X"), + (0x591, "V"), + (0x5C8, "X"), + (0x5D0, "V"), + (0x5EB, "X"), + (0x5EF, "V"), + (0x5F5, "X"), + (0x606, "V"), + (0x61C, "X"), + (0x61D, "V"), + ] + + +def _seg_10() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x675, "M", "اٴ"), + (0x676, "M", "وٴ"), + (0x677, "M", "ۇٴ"), + (0x678, "M", "يٴ"), + (0x679, "V"), + (0x6DD, "X"), + (0x6DE, "V"), + (0x70E, "X"), + (0x710, "V"), + (0x74B, "X"), + (0x74D, "V"), + (0x7B2, "X"), + (0x7C0, "V"), + (0x7FB, "X"), + (0x7FD, "V"), + (0x82E, "X"), + (0x830, "V"), + (0x83F, "X"), + (0x840, "V"), + (0x85C, "X"), + (0x85E, "V"), + (0x85F, "X"), + (0x860, "V"), + (0x86B, "X"), + (0x870, "V"), + (0x88F, "X"), + (0x897, "V"), + (0x8E2, "X"), + (0x8E3, "V"), + (0x958, "M", "क़"), + (0x959, "M", "ख़"), + (0x95A, "M", "ग़"), + (0x95B, "M", "ज़"), + (0x95C, "M", "ड़"), + (0x95D, "M", "ढ़"), + (0x95E, "M", "फ़"), + (0x95F, "M", "य़"), + (0x960, "V"), + (0x984, "X"), + (0x985, "V"), + (0x98D, "X"), + (0x98F, "V"), + (0x991, "X"), + (0x993, "V"), + (0x9A9, "X"), + (0x9AA, "V"), + (0x9B1, "X"), + (0x9B2, "V"), + (0x9B3, "X"), + (0x9B6, "V"), + (0x9BA, "X"), + (0x9BC, "V"), + (0x9C5, "X"), + (0x9C7, "V"), + (0x9C9, "X"), + (0x9CB, "V"), + (0x9CF, "X"), + (0x9D7, "V"), + (0x9D8, "X"), + (0x9DC, "M", "ড়"), + (0x9DD, "M", "ঢ়"), + (0x9DE, "X"), + (0x9DF, "M", "য়"), + (0x9E0, "V"), + (0x9E4, "X"), + (0x9E6, "V"), + (0x9FF, "X"), + (0xA01, "V"), + (0xA04, "X"), + (0xA05, "V"), + (0xA0B, "X"), + (0xA0F, "V"), + (0xA11, "X"), + (0xA13, "V"), + (0xA29, "X"), + (0xA2A, "V"), + (0xA31, "X"), + (0xA32, "V"), + (0xA33, "M", "ਲ਼"), + (0xA34, "X"), + (0xA35, "V"), + (0xA36, "M", "ਸ਼"), + (0xA37, "X"), + (0xA38, "V"), + (0xA3A, "X"), + (0xA3C, "V"), + (0xA3D, "X"), + (0xA3E, "V"), + (0xA43, "X"), + (0xA47, "V"), + (0xA49, "X"), + (0xA4B, "V"), + (0xA4E, "X"), + (0xA51, "V"), + (0xA52, "X"), + (0xA59, "M", "ਖ਼"), + (0xA5A, "M", "ਗ਼"), + (0xA5B, "M", "ਜ਼"), + (0xA5C, "V"), + (0xA5D, "X"), + ] + + +def _seg_11() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA5E, "M", "ਫ਼"), + (0xA5F, "X"), + (0xA66, "V"), + (0xA77, "X"), + (0xA81, "V"), + (0xA84, "X"), + (0xA85, "V"), + (0xA8E, "X"), + (0xA8F, "V"), + (0xA92, "X"), + (0xA93, "V"), + (0xAA9, "X"), + (0xAAA, "V"), + (0xAB1, "X"), + (0xAB2, "V"), + (0xAB4, "X"), + (0xAB5, "V"), + (0xABA, "X"), + (0xABC, "V"), + (0xAC6, "X"), + (0xAC7, "V"), + (0xACA, "X"), + (0xACB, "V"), + (0xACE, "X"), + (0xAD0, "V"), + (0xAD1, "X"), + (0xAE0, "V"), + (0xAE4, "X"), + (0xAE6, "V"), + (0xAF2, "X"), + (0xAF9, "V"), + (0xB00, "X"), + (0xB01, "V"), + (0xB04, "X"), + (0xB05, "V"), + (0xB0D, "X"), + (0xB0F, "V"), + (0xB11, "X"), + (0xB13, "V"), + (0xB29, "X"), + (0xB2A, "V"), + (0xB31, "X"), + (0xB32, "V"), + (0xB34, "X"), + (0xB35, "V"), + (0xB3A, "X"), + (0xB3C, "V"), + (0xB45, "X"), + (0xB47, "V"), + (0xB49, "X"), + (0xB4B, "V"), + (0xB4E, "X"), + (0xB55, "V"), + (0xB58, "X"), + (0xB5C, "M", "ଡ଼"), + (0xB5D, "M", "ଢ଼"), + (0xB5E, "X"), + (0xB5F, "V"), + (0xB64, "X"), + (0xB66, "V"), + (0xB78, "X"), + (0xB82, "V"), + (0xB84, "X"), + (0xB85, "V"), + (0xB8B, "X"), + (0xB8E, "V"), + (0xB91, "X"), + (0xB92, "V"), + (0xB96, "X"), + (0xB99, "V"), + (0xB9B, "X"), + (0xB9C, "V"), + (0xB9D, "X"), + (0xB9E, "V"), + (0xBA0, "X"), + (0xBA3, "V"), + (0xBA5, "X"), + (0xBA8, "V"), + (0xBAB, "X"), + (0xBAE, "V"), + (0xBBA, "X"), + (0xBBE, "V"), + (0xBC3, "X"), + (0xBC6, "V"), + (0xBC9, "X"), + (0xBCA, "V"), + (0xBCE, "X"), + (0xBD0, "V"), + (0xBD1, "X"), + (0xBD7, "V"), + (0xBD8, "X"), + (0xBE6, "V"), + (0xBFB, "X"), + (0xC00, "V"), + (0xC0D, "X"), + (0xC0E, "V"), + (0xC11, "X"), + (0xC12, "V"), + (0xC29, "X"), + (0xC2A, "V"), + ] + + +def _seg_12() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xC3A, "X"), + (0xC3C, "V"), + (0xC45, "X"), + (0xC46, "V"), + (0xC49, "X"), + (0xC4A, "V"), + (0xC4E, "X"), + (0xC55, "V"), + (0xC57, "X"), + (0xC58, "V"), + (0xC5B, "X"), + (0xC5D, "V"), + (0xC5E, "X"), + (0xC60, "V"), + (0xC64, "X"), + (0xC66, "V"), + (0xC70, "X"), + (0xC77, "V"), + (0xC8D, "X"), + (0xC8E, "V"), + (0xC91, "X"), + (0xC92, "V"), + (0xCA9, "X"), + (0xCAA, "V"), + (0xCB4, "X"), + (0xCB5, "V"), + (0xCBA, "X"), + (0xCBC, "V"), + (0xCC5, "X"), + (0xCC6, "V"), + (0xCC9, "X"), + (0xCCA, "V"), + (0xCCE, "X"), + (0xCD5, "V"), + (0xCD7, "X"), + (0xCDD, "V"), + (0xCDF, "X"), + (0xCE0, "V"), + (0xCE4, "X"), + (0xCE6, "V"), + (0xCF0, "X"), + (0xCF1, "V"), + (0xCF4, "X"), + (0xD00, "V"), + (0xD0D, "X"), + (0xD0E, "V"), + (0xD11, "X"), + (0xD12, "V"), + (0xD45, "X"), + (0xD46, "V"), + (0xD49, "X"), + (0xD4A, "V"), + (0xD50, "X"), + (0xD54, "V"), + (0xD64, "X"), + (0xD66, "V"), + (0xD80, "X"), + (0xD81, "V"), + (0xD84, "X"), + (0xD85, "V"), + (0xD97, "X"), + (0xD9A, "V"), + (0xDB2, "X"), + (0xDB3, "V"), + (0xDBC, "X"), + (0xDBD, "V"), + (0xDBE, "X"), + (0xDC0, "V"), + (0xDC7, "X"), + (0xDCA, "V"), + (0xDCB, "X"), + (0xDCF, "V"), + (0xDD5, "X"), + (0xDD6, "V"), + (0xDD7, "X"), + (0xDD8, "V"), + (0xDE0, "X"), + (0xDE6, "V"), + (0xDF0, "X"), + (0xDF2, "V"), + (0xDF5, "X"), + (0xE01, "V"), + (0xE33, "M", "ํา"), + (0xE34, "V"), + (0xE3B, "X"), + (0xE3F, "V"), + (0xE5C, "X"), + (0xE81, "V"), + (0xE83, "X"), + (0xE84, "V"), + (0xE85, "X"), + (0xE86, "V"), + (0xE8B, "X"), + (0xE8C, "V"), + (0xEA4, "X"), + (0xEA5, "V"), + (0xEA6, "X"), + (0xEA7, "V"), + (0xEB3, "M", "ໍາ"), + (0xEB4, "V"), + ] + + +def _seg_13() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xEBE, "X"), + (0xEC0, "V"), + (0xEC5, "X"), + (0xEC6, "V"), + (0xEC7, "X"), + (0xEC8, "V"), + (0xECF, "X"), + (0xED0, "V"), + (0xEDA, "X"), + (0xEDC, "M", "ຫນ"), + (0xEDD, "M", "ຫມ"), + (0xEDE, "V"), + (0xEE0, "X"), + (0xF00, "V"), + (0xF0C, "M", "་"), + (0xF0D, "V"), + (0xF43, "M", "གྷ"), + (0xF44, "V"), + (0xF48, "X"), + (0xF49, "V"), + (0xF4D, "M", "ཌྷ"), + (0xF4E, "V"), + (0xF52, "M", "དྷ"), + (0xF53, "V"), + (0xF57, "M", "བྷ"), + (0xF58, "V"), + (0xF5C, "M", "ཛྷ"), + (0xF5D, "V"), + (0xF69, "M", "ཀྵ"), + (0xF6A, "V"), + (0xF6D, "X"), + (0xF71, "V"), + (0xF73, "M", "ཱི"), + (0xF74, "V"), + (0xF75, "M", "ཱུ"), + (0xF76, "M", "ྲྀ"), + (0xF77, "M", "ྲཱྀ"), + (0xF78, "M", "ླྀ"), + (0xF79, "M", "ླཱྀ"), + (0xF7A, "V"), + (0xF81, "M", "ཱྀ"), + (0xF82, "V"), + (0xF93, "M", "ྒྷ"), + (0xF94, "V"), + (0xF98, "X"), + (0xF99, "V"), + (0xF9D, "M", "ྜྷ"), + (0xF9E, "V"), + (0xFA2, "M", "ྡྷ"), + (0xFA3, "V"), + (0xFA7, "M", "ྦྷ"), + (0xFA8, "V"), + (0xFAC, "M", "ྫྷ"), + (0xFAD, "V"), + (0xFB9, "M", "ྐྵ"), + (0xFBA, "V"), + (0xFBD, "X"), + (0xFBE, "V"), + (0xFCD, "X"), + (0xFCE, "V"), + (0xFDB, "X"), + (0x1000, "V"), + (0x10A0, "M", "ⴀ"), + (0x10A1, "M", "ⴁ"), + (0x10A2, "M", "ⴂ"), + (0x10A3, "M", "ⴃ"), + (0x10A4, "M", "ⴄ"), + (0x10A5, "M", "ⴅ"), + (0x10A6, "M", "ⴆ"), + (0x10A7, "M", "ⴇ"), + (0x10A8, "M", "ⴈ"), + (0x10A9, "M", "ⴉ"), + (0x10AA, "M", "ⴊ"), + (0x10AB, "M", "ⴋ"), + (0x10AC, "M", "ⴌ"), + (0x10AD, "M", "ⴍ"), + (0x10AE, "M", "ⴎ"), + (0x10AF, "M", "ⴏ"), + (0x10B0, "M", "ⴐ"), + (0x10B1, "M", "ⴑ"), + (0x10B2, "M", "ⴒ"), + (0x10B3, "M", "ⴓ"), + (0x10B4, "M", "ⴔ"), + (0x10B5, "M", "ⴕ"), + (0x10B6, "M", "ⴖ"), + (0x10B7, "M", "ⴗ"), + (0x10B8, "M", "ⴘ"), + (0x10B9, "M", "ⴙ"), + (0x10BA, "M", "ⴚ"), + (0x10BB, "M", "ⴛ"), + (0x10BC, "M", "ⴜ"), + (0x10BD, "M", "ⴝ"), + (0x10BE, "M", "ⴞ"), + (0x10BF, "M", "ⴟ"), + (0x10C0, "M", "ⴠ"), + (0x10C1, "M", "ⴡ"), + (0x10C2, "M", "ⴢ"), + (0x10C3, "M", "ⴣ"), + (0x10C4, "M", "ⴤ"), + (0x10C5, "M", "ⴥ"), + ] + + +def _seg_14() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x10C6, "X"), + (0x10C7, "M", "ⴧ"), + (0x10C8, "X"), + (0x10CD, "M", "ⴭ"), + (0x10CE, "X"), + (0x10D0, "V"), + (0x10FC, "M", "ნ"), + (0x10FD, "V"), + (0x115F, "I"), + (0x1161, "V"), + (0x1249, "X"), + (0x124A, "V"), + (0x124E, "X"), + (0x1250, "V"), + (0x1257, "X"), + (0x1258, "V"), + (0x1259, "X"), + (0x125A, "V"), + (0x125E, "X"), + (0x1260, "V"), + (0x1289, "X"), + (0x128A, "V"), + (0x128E, "X"), + (0x1290, "V"), + (0x12B1, "X"), + (0x12B2, "V"), + (0x12B6, "X"), + (0x12B8, "V"), + (0x12BF, "X"), + (0x12C0, "V"), + (0x12C1, "X"), + (0x12C2, "V"), + (0x12C6, "X"), + (0x12C8, "V"), + (0x12D7, "X"), + (0x12D8, "V"), + (0x1311, "X"), + (0x1312, "V"), + (0x1316, "X"), + (0x1318, "V"), + (0x135B, "X"), + (0x135D, "V"), + (0x137D, "X"), + (0x1380, "V"), + (0x139A, "X"), + (0x13A0, "V"), + (0x13F6, "X"), + (0x13F8, "M", "Ᏸ"), + (0x13F9, "M", "Ᏹ"), + (0x13FA, "M", "Ᏺ"), + (0x13FB, "M", "Ᏻ"), + (0x13FC, "M", "Ᏼ"), + (0x13FD, "M", "Ᏽ"), + (0x13FE, "X"), + (0x1400, "V"), + (0x1680, "X"), + (0x1681, "V"), + (0x169D, "X"), + (0x16A0, "V"), + (0x16F9, "X"), + (0x1700, "V"), + (0x1716, "X"), + (0x171F, "V"), + (0x1737, "X"), + (0x1740, "V"), + (0x1754, "X"), + (0x1760, "V"), + (0x176D, "X"), + (0x176E, "V"), + (0x1771, "X"), + (0x1772, "V"), + (0x1774, "X"), + (0x1780, "V"), + (0x17B4, "I"), + (0x17B6, "V"), + (0x17DE, "X"), + (0x17E0, "V"), + (0x17EA, "X"), + (0x17F0, "V"), + (0x17FA, "X"), + (0x1800, "V"), + (0x180B, "I"), + (0x1810, "V"), + (0x181A, "X"), + (0x1820, "V"), + (0x1879, "X"), + (0x1880, "V"), + (0x18AB, "X"), + (0x18B0, "V"), + (0x18F6, "X"), + (0x1900, "V"), + (0x191F, "X"), + (0x1920, "V"), + (0x192C, "X"), + (0x1930, "V"), + (0x193C, "X"), + (0x1940, "V"), + (0x1941, "X"), + (0x1944, "V"), + (0x196E, "X"), + ] + + +def _seg_15() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1970, "V"), + (0x1975, "X"), + (0x1980, "V"), + (0x19AC, "X"), + (0x19B0, "V"), + (0x19CA, "X"), + (0x19D0, "V"), + (0x19DB, "X"), + (0x19DE, "V"), + (0x1A1C, "X"), + (0x1A1E, "V"), + (0x1A5F, "X"), + (0x1A60, "V"), + (0x1A7D, "X"), + (0x1A7F, "V"), + (0x1A8A, "X"), + (0x1A90, "V"), + (0x1A9A, "X"), + (0x1AA0, "V"), + (0x1AAE, "X"), + (0x1AB0, "V"), + (0x1ACF, "X"), + (0x1B00, "V"), + (0x1B4D, "X"), + (0x1B4E, "V"), + (0x1BF4, "X"), + (0x1BFC, "V"), + (0x1C38, "X"), + (0x1C3B, "V"), + (0x1C4A, "X"), + (0x1C4D, "V"), + (0x1C80, "M", "в"), + (0x1C81, "M", "д"), + (0x1C82, "M", "о"), + (0x1C83, "M", "с"), + (0x1C84, "M", "т"), + (0x1C86, "M", "ъ"), + (0x1C87, "M", "ѣ"), + (0x1C88, "M", "ꙋ"), + (0x1C89, "M", "ᲊ"), + (0x1C8A, "V"), + (0x1C8B, "X"), + (0x1C90, "M", "ა"), + (0x1C91, "M", "ბ"), + (0x1C92, "M", "გ"), + (0x1C93, "M", "დ"), + (0x1C94, "M", "ე"), + (0x1C95, "M", "ვ"), + (0x1C96, "M", "ზ"), + (0x1C97, "M", "თ"), + (0x1C98, "M", "ი"), + (0x1C99, "M", "კ"), + (0x1C9A, "M", "ლ"), + (0x1C9B, "M", "მ"), + (0x1C9C, "M", "ნ"), + (0x1C9D, "M", "ო"), + (0x1C9E, "M", "პ"), + (0x1C9F, "M", "ჟ"), + (0x1CA0, "M", "რ"), + (0x1CA1, "M", "ს"), + (0x1CA2, "M", "ტ"), + (0x1CA3, "M", "უ"), + (0x1CA4, "M", "ფ"), + (0x1CA5, "M", "ქ"), + (0x1CA6, "M", "ღ"), + (0x1CA7, "M", "ყ"), + (0x1CA8, "M", "შ"), + (0x1CA9, "M", "ჩ"), + (0x1CAA, "M", "ც"), + (0x1CAB, "M", "ძ"), + (0x1CAC, "M", "წ"), + (0x1CAD, "M", "ჭ"), + (0x1CAE, "M", "ხ"), + (0x1CAF, "M", "ჯ"), + (0x1CB0, "M", "ჰ"), + (0x1CB1, "M", "ჱ"), + (0x1CB2, "M", "ჲ"), + (0x1CB3, "M", "ჳ"), + (0x1CB4, "M", "ჴ"), + (0x1CB5, "M", "ჵ"), + (0x1CB6, "M", "ჶ"), + (0x1CB7, "M", "ჷ"), + (0x1CB8, "M", "ჸ"), + (0x1CB9, "M", "ჹ"), + (0x1CBA, "M", "ჺ"), + (0x1CBB, "X"), + (0x1CBD, "M", "ჽ"), + (0x1CBE, "M", "ჾ"), + (0x1CBF, "M", "ჿ"), + (0x1CC0, "V"), + (0x1CC8, "X"), + (0x1CD0, "V"), + (0x1CFB, "X"), + (0x1D00, "V"), + (0x1D2C, "M", "a"), + (0x1D2D, "M", "æ"), + (0x1D2E, "M", "b"), + (0x1D2F, "V"), + (0x1D30, "M", "d"), + (0x1D31, "M", "e"), + ] + + +def _seg_16() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D32, "M", "ǝ"), + (0x1D33, "M", "g"), + (0x1D34, "M", "h"), + (0x1D35, "M", "i"), + (0x1D36, "M", "j"), + (0x1D37, "M", "k"), + (0x1D38, "M", "l"), + (0x1D39, "M", "m"), + (0x1D3A, "M", "n"), + (0x1D3B, "V"), + (0x1D3C, "M", "o"), + (0x1D3D, "M", "ȣ"), + (0x1D3E, "M", "p"), + (0x1D3F, "M", "r"), + (0x1D40, "M", "t"), + (0x1D41, "M", "u"), + (0x1D42, "M", "w"), + (0x1D43, "M", "a"), + (0x1D44, "M", "ɐ"), + (0x1D45, "M", "ɑ"), + (0x1D46, "M", "ᴂ"), + (0x1D47, "M", "b"), + (0x1D48, "M", "d"), + (0x1D49, "M", "e"), + (0x1D4A, "M", "ə"), + (0x1D4B, "M", "ɛ"), + (0x1D4C, "M", "ɜ"), + (0x1D4D, "M", "g"), + (0x1D4E, "V"), + (0x1D4F, "M", "k"), + (0x1D50, "M", "m"), + (0x1D51, "M", "ŋ"), + (0x1D52, "M", "o"), + (0x1D53, "M", "ɔ"), + (0x1D54, "M", "ᴖ"), + (0x1D55, "M", "ᴗ"), + (0x1D56, "M", "p"), + (0x1D57, "M", "t"), + (0x1D58, "M", "u"), + (0x1D59, "M", "ᴝ"), + (0x1D5A, "M", "ɯ"), + (0x1D5B, "M", "v"), + (0x1D5C, "M", "ᴥ"), + (0x1D5D, "M", "β"), + (0x1D5E, "M", "γ"), + (0x1D5F, "M", "δ"), + (0x1D60, "M", "φ"), + (0x1D61, "M", "χ"), + (0x1D62, "M", "i"), + (0x1D63, "M", "r"), + (0x1D64, "M", "u"), + (0x1D65, "M", "v"), + (0x1D66, "M", "β"), + (0x1D67, "M", "γ"), + (0x1D68, "M", "ρ"), + (0x1D69, "M", "φ"), + (0x1D6A, "M", "χ"), + (0x1D6B, "V"), + (0x1D78, "M", "н"), + (0x1D79, "V"), + (0x1D9B, "M", "ɒ"), + (0x1D9C, "M", "c"), + (0x1D9D, "M", "ɕ"), + (0x1D9E, "M", "ð"), + (0x1D9F, "M", "ɜ"), + (0x1DA0, "M", "f"), + (0x1DA1, "M", "ɟ"), + (0x1DA2, "M", "ɡ"), + (0x1DA3, "M", "ɥ"), + (0x1DA4, "M", "ɨ"), + (0x1DA5, "M", "ɩ"), + (0x1DA6, "M", "ɪ"), + (0x1DA7, "M", "ᵻ"), + (0x1DA8, "M", "ʝ"), + (0x1DA9, "M", "ɭ"), + (0x1DAA, "M", "ᶅ"), + (0x1DAB, "M", "ʟ"), + (0x1DAC, "M", "ɱ"), + (0x1DAD, "M", "ɰ"), + (0x1DAE, "M", "ɲ"), + (0x1DAF, "M", "ɳ"), + (0x1DB0, "M", "ɴ"), + (0x1DB1, "M", "ɵ"), + (0x1DB2, "M", "ɸ"), + (0x1DB3, "M", "ʂ"), + (0x1DB4, "M", "ʃ"), + (0x1DB5, "M", "ƫ"), + (0x1DB6, "M", "ʉ"), + (0x1DB7, "M", "ʊ"), + (0x1DB8, "M", "ᴜ"), + (0x1DB9, "M", "ʋ"), + (0x1DBA, "M", "ʌ"), + (0x1DBB, "M", "z"), + (0x1DBC, "M", "ʐ"), + (0x1DBD, "M", "ʑ"), + (0x1DBE, "M", "ʒ"), + (0x1DBF, "M", "θ"), + (0x1DC0, "V"), + (0x1E00, "M", "ḁ"), + (0x1E01, "V"), + ] + + +def _seg_17() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E02, "M", "ḃ"), + (0x1E03, "V"), + (0x1E04, "M", "ḅ"), + (0x1E05, "V"), + (0x1E06, "M", "ḇ"), + (0x1E07, "V"), + (0x1E08, "M", "ḉ"), + (0x1E09, "V"), + (0x1E0A, "M", "ḋ"), + (0x1E0B, "V"), + (0x1E0C, "M", "ḍ"), + (0x1E0D, "V"), + (0x1E0E, "M", "ḏ"), + (0x1E0F, "V"), + (0x1E10, "M", "ḑ"), + (0x1E11, "V"), + (0x1E12, "M", "ḓ"), + (0x1E13, "V"), + (0x1E14, "M", "ḕ"), + (0x1E15, "V"), + (0x1E16, "M", "ḗ"), + (0x1E17, "V"), + (0x1E18, "M", "ḙ"), + (0x1E19, "V"), + (0x1E1A, "M", "ḛ"), + (0x1E1B, "V"), + (0x1E1C, "M", "ḝ"), + (0x1E1D, "V"), + (0x1E1E, "M", "ḟ"), + (0x1E1F, "V"), + (0x1E20, "M", "ḡ"), + (0x1E21, "V"), + (0x1E22, "M", "ḣ"), + (0x1E23, "V"), + (0x1E24, "M", "ḥ"), + (0x1E25, "V"), + (0x1E26, "M", "ḧ"), + (0x1E27, "V"), + (0x1E28, "M", "ḩ"), + (0x1E29, "V"), + (0x1E2A, "M", "ḫ"), + (0x1E2B, "V"), + (0x1E2C, "M", "ḭ"), + (0x1E2D, "V"), + (0x1E2E, "M", "ḯ"), + (0x1E2F, "V"), + (0x1E30, "M", "ḱ"), + (0x1E31, "V"), + (0x1E32, "M", "ḳ"), + (0x1E33, "V"), + (0x1E34, "M", "ḵ"), + (0x1E35, "V"), + (0x1E36, "M", "ḷ"), + (0x1E37, "V"), + (0x1E38, "M", "ḹ"), + (0x1E39, "V"), + (0x1E3A, "M", "ḻ"), + (0x1E3B, "V"), + (0x1E3C, "M", "ḽ"), + (0x1E3D, "V"), + (0x1E3E, "M", "ḿ"), + (0x1E3F, "V"), + (0x1E40, "M", "ṁ"), + (0x1E41, "V"), + (0x1E42, "M", "ṃ"), + (0x1E43, "V"), + (0x1E44, "M", "ṅ"), + (0x1E45, "V"), + (0x1E46, "M", "ṇ"), + (0x1E47, "V"), + (0x1E48, "M", "ṉ"), + (0x1E49, "V"), + (0x1E4A, "M", "ṋ"), + (0x1E4B, "V"), + (0x1E4C, "M", "ṍ"), + (0x1E4D, "V"), + (0x1E4E, "M", "ṏ"), + (0x1E4F, "V"), + (0x1E50, "M", "ṑ"), + (0x1E51, "V"), + (0x1E52, "M", "ṓ"), + (0x1E53, "V"), + (0x1E54, "M", "ṕ"), + (0x1E55, "V"), + (0x1E56, "M", "ṗ"), + (0x1E57, "V"), + (0x1E58, "M", "ṙ"), + (0x1E59, "V"), + (0x1E5A, "M", "ṛ"), + (0x1E5B, "V"), + (0x1E5C, "M", "ṝ"), + (0x1E5D, "V"), + (0x1E5E, "M", "ṟ"), + (0x1E5F, "V"), + (0x1E60, "M", "ṡ"), + (0x1E61, "V"), + (0x1E62, "M", "ṣ"), + (0x1E63, "V"), + (0x1E64, "M", "ṥ"), + (0x1E65, "V"), + ] + + +def _seg_18() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E66, "M", "ṧ"), + (0x1E67, "V"), + (0x1E68, "M", "ṩ"), + (0x1E69, "V"), + (0x1E6A, "M", "ṫ"), + (0x1E6B, "V"), + (0x1E6C, "M", "ṭ"), + (0x1E6D, "V"), + (0x1E6E, "M", "ṯ"), + (0x1E6F, "V"), + (0x1E70, "M", "ṱ"), + (0x1E71, "V"), + (0x1E72, "M", "ṳ"), + (0x1E73, "V"), + (0x1E74, "M", "ṵ"), + (0x1E75, "V"), + (0x1E76, "M", "ṷ"), + (0x1E77, "V"), + (0x1E78, "M", "ṹ"), + (0x1E79, "V"), + (0x1E7A, "M", "ṻ"), + (0x1E7B, "V"), + (0x1E7C, "M", "ṽ"), + (0x1E7D, "V"), + (0x1E7E, "M", "ṿ"), + (0x1E7F, "V"), + (0x1E80, "M", "ẁ"), + (0x1E81, "V"), + (0x1E82, "M", "ẃ"), + (0x1E83, "V"), + (0x1E84, "M", "ẅ"), + (0x1E85, "V"), + (0x1E86, "M", "ẇ"), + (0x1E87, "V"), + (0x1E88, "M", "ẉ"), + (0x1E89, "V"), + (0x1E8A, "M", "ẋ"), + (0x1E8B, "V"), + (0x1E8C, "M", "ẍ"), + (0x1E8D, "V"), + (0x1E8E, "M", "ẏ"), + (0x1E8F, "V"), + (0x1E90, "M", "ẑ"), + (0x1E91, "V"), + (0x1E92, "M", "ẓ"), + (0x1E93, "V"), + (0x1E94, "M", "ẕ"), + (0x1E95, "V"), + (0x1E9A, "M", "aʾ"), + (0x1E9B, "M", "ṡ"), + (0x1E9C, "V"), + (0x1E9E, "M", "ß"), + (0x1E9F, "V"), + (0x1EA0, "M", "ạ"), + (0x1EA1, "V"), + (0x1EA2, "M", "ả"), + (0x1EA3, "V"), + (0x1EA4, "M", "ấ"), + (0x1EA5, "V"), + (0x1EA6, "M", "ầ"), + (0x1EA7, "V"), + (0x1EA8, "M", "ẩ"), + (0x1EA9, "V"), + (0x1EAA, "M", "ẫ"), + (0x1EAB, "V"), + (0x1EAC, "M", "ậ"), + (0x1EAD, "V"), + (0x1EAE, "M", "ắ"), + (0x1EAF, "V"), + (0x1EB0, "M", "ằ"), + (0x1EB1, "V"), + (0x1EB2, "M", "ẳ"), + (0x1EB3, "V"), + (0x1EB4, "M", "ẵ"), + (0x1EB5, "V"), + (0x1EB6, "M", "ặ"), + (0x1EB7, "V"), + (0x1EB8, "M", "ẹ"), + (0x1EB9, "V"), + (0x1EBA, "M", "ẻ"), + (0x1EBB, "V"), + (0x1EBC, "M", "ẽ"), + (0x1EBD, "V"), + (0x1EBE, "M", "ế"), + (0x1EBF, "V"), + (0x1EC0, "M", "ề"), + (0x1EC1, "V"), + (0x1EC2, "M", "ể"), + (0x1EC3, "V"), + (0x1EC4, "M", "ễ"), + (0x1EC5, "V"), + (0x1EC6, "M", "ệ"), + (0x1EC7, "V"), + (0x1EC8, "M", "ỉ"), + (0x1EC9, "V"), + (0x1ECA, "M", "ị"), + (0x1ECB, "V"), + (0x1ECC, "M", "ọ"), + (0x1ECD, "V"), + (0x1ECE, "M", "ỏ"), + ] + + +def _seg_19() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1ECF, "V"), + (0x1ED0, "M", "ố"), + (0x1ED1, "V"), + (0x1ED2, "M", "ồ"), + (0x1ED3, "V"), + (0x1ED4, "M", "ổ"), + (0x1ED5, "V"), + (0x1ED6, "M", "ỗ"), + (0x1ED7, "V"), + (0x1ED8, "M", "ộ"), + (0x1ED9, "V"), + (0x1EDA, "M", "ớ"), + (0x1EDB, "V"), + (0x1EDC, "M", "ờ"), + (0x1EDD, "V"), + (0x1EDE, "M", "ở"), + (0x1EDF, "V"), + (0x1EE0, "M", "ỡ"), + (0x1EE1, "V"), + (0x1EE2, "M", "ợ"), + (0x1EE3, "V"), + (0x1EE4, "M", "ụ"), + (0x1EE5, "V"), + (0x1EE6, "M", "ủ"), + (0x1EE7, "V"), + (0x1EE8, "M", "ứ"), + (0x1EE9, "V"), + (0x1EEA, "M", "ừ"), + (0x1EEB, "V"), + (0x1EEC, "M", "ử"), + (0x1EED, "V"), + (0x1EEE, "M", "ữ"), + (0x1EEF, "V"), + (0x1EF0, "M", "ự"), + (0x1EF1, "V"), + (0x1EF2, "M", "ỳ"), + (0x1EF3, "V"), + (0x1EF4, "M", "ỵ"), + (0x1EF5, "V"), + (0x1EF6, "M", "ỷ"), + (0x1EF7, "V"), + (0x1EF8, "M", "ỹ"), + (0x1EF9, "V"), + (0x1EFA, "M", "ỻ"), + (0x1EFB, "V"), + (0x1EFC, "M", "ỽ"), + (0x1EFD, "V"), + (0x1EFE, "M", "ỿ"), + (0x1EFF, "V"), + (0x1F08, "M", "ἀ"), + (0x1F09, "M", "ἁ"), + (0x1F0A, "M", "ἂ"), + (0x1F0B, "M", "ἃ"), + (0x1F0C, "M", "ἄ"), + (0x1F0D, "M", "ἅ"), + (0x1F0E, "M", "ἆ"), + (0x1F0F, "M", "ἇ"), + (0x1F10, "V"), + (0x1F16, "X"), + (0x1F18, "M", "ἐ"), + (0x1F19, "M", "ἑ"), + (0x1F1A, "M", "ἒ"), + (0x1F1B, "M", "ἓ"), + (0x1F1C, "M", "ἔ"), + (0x1F1D, "M", "ἕ"), + (0x1F1E, "X"), + (0x1F20, "V"), + (0x1F28, "M", "ἠ"), + (0x1F29, "M", "ἡ"), + (0x1F2A, "M", "ἢ"), + (0x1F2B, "M", "ἣ"), + (0x1F2C, "M", "ἤ"), + (0x1F2D, "M", "ἥ"), + (0x1F2E, "M", "ἦ"), + (0x1F2F, "M", "ἧ"), + (0x1F30, "V"), + (0x1F38, "M", "ἰ"), + (0x1F39, "M", "ἱ"), + (0x1F3A, "M", "ἲ"), + (0x1F3B, "M", "ἳ"), + (0x1F3C, "M", "ἴ"), + (0x1F3D, "M", "ἵ"), + (0x1F3E, "M", "ἶ"), + (0x1F3F, "M", "ἷ"), + (0x1F40, "V"), + (0x1F46, "X"), + (0x1F48, "M", "ὀ"), + (0x1F49, "M", "ὁ"), + (0x1F4A, "M", "ὂ"), + (0x1F4B, "M", "ὃ"), + (0x1F4C, "M", "ὄ"), + (0x1F4D, "M", "ὅ"), + (0x1F4E, "X"), + (0x1F50, "V"), + (0x1F58, "X"), + (0x1F59, "M", "ὑ"), + (0x1F5A, "X"), + (0x1F5B, "M", "ὓ"), + (0x1F5C, "X"), + (0x1F5D, "M", "ὕ"), + ] + + +def _seg_20() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1F5E, "X"), + (0x1F5F, "M", "ὗ"), + (0x1F60, "V"), + (0x1F68, "M", "ὠ"), + (0x1F69, "M", "ὡ"), + (0x1F6A, "M", "ὢ"), + (0x1F6B, "M", "ὣ"), + (0x1F6C, "M", "ὤ"), + (0x1F6D, "M", "ὥ"), + (0x1F6E, "M", "ὦ"), + (0x1F6F, "M", "ὧ"), + (0x1F70, "V"), + (0x1F71, "M", "ά"), + (0x1F72, "V"), + (0x1F73, "M", "έ"), + (0x1F74, "V"), + (0x1F75, "M", "ή"), + (0x1F76, "V"), + (0x1F77, "M", "ί"), + (0x1F78, "V"), + (0x1F79, "M", "ό"), + (0x1F7A, "V"), + (0x1F7B, "M", "ύ"), + (0x1F7C, "V"), + (0x1F7D, "M", "ώ"), + (0x1F7E, "X"), + (0x1F80, "M", "ἀι"), + (0x1F81, "M", "ἁι"), + (0x1F82, "M", "ἂι"), + (0x1F83, "M", "ἃι"), + (0x1F84, "M", "ἄι"), + (0x1F85, "M", "ἅι"), + (0x1F86, "M", "ἆι"), + (0x1F87, "M", "ἇι"), + (0x1F88, "M", "ἀι"), + (0x1F89, "M", "ἁι"), + (0x1F8A, "M", "ἂι"), + (0x1F8B, "M", "ἃι"), + (0x1F8C, "M", "ἄι"), + (0x1F8D, "M", "ἅι"), + (0x1F8E, "M", "ἆι"), + (0x1F8F, "M", "ἇι"), + (0x1F90, "M", "ἠι"), + (0x1F91, "M", "ἡι"), + (0x1F92, "M", "ἢι"), + (0x1F93, "M", "ἣι"), + (0x1F94, "M", "ἤι"), + (0x1F95, "M", "ἥι"), + (0x1F96, "M", "ἦι"), + (0x1F97, "M", "ἧι"), + (0x1F98, "M", "ἠι"), + (0x1F99, "M", "ἡι"), + (0x1F9A, "M", "ἢι"), + (0x1F9B, "M", "ἣι"), + (0x1F9C, "M", "ἤι"), + (0x1F9D, "M", "ἥι"), + (0x1F9E, "M", "ἦι"), + (0x1F9F, "M", "ἧι"), + (0x1FA0, "M", "ὠι"), + (0x1FA1, "M", "ὡι"), + (0x1FA2, "M", "ὢι"), + (0x1FA3, "M", "ὣι"), + (0x1FA4, "M", "ὤι"), + (0x1FA5, "M", "ὥι"), + (0x1FA6, "M", "ὦι"), + (0x1FA7, "M", "ὧι"), + (0x1FA8, "M", "ὠι"), + (0x1FA9, "M", "ὡι"), + (0x1FAA, "M", "ὢι"), + (0x1FAB, "M", "ὣι"), + (0x1FAC, "M", "ὤι"), + (0x1FAD, "M", "ὥι"), + (0x1FAE, "M", "ὦι"), + (0x1FAF, "M", "ὧι"), + (0x1FB0, "V"), + (0x1FB2, "M", "ὰι"), + (0x1FB3, "M", "αι"), + (0x1FB4, "M", "άι"), + (0x1FB5, "X"), + (0x1FB6, "V"), + (0x1FB7, "M", "ᾶι"), + (0x1FB8, "M", "ᾰ"), + (0x1FB9, "M", "ᾱ"), + (0x1FBA, "M", "ὰ"), + (0x1FBB, "M", "ά"), + (0x1FBC, "M", "αι"), + (0x1FBD, "M", " ̓"), + (0x1FBE, "M", "ι"), + (0x1FBF, "M", " ̓"), + (0x1FC0, "M", " ͂"), + (0x1FC1, "M", " ̈͂"), + (0x1FC2, "M", "ὴι"), + (0x1FC3, "M", "ηι"), + (0x1FC4, "M", "ήι"), + (0x1FC5, "X"), + (0x1FC6, "V"), + (0x1FC7, "M", "ῆι"), + (0x1FC8, "M", "ὲ"), + (0x1FC9, "M", "έ"), + (0x1FCA, "M", "ὴ"), + ] + + +def _seg_21() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1FCB, "M", "ή"), + (0x1FCC, "M", "ηι"), + (0x1FCD, "M", " ̓̀"), + (0x1FCE, "M", " ̓́"), + (0x1FCF, "M", " ̓͂"), + (0x1FD0, "V"), + (0x1FD3, "M", "ΐ"), + (0x1FD4, "X"), + (0x1FD6, "V"), + (0x1FD8, "M", "ῐ"), + (0x1FD9, "M", "ῑ"), + (0x1FDA, "M", "ὶ"), + (0x1FDB, "M", "ί"), + (0x1FDC, "X"), + (0x1FDD, "M", " ̔̀"), + (0x1FDE, "M", " ̔́"), + (0x1FDF, "M", " ̔͂"), + (0x1FE0, "V"), + (0x1FE3, "M", "ΰ"), + (0x1FE4, "V"), + (0x1FE8, "M", "ῠ"), + (0x1FE9, "M", "ῡ"), + (0x1FEA, "M", "ὺ"), + (0x1FEB, "M", "ύ"), + (0x1FEC, "M", "ῥ"), + (0x1FED, "M", " ̈̀"), + (0x1FEE, "M", " ̈́"), + (0x1FEF, "M", "`"), + (0x1FF0, "X"), + (0x1FF2, "M", "ὼι"), + (0x1FF3, "M", "ωι"), + (0x1FF4, "M", "ώι"), + (0x1FF5, "X"), + (0x1FF6, "V"), + (0x1FF7, "M", "ῶι"), + (0x1FF8, "M", "ὸ"), + (0x1FF9, "M", "ό"), + (0x1FFA, "M", "ὼ"), + (0x1FFB, "M", "ώ"), + (0x1FFC, "M", "ωι"), + (0x1FFD, "M", " ́"), + (0x1FFE, "M", " ̔"), + (0x1FFF, "X"), + (0x2000, "M", " "), + (0x200B, "I"), + (0x200C, "D", ""), + (0x200E, "X"), + (0x2010, "V"), + (0x2011, "M", "‐"), + (0x2012, "V"), + (0x2017, "M", " ̳"), + (0x2018, "V"), + (0x2024, "X"), + (0x2027, "V"), + (0x2028, "X"), + (0x202F, "M", " "), + (0x2030, "V"), + (0x2033, "M", "′′"), + (0x2034, "M", "′′′"), + (0x2035, "V"), + (0x2036, "M", "‵‵"), + (0x2037, "M", "‵‵‵"), + (0x2038, "V"), + (0x203C, "M", "!!"), + (0x203D, "V"), + (0x203E, "M", " ̅"), + (0x203F, "V"), + (0x2047, "M", "??"), + (0x2048, "M", "?!"), + (0x2049, "M", "!?"), + (0x204A, "V"), + (0x2057, "M", "′′′′"), + (0x2058, "V"), + (0x205F, "M", " "), + (0x2060, "I"), + (0x2065, "X"), + (0x206A, "I"), + (0x2070, "M", "0"), + (0x2071, "M", "i"), + (0x2072, "X"), + (0x2074, "M", "4"), + (0x2075, "M", "5"), + (0x2076, "M", "6"), + (0x2077, "M", "7"), + (0x2078, "M", "8"), + (0x2079, "M", "9"), + (0x207A, "M", "+"), + (0x207B, "M", "−"), + (0x207C, "M", "="), + (0x207D, "M", "("), + (0x207E, "M", ")"), + (0x207F, "M", "n"), + (0x2080, "M", "0"), + (0x2081, "M", "1"), + (0x2082, "M", "2"), + (0x2083, "M", "3"), + (0x2084, "M", "4"), + (0x2085, "M", "5"), + (0x2086, "M", "6"), + (0x2087, "M", "7"), + ] + + +def _seg_22() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2088, "M", "8"), + (0x2089, "M", "9"), + (0x208A, "M", "+"), + (0x208B, "M", "−"), + (0x208C, "M", "="), + (0x208D, "M", "("), + (0x208E, "M", ")"), + (0x208F, "X"), + (0x2090, "M", "a"), + (0x2091, "M", "e"), + (0x2092, "M", "o"), + (0x2093, "M", "x"), + (0x2094, "M", "ə"), + (0x2095, "M", "h"), + (0x2096, "M", "k"), + (0x2097, "M", "l"), + (0x2098, "M", "m"), + (0x2099, "M", "n"), + (0x209A, "M", "p"), + (0x209B, "M", "s"), + (0x209C, "M", "t"), + (0x209D, "X"), + (0x20A0, "V"), + (0x20A8, "M", "rs"), + (0x20A9, "V"), + (0x20C1, "X"), + (0x20D0, "V"), + (0x20F1, "X"), + (0x2100, "M", "a/c"), + (0x2101, "M", "a/s"), + (0x2102, "M", "c"), + (0x2103, "M", "°c"), + (0x2104, "V"), + (0x2105, "M", "c/o"), + (0x2106, "M", "c/u"), + (0x2107, "M", "ɛ"), + (0x2108, "V"), + (0x2109, "M", "°f"), + (0x210A, "M", "g"), + (0x210B, "M", "h"), + (0x210F, "M", "ħ"), + (0x2110, "M", "i"), + (0x2112, "M", "l"), + (0x2114, "V"), + (0x2115, "M", "n"), + (0x2116, "M", "no"), + (0x2117, "V"), + (0x2119, "M", "p"), + (0x211A, "M", "q"), + (0x211B, "M", "r"), + (0x211E, "V"), + (0x2120, "M", "sm"), + (0x2121, "M", "tel"), + (0x2122, "M", "tm"), + (0x2123, "V"), + (0x2124, "M", "z"), + (0x2125, "V"), + (0x2126, "M", "ω"), + (0x2127, "V"), + (0x2128, "M", "z"), + (0x2129, "V"), + (0x212A, "M", "k"), + (0x212B, "M", "å"), + (0x212C, "M", "b"), + (0x212D, "M", "c"), + (0x212E, "V"), + (0x212F, "M", "e"), + (0x2131, "M", "f"), + (0x2132, "M", "ⅎ"), + (0x2133, "M", "m"), + (0x2134, "M", "o"), + (0x2135, "M", "א"), + (0x2136, "M", "ב"), + (0x2137, "M", "ג"), + (0x2138, "M", "ד"), + (0x2139, "M", "i"), + (0x213A, "V"), + (0x213B, "M", "fax"), + (0x213C, "M", "π"), + (0x213D, "M", "γ"), + (0x213F, "M", "π"), + (0x2140, "M", "∑"), + (0x2141, "V"), + (0x2145, "M", "d"), + (0x2147, "M", "e"), + (0x2148, "M", "i"), + (0x2149, "M", "j"), + (0x214A, "V"), + (0x2150, "M", "1⁄7"), + (0x2151, "M", "1⁄9"), + (0x2152, "M", "1⁄10"), + (0x2153, "M", "1⁄3"), + (0x2154, "M", "2⁄3"), + (0x2155, "M", "1⁄5"), + (0x2156, "M", "2⁄5"), + (0x2157, "M", "3⁄5"), + (0x2158, "M", "4⁄5"), + (0x2159, "M", "1⁄6"), + (0x215A, "M", "5⁄6"), + (0x215B, "M", "1⁄8"), + ] + + +def _seg_23() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x215C, "M", "3⁄8"), + (0x215D, "M", "5⁄8"), + (0x215E, "M", "7⁄8"), + (0x215F, "M", "1⁄"), + (0x2160, "M", "i"), + (0x2161, "M", "ii"), + (0x2162, "M", "iii"), + (0x2163, "M", "iv"), + (0x2164, "M", "v"), + (0x2165, "M", "vi"), + (0x2166, "M", "vii"), + (0x2167, "M", "viii"), + (0x2168, "M", "ix"), + (0x2169, "M", "x"), + (0x216A, "M", "xi"), + (0x216B, "M", "xii"), + (0x216C, "M", "l"), + (0x216D, "M", "c"), + (0x216E, "M", "d"), + (0x216F, "M", "m"), + (0x2170, "M", "i"), + (0x2171, "M", "ii"), + (0x2172, "M", "iii"), + (0x2173, "M", "iv"), + (0x2174, "M", "v"), + (0x2175, "M", "vi"), + (0x2176, "M", "vii"), + (0x2177, "M", "viii"), + (0x2178, "M", "ix"), + (0x2179, "M", "x"), + (0x217A, "M", "xi"), + (0x217B, "M", "xii"), + (0x217C, "M", "l"), + (0x217D, "M", "c"), + (0x217E, "M", "d"), + (0x217F, "M", "m"), + (0x2180, "V"), + (0x2183, "M", "ↄ"), + (0x2184, "V"), + (0x2189, "M", "0⁄3"), + (0x218A, "V"), + (0x218C, "X"), + (0x2190, "V"), + (0x222C, "M", "∫∫"), + (0x222D, "M", "∫∫∫"), + (0x222E, "V"), + (0x222F, "M", "∮∮"), + (0x2230, "M", "∮∮∮"), + (0x2231, "V"), + (0x2329, "M", "〈"), + (0x232A, "M", "〉"), + (0x232B, "V"), + (0x242A, "X"), + (0x2440, "V"), + (0x244B, "X"), + (0x2460, "M", "1"), + (0x2461, "M", "2"), + (0x2462, "M", "3"), + (0x2463, "M", "4"), + (0x2464, "M", "5"), + (0x2465, "M", "6"), + (0x2466, "M", "7"), + (0x2467, "M", "8"), + (0x2468, "M", "9"), + (0x2469, "M", "10"), + (0x246A, "M", "11"), + (0x246B, "M", "12"), + (0x246C, "M", "13"), + (0x246D, "M", "14"), + (0x246E, "M", "15"), + (0x246F, "M", "16"), + (0x2470, "M", "17"), + (0x2471, "M", "18"), + (0x2472, "M", "19"), + (0x2473, "M", "20"), + (0x2474, "M", "(1)"), + (0x2475, "M", "(2)"), + (0x2476, "M", "(3)"), + (0x2477, "M", "(4)"), + (0x2478, "M", "(5)"), + (0x2479, "M", "(6)"), + (0x247A, "M", "(7)"), + (0x247B, "M", "(8)"), + (0x247C, "M", "(9)"), + (0x247D, "M", "(10)"), + (0x247E, "M", "(11)"), + (0x247F, "M", "(12)"), + (0x2480, "M", "(13)"), + (0x2481, "M", "(14)"), + (0x2482, "M", "(15)"), + (0x2483, "M", "(16)"), + (0x2484, "M", "(17)"), + (0x2485, "M", "(18)"), + (0x2486, "M", "(19)"), + (0x2487, "M", "(20)"), + (0x2488, "X"), + (0x249C, "M", "(a)"), + (0x249D, "M", "(b)"), + (0x249E, "M", "(c)"), + (0x249F, "M", "(d)"), + ] + + +def _seg_24() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x24A0, "M", "(e)"), + (0x24A1, "M", "(f)"), + (0x24A2, "M", "(g)"), + (0x24A3, "M", "(h)"), + (0x24A4, "M", "(i)"), + (0x24A5, "M", "(j)"), + (0x24A6, "M", "(k)"), + (0x24A7, "M", "(l)"), + (0x24A8, "M", "(m)"), + (0x24A9, "M", "(n)"), + (0x24AA, "M", "(o)"), + (0x24AB, "M", "(p)"), + (0x24AC, "M", "(q)"), + (0x24AD, "M", "(r)"), + (0x24AE, "M", "(s)"), + (0x24AF, "M", "(t)"), + (0x24B0, "M", "(u)"), + (0x24B1, "M", "(v)"), + (0x24B2, "M", "(w)"), + (0x24B3, "M", "(x)"), + (0x24B4, "M", "(y)"), + (0x24B5, "M", "(z)"), + (0x24B6, "M", "a"), + (0x24B7, "M", "b"), + (0x24B8, "M", "c"), + (0x24B9, "M", "d"), + (0x24BA, "M", "e"), + (0x24BB, "M", "f"), + (0x24BC, "M", "g"), + (0x24BD, "M", "h"), + (0x24BE, "M", "i"), + (0x24BF, "M", "j"), + (0x24C0, "M", "k"), + (0x24C1, "M", "l"), + (0x24C2, "M", "m"), + (0x24C3, "M", "n"), + (0x24C4, "M", "o"), + (0x24C5, "M", "p"), + (0x24C6, "M", "q"), + (0x24C7, "M", "r"), + (0x24C8, "M", "s"), + (0x24C9, "M", "t"), + (0x24CA, "M", "u"), + (0x24CB, "M", "v"), + (0x24CC, "M", "w"), + (0x24CD, "M", "x"), + (0x24CE, "M", "y"), + (0x24CF, "M", "z"), + (0x24D0, "M", "a"), + (0x24D1, "M", "b"), + (0x24D2, "M", "c"), + (0x24D3, "M", "d"), + (0x24D4, "M", "e"), + (0x24D5, "M", "f"), + (0x24D6, "M", "g"), + (0x24D7, "M", "h"), + (0x24D8, "M", "i"), + (0x24D9, "M", "j"), + (0x24DA, "M", "k"), + (0x24DB, "M", "l"), + (0x24DC, "M", "m"), + (0x24DD, "M", "n"), + (0x24DE, "M", "o"), + (0x24DF, "M", "p"), + (0x24E0, "M", "q"), + (0x24E1, "M", "r"), + (0x24E2, "M", "s"), + (0x24E3, "M", "t"), + (0x24E4, "M", "u"), + (0x24E5, "M", "v"), + (0x24E6, "M", "w"), + (0x24E7, "M", "x"), + (0x24E8, "M", "y"), + (0x24E9, "M", "z"), + (0x24EA, "M", "0"), + (0x24EB, "V"), + (0x2A0C, "M", "∫∫∫∫"), + (0x2A0D, "V"), + (0x2A74, "M", "::="), + (0x2A75, "M", "=="), + (0x2A76, "M", "==="), + (0x2A77, "V"), + (0x2ADC, "M", "⫝̸"), + (0x2ADD, "V"), + (0x2B74, "X"), + (0x2B76, "V"), + (0x2B96, "X"), + (0x2B97, "V"), + (0x2C00, "M", "ⰰ"), + (0x2C01, "M", "ⰱ"), + (0x2C02, "M", "ⰲ"), + (0x2C03, "M", "ⰳ"), + (0x2C04, "M", "ⰴ"), + (0x2C05, "M", "ⰵ"), + (0x2C06, "M", "ⰶ"), + (0x2C07, "M", "ⰷ"), + (0x2C08, "M", "ⰸ"), + (0x2C09, "M", "ⰹ"), + (0x2C0A, "M", "ⰺ"), + (0x2C0B, "M", "ⰻ"), + ] + + +def _seg_25() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2C0C, "M", "ⰼ"), + (0x2C0D, "M", "ⰽ"), + (0x2C0E, "M", "ⰾ"), + (0x2C0F, "M", "ⰿ"), + (0x2C10, "M", "ⱀ"), + (0x2C11, "M", "ⱁ"), + (0x2C12, "M", "ⱂ"), + (0x2C13, "M", "ⱃ"), + (0x2C14, "M", "ⱄ"), + (0x2C15, "M", "ⱅ"), + (0x2C16, "M", "ⱆ"), + (0x2C17, "M", "ⱇ"), + (0x2C18, "M", "ⱈ"), + (0x2C19, "M", "ⱉ"), + (0x2C1A, "M", "ⱊ"), + (0x2C1B, "M", "ⱋ"), + (0x2C1C, "M", "ⱌ"), + (0x2C1D, "M", "ⱍ"), + (0x2C1E, "M", "ⱎ"), + (0x2C1F, "M", "ⱏ"), + (0x2C20, "M", "ⱐ"), + (0x2C21, "M", "ⱑ"), + (0x2C22, "M", "ⱒ"), + (0x2C23, "M", "ⱓ"), + (0x2C24, "M", "ⱔ"), + (0x2C25, "M", "ⱕ"), + (0x2C26, "M", "ⱖ"), + (0x2C27, "M", "ⱗ"), + (0x2C28, "M", "ⱘ"), + (0x2C29, "M", "ⱙ"), + (0x2C2A, "M", "ⱚ"), + (0x2C2B, "M", "ⱛ"), + (0x2C2C, "M", "ⱜ"), + (0x2C2D, "M", "ⱝ"), + (0x2C2E, "M", "ⱞ"), + (0x2C2F, "M", "ⱟ"), + (0x2C30, "V"), + (0x2C60, "M", "ⱡ"), + (0x2C61, "V"), + (0x2C62, "M", "ɫ"), + (0x2C63, "M", "ᵽ"), + (0x2C64, "M", "ɽ"), + (0x2C65, "V"), + (0x2C67, "M", "ⱨ"), + (0x2C68, "V"), + (0x2C69, "M", "ⱪ"), + (0x2C6A, "V"), + (0x2C6B, "M", "ⱬ"), + (0x2C6C, "V"), + (0x2C6D, "M", "ɑ"), + (0x2C6E, "M", "ɱ"), + (0x2C6F, "M", "ɐ"), + (0x2C70, "M", "ɒ"), + (0x2C71, "V"), + (0x2C72, "M", "ⱳ"), + (0x2C73, "V"), + (0x2C75, "M", "ⱶ"), + (0x2C76, "V"), + (0x2C7C, "M", "j"), + (0x2C7D, "M", "v"), + (0x2C7E, "M", "ȿ"), + (0x2C7F, "M", "ɀ"), + (0x2C80, "M", "ⲁ"), + (0x2C81, "V"), + (0x2C82, "M", "ⲃ"), + (0x2C83, "V"), + (0x2C84, "M", "ⲅ"), + (0x2C85, "V"), + (0x2C86, "M", "ⲇ"), + (0x2C87, "V"), + (0x2C88, "M", "ⲉ"), + (0x2C89, "V"), + (0x2C8A, "M", "ⲋ"), + (0x2C8B, "V"), + (0x2C8C, "M", "ⲍ"), + (0x2C8D, "V"), + (0x2C8E, "M", "ⲏ"), + (0x2C8F, "V"), + (0x2C90, "M", "ⲑ"), + (0x2C91, "V"), + (0x2C92, "M", "ⲓ"), + (0x2C93, "V"), + (0x2C94, "M", "ⲕ"), + (0x2C95, "V"), + (0x2C96, "M", "ⲗ"), + (0x2C97, "V"), + (0x2C98, "M", "ⲙ"), + (0x2C99, "V"), + (0x2C9A, "M", "ⲛ"), + (0x2C9B, "V"), + (0x2C9C, "M", "ⲝ"), + (0x2C9D, "V"), + (0x2C9E, "M", "ⲟ"), + (0x2C9F, "V"), + (0x2CA0, "M", "ⲡ"), + (0x2CA1, "V"), + (0x2CA2, "M", "ⲣ"), + (0x2CA3, "V"), + (0x2CA4, "M", "ⲥ"), + (0x2CA5, "V"), + ] + + +def _seg_26() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2CA6, "M", "ⲧ"), + (0x2CA7, "V"), + (0x2CA8, "M", "ⲩ"), + (0x2CA9, "V"), + (0x2CAA, "M", "ⲫ"), + (0x2CAB, "V"), + (0x2CAC, "M", "ⲭ"), + (0x2CAD, "V"), + (0x2CAE, "M", "ⲯ"), + (0x2CAF, "V"), + (0x2CB0, "M", "ⲱ"), + (0x2CB1, "V"), + (0x2CB2, "M", "ⲳ"), + (0x2CB3, "V"), + (0x2CB4, "M", "ⲵ"), + (0x2CB5, "V"), + (0x2CB6, "M", "ⲷ"), + (0x2CB7, "V"), + (0x2CB8, "M", "ⲹ"), + (0x2CB9, "V"), + (0x2CBA, "M", "ⲻ"), + (0x2CBB, "V"), + (0x2CBC, "M", "ⲽ"), + (0x2CBD, "V"), + (0x2CBE, "M", "ⲿ"), + (0x2CBF, "V"), + (0x2CC0, "M", "ⳁ"), + (0x2CC1, "V"), + (0x2CC2, "M", "ⳃ"), + (0x2CC3, "V"), + (0x2CC4, "M", "ⳅ"), + (0x2CC5, "V"), + (0x2CC6, "M", "ⳇ"), + (0x2CC7, "V"), + (0x2CC8, "M", "ⳉ"), + (0x2CC9, "V"), + (0x2CCA, "M", "ⳋ"), + (0x2CCB, "V"), + (0x2CCC, "M", "ⳍ"), + (0x2CCD, "V"), + (0x2CCE, "M", "ⳏ"), + (0x2CCF, "V"), + (0x2CD0, "M", "ⳑ"), + (0x2CD1, "V"), + (0x2CD2, "M", "ⳓ"), + (0x2CD3, "V"), + (0x2CD4, "M", "ⳕ"), + (0x2CD5, "V"), + (0x2CD6, "M", "ⳗ"), + (0x2CD7, "V"), + (0x2CD8, "M", "ⳙ"), + (0x2CD9, "V"), + (0x2CDA, "M", "ⳛ"), + (0x2CDB, "V"), + (0x2CDC, "M", "ⳝ"), + (0x2CDD, "V"), + (0x2CDE, "M", "ⳟ"), + (0x2CDF, "V"), + (0x2CE0, "M", "ⳡ"), + (0x2CE1, "V"), + (0x2CE2, "M", "ⳣ"), + (0x2CE3, "V"), + (0x2CEB, "M", "ⳬ"), + (0x2CEC, "V"), + (0x2CED, "M", "ⳮ"), + (0x2CEE, "V"), + (0x2CF2, "M", "ⳳ"), + (0x2CF3, "V"), + (0x2CF4, "X"), + (0x2CF9, "V"), + (0x2D26, "X"), + (0x2D27, "V"), + (0x2D28, "X"), + (0x2D2D, "V"), + (0x2D2E, "X"), + (0x2D30, "V"), + (0x2D68, "X"), + (0x2D6F, "M", "ⵡ"), + (0x2D70, "V"), + (0x2D71, "X"), + (0x2D7F, "V"), + (0x2D97, "X"), + (0x2DA0, "V"), + (0x2DA7, "X"), + (0x2DA8, "V"), + (0x2DAF, "X"), + (0x2DB0, "V"), + (0x2DB7, "X"), + (0x2DB8, "V"), + (0x2DBF, "X"), + (0x2DC0, "V"), + (0x2DC7, "X"), + (0x2DC8, "V"), + (0x2DCF, "X"), + (0x2DD0, "V"), + (0x2DD7, "X"), + (0x2DD8, "V"), + (0x2DDF, "X"), + (0x2DE0, "V"), + (0x2E5E, "X"), + ] + + +def _seg_27() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2E80, "V"), + (0x2E9A, "X"), + (0x2E9B, "V"), + (0x2E9F, "M", "母"), + (0x2EA0, "V"), + (0x2EF3, "M", "龟"), + (0x2EF4, "X"), + (0x2F00, "M", "一"), + (0x2F01, "M", "丨"), + (0x2F02, "M", "丶"), + (0x2F03, "M", "丿"), + (0x2F04, "M", "乙"), + (0x2F05, "M", "亅"), + (0x2F06, "M", "二"), + (0x2F07, "M", "亠"), + (0x2F08, "M", "人"), + (0x2F09, "M", "儿"), + (0x2F0A, "M", "入"), + (0x2F0B, "M", "八"), + (0x2F0C, "M", "冂"), + (0x2F0D, "M", "冖"), + (0x2F0E, "M", "冫"), + (0x2F0F, "M", "几"), + (0x2F10, "M", "凵"), + (0x2F11, "M", "刀"), + (0x2F12, "M", "力"), + (0x2F13, "M", "勹"), + (0x2F14, "M", "匕"), + (0x2F15, "M", "匚"), + (0x2F16, "M", "匸"), + (0x2F17, "M", "十"), + (0x2F18, "M", "卜"), + (0x2F19, "M", "卩"), + (0x2F1A, "M", "厂"), + (0x2F1B, "M", "厶"), + (0x2F1C, "M", "又"), + (0x2F1D, "M", "口"), + (0x2F1E, "M", "囗"), + (0x2F1F, "M", "土"), + (0x2F20, "M", "士"), + (0x2F21, "M", "夂"), + (0x2F22, "M", "夊"), + (0x2F23, "M", "夕"), + (0x2F24, "M", "大"), + (0x2F25, "M", "女"), + (0x2F26, "M", "子"), + (0x2F27, "M", "宀"), + (0x2F28, "M", "寸"), + (0x2F29, "M", "小"), + (0x2F2A, "M", "尢"), + (0x2F2B, "M", "尸"), + (0x2F2C, "M", "屮"), + (0x2F2D, "M", "山"), + (0x2F2E, "M", "巛"), + (0x2F2F, "M", "工"), + (0x2F30, "M", "己"), + (0x2F31, "M", "巾"), + (0x2F32, "M", "干"), + (0x2F33, "M", "幺"), + (0x2F34, "M", "广"), + (0x2F35, "M", "廴"), + (0x2F36, "M", "廾"), + (0x2F37, "M", "弋"), + (0x2F38, "M", "弓"), + (0x2F39, "M", "彐"), + (0x2F3A, "M", "彡"), + (0x2F3B, "M", "彳"), + (0x2F3C, "M", "心"), + (0x2F3D, "M", "戈"), + (0x2F3E, "M", "戶"), + (0x2F3F, "M", "手"), + (0x2F40, "M", "支"), + (0x2F41, "M", "攴"), + (0x2F42, "M", "文"), + (0x2F43, "M", "斗"), + (0x2F44, "M", "斤"), + (0x2F45, "M", "方"), + (0x2F46, "M", "无"), + (0x2F47, "M", "日"), + (0x2F48, "M", "曰"), + (0x2F49, "M", "月"), + (0x2F4A, "M", "木"), + (0x2F4B, "M", "欠"), + (0x2F4C, "M", "止"), + (0x2F4D, "M", "歹"), + (0x2F4E, "M", "殳"), + (0x2F4F, "M", "毋"), + (0x2F50, "M", "比"), + (0x2F51, "M", "毛"), + (0x2F52, "M", "氏"), + (0x2F53, "M", "气"), + (0x2F54, "M", "水"), + (0x2F55, "M", "火"), + (0x2F56, "M", "爪"), + (0x2F57, "M", "父"), + (0x2F58, "M", "爻"), + (0x2F59, "M", "爿"), + (0x2F5A, "M", "片"), + (0x2F5B, "M", "牙"), + (0x2F5C, "M", "牛"), + ] + + +def _seg_28() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F5D, "M", "犬"), + (0x2F5E, "M", "玄"), + (0x2F5F, "M", "玉"), + (0x2F60, "M", "瓜"), + (0x2F61, "M", "瓦"), + (0x2F62, "M", "甘"), + (0x2F63, "M", "生"), + (0x2F64, "M", "用"), + (0x2F65, "M", "田"), + (0x2F66, "M", "疋"), + (0x2F67, "M", "疒"), + (0x2F68, "M", "癶"), + (0x2F69, "M", "白"), + (0x2F6A, "M", "皮"), + (0x2F6B, "M", "皿"), + (0x2F6C, "M", "目"), + (0x2F6D, "M", "矛"), + (0x2F6E, "M", "矢"), + (0x2F6F, "M", "石"), + (0x2F70, "M", "示"), + (0x2F71, "M", "禸"), + (0x2F72, "M", "禾"), + (0x2F73, "M", "穴"), + (0x2F74, "M", "立"), + (0x2F75, "M", "竹"), + (0x2F76, "M", "米"), + (0x2F77, "M", "糸"), + (0x2F78, "M", "缶"), + (0x2F79, "M", "网"), + (0x2F7A, "M", "羊"), + (0x2F7B, "M", "羽"), + (0x2F7C, "M", "老"), + (0x2F7D, "M", "而"), + (0x2F7E, "M", "耒"), + (0x2F7F, "M", "耳"), + (0x2F80, "M", "聿"), + (0x2F81, "M", "肉"), + (0x2F82, "M", "臣"), + (0x2F83, "M", "自"), + (0x2F84, "M", "至"), + (0x2F85, "M", "臼"), + (0x2F86, "M", "舌"), + (0x2F87, "M", "舛"), + (0x2F88, "M", "舟"), + (0x2F89, "M", "艮"), + (0x2F8A, "M", "色"), + (0x2F8B, "M", "艸"), + (0x2F8C, "M", "虍"), + (0x2F8D, "M", "虫"), + (0x2F8E, "M", "血"), + (0x2F8F, "M", "行"), + (0x2F90, "M", "衣"), + (0x2F91, "M", "襾"), + (0x2F92, "M", "見"), + (0x2F93, "M", "角"), + (0x2F94, "M", "言"), + (0x2F95, "M", "谷"), + (0x2F96, "M", "豆"), + (0x2F97, "M", "豕"), + (0x2F98, "M", "豸"), + (0x2F99, "M", "貝"), + (0x2F9A, "M", "赤"), + (0x2F9B, "M", "走"), + (0x2F9C, "M", "足"), + (0x2F9D, "M", "身"), + (0x2F9E, "M", "車"), + (0x2F9F, "M", "辛"), + (0x2FA0, "M", "辰"), + (0x2FA1, "M", "辵"), + (0x2FA2, "M", "邑"), + (0x2FA3, "M", "酉"), + (0x2FA4, "M", "釆"), + (0x2FA5, "M", "里"), + (0x2FA6, "M", "金"), + (0x2FA7, "M", "長"), + (0x2FA8, "M", "門"), + (0x2FA9, "M", "阜"), + (0x2FAA, "M", "隶"), + (0x2FAB, "M", "隹"), + (0x2FAC, "M", "雨"), + (0x2FAD, "M", "靑"), + (0x2FAE, "M", "非"), + (0x2FAF, "M", "面"), + (0x2FB0, "M", "革"), + (0x2FB1, "M", "韋"), + (0x2FB2, "M", "韭"), + (0x2FB3, "M", "音"), + (0x2FB4, "M", "頁"), + (0x2FB5, "M", "風"), + (0x2FB6, "M", "飛"), + (0x2FB7, "M", "食"), + (0x2FB8, "M", "首"), + (0x2FB9, "M", "香"), + (0x2FBA, "M", "馬"), + (0x2FBB, "M", "骨"), + (0x2FBC, "M", "高"), + (0x2FBD, "M", "髟"), + (0x2FBE, "M", "鬥"), + (0x2FBF, "M", "鬯"), + (0x2FC0, "M", "鬲"), + ] + + +def _seg_29() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2FC1, "M", "鬼"), + (0x2FC2, "M", "魚"), + (0x2FC3, "M", "鳥"), + (0x2FC4, "M", "鹵"), + (0x2FC5, "M", "鹿"), + (0x2FC6, "M", "麥"), + (0x2FC7, "M", "麻"), + (0x2FC8, "M", "黃"), + (0x2FC9, "M", "黍"), + (0x2FCA, "M", "黑"), + (0x2FCB, "M", "黹"), + (0x2FCC, "M", "黽"), + (0x2FCD, "M", "鼎"), + (0x2FCE, "M", "鼓"), + (0x2FCF, "M", "鼠"), + (0x2FD0, "M", "鼻"), + (0x2FD1, "M", "齊"), + (0x2FD2, "M", "齒"), + (0x2FD3, "M", "龍"), + (0x2FD4, "M", "龜"), + (0x2FD5, "M", "龠"), + (0x2FD6, "X"), + (0x3000, "M", " "), + (0x3001, "V"), + (0x3002, "M", "."), + (0x3003, "V"), + (0x3036, "M", "〒"), + (0x3037, "V"), + (0x3038, "M", "十"), + (0x3039, "M", "卄"), + (0x303A, "M", "卅"), + (0x303B, "V"), + (0x3040, "X"), + (0x3041, "V"), + (0x3097, "X"), + (0x3099, "V"), + (0x309B, "M", " ゙"), + (0x309C, "M", " ゚"), + (0x309D, "V"), + (0x309F, "M", "より"), + (0x30A0, "V"), + (0x30FF, "M", "コト"), + (0x3100, "X"), + (0x3105, "V"), + (0x3130, "X"), + (0x3131, "M", "ᄀ"), + (0x3132, "M", "ᄁ"), + (0x3133, "M", "ᆪ"), + (0x3134, "M", "ᄂ"), + (0x3135, "M", "ᆬ"), + (0x3136, "M", "ᆭ"), + (0x3137, "M", "ᄃ"), + (0x3138, "M", "ᄄ"), + (0x3139, "M", "ᄅ"), + (0x313A, "M", "ᆰ"), + (0x313B, "M", "ᆱ"), + (0x313C, "M", "ᆲ"), + (0x313D, "M", "ᆳ"), + (0x313E, "M", "ᆴ"), + (0x313F, "M", "ᆵ"), + (0x3140, "M", "ᄚ"), + (0x3141, "M", "ᄆ"), + (0x3142, "M", "ᄇ"), + (0x3143, "M", "ᄈ"), + (0x3144, "M", "ᄡ"), + (0x3145, "M", "ᄉ"), + (0x3146, "M", "ᄊ"), + (0x3147, "M", "ᄋ"), + (0x3148, "M", "ᄌ"), + (0x3149, "M", "ᄍ"), + (0x314A, "M", "ᄎ"), + (0x314B, "M", "ᄏ"), + (0x314C, "M", "ᄐ"), + (0x314D, "M", "ᄑ"), + (0x314E, "M", "ᄒ"), + (0x314F, "M", "ᅡ"), + (0x3150, "M", "ᅢ"), + (0x3151, "M", "ᅣ"), + (0x3152, "M", "ᅤ"), + (0x3153, "M", "ᅥ"), + (0x3154, "M", "ᅦ"), + (0x3155, "M", "ᅧ"), + (0x3156, "M", "ᅨ"), + (0x3157, "M", "ᅩ"), + (0x3158, "M", "ᅪ"), + (0x3159, "M", "ᅫ"), + (0x315A, "M", "ᅬ"), + (0x315B, "M", "ᅭ"), + (0x315C, "M", "ᅮ"), + (0x315D, "M", "ᅯ"), + (0x315E, "M", "ᅰ"), + (0x315F, "M", "ᅱ"), + (0x3160, "M", "ᅲ"), + (0x3161, "M", "ᅳ"), + (0x3162, "M", "ᅴ"), + (0x3163, "M", "ᅵ"), + (0x3164, "I"), + (0x3165, "M", "ᄔ"), + (0x3166, "M", "ᄕ"), + (0x3167, "M", "ᇇ"), + ] + + +def _seg_30() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x3168, "M", "ᇈ"), + (0x3169, "M", "ᇌ"), + (0x316A, "M", "ᇎ"), + (0x316B, "M", "ᇓ"), + (0x316C, "M", "ᇗ"), + (0x316D, "M", "ᇙ"), + (0x316E, "M", "ᄜ"), + (0x316F, "M", "ᇝ"), + (0x3170, "M", "ᇟ"), + (0x3171, "M", "ᄝ"), + (0x3172, "M", "ᄞ"), + (0x3173, "M", "ᄠ"), + (0x3174, "M", "ᄢ"), + (0x3175, "M", "ᄣ"), + (0x3176, "M", "ᄧ"), + (0x3177, "M", "ᄩ"), + (0x3178, "M", "ᄫ"), + (0x3179, "M", "ᄬ"), + (0x317A, "M", "ᄭ"), + (0x317B, "M", "ᄮ"), + (0x317C, "M", "ᄯ"), + (0x317D, "M", "ᄲ"), + (0x317E, "M", "ᄶ"), + (0x317F, "M", "ᅀ"), + (0x3180, "M", "ᅇ"), + (0x3181, "M", "ᅌ"), + (0x3182, "M", "ᇱ"), + (0x3183, "M", "ᇲ"), + (0x3184, "M", "ᅗ"), + (0x3185, "M", "ᅘ"), + (0x3186, "M", "ᅙ"), + (0x3187, "M", "ᆄ"), + (0x3188, "M", "ᆅ"), + (0x3189, "M", "ᆈ"), + (0x318A, "M", "ᆑ"), + (0x318B, "M", "ᆒ"), + (0x318C, "M", "ᆔ"), + (0x318D, "M", "ᆞ"), + (0x318E, "M", "ᆡ"), + (0x318F, "X"), + (0x3190, "V"), + (0x3192, "M", "一"), + (0x3193, "M", "二"), + (0x3194, "M", "三"), + (0x3195, "M", "四"), + (0x3196, "M", "上"), + (0x3197, "M", "中"), + (0x3198, "M", "下"), + (0x3199, "M", "甲"), + (0x319A, "M", "乙"), + (0x319B, "M", "丙"), + (0x319C, "M", "丁"), + (0x319D, "M", "天"), + (0x319E, "M", "地"), + (0x319F, "M", "人"), + (0x31A0, "V"), + (0x31E6, "X"), + (0x31F0, "V"), + (0x3200, "M", "(ᄀ)"), + (0x3201, "M", "(ᄂ)"), + (0x3202, "M", "(ᄃ)"), + (0x3203, "M", "(ᄅ)"), + (0x3204, "M", "(ᄆ)"), + (0x3205, "M", "(ᄇ)"), + (0x3206, "M", "(ᄉ)"), + (0x3207, "M", "(ᄋ)"), + (0x3208, "M", "(ᄌ)"), + (0x3209, "M", "(ᄎ)"), + (0x320A, "M", "(ᄏ)"), + (0x320B, "M", "(ᄐ)"), + (0x320C, "M", "(ᄑ)"), + (0x320D, "M", "(ᄒ)"), + (0x320E, "M", "(가)"), + (0x320F, "M", "(나)"), + (0x3210, "M", "(다)"), + (0x3211, "M", "(라)"), + (0x3212, "M", "(마)"), + (0x3213, "M", "(바)"), + (0x3214, "M", "(사)"), + (0x3215, "M", "(아)"), + (0x3216, "M", "(자)"), + (0x3217, "M", "(차)"), + (0x3218, "M", "(카)"), + (0x3219, "M", "(타)"), + (0x321A, "M", "(파)"), + (0x321B, "M", "(하)"), + (0x321C, "M", "(주)"), + (0x321D, "M", "(오전)"), + (0x321E, "M", "(오후)"), + (0x321F, "X"), + (0x3220, "M", "(一)"), + (0x3221, "M", "(二)"), + (0x3222, "M", "(三)"), + (0x3223, "M", "(四)"), + (0x3224, "M", "(五)"), + (0x3225, "M", "(六)"), + (0x3226, "M", "(七)"), + (0x3227, "M", "(八)"), + (0x3228, "M", "(九)"), + (0x3229, "M", "(十)"), + ] + + +def _seg_31() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x322A, "M", "(月)"), + (0x322B, "M", "(火)"), + (0x322C, "M", "(水)"), + (0x322D, "M", "(木)"), + (0x322E, "M", "(金)"), + (0x322F, "M", "(土)"), + (0x3230, "M", "(日)"), + (0x3231, "M", "(株)"), + (0x3232, "M", "(有)"), + (0x3233, "M", "(社)"), + (0x3234, "M", "(名)"), + (0x3235, "M", "(特)"), + (0x3236, "M", "(財)"), + (0x3237, "M", "(祝)"), + (0x3238, "M", "(労)"), + (0x3239, "M", "(代)"), + (0x323A, "M", "(呼)"), + (0x323B, "M", "(学)"), + (0x323C, "M", "(監)"), + (0x323D, "M", "(企)"), + (0x323E, "M", "(資)"), + (0x323F, "M", "(協)"), + (0x3240, "M", "(祭)"), + (0x3241, "M", "(休)"), + (0x3242, "M", "(自)"), + (0x3243, "M", "(至)"), + (0x3244, "M", "問"), + (0x3245, "M", "幼"), + (0x3246, "M", "文"), + (0x3247, "M", "箏"), + (0x3248, "V"), + (0x3250, "M", "pte"), + (0x3251, "M", "21"), + (0x3252, "M", "22"), + (0x3253, "M", "23"), + (0x3254, "M", "24"), + (0x3255, "M", "25"), + (0x3256, "M", "26"), + (0x3257, "M", "27"), + (0x3258, "M", "28"), + (0x3259, "M", "29"), + (0x325A, "M", "30"), + (0x325B, "M", "31"), + (0x325C, "M", "32"), + (0x325D, "M", "33"), + (0x325E, "M", "34"), + (0x325F, "M", "35"), + (0x3260, "M", "ᄀ"), + (0x3261, "M", "ᄂ"), + (0x3262, "M", "ᄃ"), + (0x3263, "M", "ᄅ"), + (0x3264, "M", "ᄆ"), + (0x3265, "M", "ᄇ"), + (0x3266, "M", "ᄉ"), + (0x3267, "M", "ᄋ"), + (0x3268, "M", "ᄌ"), + (0x3269, "M", "ᄎ"), + (0x326A, "M", "ᄏ"), + (0x326B, "M", "ᄐ"), + (0x326C, "M", "ᄑ"), + (0x326D, "M", "ᄒ"), + (0x326E, "M", "가"), + (0x326F, "M", "나"), + (0x3270, "M", "다"), + (0x3271, "M", "라"), + (0x3272, "M", "마"), + (0x3273, "M", "바"), + (0x3274, "M", "사"), + (0x3275, "M", "아"), + (0x3276, "M", "자"), + (0x3277, "M", "차"), + (0x3278, "M", "카"), + (0x3279, "M", "타"), + (0x327A, "M", "파"), + (0x327B, "M", "하"), + (0x327C, "M", "참고"), + (0x327D, "M", "주의"), + (0x327E, "M", "우"), + (0x327F, "V"), + (0x3280, "M", "一"), + (0x3281, "M", "二"), + (0x3282, "M", "三"), + (0x3283, "M", "四"), + (0x3284, "M", "五"), + (0x3285, "M", "六"), + (0x3286, "M", "七"), + (0x3287, "M", "八"), + (0x3288, "M", "九"), + (0x3289, "M", "十"), + (0x328A, "M", "月"), + (0x328B, "M", "火"), + (0x328C, "M", "水"), + (0x328D, "M", "木"), + (0x328E, "M", "金"), + (0x328F, "M", "土"), + (0x3290, "M", "日"), + (0x3291, "M", "株"), + (0x3292, "M", "有"), + (0x3293, "M", "社"), + (0x3294, "M", "名"), + ] + + +def _seg_32() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x3295, "M", "特"), + (0x3296, "M", "財"), + (0x3297, "M", "祝"), + (0x3298, "M", "労"), + (0x3299, "M", "秘"), + (0x329A, "M", "男"), + (0x329B, "M", "女"), + (0x329C, "M", "適"), + (0x329D, "M", "優"), + (0x329E, "M", "印"), + (0x329F, "M", "注"), + (0x32A0, "M", "項"), + (0x32A1, "M", "休"), + (0x32A2, "M", "写"), + (0x32A3, "M", "正"), + (0x32A4, "M", "上"), + (0x32A5, "M", "中"), + (0x32A6, "M", "下"), + (0x32A7, "M", "左"), + (0x32A8, "M", "右"), + (0x32A9, "M", "医"), + (0x32AA, "M", "宗"), + (0x32AB, "M", "学"), + (0x32AC, "M", "監"), + (0x32AD, "M", "企"), + (0x32AE, "M", "資"), + (0x32AF, "M", "協"), + (0x32B0, "M", "夜"), + (0x32B1, "M", "36"), + (0x32B2, "M", "37"), + (0x32B3, "M", "38"), + (0x32B4, "M", "39"), + (0x32B5, "M", "40"), + (0x32B6, "M", "41"), + (0x32B7, "M", "42"), + (0x32B8, "M", "43"), + (0x32B9, "M", "44"), + (0x32BA, "M", "45"), + (0x32BB, "M", "46"), + (0x32BC, "M", "47"), + (0x32BD, "M", "48"), + (0x32BE, "M", "49"), + (0x32BF, "M", "50"), + (0x32C0, "M", "1月"), + (0x32C1, "M", "2月"), + (0x32C2, "M", "3月"), + (0x32C3, "M", "4月"), + (0x32C4, "M", "5月"), + (0x32C5, "M", "6月"), + (0x32C6, "M", "7月"), + (0x32C7, "M", "8月"), + (0x32C8, "M", "9月"), + (0x32C9, "M", "10月"), + (0x32CA, "M", "11月"), + (0x32CB, "M", "12月"), + (0x32CC, "M", "hg"), + (0x32CD, "M", "erg"), + (0x32CE, "M", "ev"), + (0x32CF, "M", "ltd"), + (0x32D0, "M", "ア"), + (0x32D1, "M", "イ"), + (0x32D2, "M", "ウ"), + (0x32D3, "M", "エ"), + (0x32D4, "M", "オ"), + (0x32D5, "M", "カ"), + (0x32D6, "M", "キ"), + (0x32D7, "M", "ク"), + (0x32D8, "M", "ケ"), + (0x32D9, "M", "コ"), + (0x32DA, "M", "サ"), + (0x32DB, "M", "シ"), + (0x32DC, "M", "ス"), + (0x32DD, "M", "セ"), + (0x32DE, "M", "ソ"), + (0x32DF, "M", "タ"), + (0x32E0, "M", "チ"), + (0x32E1, "M", "ツ"), + (0x32E2, "M", "テ"), + (0x32E3, "M", "ト"), + (0x32E4, "M", "ナ"), + (0x32E5, "M", "ニ"), + (0x32E6, "M", "ヌ"), + (0x32E7, "M", "ネ"), + (0x32E8, "M", "ノ"), + (0x32E9, "M", "ハ"), + (0x32EA, "M", "ヒ"), + (0x32EB, "M", "フ"), + (0x32EC, "M", "ヘ"), + (0x32ED, "M", "ホ"), + (0x32EE, "M", "マ"), + (0x32EF, "M", "ミ"), + (0x32F0, "M", "ム"), + (0x32F1, "M", "メ"), + (0x32F2, "M", "モ"), + (0x32F3, "M", "ヤ"), + (0x32F4, "M", "ユ"), + (0x32F5, "M", "ヨ"), + (0x32F6, "M", "ラ"), + (0x32F7, "M", "リ"), + (0x32F8, "M", "ル"), + ] + + +def _seg_33() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x32F9, "M", "レ"), + (0x32FA, "M", "ロ"), + (0x32FB, "M", "ワ"), + (0x32FC, "M", "ヰ"), + (0x32FD, "M", "ヱ"), + (0x32FE, "M", "ヲ"), + (0x32FF, "M", "令和"), + (0x3300, "M", "アパート"), + (0x3301, "M", "アルファ"), + (0x3302, "M", "アンペア"), + (0x3303, "M", "アール"), + (0x3304, "M", "イニング"), + (0x3305, "M", "インチ"), + (0x3306, "M", "ウォン"), + (0x3307, "M", "エスクード"), + (0x3308, "M", "エーカー"), + (0x3309, "M", "オンス"), + (0x330A, "M", "オーム"), + (0x330B, "M", "カイリ"), + (0x330C, "M", "カラット"), + (0x330D, "M", "カロリー"), + (0x330E, "M", "ガロン"), + (0x330F, "M", "ガンマ"), + (0x3310, "M", "ギガ"), + (0x3311, "M", "ギニー"), + (0x3312, "M", "キュリー"), + (0x3313, "M", "ギルダー"), + (0x3314, "M", "キロ"), + (0x3315, "M", "キログラム"), + (0x3316, "M", "キロメートル"), + (0x3317, "M", "キロワット"), + (0x3318, "M", "グラム"), + (0x3319, "M", "グラムトン"), + (0x331A, "M", "クルゼイロ"), + (0x331B, "M", "クローネ"), + (0x331C, "M", "ケース"), + (0x331D, "M", "コルナ"), + (0x331E, "M", "コーポ"), + (0x331F, "M", "サイクル"), + (0x3320, "M", "サンチーム"), + (0x3321, "M", "シリング"), + (0x3322, "M", "センチ"), + (0x3323, "M", "セント"), + (0x3324, "M", "ダース"), + (0x3325, "M", "デシ"), + (0x3326, "M", "ドル"), + (0x3327, "M", "トン"), + (0x3328, "M", "ナノ"), + (0x3329, "M", "ノット"), + (0x332A, "M", "ハイツ"), + (0x332B, "M", "パーセント"), + (0x332C, "M", "パーツ"), + (0x332D, "M", "バーレル"), + (0x332E, "M", "ピアストル"), + (0x332F, "M", "ピクル"), + (0x3330, "M", "ピコ"), + (0x3331, "M", "ビル"), + (0x3332, "M", "ファラッド"), + (0x3333, "M", "フィート"), + (0x3334, "M", "ブッシェル"), + (0x3335, "M", "フラン"), + (0x3336, "M", "ヘクタール"), + (0x3337, "M", "ペソ"), + (0x3338, "M", "ペニヒ"), + (0x3339, "M", "ヘルツ"), + (0x333A, "M", "ペンス"), + (0x333B, "M", "ページ"), + (0x333C, "M", "ベータ"), + (0x333D, "M", "ポイント"), + (0x333E, "M", "ボルト"), + (0x333F, "M", "ホン"), + (0x3340, "M", "ポンド"), + (0x3341, "M", "ホール"), + (0x3342, "M", "ホーン"), + (0x3343, "M", "マイクロ"), + (0x3344, "M", "マイル"), + (0x3345, "M", "マッハ"), + (0x3346, "M", "マルク"), + (0x3347, "M", "マンション"), + (0x3348, "M", "ミクロン"), + (0x3349, "M", "ミリ"), + (0x334A, "M", "ミリバール"), + (0x334B, "M", "メガ"), + (0x334C, "M", "メガトン"), + (0x334D, "M", "メートル"), + (0x334E, "M", "ヤード"), + (0x334F, "M", "ヤール"), + (0x3350, "M", "ユアン"), + (0x3351, "M", "リットル"), + (0x3352, "M", "リラ"), + (0x3353, "M", "ルピー"), + (0x3354, "M", "ルーブル"), + (0x3355, "M", "レム"), + (0x3356, "M", "レントゲン"), + (0x3357, "M", "ワット"), + (0x3358, "M", "0点"), + (0x3359, "M", "1点"), + (0x335A, "M", "2点"), + (0x335B, "M", "3点"), + (0x335C, "M", "4点"), + ] + + +def _seg_34() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x335D, "M", "5点"), + (0x335E, "M", "6点"), + (0x335F, "M", "7点"), + (0x3360, "M", "8点"), + (0x3361, "M", "9点"), + (0x3362, "M", "10点"), + (0x3363, "M", "11点"), + (0x3364, "M", "12点"), + (0x3365, "M", "13点"), + (0x3366, "M", "14点"), + (0x3367, "M", "15点"), + (0x3368, "M", "16点"), + (0x3369, "M", "17点"), + (0x336A, "M", "18点"), + (0x336B, "M", "19点"), + (0x336C, "M", "20点"), + (0x336D, "M", "21点"), + (0x336E, "M", "22点"), + (0x336F, "M", "23点"), + (0x3370, "M", "24点"), + (0x3371, "M", "hpa"), + (0x3372, "M", "da"), + (0x3373, "M", "au"), + (0x3374, "M", "bar"), + (0x3375, "M", "ov"), + (0x3376, "M", "pc"), + (0x3377, "M", "dm"), + (0x3378, "M", "dm2"), + (0x3379, "M", "dm3"), + (0x337A, "M", "iu"), + (0x337B, "M", "平成"), + (0x337C, "M", "昭和"), + (0x337D, "M", "大正"), + (0x337E, "M", "明治"), + (0x337F, "M", "株式会社"), + (0x3380, "M", "pa"), + (0x3381, "M", "na"), + (0x3382, "M", "μa"), + (0x3383, "M", "ma"), + (0x3384, "M", "ka"), + (0x3385, "M", "kb"), + (0x3386, "M", "mb"), + (0x3387, "M", "gb"), + (0x3388, "M", "cal"), + (0x3389, "M", "kcal"), + (0x338A, "M", "pf"), + (0x338B, "M", "nf"), + (0x338C, "M", "μf"), + (0x338D, "M", "μg"), + (0x338E, "M", "mg"), + (0x338F, "M", "kg"), + (0x3390, "M", "hz"), + (0x3391, "M", "khz"), + (0x3392, "M", "mhz"), + (0x3393, "M", "ghz"), + (0x3394, "M", "thz"), + (0x3395, "M", "μl"), + (0x3396, "M", "ml"), + (0x3397, "M", "dl"), + (0x3398, "M", "kl"), + (0x3399, "M", "fm"), + (0x339A, "M", "nm"), + (0x339B, "M", "μm"), + (0x339C, "M", "mm"), + (0x339D, "M", "cm"), + (0x339E, "M", "km"), + (0x339F, "M", "mm2"), + (0x33A0, "M", "cm2"), + (0x33A1, "M", "m2"), + (0x33A2, "M", "km2"), + (0x33A3, "M", "mm3"), + (0x33A4, "M", "cm3"), + (0x33A5, "M", "m3"), + (0x33A6, "M", "km3"), + (0x33A7, "M", "m∕s"), + (0x33A8, "M", "m∕s2"), + (0x33A9, "M", "pa"), + (0x33AA, "M", "kpa"), + (0x33AB, "M", "mpa"), + (0x33AC, "M", "gpa"), + (0x33AD, "M", "rad"), + (0x33AE, "M", "rad∕s"), + (0x33AF, "M", "rad∕s2"), + (0x33B0, "M", "ps"), + (0x33B1, "M", "ns"), + (0x33B2, "M", "μs"), + (0x33B3, "M", "ms"), + (0x33B4, "M", "pv"), + (0x33B5, "M", "nv"), + (0x33B6, "M", "μv"), + (0x33B7, "M", "mv"), + (0x33B8, "M", "kv"), + (0x33B9, "M", "mv"), + (0x33BA, "M", "pw"), + (0x33BB, "M", "nw"), + (0x33BC, "M", "μw"), + (0x33BD, "M", "mw"), + (0x33BE, "M", "kw"), + (0x33BF, "M", "mw"), + (0x33C0, "M", "kω"), + ] + + +def _seg_35() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x33C1, "M", "mω"), + (0x33C2, "X"), + (0x33C3, "M", "bq"), + (0x33C4, "M", "cc"), + (0x33C5, "M", "cd"), + (0x33C6, "M", "c∕kg"), + (0x33C7, "X"), + (0x33C8, "M", "db"), + (0x33C9, "M", "gy"), + (0x33CA, "M", "ha"), + (0x33CB, "M", "hp"), + (0x33CC, "M", "in"), + (0x33CD, "M", "kk"), + (0x33CE, "M", "km"), + (0x33CF, "M", "kt"), + (0x33D0, "M", "lm"), + (0x33D1, "M", "ln"), + (0x33D2, "M", "log"), + (0x33D3, "M", "lx"), + (0x33D4, "M", "mb"), + (0x33D5, "M", "mil"), + (0x33D6, "M", "mol"), + (0x33D7, "M", "ph"), + (0x33D8, "X"), + (0x33D9, "M", "ppm"), + (0x33DA, "M", "pr"), + (0x33DB, "M", "sr"), + (0x33DC, "M", "sv"), + (0x33DD, "M", "wb"), + (0x33DE, "M", "v∕m"), + (0x33DF, "M", "a∕m"), + (0x33E0, "M", "1日"), + (0x33E1, "M", "2日"), + (0x33E2, "M", "3日"), + (0x33E3, "M", "4日"), + (0x33E4, "M", "5日"), + (0x33E5, "M", "6日"), + (0x33E6, "M", "7日"), + (0x33E7, "M", "8日"), + (0x33E8, "M", "9日"), + (0x33E9, "M", "10日"), + (0x33EA, "M", "11日"), + (0x33EB, "M", "12日"), + (0x33EC, "M", "13日"), + (0x33ED, "M", "14日"), + (0x33EE, "M", "15日"), + (0x33EF, "M", "16日"), + (0x33F0, "M", "17日"), + (0x33F1, "M", "18日"), + (0x33F2, "M", "19日"), + (0x33F3, "M", "20日"), + (0x33F4, "M", "21日"), + (0x33F5, "M", "22日"), + (0x33F6, "M", "23日"), + (0x33F7, "M", "24日"), + (0x33F8, "M", "25日"), + (0x33F9, "M", "26日"), + (0x33FA, "M", "27日"), + (0x33FB, "M", "28日"), + (0x33FC, "M", "29日"), + (0x33FD, "M", "30日"), + (0x33FE, "M", "31日"), + (0x33FF, "M", "gal"), + (0x3400, "V"), + (0xA48D, "X"), + (0xA490, "V"), + (0xA4C7, "X"), + (0xA4D0, "V"), + (0xA62C, "X"), + (0xA640, "M", "ꙁ"), + (0xA641, "V"), + (0xA642, "M", "ꙃ"), + (0xA643, "V"), + (0xA644, "M", "ꙅ"), + (0xA645, "V"), + (0xA646, "M", "ꙇ"), + (0xA647, "V"), + (0xA648, "M", "ꙉ"), + (0xA649, "V"), + (0xA64A, "M", "ꙋ"), + (0xA64B, "V"), + (0xA64C, "M", "ꙍ"), + (0xA64D, "V"), + (0xA64E, "M", "ꙏ"), + (0xA64F, "V"), + (0xA650, "M", "ꙑ"), + (0xA651, "V"), + (0xA652, "M", "ꙓ"), + (0xA653, "V"), + (0xA654, "M", "ꙕ"), + (0xA655, "V"), + (0xA656, "M", "ꙗ"), + (0xA657, "V"), + (0xA658, "M", "ꙙ"), + (0xA659, "V"), + (0xA65A, "M", "ꙛ"), + (0xA65B, "V"), + (0xA65C, "M", "ꙝ"), + (0xA65D, "V"), + (0xA65E, "M", "ꙟ"), + ] + + +def _seg_36() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA65F, "V"), + (0xA660, "M", "ꙡ"), + (0xA661, "V"), + (0xA662, "M", "ꙣ"), + (0xA663, "V"), + (0xA664, "M", "ꙥ"), + (0xA665, "V"), + (0xA666, "M", "ꙧ"), + (0xA667, "V"), + (0xA668, "M", "ꙩ"), + (0xA669, "V"), + (0xA66A, "M", "ꙫ"), + (0xA66B, "V"), + (0xA66C, "M", "ꙭ"), + (0xA66D, "V"), + (0xA680, "M", "ꚁ"), + (0xA681, "V"), + (0xA682, "M", "ꚃ"), + (0xA683, "V"), + (0xA684, "M", "ꚅ"), + (0xA685, "V"), + (0xA686, "M", "ꚇ"), + (0xA687, "V"), + (0xA688, "M", "ꚉ"), + (0xA689, "V"), + (0xA68A, "M", "ꚋ"), + (0xA68B, "V"), + (0xA68C, "M", "ꚍ"), + (0xA68D, "V"), + (0xA68E, "M", "ꚏ"), + (0xA68F, "V"), + (0xA690, "M", "ꚑ"), + (0xA691, "V"), + (0xA692, "M", "ꚓ"), + (0xA693, "V"), + (0xA694, "M", "ꚕ"), + (0xA695, "V"), + (0xA696, "M", "ꚗ"), + (0xA697, "V"), + (0xA698, "M", "ꚙ"), + (0xA699, "V"), + (0xA69A, "M", "ꚛ"), + (0xA69B, "V"), + (0xA69C, "M", "ъ"), + (0xA69D, "M", "ь"), + (0xA69E, "V"), + (0xA6F8, "X"), + (0xA700, "V"), + (0xA722, "M", "ꜣ"), + (0xA723, "V"), + (0xA724, "M", "ꜥ"), + (0xA725, "V"), + (0xA726, "M", "ꜧ"), + (0xA727, "V"), + (0xA728, "M", "ꜩ"), + (0xA729, "V"), + (0xA72A, "M", "ꜫ"), + (0xA72B, "V"), + (0xA72C, "M", "ꜭ"), + (0xA72D, "V"), + (0xA72E, "M", "ꜯ"), + (0xA72F, "V"), + (0xA732, "M", "ꜳ"), + (0xA733, "V"), + (0xA734, "M", "ꜵ"), + (0xA735, "V"), + (0xA736, "M", "ꜷ"), + (0xA737, "V"), + (0xA738, "M", "ꜹ"), + (0xA739, "V"), + (0xA73A, "M", "ꜻ"), + (0xA73B, "V"), + (0xA73C, "M", "ꜽ"), + (0xA73D, "V"), + (0xA73E, "M", "ꜿ"), + (0xA73F, "V"), + (0xA740, "M", "ꝁ"), + (0xA741, "V"), + (0xA742, "M", "ꝃ"), + (0xA743, "V"), + (0xA744, "M", "ꝅ"), + (0xA745, "V"), + (0xA746, "M", "ꝇ"), + (0xA747, "V"), + (0xA748, "M", "ꝉ"), + (0xA749, "V"), + (0xA74A, "M", "ꝋ"), + (0xA74B, "V"), + (0xA74C, "M", "ꝍ"), + (0xA74D, "V"), + (0xA74E, "M", "ꝏ"), + (0xA74F, "V"), + (0xA750, "M", "ꝑ"), + (0xA751, "V"), + (0xA752, "M", "ꝓ"), + (0xA753, "V"), + (0xA754, "M", "ꝕ"), + (0xA755, "V"), + (0xA756, "M", "ꝗ"), + (0xA757, "V"), + ] + + +def _seg_37() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA758, "M", "ꝙ"), + (0xA759, "V"), + (0xA75A, "M", "ꝛ"), + (0xA75B, "V"), + (0xA75C, "M", "ꝝ"), + (0xA75D, "V"), + (0xA75E, "M", "ꝟ"), + (0xA75F, "V"), + (0xA760, "M", "ꝡ"), + (0xA761, "V"), + (0xA762, "M", "ꝣ"), + (0xA763, "V"), + (0xA764, "M", "ꝥ"), + (0xA765, "V"), + (0xA766, "M", "ꝧ"), + (0xA767, "V"), + (0xA768, "M", "ꝩ"), + (0xA769, "V"), + (0xA76A, "M", "ꝫ"), + (0xA76B, "V"), + (0xA76C, "M", "ꝭ"), + (0xA76D, "V"), + (0xA76E, "M", "ꝯ"), + (0xA76F, "V"), + (0xA770, "M", "ꝯ"), + (0xA771, "V"), + (0xA779, "M", "ꝺ"), + (0xA77A, "V"), + (0xA77B, "M", "ꝼ"), + (0xA77C, "V"), + (0xA77D, "M", "ᵹ"), + (0xA77E, "M", "ꝿ"), + (0xA77F, "V"), + (0xA780, "M", "ꞁ"), + (0xA781, "V"), + (0xA782, "M", "ꞃ"), + (0xA783, "V"), + (0xA784, "M", "ꞅ"), + (0xA785, "V"), + (0xA786, "M", "ꞇ"), + (0xA787, "V"), + (0xA78B, "M", "ꞌ"), + (0xA78C, "V"), + (0xA78D, "M", "ɥ"), + (0xA78E, "V"), + (0xA790, "M", "ꞑ"), + (0xA791, "V"), + (0xA792, "M", "ꞓ"), + (0xA793, "V"), + (0xA796, "M", "ꞗ"), + (0xA797, "V"), + (0xA798, "M", "ꞙ"), + (0xA799, "V"), + (0xA79A, "M", "ꞛ"), + (0xA79B, "V"), + (0xA79C, "M", "ꞝ"), + (0xA79D, "V"), + (0xA79E, "M", "ꞟ"), + (0xA79F, "V"), + (0xA7A0, "M", "ꞡ"), + (0xA7A1, "V"), + (0xA7A2, "M", "ꞣ"), + (0xA7A3, "V"), + (0xA7A4, "M", "ꞥ"), + (0xA7A5, "V"), + (0xA7A6, "M", "ꞧ"), + (0xA7A7, "V"), + (0xA7A8, "M", "ꞩ"), + (0xA7A9, "V"), + (0xA7AA, "M", "ɦ"), + (0xA7AB, "M", "ɜ"), + (0xA7AC, "M", "ɡ"), + (0xA7AD, "M", "ɬ"), + (0xA7AE, "M", "ɪ"), + (0xA7AF, "V"), + (0xA7B0, "M", "ʞ"), + (0xA7B1, "M", "ʇ"), + (0xA7B2, "M", "ʝ"), + (0xA7B3, "M", "ꭓ"), + (0xA7B4, "M", "ꞵ"), + (0xA7B5, "V"), + (0xA7B6, "M", "ꞷ"), + (0xA7B7, "V"), + (0xA7B8, "M", "ꞹ"), + (0xA7B9, "V"), + (0xA7BA, "M", "ꞻ"), + (0xA7BB, "V"), + (0xA7BC, "M", "ꞽ"), + (0xA7BD, "V"), + (0xA7BE, "M", "ꞿ"), + (0xA7BF, "V"), + (0xA7C0, "M", "ꟁ"), + (0xA7C1, "V"), + (0xA7C2, "M", "ꟃ"), + (0xA7C3, "V"), + (0xA7C4, "M", "ꞔ"), + (0xA7C5, "M", "ʂ"), + (0xA7C6, "M", "ᶎ"), + (0xA7C7, "M", "ꟈ"), + (0xA7C8, "V"), + ] + + +def _seg_38() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xA7C9, "M", "ꟊ"), + (0xA7CA, "V"), + (0xA7CB, "M", "ɤ"), + (0xA7CC, "M", "ꟍ"), + (0xA7CD, "V"), + (0xA7CE, "X"), + (0xA7D0, "M", "ꟑ"), + (0xA7D1, "V"), + (0xA7D2, "X"), + (0xA7D3, "V"), + (0xA7D4, "X"), + (0xA7D5, "V"), + (0xA7D6, "M", "ꟗ"), + (0xA7D7, "V"), + (0xA7D8, "M", "ꟙ"), + (0xA7D9, "V"), + (0xA7DA, "M", "ꟛ"), + (0xA7DB, "V"), + (0xA7DC, "M", "ƛ"), + (0xA7DD, "X"), + (0xA7F2, "M", "c"), + (0xA7F3, "M", "f"), + (0xA7F4, "M", "q"), + (0xA7F5, "M", "ꟶ"), + (0xA7F6, "V"), + (0xA7F8, "M", "ħ"), + (0xA7F9, "M", "œ"), + (0xA7FA, "V"), + (0xA82D, "X"), + (0xA830, "V"), + (0xA83A, "X"), + (0xA840, "V"), + (0xA878, "X"), + (0xA880, "V"), + (0xA8C6, "X"), + (0xA8CE, "V"), + (0xA8DA, "X"), + (0xA8E0, "V"), + (0xA954, "X"), + (0xA95F, "V"), + (0xA97D, "X"), + (0xA980, "V"), + (0xA9CE, "X"), + (0xA9CF, "V"), + (0xA9DA, "X"), + (0xA9DE, "V"), + (0xA9FF, "X"), + (0xAA00, "V"), + (0xAA37, "X"), + (0xAA40, "V"), + (0xAA4E, "X"), + (0xAA50, "V"), + (0xAA5A, "X"), + (0xAA5C, "V"), + (0xAAC3, "X"), + (0xAADB, "V"), + (0xAAF7, "X"), + (0xAB01, "V"), + (0xAB07, "X"), + (0xAB09, "V"), + (0xAB0F, "X"), + (0xAB11, "V"), + (0xAB17, "X"), + (0xAB20, "V"), + (0xAB27, "X"), + (0xAB28, "V"), + (0xAB2F, "X"), + (0xAB30, "V"), + (0xAB5C, "M", "ꜧ"), + (0xAB5D, "M", "ꬷ"), + (0xAB5E, "M", "ɫ"), + (0xAB5F, "M", "ꭒ"), + (0xAB60, "V"), + (0xAB69, "M", "ʍ"), + (0xAB6A, "V"), + (0xAB6C, "X"), + (0xAB70, "M", "Ꭰ"), + (0xAB71, "M", "Ꭱ"), + (0xAB72, "M", "Ꭲ"), + (0xAB73, "M", "Ꭳ"), + (0xAB74, "M", "Ꭴ"), + (0xAB75, "M", "Ꭵ"), + (0xAB76, "M", "Ꭶ"), + (0xAB77, "M", "Ꭷ"), + (0xAB78, "M", "Ꭸ"), + (0xAB79, "M", "Ꭹ"), + (0xAB7A, "M", "Ꭺ"), + (0xAB7B, "M", "Ꭻ"), + (0xAB7C, "M", "Ꭼ"), + (0xAB7D, "M", "Ꭽ"), + (0xAB7E, "M", "Ꭾ"), + (0xAB7F, "M", "Ꭿ"), + (0xAB80, "M", "Ꮀ"), + (0xAB81, "M", "Ꮁ"), + (0xAB82, "M", "Ꮂ"), + (0xAB83, "M", "Ꮃ"), + (0xAB84, "M", "Ꮄ"), + (0xAB85, "M", "Ꮅ"), + (0xAB86, "M", "Ꮆ"), + (0xAB87, "M", "Ꮇ"), + ] + + +def _seg_39() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xAB88, "M", "Ꮈ"), + (0xAB89, "M", "Ꮉ"), + (0xAB8A, "M", "Ꮊ"), + (0xAB8B, "M", "Ꮋ"), + (0xAB8C, "M", "Ꮌ"), + (0xAB8D, "M", "Ꮍ"), + (0xAB8E, "M", "Ꮎ"), + (0xAB8F, "M", "Ꮏ"), + (0xAB90, "M", "Ꮐ"), + (0xAB91, "M", "Ꮑ"), + (0xAB92, "M", "Ꮒ"), + (0xAB93, "M", "Ꮓ"), + (0xAB94, "M", "Ꮔ"), + (0xAB95, "M", "Ꮕ"), + (0xAB96, "M", "Ꮖ"), + (0xAB97, "M", "Ꮗ"), + (0xAB98, "M", "Ꮘ"), + (0xAB99, "M", "Ꮙ"), + (0xAB9A, "M", "Ꮚ"), + (0xAB9B, "M", "Ꮛ"), + (0xAB9C, "M", "Ꮜ"), + (0xAB9D, "M", "Ꮝ"), + (0xAB9E, "M", "Ꮞ"), + (0xAB9F, "M", "Ꮟ"), + (0xABA0, "M", "Ꮠ"), + (0xABA1, "M", "Ꮡ"), + (0xABA2, "M", "Ꮢ"), + (0xABA3, "M", "Ꮣ"), + (0xABA4, "M", "Ꮤ"), + (0xABA5, "M", "Ꮥ"), + (0xABA6, "M", "Ꮦ"), + (0xABA7, "M", "Ꮧ"), + (0xABA8, "M", "Ꮨ"), + (0xABA9, "M", "Ꮩ"), + (0xABAA, "M", "Ꮪ"), + (0xABAB, "M", "Ꮫ"), + (0xABAC, "M", "Ꮬ"), + (0xABAD, "M", "Ꮭ"), + (0xABAE, "M", "Ꮮ"), + (0xABAF, "M", "Ꮯ"), + (0xABB0, "M", "Ꮰ"), + (0xABB1, "M", "Ꮱ"), + (0xABB2, "M", "Ꮲ"), + (0xABB3, "M", "Ꮳ"), + (0xABB4, "M", "Ꮴ"), + (0xABB5, "M", "Ꮵ"), + (0xABB6, "M", "Ꮶ"), + (0xABB7, "M", "Ꮷ"), + (0xABB8, "M", "Ꮸ"), + (0xABB9, "M", "Ꮹ"), + (0xABBA, "M", "Ꮺ"), + (0xABBB, "M", "Ꮻ"), + (0xABBC, "M", "Ꮼ"), + (0xABBD, "M", "Ꮽ"), + (0xABBE, "M", "Ꮾ"), + (0xABBF, "M", "Ꮿ"), + (0xABC0, "V"), + (0xABEE, "X"), + (0xABF0, "V"), + (0xABFA, "X"), + (0xAC00, "V"), + (0xD7A4, "X"), + (0xD7B0, "V"), + (0xD7C7, "X"), + (0xD7CB, "V"), + (0xD7FC, "X"), + (0xF900, "M", "豈"), + (0xF901, "M", "更"), + (0xF902, "M", "車"), + (0xF903, "M", "賈"), + (0xF904, "M", "滑"), + (0xF905, "M", "串"), + (0xF906, "M", "句"), + (0xF907, "M", "龜"), + (0xF909, "M", "契"), + (0xF90A, "M", "金"), + (0xF90B, "M", "喇"), + (0xF90C, "M", "奈"), + (0xF90D, "M", "懶"), + (0xF90E, "M", "癩"), + (0xF90F, "M", "羅"), + (0xF910, "M", "蘿"), + (0xF911, "M", "螺"), + (0xF912, "M", "裸"), + (0xF913, "M", "邏"), + (0xF914, "M", "樂"), + (0xF915, "M", "洛"), + (0xF916, "M", "烙"), + (0xF917, "M", "珞"), + (0xF918, "M", "落"), + (0xF919, "M", "酪"), + (0xF91A, "M", "駱"), + (0xF91B, "M", "亂"), + (0xF91C, "M", "卵"), + (0xF91D, "M", "欄"), + (0xF91E, "M", "爛"), + (0xF91F, "M", "蘭"), + (0xF920, "M", "鸞"), + (0xF921, "M", "嵐"), + (0xF922, "M", "濫"), + ] + + +def _seg_40() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xF923, "M", "藍"), + (0xF924, "M", "襤"), + (0xF925, "M", "拉"), + (0xF926, "M", "臘"), + (0xF927, "M", "蠟"), + (0xF928, "M", "廊"), + (0xF929, "M", "朗"), + (0xF92A, "M", "浪"), + (0xF92B, "M", "狼"), + (0xF92C, "M", "郎"), + (0xF92D, "M", "來"), + (0xF92E, "M", "冷"), + (0xF92F, "M", "勞"), + (0xF930, "M", "擄"), + (0xF931, "M", "櫓"), + (0xF932, "M", "爐"), + (0xF933, "M", "盧"), + (0xF934, "M", "老"), + (0xF935, "M", "蘆"), + (0xF936, "M", "虜"), + (0xF937, "M", "路"), + (0xF938, "M", "露"), + (0xF939, "M", "魯"), + (0xF93A, "M", "鷺"), + (0xF93B, "M", "碌"), + (0xF93C, "M", "祿"), + (0xF93D, "M", "綠"), + (0xF93E, "M", "菉"), + (0xF93F, "M", "錄"), + (0xF940, "M", "鹿"), + (0xF941, "M", "論"), + (0xF942, "M", "壟"), + (0xF943, "M", "弄"), + (0xF944, "M", "籠"), + (0xF945, "M", "聾"), + (0xF946, "M", "牢"), + (0xF947, "M", "磊"), + (0xF948, "M", "賂"), + (0xF949, "M", "雷"), + (0xF94A, "M", "壘"), + (0xF94B, "M", "屢"), + (0xF94C, "M", "樓"), + (0xF94D, "M", "淚"), + (0xF94E, "M", "漏"), + (0xF94F, "M", "累"), + (0xF950, "M", "縷"), + (0xF951, "M", "陋"), + (0xF952, "M", "勒"), + (0xF953, "M", "肋"), + (0xF954, "M", "凜"), + (0xF955, "M", "凌"), + (0xF956, "M", "稜"), + (0xF957, "M", "綾"), + (0xF958, "M", "菱"), + (0xF959, "M", "陵"), + (0xF95A, "M", "讀"), + (0xF95B, "M", "拏"), + (0xF95C, "M", "樂"), + (0xF95D, "M", "諾"), + (0xF95E, "M", "丹"), + (0xF95F, "M", "寧"), + (0xF960, "M", "怒"), + (0xF961, "M", "率"), + (0xF962, "M", "異"), + (0xF963, "M", "北"), + (0xF964, "M", "磻"), + (0xF965, "M", "便"), + (0xF966, "M", "復"), + (0xF967, "M", "不"), + (0xF968, "M", "泌"), + (0xF969, "M", "數"), + (0xF96A, "M", "索"), + (0xF96B, "M", "參"), + (0xF96C, "M", "塞"), + (0xF96D, "M", "省"), + (0xF96E, "M", "葉"), + (0xF96F, "M", "說"), + (0xF970, "M", "殺"), + (0xF971, "M", "辰"), + (0xF972, "M", "沈"), + (0xF973, "M", "拾"), + (0xF974, "M", "若"), + (0xF975, "M", "掠"), + (0xF976, "M", "略"), + (0xF977, "M", "亮"), + (0xF978, "M", "兩"), + (0xF979, "M", "凉"), + (0xF97A, "M", "梁"), + (0xF97B, "M", "糧"), + (0xF97C, "M", "良"), + (0xF97D, "M", "諒"), + (0xF97E, "M", "量"), + (0xF97F, "M", "勵"), + (0xF980, "M", "呂"), + (0xF981, "M", "女"), + (0xF982, "M", "廬"), + (0xF983, "M", "旅"), + (0xF984, "M", "濾"), + (0xF985, "M", "礪"), + (0xF986, "M", "閭"), + ] + + +def _seg_41() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xF987, "M", "驪"), + (0xF988, "M", "麗"), + (0xF989, "M", "黎"), + (0xF98A, "M", "力"), + (0xF98B, "M", "曆"), + (0xF98C, "M", "歷"), + (0xF98D, "M", "轢"), + (0xF98E, "M", "年"), + (0xF98F, "M", "憐"), + (0xF990, "M", "戀"), + (0xF991, "M", "撚"), + (0xF992, "M", "漣"), + (0xF993, "M", "煉"), + (0xF994, "M", "璉"), + (0xF995, "M", "秊"), + (0xF996, "M", "練"), + (0xF997, "M", "聯"), + (0xF998, "M", "輦"), + (0xF999, "M", "蓮"), + (0xF99A, "M", "連"), + (0xF99B, "M", "鍊"), + (0xF99C, "M", "列"), + (0xF99D, "M", "劣"), + (0xF99E, "M", "咽"), + (0xF99F, "M", "烈"), + (0xF9A0, "M", "裂"), + (0xF9A1, "M", "說"), + (0xF9A2, "M", "廉"), + (0xF9A3, "M", "念"), + (0xF9A4, "M", "捻"), + (0xF9A5, "M", "殮"), + (0xF9A6, "M", "簾"), + (0xF9A7, "M", "獵"), + (0xF9A8, "M", "令"), + (0xF9A9, "M", "囹"), + (0xF9AA, "M", "寧"), + (0xF9AB, "M", "嶺"), + (0xF9AC, "M", "怜"), + (0xF9AD, "M", "玲"), + (0xF9AE, "M", "瑩"), + (0xF9AF, "M", "羚"), + (0xF9B0, "M", "聆"), + (0xF9B1, "M", "鈴"), + (0xF9B2, "M", "零"), + (0xF9B3, "M", "靈"), + (0xF9B4, "M", "領"), + (0xF9B5, "M", "例"), + (0xF9B6, "M", "禮"), + (0xF9B7, "M", "醴"), + (0xF9B8, "M", "隸"), + (0xF9B9, "M", "惡"), + (0xF9BA, "M", "了"), + (0xF9BB, "M", "僚"), + (0xF9BC, "M", "寮"), + (0xF9BD, "M", "尿"), + (0xF9BE, "M", "料"), + (0xF9BF, "M", "樂"), + (0xF9C0, "M", "燎"), + (0xF9C1, "M", "療"), + (0xF9C2, "M", "蓼"), + (0xF9C3, "M", "遼"), + (0xF9C4, "M", "龍"), + (0xF9C5, "M", "暈"), + (0xF9C6, "M", "阮"), + (0xF9C7, "M", "劉"), + (0xF9C8, "M", "杻"), + (0xF9C9, "M", "柳"), + (0xF9CA, "M", "流"), + (0xF9CB, "M", "溜"), + (0xF9CC, "M", "琉"), + (0xF9CD, "M", "留"), + (0xF9CE, "M", "硫"), + (0xF9CF, "M", "紐"), + (0xF9D0, "M", "類"), + (0xF9D1, "M", "六"), + (0xF9D2, "M", "戮"), + (0xF9D3, "M", "陸"), + (0xF9D4, "M", "倫"), + (0xF9D5, "M", "崙"), + (0xF9D6, "M", "淪"), + (0xF9D7, "M", "輪"), + (0xF9D8, "M", "律"), + (0xF9D9, "M", "慄"), + (0xF9DA, "M", "栗"), + (0xF9DB, "M", "率"), + (0xF9DC, "M", "隆"), + (0xF9DD, "M", "利"), + (0xF9DE, "M", "吏"), + (0xF9DF, "M", "履"), + (0xF9E0, "M", "易"), + (0xF9E1, "M", "李"), + (0xF9E2, "M", "梨"), + (0xF9E3, "M", "泥"), + (0xF9E4, "M", "理"), + (0xF9E5, "M", "痢"), + (0xF9E6, "M", "罹"), + (0xF9E7, "M", "裏"), + (0xF9E8, "M", "裡"), + (0xF9E9, "M", "里"), + (0xF9EA, "M", "離"), + ] + + +def _seg_42() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xF9EB, "M", "匿"), + (0xF9EC, "M", "溺"), + (0xF9ED, "M", "吝"), + (0xF9EE, "M", "燐"), + (0xF9EF, "M", "璘"), + (0xF9F0, "M", "藺"), + (0xF9F1, "M", "隣"), + (0xF9F2, "M", "鱗"), + (0xF9F3, "M", "麟"), + (0xF9F4, "M", "林"), + (0xF9F5, "M", "淋"), + (0xF9F6, "M", "臨"), + (0xF9F7, "M", "立"), + (0xF9F8, "M", "笠"), + (0xF9F9, "M", "粒"), + (0xF9FA, "M", "狀"), + (0xF9FB, "M", "炙"), + (0xF9FC, "M", "識"), + (0xF9FD, "M", "什"), + (0xF9FE, "M", "茶"), + (0xF9FF, "M", "刺"), + (0xFA00, "M", "切"), + (0xFA01, "M", "度"), + (0xFA02, "M", "拓"), + (0xFA03, "M", "糖"), + (0xFA04, "M", "宅"), + (0xFA05, "M", "洞"), + (0xFA06, "M", "暴"), + (0xFA07, "M", "輻"), + (0xFA08, "M", "行"), + (0xFA09, "M", "降"), + (0xFA0A, "M", "見"), + (0xFA0B, "M", "廓"), + (0xFA0C, "M", "兀"), + (0xFA0D, "M", "嗀"), + (0xFA0E, "V"), + (0xFA10, "M", "塚"), + (0xFA11, "V"), + (0xFA12, "M", "晴"), + (0xFA13, "V"), + (0xFA15, "M", "凞"), + (0xFA16, "M", "猪"), + (0xFA17, "M", "益"), + (0xFA18, "M", "礼"), + (0xFA19, "M", "神"), + (0xFA1A, "M", "祥"), + (0xFA1B, "M", "福"), + (0xFA1C, "M", "靖"), + (0xFA1D, "M", "精"), + (0xFA1E, "M", "羽"), + (0xFA1F, "V"), + (0xFA20, "M", "蘒"), + (0xFA21, "V"), + (0xFA22, "M", "諸"), + (0xFA23, "V"), + (0xFA25, "M", "逸"), + (0xFA26, "M", "都"), + (0xFA27, "V"), + (0xFA2A, "M", "飯"), + (0xFA2B, "M", "飼"), + (0xFA2C, "M", "館"), + (0xFA2D, "M", "鶴"), + (0xFA2E, "M", "郞"), + (0xFA2F, "M", "隷"), + (0xFA30, "M", "侮"), + (0xFA31, "M", "僧"), + (0xFA32, "M", "免"), + (0xFA33, "M", "勉"), + (0xFA34, "M", "勤"), + (0xFA35, "M", "卑"), + (0xFA36, "M", "喝"), + (0xFA37, "M", "嘆"), + (0xFA38, "M", "器"), + (0xFA39, "M", "塀"), + (0xFA3A, "M", "墨"), + (0xFA3B, "M", "層"), + (0xFA3C, "M", "屮"), + (0xFA3D, "M", "悔"), + (0xFA3E, "M", "慨"), + (0xFA3F, "M", "憎"), + (0xFA40, "M", "懲"), + (0xFA41, "M", "敏"), + (0xFA42, "M", "既"), + (0xFA43, "M", "暑"), + (0xFA44, "M", "梅"), + (0xFA45, "M", "海"), + (0xFA46, "M", "渚"), + (0xFA47, "M", "漢"), + (0xFA48, "M", "煮"), + (0xFA49, "M", "爫"), + (0xFA4A, "M", "琢"), + (0xFA4B, "M", "碑"), + (0xFA4C, "M", "社"), + (0xFA4D, "M", "祉"), + (0xFA4E, "M", "祈"), + (0xFA4F, "M", "祐"), + (0xFA50, "M", "祖"), + (0xFA51, "M", "祝"), + (0xFA52, "M", "禍"), + (0xFA53, "M", "禎"), + ] + + +def _seg_43() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFA54, "M", "穀"), + (0xFA55, "M", "突"), + (0xFA56, "M", "節"), + (0xFA57, "M", "練"), + (0xFA58, "M", "縉"), + (0xFA59, "M", "繁"), + (0xFA5A, "M", "署"), + (0xFA5B, "M", "者"), + (0xFA5C, "M", "臭"), + (0xFA5D, "M", "艹"), + (0xFA5F, "M", "著"), + (0xFA60, "M", "褐"), + (0xFA61, "M", "視"), + (0xFA62, "M", "謁"), + (0xFA63, "M", "謹"), + (0xFA64, "M", "賓"), + (0xFA65, "M", "贈"), + (0xFA66, "M", "辶"), + (0xFA67, "M", "逸"), + (0xFA68, "M", "難"), + (0xFA69, "M", "響"), + (0xFA6A, "M", "頻"), + (0xFA6B, "M", "恵"), + (0xFA6C, "M", "𤋮"), + (0xFA6D, "M", "舘"), + (0xFA6E, "X"), + (0xFA70, "M", "並"), + (0xFA71, "M", "况"), + (0xFA72, "M", "全"), + (0xFA73, "M", "侀"), + (0xFA74, "M", "充"), + (0xFA75, "M", "冀"), + (0xFA76, "M", "勇"), + (0xFA77, "M", "勺"), + (0xFA78, "M", "喝"), + (0xFA79, "M", "啕"), + (0xFA7A, "M", "喙"), + (0xFA7B, "M", "嗢"), + (0xFA7C, "M", "塚"), + (0xFA7D, "M", "墳"), + (0xFA7E, "M", "奄"), + (0xFA7F, "M", "奔"), + (0xFA80, "M", "婢"), + (0xFA81, "M", "嬨"), + (0xFA82, "M", "廒"), + (0xFA83, "M", "廙"), + (0xFA84, "M", "彩"), + (0xFA85, "M", "徭"), + (0xFA86, "M", "惘"), + (0xFA87, "M", "慎"), + (0xFA88, "M", "愈"), + (0xFA89, "M", "憎"), + (0xFA8A, "M", "慠"), + (0xFA8B, "M", "懲"), + (0xFA8C, "M", "戴"), + (0xFA8D, "M", "揄"), + (0xFA8E, "M", "搜"), + (0xFA8F, "M", "摒"), + (0xFA90, "M", "敖"), + (0xFA91, "M", "晴"), + (0xFA92, "M", "朗"), + (0xFA93, "M", "望"), + (0xFA94, "M", "杖"), + (0xFA95, "M", "歹"), + (0xFA96, "M", "殺"), + (0xFA97, "M", "流"), + (0xFA98, "M", "滛"), + (0xFA99, "M", "滋"), + (0xFA9A, "M", "漢"), + (0xFA9B, "M", "瀞"), + (0xFA9C, "M", "煮"), + (0xFA9D, "M", "瞧"), + (0xFA9E, "M", "爵"), + (0xFA9F, "M", "犯"), + (0xFAA0, "M", "猪"), + (0xFAA1, "M", "瑱"), + (0xFAA2, "M", "甆"), + (0xFAA3, "M", "画"), + (0xFAA4, "M", "瘝"), + (0xFAA5, "M", "瘟"), + (0xFAA6, "M", "益"), + (0xFAA7, "M", "盛"), + (0xFAA8, "M", "直"), + (0xFAA9, "M", "睊"), + (0xFAAA, "M", "着"), + (0xFAAB, "M", "磌"), + (0xFAAC, "M", "窱"), + (0xFAAD, "M", "節"), + (0xFAAE, "M", "类"), + (0xFAAF, "M", "絛"), + (0xFAB0, "M", "練"), + (0xFAB1, "M", "缾"), + (0xFAB2, "M", "者"), + (0xFAB3, "M", "荒"), + (0xFAB4, "M", "華"), + (0xFAB5, "M", "蝹"), + (0xFAB6, "M", "襁"), + (0xFAB7, "M", "覆"), + (0xFAB8, "M", "視"), + (0xFAB9, "M", "調"), + ] + + +def _seg_44() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFABA, "M", "諸"), + (0xFABB, "M", "請"), + (0xFABC, "M", "謁"), + (0xFABD, "M", "諾"), + (0xFABE, "M", "諭"), + (0xFABF, "M", "謹"), + (0xFAC0, "M", "變"), + (0xFAC1, "M", "贈"), + (0xFAC2, "M", "輸"), + (0xFAC3, "M", "遲"), + (0xFAC4, "M", "醙"), + (0xFAC5, "M", "鉶"), + (0xFAC6, "M", "陼"), + (0xFAC7, "M", "難"), + (0xFAC8, "M", "靖"), + (0xFAC9, "M", "韛"), + (0xFACA, "M", "響"), + (0xFACB, "M", "頋"), + (0xFACC, "M", "頻"), + (0xFACD, "M", "鬒"), + (0xFACE, "M", "龜"), + (0xFACF, "M", "𢡊"), + (0xFAD0, "M", "𢡄"), + (0xFAD1, "M", "𣏕"), + (0xFAD2, "M", "㮝"), + (0xFAD3, "M", "䀘"), + (0xFAD4, "M", "䀹"), + (0xFAD5, "M", "𥉉"), + (0xFAD6, "M", "𥳐"), + (0xFAD7, "M", "𧻓"), + (0xFAD8, "M", "齃"), + (0xFAD9, "M", "龎"), + (0xFADA, "X"), + (0xFB00, "M", "ff"), + (0xFB01, "M", "fi"), + (0xFB02, "M", "fl"), + (0xFB03, "M", "ffi"), + (0xFB04, "M", "ffl"), + (0xFB05, "M", "st"), + (0xFB07, "X"), + (0xFB13, "M", "մն"), + (0xFB14, "M", "մե"), + (0xFB15, "M", "մի"), + (0xFB16, "M", "վն"), + (0xFB17, "M", "մխ"), + (0xFB18, "X"), + (0xFB1D, "M", "יִ"), + (0xFB1E, "V"), + (0xFB1F, "M", "ײַ"), + (0xFB20, "M", "ע"), + (0xFB21, "M", "א"), + (0xFB22, "M", "ד"), + (0xFB23, "M", "ה"), + (0xFB24, "M", "כ"), + (0xFB25, "M", "ל"), + (0xFB26, "M", "ם"), + (0xFB27, "M", "ר"), + (0xFB28, "M", "ת"), + (0xFB29, "M", "+"), + (0xFB2A, "M", "שׁ"), + (0xFB2B, "M", "שׂ"), + (0xFB2C, "M", "שּׁ"), + (0xFB2D, "M", "שּׂ"), + (0xFB2E, "M", "אַ"), + (0xFB2F, "M", "אָ"), + (0xFB30, "M", "אּ"), + (0xFB31, "M", "בּ"), + (0xFB32, "M", "גּ"), + (0xFB33, "M", "דּ"), + (0xFB34, "M", "הּ"), + (0xFB35, "M", "וּ"), + (0xFB36, "M", "זּ"), + (0xFB37, "X"), + (0xFB38, "M", "טּ"), + (0xFB39, "M", "יּ"), + (0xFB3A, "M", "ךּ"), + (0xFB3B, "M", "כּ"), + (0xFB3C, "M", "לּ"), + (0xFB3D, "X"), + (0xFB3E, "M", "מּ"), + (0xFB3F, "X"), + (0xFB40, "M", "נּ"), + (0xFB41, "M", "סּ"), + (0xFB42, "X"), + (0xFB43, "M", "ףּ"), + (0xFB44, "M", "פּ"), + (0xFB45, "X"), + (0xFB46, "M", "צּ"), + (0xFB47, "M", "קּ"), + (0xFB48, "M", "רּ"), + (0xFB49, "M", "שּ"), + (0xFB4A, "M", "תּ"), + (0xFB4B, "M", "וֹ"), + (0xFB4C, "M", "בֿ"), + (0xFB4D, "M", "כֿ"), + (0xFB4E, "M", "פֿ"), + (0xFB4F, "M", "אל"), + (0xFB50, "M", "ٱ"), + (0xFB52, "M", "ٻ"), + (0xFB56, "M", "پ"), + ] + + +def _seg_45() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFB5A, "M", "ڀ"), + (0xFB5E, "M", "ٺ"), + (0xFB62, "M", "ٿ"), + (0xFB66, "M", "ٹ"), + (0xFB6A, "M", "ڤ"), + (0xFB6E, "M", "ڦ"), + (0xFB72, "M", "ڄ"), + (0xFB76, "M", "ڃ"), + (0xFB7A, "M", "چ"), + (0xFB7E, "M", "ڇ"), + (0xFB82, "M", "ڍ"), + (0xFB84, "M", "ڌ"), + (0xFB86, "M", "ڎ"), + (0xFB88, "M", "ڈ"), + (0xFB8A, "M", "ژ"), + (0xFB8C, "M", "ڑ"), + (0xFB8E, "M", "ک"), + (0xFB92, "M", "گ"), + (0xFB96, "M", "ڳ"), + (0xFB9A, "M", "ڱ"), + (0xFB9E, "M", "ں"), + (0xFBA0, "M", "ڻ"), + (0xFBA4, "M", "ۀ"), + (0xFBA6, "M", "ہ"), + (0xFBAA, "M", "ھ"), + (0xFBAE, "M", "ے"), + (0xFBB0, "M", "ۓ"), + (0xFBB2, "V"), + (0xFBC3, "X"), + (0xFBD3, "M", "ڭ"), + (0xFBD7, "M", "ۇ"), + (0xFBD9, "M", "ۆ"), + (0xFBDB, "M", "ۈ"), + (0xFBDD, "M", "ۇٴ"), + (0xFBDE, "M", "ۋ"), + (0xFBE0, "M", "ۅ"), + (0xFBE2, "M", "ۉ"), + (0xFBE4, "M", "ې"), + (0xFBE8, "M", "ى"), + (0xFBEA, "M", "ئا"), + (0xFBEC, "M", "ئە"), + (0xFBEE, "M", "ئو"), + (0xFBF0, "M", "ئۇ"), + (0xFBF2, "M", "ئۆ"), + (0xFBF4, "M", "ئۈ"), + (0xFBF6, "M", "ئې"), + (0xFBF9, "M", "ئى"), + (0xFBFC, "M", "ی"), + (0xFC00, "M", "ئج"), + (0xFC01, "M", "ئح"), + (0xFC02, "M", "ئم"), + (0xFC03, "M", "ئى"), + (0xFC04, "M", "ئي"), + (0xFC05, "M", "بج"), + (0xFC06, "M", "بح"), + (0xFC07, "M", "بخ"), + (0xFC08, "M", "بم"), + (0xFC09, "M", "بى"), + (0xFC0A, "M", "بي"), + (0xFC0B, "M", "تج"), + (0xFC0C, "M", "تح"), + (0xFC0D, "M", "تخ"), + (0xFC0E, "M", "تم"), + (0xFC0F, "M", "تى"), + (0xFC10, "M", "تي"), + (0xFC11, "M", "ثج"), + (0xFC12, "M", "ثم"), + (0xFC13, "M", "ثى"), + (0xFC14, "M", "ثي"), + (0xFC15, "M", "جح"), + (0xFC16, "M", "جم"), + (0xFC17, "M", "حج"), + (0xFC18, "M", "حم"), + (0xFC19, "M", "خج"), + (0xFC1A, "M", "خح"), + (0xFC1B, "M", "خم"), + (0xFC1C, "M", "سج"), + (0xFC1D, "M", "سح"), + (0xFC1E, "M", "سخ"), + (0xFC1F, "M", "سم"), + (0xFC20, "M", "صح"), + (0xFC21, "M", "صم"), + (0xFC22, "M", "ضج"), + (0xFC23, "M", "ضح"), + (0xFC24, "M", "ضخ"), + (0xFC25, "M", "ضم"), + (0xFC26, "M", "طح"), + (0xFC27, "M", "طم"), + (0xFC28, "M", "ظم"), + (0xFC29, "M", "عج"), + (0xFC2A, "M", "عم"), + (0xFC2B, "M", "غج"), + (0xFC2C, "M", "غم"), + (0xFC2D, "M", "فج"), + (0xFC2E, "M", "فح"), + (0xFC2F, "M", "فخ"), + (0xFC30, "M", "فم"), + (0xFC31, "M", "فى"), + (0xFC32, "M", "في"), + (0xFC33, "M", "قح"), + ] + + +def _seg_46() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFC34, "M", "قم"), + (0xFC35, "M", "قى"), + (0xFC36, "M", "قي"), + (0xFC37, "M", "كا"), + (0xFC38, "M", "كج"), + (0xFC39, "M", "كح"), + (0xFC3A, "M", "كخ"), + (0xFC3B, "M", "كل"), + (0xFC3C, "M", "كم"), + (0xFC3D, "M", "كى"), + (0xFC3E, "M", "كي"), + (0xFC3F, "M", "لج"), + (0xFC40, "M", "لح"), + (0xFC41, "M", "لخ"), + (0xFC42, "M", "لم"), + (0xFC43, "M", "لى"), + (0xFC44, "M", "لي"), + (0xFC45, "M", "مج"), + (0xFC46, "M", "مح"), + (0xFC47, "M", "مخ"), + (0xFC48, "M", "مم"), + (0xFC49, "M", "مى"), + (0xFC4A, "M", "مي"), + (0xFC4B, "M", "نج"), + (0xFC4C, "M", "نح"), + (0xFC4D, "M", "نخ"), + (0xFC4E, "M", "نم"), + (0xFC4F, "M", "نى"), + (0xFC50, "M", "ني"), + (0xFC51, "M", "هج"), + (0xFC52, "M", "هم"), + (0xFC53, "M", "هى"), + (0xFC54, "M", "هي"), + (0xFC55, "M", "يج"), + (0xFC56, "M", "يح"), + (0xFC57, "M", "يخ"), + (0xFC58, "M", "يم"), + (0xFC59, "M", "يى"), + (0xFC5A, "M", "يي"), + (0xFC5B, "M", "ذٰ"), + (0xFC5C, "M", "رٰ"), + (0xFC5D, "M", "ىٰ"), + (0xFC5E, "M", " ٌّ"), + (0xFC5F, "M", " ٍّ"), + (0xFC60, "M", " َّ"), + (0xFC61, "M", " ُّ"), + (0xFC62, "M", " ِّ"), + (0xFC63, "M", " ّٰ"), + (0xFC64, "M", "ئر"), + (0xFC65, "M", "ئز"), + (0xFC66, "M", "ئم"), + (0xFC67, "M", "ئن"), + (0xFC68, "M", "ئى"), + (0xFC69, "M", "ئي"), + (0xFC6A, "M", "بر"), + (0xFC6B, "M", "بز"), + (0xFC6C, "M", "بم"), + (0xFC6D, "M", "بن"), + (0xFC6E, "M", "بى"), + (0xFC6F, "M", "بي"), + (0xFC70, "M", "تر"), + (0xFC71, "M", "تز"), + (0xFC72, "M", "تم"), + (0xFC73, "M", "تن"), + (0xFC74, "M", "تى"), + (0xFC75, "M", "تي"), + (0xFC76, "M", "ثر"), + (0xFC77, "M", "ثز"), + (0xFC78, "M", "ثم"), + (0xFC79, "M", "ثن"), + (0xFC7A, "M", "ثى"), + (0xFC7B, "M", "ثي"), + (0xFC7C, "M", "فى"), + (0xFC7D, "M", "في"), + (0xFC7E, "M", "قى"), + (0xFC7F, "M", "قي"), + (0xFC80, "M", "كا"), + (0xFC81, "M", "كل"), + (0xFC82, "M", "كم"), + (0xFC83, "M", "كى"), + (0xFC84, "M", "كي"), + (0xFC85, "M", "لم"), + (0xFC86, "M", "لى"), + (0xFC87, "M", "لي"), + (0xFC88, "M", "ما"), + (0xFC89, "M", "مم"), + (0xFC8A, "M", "نر"), + (0xFC8B, "M", "نز"), + (0xFC8C, "M", "نم"), + (0xFC8D, "M", "نن"), + (0xFC8E, "M", "نى"), + (0xFC8F, "M", "ني"), + (0xFC90, "M", "ىٰ"), + (0xFC91, "M", "ير"), + (0xFC92, "M", "يز"), + (0xFC93, "M", "يم"), + (0xFC94, "M", "ين"), + (0xFC95, "M", "يى"), + (0xFC96, "M", "يي"), + (0xFC97, "M", "ئج"), + ] + + +def _seg_47() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFC98, "M", "ئح"), + (0xFC99, "M", "ئخ"), + (0xFC9A, "M", "ئم"), + (0xFC9B, "M", "ئه"), + (0xFC9C, "M", "بج"), + (0xFC9D, "M", "بح"), + (0xFC9E, "M", "بخ"), + (0xFC9F, "M", "بم"), + (0xFCA0, "M", "به"), + (0xFCA1, "M", "تج"), + (0xFCA2, "M", "تح"), + (0xFCA3, "M", "تخ"), + (0xFCA4, "M", "تم"), + (0xFCA5, "M", "ته"), + (0xFCA6, "M", "ثم"), + (0xFCA7, "M", "جح"), + (0xFCA8, "M", "جم"), + (0xFCA9, "M", "حج"), + (0xFCAA, "M", "حم"), + (0xFCAB, "M", "خج"), + (0xFCAC, "M", "خم"), + (0xFCAD, "M", "سج"), + (0xFCAE, "M", "سح"), + (0xFCAF, "M", "سخ"), + (0xFCB0, "M", "سم"), + (0xFCB1, "M", "صح"), + (0xFCB2, "M", "صخ"), + (0xFCB3, "M", "صم"), + (0xFCB4, "M", "ضج"), + (0xFCB5, "M", "ضح"), + (0xFCB6, "M", "ضخ"), + (0xFCB7, "M", "ضم"), + (0xFCB8, "M", "طح"), + (0xFCB9, "M", "ظم"), + (0xFCBA, "M", "عج"), + (0xFCBB, "M", "عم"), + (0xFCBC, "M", "غج"), + (0xFCBD, "M", "غم"), + (0xFCBE, "M", "فج"), + (0xFCBF, "M", "فح"), + (0xFCC0, "M", "فخ"), + (0xFCC1, "M", "فم"), + (0xFCC2, "M", "قح"), + (0xFCC3, "M", "قم"), + (0xFCC4, "M", "كج"), + (0xFCC5, "M", "كح"), + (0xFCC6, "M", "كخ"), + (0xFCC7, "M", "كل"), + (0xFCC8, "M", "كم"), + (0xFCC9, "M", "لج"), + (0xFCCA, "M", "لح"), + (0xFCCB, "M", "لخ"), + (0xFCCC, "M", "لم"), + (0xFCCD, "M", "له"), + (0xFCCE, "M", "مج"), + (0xFCCF, "M", "مح"), + (0xFCD0, "M", "مخ"), + (0xFCD1, "M", "مم"), + (0xFCD2, "M", "نج"), + (0xFCD3, "M", "نح"), + (0xFCD4, "M", "نخ"), + (0xFCD5, "M", "نم"), + (0xFCD6, "M", "نه"), + (0xFCD7, "M", "هج"), + (0xFCD8, "M", "هم"), + (0xFCD9, "M", "هٰ"), + (0xFCDA, "M", "يج"), + (0xFCDB, "M", "يح"), + (0xFCDC, "M", "يخ"), + (0xFCDD, "M", "يم"), + (0xFCDE, "M", "يه"), + (0xFCDF, "M", "ئم"), + (0xFCE0, "M", "ئه"), + (0xFCE1, "M", "بم"), + (0xFCE2, "M", "به"), + (0xFCE3, "M", "تم"), + (0xFCE4, "M", "ته"), + (0xFCE5, "M", "ثم"), + (0xFCE6, "M", "ثه"), + (0xFCE7, "M", "سم"), + (0xFCE8, "M", "سه"), + (0xFCE9, "M", "شم"), + (0xFCEA, "M", "شه"), + (0xFCEB, "M", "كل"), + (0xFCEC, "M", "كم"), + (0xFCED, "M", "لم"), + (0xFCEE, "M", "نم"), + (0xFCEF, "M", "نه"), + (0xFCF0, "M", "يم"), + (0xFCF1, "M", "يه"), + (0xFCF2, "M", "ـَّ"), + (0xFCF3, "M", "ـُّ"), + (0xFCF4, "M", "ـِّ"), + (0xFCF5, "M", "طى"), + (0xFCF6, "M", "طي"), + (0xFCF7, "M", "عى"), + (0xFCF8, "M", "عي"), + (0xFCF9, "M", "غى"), + (0xFCFA, "M", "غي"), + (0xFCFB, "M", "سى"), + ] + + +def _seg_48() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFCFC, "M", "سي"), + (0xFCFD, "M", "شى"), + (0xFCFE, "M", "شي"), + (0xFCFF, "M", "حى"), + (0xFD00, "M", "حي"), + (0xFD01, "M", "جى"), + (0xFD02, "M", "جي"), + (0xFD03, "M", "خى"), + (0xFD04, "M", "خي"), + (0xFD05, "M", "صى"), + (0xFD06, "M", "صي"), + (0xFD07, "M", "ضى"), + (0xFD08, "M", "ضي"), + (0xFD09, "M", "شج"), + (0xFD0A, "M", "شح"), + (0xFD0B, "M", "شخ"), + (0xFD0C, "M", "شم"), + (0xFD0D, "M", "شر"), + (0xFD0E, "M", "سر"), + (0xFD0F, "M", "صر"), + (0xFD10, "M", "ضر"), + (0xFD11, "M", "طى"), + (0xFD12, "M", "طي"), + (0xFD13, "M", "عى"), + (0xFD14, "M", "عي"), + (0xFD15, "M", "غى"), + (0xFD16, "M", "غي"), + (0xFD17, "M", "سى"), + (0xFD18, "M", "سي"), + (0xFD19, "M", "شى"), + (0xFD1A, "M", "شي"), + (0xFD1B, "M", "حى"), + (0xFD1C, "M", "حي"), + (0xFD1D, "M", "جى"), + (0xFD1E, "M", "جي"), + (0xFD1F, "M", "خى"), + (0xFD20, "M", "خي"), + (0xFD21, "M", "صى"), + (0xFD22, "M", "صي"), + (0xFD23, "M", "ضى"), + (0xFD24, "M", "ضي"), + (0xFD25, "M", "شج"), + (0xFD26, "M", "شح"), + (0xFD27, "M", "شخ"), + (0xFD28, "M", "شم"), + (0xFD29, "M", "شر"), + (0xFD2A, "M", "سر"), + (0xFD2B, "M", "صر"), + (0xFD2C, "M", "ضر"), + (0xFD2D, "M", "شج"), + (0xFD2E, "M", "شح"), + (0xFD2F, "M", "شخ"), + (0xFD30, "M", "شم"), + (0xFD31, "M", "سه"), + (0xFD32, "M", "شه"), + (0xFD33, "M", "طم"), + (0xFD34, "M", "سج"), + (0xFD35, "M", "سح"), + (0xFD36, "M", "سخ"), + (0xFD37, "M", "شج"), + (0xFD38, "M", "شح"), + (0xFD39, "M", "شخ"), + (0xFD3A, "M", "طم"), + (0xFD3B, "M", "ظم"), + (0xFD3C, "M", "اً"), + (0xFD3E, "V"), + (0xFD50, "M", "تجم"), + (0xFD51, "M", "تحج"), + (0xFD53, "M", "تحم"), + (0xFD54, "M", "تخم"), + (0xFD55, "M", "تمج"), + (0xFD56, "M", "تمح"), + (0xFD57, "M", "تمخ"), + (0xFD58, "M", "جمح"), + (0xFD5A, "M", "حمي"), + (0xFD5B, "M", "حمى"), + (0xFD5C, "M", "سحج"), + (0xFD5D, "M", "سجح"), + (0xFD5E, "M", "سجى"), + (0xFD5F, "M", "سمح"), + (0xFD61, "M", "سمج"), + (0xFD62, "M", "سمم"), + (0xFD64, "M", "صحح"), + (0xFD66, "M", "صمم"), + (0xFD67, "M", "شحم"), + (0xFD69, "M", "شجي"), + (0xFD6A, "M", "شمخ"), + (0xFD6C, "M", "شمم"), + (0xFD6E, "M", "ضحى"), + (0xFD6F, "M", "ضخم"), + (0xFD71, "M", "طمح"), + (0xFD73, "M", "طمم"), + (0xFD74, "M", "طمي"), + (0xFD75, "M", "عجم"), + (0xFD76, "M", "عمم"), + (0xFD78, "M", "عمى"), + (0xFD79, "M", "غمم"), + (0xFD7A, "M", "غمي"), + (0xFD7B, "M", "غمى"), + (0xFD7C, "M", "فخم"), + ] + + +def _seg_49() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFD7E, "M", "قمح"), + (0xFD7F, "M", "قمم"), + (0xFD80, "M", "لحم"), + (0xFD81, "M", "لحي"), + (0xFD82, "M", "لحى"), + (0xFD83, "M", "لجج"), + (0xFD85, "M", "لخم"), + (0xFD87, "M", "لمح"), + (0xFD89, "M", "محج"), + (0xFD8A, "M", "محم"), + (0xFD8B, "M", "محي"), + (0xFD8C, "M", "مجح"), + (0xFD8D, "M", "مجم"), + (0xFD8E, "M", "مخج"), + (0xFD8F, "M", "مخم"), + (0xFD90, "X"), + (0xFD92, "M", "مجخ"), + (0xFD93, "M", "همج"), + (0xFD94, "M", "همم"), + (0xFD95, "M", "نحم"), + (0xFD96, "M", "نحى"), + (0xFD97, "M", "نجم"), + (0xFD99, "M", "نجى"), + (0xFD9A, "M", "نمي"), + (0xFD9B, "M", "نمى"), + (0xFD9C, "M", "يمم"), + (0xFD9E, "M", "بخي"), + (0xFD9F, "M", "تجي"), + (0xFDA0, "M", "تجى"), + (0xFDA1, "M", "تخي"), + (0xFDA2, "M", "تخى"), + (0xFDA3, "M", "تمي"), + (0xFDA4, "M", "تمى"), + (0xFDA5, "M", "جمي"), + (0xFDA6, "M", "جحى"), + (0xFDA7, "M", "جمى"), + (0xFDA8, "M", "سخى"), + (0xFDA9, "M", "صحي"), + (0xFDAA, "M", "شحي"), + (0xFDAB, "M", "ضحي"), + (0xFDAC, "M", "لجي"), + (0xFDAD, "M", "لمي"), + (0xFDAE, "M", "يحي"), + (0xFDAF, "M", "يجي"), + (0xFDB0, "M", "يمي"), + (0xFDB1, "M", "ممي"), + (0xFDB2, "M", "قمي"), + (0xFDB3, "M", "نحي"), + (0xFDB4, "M", "قمح"), + (0xFDB5, "M", "لحم"), + (0xFDB6, "M", "عمي"), + (0xFDB7, "M", "كمي"), + (0xFDB8, "M", "نجح"), + (0xFDB9, "M", "مخي"), + (0xFDBA, "M", "لجم"), + (0xFDBB, "M", "كمم"), + (0xFDBC, "M", "لجم"), + (0xFDBD, "M", "نجح"), + (0xFDBE, "M", "جحي"), + (0xFDBF, "M", "حجي"), + (0xFDC0, "M", "مجي"), + (0xFDC1, "M", "فمي"), + (0xFDC2, "M", "بحي"), + (0xFDC3, "M", "كمم"), + (0xFDC4, "M", "عجم"), + (0xFDC5, "M", "صمم"), + (0xFDC6, "M", "سخي"), + (0xFDC7, "M", "نجي"), + (0xFDC8, "X"), + (0xFDCF, "V"), + (0xFDD0, "X"), + (0xFDF0, "M", "صلے"), + (0xFDF1, "M", "قلے"), + (0xFDF2, "M", "الله"), + (0xFDF3, "M", "اكبر"), + (0xFDF4, "M", "محمد"), + (0xFDF5, "M", "صلعم"), + (0xFDF6, "M", "رسول"), + (0xFDF7, "M", "عليه"), + (0xFDF8, "M", "وسلم"), + (0xFDF9, "M", "صلى"), + (0xFDFA, "M", "صلى الله عليه وسلم"), + (0xFDFB, "M", "جل جلاله"), + (0xFDFC, "M", "ریال"), + (0xFDFD, "V"), + (0xFE00, "I"), + (0xFE10, "M", ","), + (0xFE11, "M", "、"), + (0xFE12, "X"), + (0xFE13, "M", ":"), + (0xFE14, "M", ";"), + (0xFE15, "M", "!"), + (0xFE16, "M", "?"), + (0xFE17, "M", "〖"), + (0xFE18, "M", "〗"), + (0xFE19, "X"), + (0xFE20, "V"), + (0xFE30, "X"), + (0xFE31, "M", "—"), + (0xFE32, "M", "–"), + ] + + +def _seg_50() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFE33, "M", "_"), + (0xFE35, "M", "("), + (0xFE36, "M", ")"), + (0xFE37, "M", "{"), + (0xFE38, "M", "}"), + (0xFE39, "M", "〔"), + (0xFE3A, "M", "〕"), + (0xFE3B, "M", "【"), + (0xFE3C, "M", "】"), + (0xFE3D, "M", "《"), + (0xFE3E, "M", "》"), + (0xFE3F, "M", "〈"), + (0xFE40, "M", "〉"), + (0xFE41, "M", "「"), + (0xFE42, "M", "」"), + (0xFE43, "M", "『"), + (0xFE44, "M", "』"), + (0xFE45, "V"), + (0xFE47, "M", "["), + (0xFE48, "M", "]"), + (0xFE49, "M", " ̅"), + (0xFE4D, "M", "_"), + (0xFE50, "M", ","), + (0xFE51, "M", "、"), + (0xFE52, "X"), + (0xFE54, "M", ";"), + (0xFE55, "M", ":"), + (0xFE56, "M", "?"), + (0xFE57, "M", "!"), + (0xFE58, "M", "—"), + (0xFE59, "M", "("), + (0xFE5A, "M", ")"), + (0xFE5B, "M", "{"), + (0xFE5C, "M", "}"), + (0xFE5D, "M", "〔"), + (0xFE5E, "M", "〕"), + (0xFE5F, "M", "#"), + (0xFE60, "M", "&"), + (0xFE61, "M", "*"), + (0xFE62, "M", "+"), + (0xFE63, "M", "-"), + (0xFE64, "M", "<"), + (0xFE65, "M", ">"), + (0xFE66, "M", "="), + (0xFE67, "X"), + (0xFE68, "M", "\\"), + (0xFE69, "M", "$"), + (0xFE6A, "M", "%"), + (0xFE6B, "M", "@"), + (0xFE6C, "X"), + (0xFE70, "M", " ً"), + (0xFE71, "M", "ـً"), + (0xFE72, "M", " ٌ"), + (0xFE73, "V"), + (0xFE74, "M", " ٍ"), + (0xFE75, "X"), + (0xFE76, "M", " َ"), + (0xFE77, "M", "ـَ"), + (0xFE78, "M", " ُ"), + (0xFE79, "M", "ـُ"), + (0xFE7A, "M", " ِ"), + (0xFE7B, "M", "ـِ"), + (0xFE7C, "M", " ّ"), + (0xFE7D, "M", "ـّ"), + (0xFE7E, "M", " ْ"), + (0xFE7F, "M", "ـْ"), + (0xFE80, "M", "ء"), + (0xFE81, "M", "آ"), + (0xFE83, "M", "أ"), + (0xFE85, "M", "ؤ"), + (0xFE87, "M", "إ"), + (0xFE89, "M", "ئ"), + (0xFE8D, "M", "ا"), + (0xFE8F, "M", "ب"), + (0xFE93, "M", "ة"), + (0xFE95, "M", "ت"), + (0xFE99, "M", "ث"), + (0xFE9D, "M", "ج"), + (0xFEA1, "M", "ح"), + (0xFEA5, "M", "خ"), + (0xFEA9, "M", "د"), + (0xFEAB, "M", "ذ"), + (0xFEAD, "M", "ر"), + (0xFEAF, "M", "ز"), + (0xFEB1, "M", "س"), + (0xFEB5, "M", "ش"), + (0xFEB9, "M", "ص"), + (0xFEBD, "M", "ض"), + (0xFEC1, "M", "ط"), + (0xFEC5, "M", "ظ"), + (0xFEC9, "M", "ع"), + (0xFECD, "M", "غ"), + (0xFED1, "M", "ف"), + (0xFED5, "M", "ق"), + (0xFED9, "M", "ك"), + (0xFEDD, "M", "ل"), + (0xFEE1, "M", "م"), + (0xFEE5, "M", "ن"), + (0xFEE9, "M", "ه"), + (0xFEED, "M", "و"), + ] + + +def _seg_51() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFEEF, "M", "ى"), + (0xFEF1, "M", "ي"), + (0xFEF5, "M", "لآ"), + (0xFEF7, "M", "لأ"), + (0xFEF9, "M", "لإ"), + (0xFEFB, "M", "لا"), + (0xFEFD, "X"), + (0xFEFF, "I"), + (0xFF00, "X"), + (0xFF01, "M", "!"), + (0xFF02, "M", '"'), + (0xFF03, "M", "#"), + (0xFF04, "M", "$"), + (0xFF05, "M", "%"), + (0xFF06, "M", "&"), + (0xFF07, "M", "'"), + (0xFF08, "M", "("), + (0xFF09, "M", ")"), + (0xFF0A, "M", "*"), + (0xFF0B, "M", "+"), + (0xFF0C, "M", ","), + (0xFF0D, "M", "-"), + (0xFF0E, "M", "."), + (0xFF0F, "M", "/"), + (0xFF10, "M", "0"), + (0xFF11, "M", "1"), + (0xFF12, "M", "2"), + (0xFF13, "M", "3"), + (0xFF14, "M", "4"), + (0xFF15, "M", "5"), + (0xFF16, "M", "6"), + (0xFF17, "M", "7"), + (0xFF18, "M", "8"), + (0xFF19, "M", "9"), + (0xFF1A, "M", ":"), + (0xFF1B, "M", ";"), + (0xFF1C, "M", "<"), + (0xFF1D, "M", "="), + (0xFF1E, "M", ">"), + (0xFF1F, "M", "?"), + (0xFF20, "M", "@"), + (0xFF21, "M", "a"), + (0xFF22, "M", "b"), + (0xFF23, "M", "c"), + (0xFF24, "M", "d"), + (0xFF25, "M", "e"), + (0xFF26, "M", "f"), + (0xFF27, "M", "g"), + (0xFF28, "M", "h"), + (0xFF29, "M", "i"), + (0xFF2A, "M", "j"), + (0xFF2B, "M", "k"), + (0xFF2C, "M", "l"), + (0xFF2D, "M", "m"), + (0xFF2E, "M", "n"), + (0xFF2F, "M", "o"), + (0xFF30, "M", "p"), + (0xFF31, "M", "q"), + (0xFF32, "M", "r"), + (0xFF33, "M", "s"), + (0xFF34, "M", "t"), + (0xFF35, "M", "u"), + (0xFF36, "M", "v"), + (0xFF37, "M", "w"), + (0xFF38, "M", "x"), + (0xFF39, "M", "y"), + (0xFF3A, "M", "z"), + (0xFF3B, "M", "["), + (0xFF3C, "M", "\\"), + (0xFF3D, "M", "]"), + (0xFF3E, "M", "^"), + (0xFF3F, "M", "_"), + (0xFF40, "M", "`"), + (0xFF41, "M", "a"), + (0xFF42, "M", "b"), + (0xFF43, "M", "c"), + (0xFF44, "M", "d"), + (0xFF45, "M", "e"), + (0xFF46, "M", "f"), + (0xFF47, "M", "g"), + (0xFF48, "M", "h"), + (0xFF49, "M", "i"), + (0xFF4A, "M", "j"), + (0xFF4B, "M", "k"), + (0xFF4C, "M", "l"), + (0xFF4D, "M", "m"), + (0xFF4E, "M", "n"), + (0xFF4F, "M", "o"), + (0xFF50, "M", "p"), + (0xFF51, "M", "q"), + (0xFF52, "M", "r"), + (0xFF53, "M", "s"), + (0xFF54, "M", "t"), + (0xFF55, "M", "u"), + (0xFF56, "M", "v"), + (0xFF57, "M", "w"), + (0xFF58, "M", "x"), + (0xFF59, "M", "y"), + (0xFF5A, "M", "z"), + (0xFF5B, "M", "{"), + ] + + +def _seg_52() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFF5C, "M", "|"), + (0xFF5D, "M", "}"), + (0xFF5E, "M", "~"), + (0xFF5F, "M", "⦅"), + (0xFF60, "M", "⦆"), + (0xFF61, "M", "."), + (0xFF62, "M", "「"), + (0xFF63, "M", "」"), + (0xFF64, "M", "、"), + (0xFF65, "M", "・"), + (0xFF66, "M", "ヲ"), + (0xFF67, "M", "ァ"), + (0xFF68, "M", "ィ"), + (0xFF69, "M", "ゥ"), + (0xFF6A, "M", "ェ"), + (0xFF6B, "M", "ォ"), + (0xFF6C, "M", "ャ"), + (0xFF6D, "M", "ュ"), + (0xFF6E, "M", "ョ"), + (0xFF6F, "M", "ッ"), + (0xFF70, "M", "ー"), + (0xFF71, "M", "ア"), + (0xFF72, "M", "イ"), + (0xFF73, "M", "ウ"), + (0xFF74, "M", "エ"), + (0xFF75, "M", "オ"), + (0xFF76, "M", "カ"), + (0xFF77, "M", "キ"), + (0xFF78, "M", "ク"), + (0xFF79, "M", "ケ"), + (0xFF7A, "M", "コ"), + (0xFF7B, "M", "サ"), + (0xFF7C, "M", "シ"), + (0xFF7D, "M", "ス"), + (0xFF7E, "M", "セ"), + (0xFF7F, "M", "ソ"), + (0xFF80, "M", "タ"), + (0xFF81, "M", "チ"), + (0xFF82, "M", "ツ"), + (0xFF83, "M", "テ"), + (0xFF84, "M", "ト"), + (0xFF85, "M", "ナ"), + (0xFF86, "M", "ニ"), + (0xFF87, "M", "ヌ"), + (0xFF88, "M", "ネ"), + (0xFF89, "M", "ノ"), + (0xFF8A, "M", "ハ"), + (0xFF8B, "M", "ヒ"), + (0xFF8C, "M", "フ"), + (0xFF8D, "M", "ヘ"), + (0xFF8E, "M", "ホ"), + (0xFF8F, "M", "マ"), + (0xFF90, "M", "ミ"), + (0xFF91, "M", "ム"), + (0xFF92, "M", "メ"), + (0xFF93, "M", "モ"), + (0xFF94, "M", "ヤ"), + (0xFF95, "M", "ユ"), + (0xFF96, "M", "ヨ"), + (0xFF97, "M", "ラ"), + (0xFF98, "M", "リ"), + (0xFF99, "M", "ル"), + (0xFF9A, "M", "レ"), + (0xFF9B, "M", "ロ"), + (0xFF9C, "M", "ワ"), + (0xFF9D, "M", "ン"), + (0xFF9E, "M", "゙"), + (0xFF9F, "M", "゚"), + (0xFFA0, "I"), + (0xFFA1, "M", "ᄀ"), + (0xFFA2, "M", "ᄁ"), + (0xFFA3, "M", "ᆪ"), + (0xFFA4, "M", "ᄂ"), + (0xFFA5, "M", "ᆬ"), + (0xFFA6, "M", "ᆭ"), + (0xFFA7, "M", "ᄃ"), + (0xFFA8, "M", "ᄄ"), + (0xFFA9, "M", "ᄅ"), + (0xFFAA, "M", "ᆰ"), + (0xFFAB, "M", "ᆱ"), + (0xFFAC, "M", "ᆲ"), + (0xFFAD, "M", "ᆳ"), + (0xFFAE, "M", "ᆴ"), + (0xFFAF, "M", "ᆵ"), + (0xFFB0, "M", "ᄚ"), + (0xFFB1, "M", "ᄆ"), + (0xFFB2, "M", "ᄇ"), + (0xFFB3, "M", "ᄈ"), + (0xFFB4, "M", "ᄡ"), + (0xFFB5, "M", "ᄉ"), + (0xFFB6, "M", "ᄊ"), + (0xFFB7, "M", "ᄋ"), + (0xFFB8, "M", "ᄌ"), + (0xFFB9, "M", "ᄍ"), + (0xFFBA, "M", "ᄎ"), + (0xFFBB, "M", "ᄏ"), + (0xFFBC, "M", "ᄐ"), + (0xFFBD, "M", "ᄑ"), + (0xFFBE, "M", "ᄒ"), + (0xFFBF, "X"), + ] + + +def _seg_53() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0xFFC2, "M", "ᅡ"), + (0xFFC3, "M", "ᅢ"), + (0xFFC4, "M", "ᅣ"), + (0xFFC5, "M", "ᅤ"), + (0xFFC6, "M", "ᅥ"), + (0xFFC7, "M", "ᅦ"), + (0xFFC8, "X"), + (0xFFCA, "M", "ᅧ"), + (0xFFCB, "M", "ᅨ"), + (0xFFCC, "M", "ᅩ"), + (0xFFCD, "M", "ᅪ"), + (0xFFCE, "M", "ᅫ"), + (0xFFCF, "M", "ᅬ"), + (0xFFD0, "X"), + (0xFFD2, "M", "ᅭ"), + (0xFFD3, "M", "ᅮ"), + (0xFFD4, "M", "ᅯ"), + (0xFFD5, "M", "ᅰ"), + (0xFFD6, "M", "ᅱ"), + (0xFFD7, "M", "ᅲ"), + (0xFFD8, "X"), + (0xFFDA, "M", "ᅳ"), + (0xFFDB, "M", "ᅴ"), + (0xFFDC, "M", "ᅵ"), + (0xFFDD, "X"), + (0xFFE0, "M", "¢"), + (0xFFE1, "M", "£"), + (0xFFE2, "M", "¬"), + (0xFFE3, "M", " ̄"), + (0xFFE4, "M", "¦"), + (0xFFE5, "M", "¥"), + (0xFFE6, "M", "₩"), + (0xFFE7, "X"), + (0xFFE8, "M", "│"), + (0xFFE9, "M", "←"), + (0xFFEA, "M", "↑"), + (0xFFEB, "M", "→"), + (0xFFEC, "M", "↓"), + (0xFFED, "M", "■"), + (0xFFEE, "M", "○"), + (0xFFEF, "X"), + (0x10000, "V"), + (0x1000C, "X"), + (0x1000D, "V"), + (0x10027, "X"), + (0x10028, "V"), + (0x1003B, "X"), + (0x1003C, "V"), + (0x1003E, "X"), + (0x1003F, "V"), + (0x1004E, "X"), + (0x10050, "V"), + (0x1005E, "X"), + (0x10080, "V"), + (0x100FB, "X"), + (0x10100, "V"), + (0x10103, "X"), + (0x10107, "V"), + (0x10134, "X"), + (0x10137, "V"), + (0x1018F, "X"), + (0x10190, "V"), + (0x1019D, "X"), + (0x101A0, "V"), + (0x101A1, "X"), + (0x101D0, "V"), + (0x101FE, "X"), + (0x10280, "V"), + (0x1029D, "X"), + (0x102A0, "V"), + (0x102D1, "X"), + (0x102E0, "V"), + (0x102FC, "X"), + (0x10300, "V"), + (0x10324, "X"), + (0x1032D, "V"), + (0x1034B, "X"), + (0x10350, "V"), + (0x1037B, "X"), + (0x10380, "V"), + (0x1039E, "X"), + (0x1039F, "V"), + (0x103C4, "X"), + (0x103C8, "V"), + (0x103D6, "X"), + (0x10400, "M", "𐐨"), + (0x10401, "M", "𐐩"), + (0x10402, "M", "𐐪"), + (0x10403, "M", "𐐫"), + (0x10404, "M", "𐐬"), + (0x10405, "M", "𐐭"), + (0x10406, "M", "𐐮"), + (0x10407, "M", "𐐯"), + (0x10408, "M", "𐐰"), + (0x10409, "M", "𐐱"), + (0x1040A, "M", "𐐲"), + (0x1040B, "M", "𐐳"), + (0x1040C, "M", "𐐴"), + (0x1040D, "M", "𐐵"), + (0x1040E, "M", "𐐶"), + ] + + +def _seg_54() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1040F, "M", "𐐷"), + (0x10410, "M", "𐐸"), + (0x10411, "M", "𐐹"), + (0x10412, "M", "𐐺"), + (0x10413, "M", "𐐻"), + (0x10414, "M", "𐐼"), + (0x10415, "M", "𐐽"), + (0x10416, "M", "𐐾"), + (0x10417, "M", "𐐿"), + (0x10418, "M", "𐑀"), + (0x10419, "M", "𐑁"), + (0x1041A, "M", "𐑂"), + (0x1041B, "M", "𐑃"), + (0x1041C, "M", "𐑄"), + (0x1041D, "M", "𐑅"), + (0x1041E, "M", "𐑆"), + (0x1041F, "M", "𐑇"), + (0x10420, "M", "𐑈"), + (0x10421, "M", "𐑉"), + (0x10422, "M", "𐑊"), + (0x10423, "M", "𐑋"), + (0x10424, "M", "𐑌"), + (0x10425, "M", "𐑍"), + (0x10426, "M", "𐑎"), + (0x10427, "M", "𐑏"), + (0x10428, "V"), + (0x1049E, "X"), + (0x104A0, "V"), + (0x104AA, "X"), + (0x104B0, "M", "𐓘"), + (0x104B1, "M", "𐓙"), + (0x104B2, "M", "𐓚"), + (0x104B3, "M", "𐓛"), + (0x104B4, "M", "𐓜"), + (0x104B5, "M", "𐓝"), + (0x104B6, "M", "𐓞"), + (0x104B7, "M", "𐓟"), + (0x104B8, "M", "𐓠"), + (0x104B9, "M", "𐓡"), + (0x104BA, "M", "𐓢"), + (0x104BB, "M", "𐓣"), + (0x104BC, "M", "𐓤"), + (0x104BD, "M", "𐓥"), + (0x104BE, "M", "𐓦"), + (0x104BF, "M", "𐓧"), + (0x104C0, "M", "𐓨"), + (0x104C1, "M", "𐓩"), + (0x104C2, "M", "𐓪"), + (0x104C3, "M", "𐓫"), + (0x104C4, "M", "𐓬"), + (0x104C5, "M", "𐓭"), + (0x104C6, "M", "𐓮"), + (0x104C7, "M", "𐓯"), + (0x104C8, "M", "𐓰"), + (0x104C9, "M", "𐓱"), + (0x104CA, "M", "𐓲"), + (0x104CB, "M", "𐓳"), + (0x104CC, "M", "𐓴"), + (0x104CD, "M", "𐓵"), + (0x104CE, "M", "𐓶"), + (0x104CF, "M", "𐓷"), + (0x104D0, "M", "𐓸"), + (0x104D1, "M", "𐓹"), + (0x104D2, "M", "𐓺"), + (0x104D3, "M", "𐓻"), + (0x104D4, "X"), + (0x104D8, "V"), + (0x104FC, "X"), + (0x10500, "V"), + (0x10528, "X"), + (0x10530, "V"), + (0x10564, "X"), + (0x1056F, "V"), + (0x10570, "M", "𐖗"), + (0x10571, "M", "𐖘"), + (0x10572, "M", "𐖙"), + (0x10573, "M", "𐖚"), + (0x10574, "M", "𐖛"), + (0x10575, "M", "𐖜"), + (0x10576, "M", "𐖝"), + (0x10577, "M", "𐖞"), + (0x10578, "M", "𐖟"), + (0x10579, "M", "𐖠"), + (0x1057A, "M", "𐖡"), + (0x1057B, "X"), + (0x1057C, "M", "𐖣"), + (0x1057D, "M", "𐖤"), + (0x1057E, "M", "𐖥"), + (0x1057F, "M", "𐖦"), + (0x10580, "M", "𐖧"), + (0x10581, "M", "𐖨"), + (0x10582, "M", "𐖩"), + (0x10583, "M", "𐖪"), + (0x10584, "M", "𐖫"), + (0x10585, "M", "𐖬"), + (0x10586, "M", "𐖭"), + (0x10587, "M", "𐖮"), + (0x10588, "M", "𐖯"), + (0x10589, "M", "𐖰"), + (0x1058A, "M", "𐖱"), + ] + + +def _seg_55() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1058B, "X"), + (0x1058C, "M", "𐖳"), + (0x1058D, "M", "𐖴"), + (0x1058E, "M", "𐖵"), + (0x1058F, "M", "𐖶"), + (0x10590, "M", "𐖷"), + (0x10591, "M", "𐖸"), + (0x10592, "M", "𐖹"), + (0x10593, "X"), + (0x10594, "M", "𐖻"), + (0x10595, "M", "𐖼"), + (0x10596, "X"), + (0x10597, "V"), + (0x105A2, "X"), + (0x105A3, "V"), + (0x105B2, "X"), + (0x105B3, "V"), + (0x105BA, "X"), + (0x105BB, "V"), + (0x105BD, "X"), + (0x105C0, "V"), + (0x105F4, "X"), + (0x10600, "V"), + (0x10737, "X"), + (0x10740, "V"), + (0x10756, "X"), + (0x10760, "V"), + (0x10768, "X"), + (0x10780, "V"), + (0x10781, "M", "ː"), + (0x10782, "M", "ˑ"), + (0x10783, "M", "æ"), + (0x10784, "M", "ʙ"), + (0x10785, "M", "ɓ"), + (0x10786, "X"), + (0x10787, "M", "ʣ"), + (0x10788, "M", "ꭦ"), + (0x10789, "M", "ʥ"), + (0x1078A, "M", "ʤ"), + (0x1078B, "M", "ɖ"), + (0x1078C, "M", "ɗ"), + (0x1078D, "M", "ᶑ"), + (0x1078E, "M", "ɘ"), + (0x1078F, "M", "ɞ"), + (0x10790, "M", "ʩ"), + (0x10791, "M", "ɤ"), + (0x10792, "M", "ɢ"), + (0x10793, "M", "ɠ"), + (0x10794, "M", "ʛ"), + (0x10795, "M", "ħ"), + (0x10796, "M", "ʜ"), + (0x10797, "M", "ɧ"), + (0x10798, "M", "ʄ"), + (0x10799, "M", "ʪ"), + (0x1079A, "M", "ʫ"), + (0x1079B, "M", "ɬ"), + (0x1079C, "M", "𝼄"), + (0x1079D, "M", "ꞎ"), + (0x1079E, "M", "ɮ"), + (0x1079F, "M", "𝼅"), + (0x107A0, "M", "ʎ"), + (0x107A1, "M", "𝼆"), + (0x107A2, "M", "ø"), + (0x107A3, "M", "ɶ"), + (0x107A4, "M", "ɷ"), + (0x107A5, "M", "q"), + (0x107A6, "M", "ɺ"), + (0x107A7, "M", "𝼈"), + (0x107A8, "M", "ɽ"), + (0x107A9, "M", "ɾ"), + (0x107AA, "M", "ʀ"), + (0x107AB, "M", "ʨ"), + (0x107AC, "M", "ʦ"), + (0x107AD, "M", "ꭧ"), + (0x107AE, "M", "ʧ"), + (0x107AF, "M", "ʈ"), + (0x107B0, "M", "ⱱ"), + (0x107B1, "X"), + (0x107B2, "M", "ʏ"), + (0x107B3, "M", "ʡ"), + (0x107B4, "M", "ʢ"), + (0x107B5, "M", "ʘ"), + (0x107B6, "M", "ǀ"), + (0x107B7, "M", "ǁ"), + (0x107B8, "M", "ǂ"), + (0x107B9, "M", "𝼊"), + (0x107BA, "M", "𝼞"), + (0x107BB, "X"), + (0x10800, "V"), + (0x10806, "X"), + (0x10808, "V"), + (0x10809, "X"), + (0x1080A, "V"), + (0x10836, "X"), + (0x10837, "V"), + (0x10839, "X"), + (0x1083C, "V"), + (0x1083D, "X"), + (0x1083F, "V"), + (0x10856, "X"), + ] + + +def _seg_56() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x10857, "V"), + (0x1089F, "X"), + (0x108A7, "V"), + (0x108B0, "X"), + (0x108E0, "V"), + (0x108F3, "X"), + (0x108F4, "V"), + (0x108F6, "X"), + (0x108FB, "V"), + (0x1091C, "X"), + (0x1091F, "V"), + (0x1093A, "X"), + (0x1093F, "V"), + (0x10940, "X"), + (0x10980, "V"), + (0x109B8, "X"), + (0x109BC, "V"), + (0x109D0, "X"), + (0x109D2, "V"), + (0x10A04, "X"), + (0x10A05, "V"), + (0x10A07, "X"), + (0x10A0C, "V"), + (0x10A14, "X"), + (0x10A15, "V"), + (0x10A18, "X"), + (0x10A19, "V"), + (0x10A36, "X"), + (0x10A38, "V"), + (0x10A3B, "X"), + (0x10A3F, "V"), + (0x10A49, "X"), + (0x10A50, "V"), + (0x10A59, "X"), + (0x10A60, "V"), + (0x10AA0, "X"), + (0x10AC0, "V"), + (0x10AE7, "X"), + (0x10AEB, "V"), + (0x10AF7, "X"), + (0x10B00, "V"), + (0x10B36, "X"), + (0x10B39, "V"), + (0x10B56, "X"), + (0x10B58, "V"), + (0x10B73, "X"), + (0x10B78, "V"), + (0x10B92, "X"), + (0x10B99, "V"), + (0x10B9D, "X"), + (0x10BA9, "V"), + (0x10BB0, "X"), + (0x10C00, "V"), + (0x10C49, "X"), + (0x10C80, "M", "𐳀"), + (0x10C81, "M", "𐳁"), + (0x10C82, "M", "𐳂"), + (0x10C83, "M", "𐳃"), + (0x10C84, "M", "𐳄"), + (0x10C85, "M", "𐳅"), + (0x10C86, "M", "𐳆"), + (0x10C87, "M", "𐳇"), + (0x10C88, "M", "𐳈"), + (0x10C89, "M", "𐳉"), + (0x10C8A, "M", "𐳊"), + (0x10C8B, "M", "𐳋"), + (0x10C8C, "M", "𐳌"), + (0x10C8D, "M", "𐳍"), + (0x10C8E, "M", "𐳎"), + (0x10C8F, "M", "𐳏"), + (0x10C90, "M", "𐳐"), + (0x10C91, "M", "𐳑"), + (0x10C92, "M", "𐳒"), + (0x10C93, "M", "𐳓"), + (0x10C94, "M", "𐳔"), + (0x10C95, "M", "𐳕"), + (0x10C96, "M", "𐳖"), + (0x10C97, "M", "𐳗"), + (0x10C98, "M", "𐳘"), + (0x10C99, "M", "𐳙"), + (0x10C9A, "M", "𐳚"), + (0x10C9B, "M", "𐳛"), + (0x10C9C, "M", "𐳜"), + (0x10C9D, "M", "𐳝"), + (0x10C9E, "M", "𐳞"), + (0x10C9F, "M", "𐳟"), + (0x10CA0, "M", "𐳠"), + (0x10CA1, "M", "𐳡"), + (0x10CA2, "M", "𐳢"), + (0x10CA3, "M", "𐳣"), + (0x10CA4, "M", "𐳤"), + (0x10CA5, "M", "𐳥"), + (0x10CA6, "M", "𐳦"), + (0x10CA7, "M", "𐳧"), + (0x10CA8, "M", "𐳨"), + (0x10CA9, "M", "𐳩"), + (0x10CAA, "M", "𐳪"), + (0x10CAB, "M", "𐳫"), + (0x10CAC, "M", "𐳬"), + (0x10CAD, "M", "𐳭"), + ] + + +def _seg_57() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x10CAE, "M", "𐳮"), + (0x10CAF, "M", "𐳯"), + (0x10CB0, "M", "𐳰"), + (0x10CB1, "M", "𐳱"), + (0x10CB2, "M", "𐳲"), + (0x10CB3, "X"), + (0x10CC0, "V"), + (0x10CF3, "X"), + (0x10CFA, "V"), + (0x10D28, "X"), + (0x10D30, "V"), + (0x10D3A, "X"), + (0x10D40, "V"), + (0x10D50, "M", "𐵰"), + (0x10D51, "M", "𐵱"), + (0x10D52, "M", "𐵲"), + (0x10D53, "M", "𐵳"), + (0x10D54, "M", "𐵴"), + (0x10D55, "M", "𐵵"), + (0x10D56, "M", "𐵶"), + (0x10D57, "M", "𐵷"), + (0x10D58, "M", "𐵸"), + (0x10D59, "M", "𐵹"), + (0x10D5A, "M", "𐵺"), + (0x10D5B, "M", "𐵻"), + (0x10D5C, "M", "𐵼"), + (0x10D5D, "M", "𐵽"), + (0x10D5E, "M", "𐵾"), + (0x10D5F, "M", "𐵿"), + (0x10D60, "M", "𐶀"), + (0x10D61, "M", "𐶁"), + (0x10D62, "M", "𐶂"), + (0x10D63, "M", "𐶃"), + (0x10D64, "M", "𐶄"), + (0x10D65, "M", "𐶅"), + (0x10D66, "X"), + (0x10D69, "V"), + (0x10D86, "X"), + (0x10D8E, "V"), + (0x10D90, "X"), + (0x10E60, "V"), + (0x10E7F, "X"), + (0x10E80, "V"), + (0x10EAA, "X"), + (0x10EAB, "V"), + (0x10EAE, "X"), + (0x10EB0, "V"), + (0x10EB2, "X"), + (0x10EC2, "V"), + (0x10EC5, "X"), + (0x10EFC, "V"), + (0x10F28, "X"), + (0x10F30, "V"), + (0x10F5A, "X"), + (0x10F70, "V"), + (0x10F8A, "X"), + (0x10FB0, "V"), + (0x10FCC, "X"), + (0x10FE0, "V"), + (0x10FF7, "X"), + (0x11000, "V"), + (0x1104E, "X"), + (0x11052, "V"), + (0x11076, "X"), + (0x1107F, "V"), + (0x110BD, "X"), + (0x110BE, "V"), + (0x110C3, "X"), + (0x110D0, "V"), + (0x110E9, "X"), + (0x110F0, "V"), + (0x110FA, "X"), + (0x11100, "V"), + (0x11135, "X"), + (0x11136, "V"), + (0x11148, "X"), + (0x11150, "V"), + (0x11177, "X"), + (0x11180, "V"), + (0x111E0, "X"), + (0x111E1, "V"), + (0x111F5, "X"), + (0x11200, "V"), + (0x11212, "X"), + (0x11213, "V"), + (0x11242, "X"), + (0x11280, "V"), + (0x11287, "X"), + (0x11288, "V"), + (0x11289, "X"), + (0x1128A, "V"), + (0x1128E, "X"), + (0x1128F, "V"), + (0x1129E, "X"), + (0x1129F, "V"), + (0x112AA, "X"), + (0x112B0, "V"), + (0x112EB, "X"), + (0x112F0, "V"), + (0x112FA, "X"), + ] + + +def _seg_58() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x11300, "V"), + (0x11304, "X"), + (0x11305, "V"), + (0x1130D, "X"), + (0x1130F, "V"), + (0x11311, "X"), + (0x11313, "V"), + (0x11329, "X"), + (0x1132A, "V"), + (0x11331, "X"), + (0x11332, "V"), + (0x11334, "X"), + (0x11335, "V"), + (0x1133A, "X"), + (0x1133B, "V"), + (0x11345, "X"), + (0x11347, "V"), + (0x11349, "X"), + (0x1134B, "V"), + (0x1134E, "X"), + (0x11350, "V"), + (0x11351, "X"), + (0x11357, "V"), + (0x11358, "X"), + (0x1135D, "V"), + (0x11364, "X"), + (0x11366, "V"), + (0x1136D, "X"), + (0x11370, "V"), + (0x11375, "X"), + (0x11380, "V"), + (0x1138A, "X"), + (0x1138B, "V"), + (0x1138C, "X"), + (0x1138E, "V"), + (0x1138F, "X"), + (0x11390, "V"), + (0x113B6, "X"), + (0x113B7, "V"), + (0x113C1, "X"), + (0x113C2, "V"), + (0x113C3, "X"), + (0x113C5, "V"), + (0x113C6, "X"), + (0x113C7, "V"), + (0x113CB, "X"), + (0x113CC, "V"), + (0x113D6, "X"), + (0x113D7, "V"), + (0x113D9, "X"), + (0x113E1, "V"), + (0x113E3, "X"), + (0x11400, "V"), + (0x1145C, "X"), + (0x1145D, "V"), + (0x11462, "X"), + (0x11480, "V"), + (0x114C8, "X"), + (0x114D0, "V"), + (0x114DA, "X"), + (0x11580, "V"), + (0x115B6, "X"), + (0x115B8, "V"), + (0x115DE, "X"), + (0x11600, "V"), + (0x11645, "X"), + (0x11650, "V"), + (0x1165A, "X"), + (0x11660, "V"), + (0x1166D, "X"), + (0x11680, "V"), + (0x116BA, "X"), + (0x116C0, "V"), + (0x116CA, "X"), + (0x116D0, "V"), + (0x116E4, "X"), + (0x11700, "V"), + (0x1171B, "X"), + (0x1171D, "V"), + (0x1172C, "X"), + (0x11730, "V"), + (0x11747, "X"), + (0x11800, "V"), + (0x1183C, "X"), + (0x118A0, "M", "𑣀"), + (0x118A1, "M", "𑣁"), + (0x118A2, "M", "𑣂"), + (0x118A3, "M", "𑣃"), + (0x118A4, "M", "𑣄"), + (0x118A5, "M", "𑣅"), + (0x118A6, "M", "𑣆"), + (0x118A7, "M", "𑣇"), + (0x118A8, "M", "𑣈"), + (0x118A9, "M", "𑣉"), + (0x118AA, "M", "𑣊"), + (0x118AB, "M", "𑣋"), + (0x118AC, "M", "𑣌"), + (0x118AD, "M", "𑣍"), + (0x118AE, "M", "𑣎"), + (0x118AF, "M", "𑣏"), + ] + + +def _seg_59() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x118B0, "M", "𑣐"), + (0x118B1, "M", "𑣑"), + (0x118B2, "M", "𑣒"), + (0x118B3, "M", "𑣓"), + (0x118B4, "M", "𑣔"), + (0x118B5, "M", "𑣕"), + (0x118B6, "M", "𑣖"), + (0x118B7, "M", "𑣗"), + (0x118B8, "M", "𑣘"), + (0x118B9, "M", "𑣙"), + (0x118BA, "M", "𑣚"), + (0x118BB, "M", "𑣛"), + (0x118BC, "M", "𑣜"), + (0x118BD, "M", "𑣝"), + (0x118BE, "M", "𑣞"), + (0x118BF, "M", "𑣟"), + (0x118C0, "V"), + (0x118F3, "X"), + (0x118FF, "V"), + (0x11907, "X"), + (0x11909, "V"), + (0x1190A, "X"), + (0x1190C, "V"), + (0x11914, "X"), + (0x11915, "V"), + (0x11917, "X"), + (0x11918, "V"), + (0x11936, "X"), + (0x11937, "V"), + (0x11939, "X"), + (0x1193B, "V"), + (0x11947, "X"), + (0x11950, "V"), + (0x1195A, "X"), + (0x119A0, "V"), + (0x119A8, "X"), + (0x119AA, "V"), + (0x119D8, "X"), + (0x119DA, "V"), + (0x119E5, "X"), + (0x11A00, "V"), + (0x11A48, "X"), + (0x11A50, "V"), + (0x11AA3, "X"), + (0x11AB0, "V"), + (0x11AF9, "X"), + (0x11B00, "V"), + (0x11B0A, "X"), + (0x11BC0, "V"), + (0x11BE2, "X"), + (0x11BF0, "V"), + (0x11BFA, "X"), + (0x11C00, "V"), + (0x11C09, "X"), + (0x11C0A, "V"), + (0x11C37, "X"), + (0x11C38, "V"), + (0x11C46, "X"), + (0x11C50, "V"), + (0x11C6D, "X"), + (0x11C70, "V"), + (0x11C90, "X"), + (0x11C92, "V"), + (0x11CA8, "X"), + (0x11CA9, "V"), + (0x11CB7, "X"), + (0x11D00, "V"), + (0x11D07, "X"), + (0x11D08, "V"), + (0x11D0A, "X"), + (0x11D0B, "V"), + (0x11D37, "X"), + (0x11D3A, "V"), + (0x11D3B, "X"), + (0x11D3C, "V"), + (0x11D3E, "X"), + (0x11D3F, "V"), + (0x11D48, "X"), + (0x11D50, "V"), + (0x11D5A, "X"), + (0x11D60, "V"), + (0x11D66, "X"), + (0x11D67, "V"), + (0x11D69, "X"), + (0x11D6A, "V"), + (0x11D8F, "X"), + (0x11D90, "V"), + (0x11D92, "X"), + (0x11D93, "V"), + (0x11D99, "X"), + (0x11DA0, "V"), + (0x11DAA, "X"), + (0x11EE0, "V"), + (0x11EF9, "X"), + (0x11F00, "V"), + (0x11F11, "X"), + (0x11F12, "V"), + (0x11F3B, "X"), + (0x11F3E, "V"), + (0x11F5B, "X"), + ] + + +def _seg_60() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x11FB0, "V"), + (0x11FB1, "X"), + (0x11FC0, "V"), + (0x11FF2, "X"), + (0x11FFF, "V"), + (0x1239A, "X"), + (0x12400, "V"), + (0x1246F, "X"), + (0x12470, "V"), + (0x12475, "X"), + (0x12480, "V"), + (0x12544, "X"), + (0x12F90, "V"), + (0x12FF3, "X"), + (0x13000, "V"), + (0x13430, "X"), + (0x13440, "V"), + (0x13456, "X"), + (0x13460, "V"), + (0x143FB, "X"), + (0x14400, "V"), + (0x14647, "X"), + (0x16100, "V"), + (0x1613A, "X"), + (0x16800, "V"), + (0x16A39, "X"), + (0x16A40, "V"), + (0x16A5F, "X"), + (0x16A60, "V"), + (0x16A6A, "X"), + (0x16A6E, "V"), + (0x16ABF, "X"), + (0x16AC0, "V"), + (0x16ACA, "X"), + (0x16AD0, "V"), + (0x16AEE, "X"), + (0x16AF0, "V"), + (0x16AF6, "X"), + (0x16B00, "V"), + (0x16B46, "X"), + (0x16B50, "V"), + (0x16B5A, "X"), + (0x16B5B, "V"), + (0x16B62, "X"), + (0x16B63, "V"), + (0x16B78, "X"), + (0x16B7D, "V"), + (0x16B90, "X"), + (0x16D40, "V"), + (0x16D7A, "X"), + (0x16E40, "M", "𖹠"), + (0x16E41, "M", "𖹡"), + (0x16E42, "M", "𖹢"), + (0x16E43, "M", "𖹣"), + (0x16E44, "M", "𖹤"), + (0x16E45, "M", "𖹥"), + (0x16E46, "M", "𖹦"), + (0x16E47, "M", "𖹧"), + (0x16E48, "M", "𖹨"), + (0x16E49, "M", "𖹩"), + (0x16E4A, "M", "𖹪"), + (0x16E4B, "M", "𖹫"), + (0x16E4C, "M", "𖹬"), + (0x16E4D, "M", "𖹭"), + (0x16E4E, "M", "𖹮"), + (0x16E4F, "M", "𖹯"), + (0x16E50, "M", "𖹰"), + (0x16E51, "M", "𖹱"), + (0x16E52, "M", "𖹲"), + (0x16E53, "M", "𖹳"), + (0x16E54, "M", "𖹴"), + (0x16E55, "M", "𖹵"), + (0x16E56, "M", "𖹶"), + (0x16E57, "M", "𖹷"), + (0x16E58, "M", "𖹸"), + (0x16E59, "M", "𖹹"), + (0x16E5A, "M", "𖹺"), + (0x16E5B, "M", "𖹻"), + (0x16E5C, "M", "𖹼"), + (0x16E5D, "M", "𖹽"), + (0x16E5E, "M", "𖹾"), + (0x16E5F, "M", "𖹿"), + (0x16E60, "V"), + (0x16E9B, "X"), + (0x16F00, "V"), + (0x16F4B, "X"), + (0x16F4F, "V"), + (0x16F88, "X"), + (0x16F8F, "V"), + (0x16FA0, "X"), + (0x16FE0, "V"), + (0x16FE5, "X"), + (0x16FF0, "V"), + (0x16FF2, "X"), + (0x17000, "V"), + (0x187F8, "X"), + (0x18800, "V"), + (0x18CD6, "X"), + (0x18CFF, "V"), + (0x18D09, "X"), + ] + + +def _seg_61() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1AFF0, "V"), + (0x1AFF4, "X"), + (0x1AFF5, "V"), + (0x1AFFC, "X"), + (0x1AFFD, "V"), + (0x1AFFF, "X"), + (0x1B000, "V"), + (0x1B123, "X"), + (0x1B132, "V"), + (0x1B133, "X"), + (0x1B150, "V"), + (0x1B153, "X"), + (0x1B155, "V"), + (0x1B156, "X"), + (0x1B164, "V"), + (0x1B168, "X"), + (0x1B170, "V"), + (0x1B2FC, "X"), + (0x1BC00, "V"), + (0x1BC6B, "X"), + (0x1BC70, "V"), + (0x1BC7D, "X"), + (0x1BC80, "V"), + (0x1BC89, "X"), + (0x1BC90, "V"), + (0x1BC9A, "X"), + (0x1BC9C, "V"), + (0x1BCA0, "I"), + (0x1BCA4, "X"), + (0x1CC00, "V"), + (0x1CCD6, "M", "a"), + (0x1CCD7, "M", "b"), + (0x1CCD8, "M", "c"), + (0x1CCD9, "M", "d"), + (0x1CCDA, "M", "e"), + (0x1CCDB, "M", "f"), + (0x1CCDC, "M", "g"), + (0x1CCDD, "M", "h"), + (0x1CCDE, "M", "i"), + (0x1CCDF, "M", "j"), + (0x1CCE0, "M", "k"), + (0x1CCE1, "M", "l"), + (0x1CCE2, "M", "m"), + (0x1CCE3, "M", "n"), + (0x1CCE4, "M", "o"), + (0x1CCE5, "M", "p"), + (0x1CCE6, "M", "q"), + (0x1CCE7, "M", "r"), + (0x1CCE8, "M", "s"), + (0x1CCE9, "M", "t"), + (0x1CCEA, "M", "u"), + (0x1CCEB, "M", "v"), + (0x1CCEC, "M", "w"), + (0x1CCED, "M", "x"), + (0x1CCEE, "M", "y"), + (0x1CCEF, "M", "z"), + (0x1CCF0, "M", "0"), + (0x1CCF1, "M", "1"), + (0x1CCF2, "M", "2"), + (0x1CCF3, "M", "3"), + (0x1CCF4, "M", "4"), + (0x1CCF5, "M", "5"), + (0x1CCF6, "M", "6"), + (0x1CCF7, "M", "7"), + (0x1CCF8, "M", "8"), + (0x1CCF9, "M", "9"), + (0x1CCFA, "X"), + (0x1CD00, "V"), + (0x1CEB4, "X"), + (0x1CF00, "V"), + (0x1CF2E, "X"), + (0x1CF30, "V"), + (0x1CF47, "X"), + (0x1CF50, "V"), + (0x1CFC4, "X"), + (0x1D000, "V"), + (0x1D0F6, "X"), + (0x1D100, "V"), + (0x1D127, "X"), + (0x1D129, "V"), + (0x1D15E, "M", "𝅗𝅥"), + (0x1D15F, "M", "𝅘𝅥"), + (0x1D160, "M", "𝅘𝅥𝅮"), + (0x1D161, "M", "𝅘𝅥𝅯"), + (0x1D162, "M", "𝅘𝅥𝅰"), + (0x1D163, "M", "𝅘𝅥𝅱"), + (0x1D164, "M", "𝅘𝅥𝅲"), + (0x1D165, "V"), + (0x1D173, "I"), + (0x1D17B, "V"), + (0x1D1BB, "M", "𝆹𝅥"), + (0x1D1BC, "M", "𝆺𝅥"), + (0x1D1BD, "M", "𝆹𝅥𝅮"), + (0x1D1BE, "M", "𝆺𝅥𝅮"), + (0x1D1BF, "M", "𝆹𝅥𝅯"), + (0x1D1C0, "M", "𝆺𝅥𝅯"), + (0x1D1C1, "V"), + (0x1D1EB, "X"), + (0x1D200, "V"), + (0x1D246, "X"), + ] + + +def _seg_62() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D2C0, "V"), + (0x1D2D4, "X"), + (0x1D2E0, "V"), + (0x1D2F4, "X"), + (0x1D300, "V"), + (0x1D357, "X"), + (0x1D360, "V"), + (0x1D379, "X"), + (0x1D400, "M", "a"), + (0x1D401, "M", "b"), + (0x1D402, "M", "c"), + (0x1D403, "M", "d"), + (0x1D404, "M", "e"), + (0x1D405, "M", "f"), + (0x1D406, "M", "g"), + (0x1D407, "M", "h"), + (0x1D408, "M", "i"), + (0x1D409, "M", "j"), + (0x1D40A, "M", "k"), + (0x1D40B, "M", "l"), + (0x1D40C, "M", "m"), + (0x1D40D, "M", "n"), + (0x1D40E, "M", "o"), + (0x1D40F, "M", "p"), + (0x1D410, "M", "q"), + (0x1D411, "M", "r"), + (0x1D412, "M", "s"), + (0x1D413, "M", "t"), + (0x1D414, "M", "u"), + (0x1D415, "M", "v"), + (0x1D416, "M", "w"), + (0x1D417, "M", "x"), + (0x1D418, "M", "y"), + (0x1D419, "M", "z"), + (0x1D41A, "M", "a"), + (0x1D41B, "M", "b"), + (0x1D41C, "M", "c"), + (0x1D41D, "M", "d"), + (0x1D41E, "M", "e"), + (0x1D41F, "M", "f"), + (0x1D420, "M", "g"), + (0x1D421, "M", "h"), + (0x1D422, "M", "i"), + (0x1D423, "M", "j"), + (0x1D424, "M", "k"), + (0x1D425, "M", "l"), + (0x1D426, "M", "m"), + (0x1D427, "M", "n"), + (0x1D428, "M", "o"), + (0x1D429, "M", "p"), + (0x1D42A, "M", "q"), + (0x1D42B, "M", "r"), + (0x1D42C, "M", "s"), + (0x1D42D, "M", "t"), + (0x1D42E, "M", "u"), + (0x1D42F, "M", "v"), + (0x1D430, "M", "w"), + (0x1D431, "M", "x"), + (0x1D432, "M", "y"), + (0x1D433, "M", "z"), + (0x1D434, "M", "a"), + (0x1D435, "M", "b"), + (0x1D436, "M", "c"), + (0x1D437, "M", "d"), + (0x1D438, "M", "e"), + (0x1D439, "M", "f"), + (0x1D43A, "M", "g"), + (0x1D43B, "M", "h"), + (0x1D43C, "M", "i"), + (0x1D43D, "M", "j"), + (0x1D43E, "M", "k"), + (0x1D43F, "M", "l"), + (0x1D440, "M", "m"), + (0x1D441, "M", "n"), + (0x1D442, "M", "o"), + (0x1D443, "M", "p"), + (0x1D444, "M", "q"), + (0x1D445, "M", "r"), + (0x1D446, "M", "s"), + (0x1D447, "M", "t"), + (0x1D448, "M", "u"), + (0x1D449, "M", "v"), + (0x1D44A, "M", "w"), + (0x1D44B, "M", "x"), + (0x1D44C, "M", "y"), + (0x1D44D, "M", "z"), + (0x1D44E, "M", "a"), + (0x1D44F, "M", "b"), + (0x1D450, "M", "c"), + (0x1D451, "M", "d"), + (0x1D452, "M", "e"), + (0x1D453, "M", "f"), + (0x1D454, "M", "g"), + (0x1D455, "X"), + (0x1D456, "M", "i"), + (0x1D457, "M", "j"), + (0x1D458, "M", "k"), + (0x1D459, "M", "l"), + (0x1D45A, "M", "m"), + (0x1D45B, "M", "n"), + ] + + +def _seg_63() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D45C, "M", "o"), + (0x1D45D, "M", "p"), + (0x1D45E, "M", "q"), + (0x1D45F, "M", "r"), + (0x1D460, "M", "s"), + (0x1D461, "M", "t"), + (0x1D462, "M", "u"), + (0x1D463, "M", "v"), + (0x1D464, "M", "w"), + (0x1D465, "M", "x"), + (0x1D466, "M", "y"), + (0x1D467, "M", "z"), + (0x1D468, "M", "a"), + (0x1D469, "M", "b"), + (0x1D46A, "M", "c"), + (0x1D46B, "M", "d"), + (0x1D46C, "M", "e"), + (0x1D46D, "M", "f"), + (0x1D46E, "M", "g"), + (0x1D46F, "M", "h"), + (0x1D470, "M", "i"), + (0x1D471, "M", "j"), + (0x1D472, "M", "k"), + (0x1D473, "M", "l"), + (0x1D474, "M", "m"), + (0x1D475, "M", "n"), + (0x1D476, "M", "o"), + (0x1D477, "M", "p"), + (0x1D478, "M", "q"), + (0x1D479, "M", "r"), + (0x1D47A, "M", "s"), + (0x1D47B, "M", "t"), + (0x1D47C, "M", "u"), + (0x1D47D, "M", "v"), + (0x1D47E, "M", "w"), + (0x1D47F, "M", "x"), + (0x1D480, "M", "y"), + (0x1D481, "M", "z"), + (0x1D482, "M", "a"), + (0x1D483, "M", "b"), + (0x1D484, "M", "c"), + (0x1D485, "M", "d"), + (0x1D486, "M", "e"), + (0x1D487, "M", "f"), + (0x1D488, "M", "g"), + (0x1D489, "M", "h"), + (0x1D48A, "M", "i"), + (0x1D48B, "M", "j"), + (0x1D48C, "M", "k"), + (0x1D48D, "M", "l"), + (0x1D48E, "M", "m"), + (0x1D48F, "M", "n"), + (0x1D490, "M", "o"), + (0x1D491, "M", "p"), + (0x1D492, "M", "q"), + (0x1D493, "M", "r"), + (0x1D494, "M", "s"), + (0x1D495, "M", "t"), + (0x1D496, "M", "u"), + (0x1D497, "M", "v"), + (0x1D498, "M", "w"), + (0x1D499, "M", "x"), + (0x1D49A, "M", "y"), + (0x1D49B, "M", "z"), + (0x1D49C, "M", "a"), + (0x1D49D, "X"), + (0x1D49E, "M", "c"), + (0x1D49F, "M", "d"), + (0x1D4A0, "X"), + (0x1D4A2, "M", "g"), + (0x1D4A3, "X"), + (0x1D4A5, "M", "j"), + (0x1D4A6, "M", "k"), + (0x1D4A7, "X"), + (0x1D4A9, "M", "n"), + (0x1D4AA, "M", "o"), + (0x1D4AB, "M", "p"), + (0x1D4AC, "M", "q"), + (0x1D4AD, "X"), + (0x1D4AE, "M", "s"), + (0x1D4AF, "M", "t"), + (0x1D4B0, "M", "u"), + (0x1D4B1, "M", "v"), + (0x1D4B2, "M", "w"), + (0x1D4B3, "M", "x"), + (0x1D4B4, "M", "y"), + (0x1D4B5, "M", "z"), + (0x1D4B6, "M", "a"), + (0x1D4B7, "M", "b"), + (0x1D4B8, "M", "c"), + (0x1D4B9, "M", "d"), + (0x1D4BA, "X"), + (0x1D4BB, "M", "f"), + (0x1D4BC, "X"), + (0x1D4BD, "M", "h"), + (0x1D4BE, "M", "i"), + (0x1D4BF, "M", "j"), + (0x1D4C0, "M", "k"), + (0x1D4C1, "M", "l"), + (0x1D4C2, "M", "m"), + ] + + +def _seg_64() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D4C3, "M", "n"), + (0x1D4C4, "X"), + (0x1D4C5, "M", "p"), + (0x1D4C6, "M", "q"), + (0x1D4C7, "M", "r"), + (0x1D4C8, "M", "s"), + (0x1D4C9, "M", "t"), + (0x1D4CA, "M", "u"), + (0x1D4CB, "M", "v"), + (0x1D4CC, "M", "w"), + (0x1D4CD, "M", "x"), + (0x1D4CE, "M", "y"), + (0x1D4CF, "M", "z"), + (0x1D4D0, "M", "a"), + (0x1D4D1, "M", "b"), + (0x1D4D2, "M", "c"), + (0x1D4D3, "M", "d"), + (0x1D4D4, "M", "e"), + (0x1D4D5, "M", "f"), + (0x1D4D6, "M", "g"), + (0x1D4D7, "M", "h"), + (0x1D4D8, "M", "i"), + (0x1D4D9, "M", "j"), + (0x1D4DA, "M", "k"), + (0x1D4DB, "M", "l"), + (0x1D4DC, "M", "m"), + (0x1D4DD, "M", "n"), + (0x1D4DE, "M", "o"), + (0x1D4DF, "M", "p"), + (0x1D4E0, "M", "q"), + (0x1D4E1, "M", "r"), + (0x1D4E2, "M", "s"), + (0x1D4E3, "M", "t"), + (0x1D4E4, "M", "u"), + (0x1D4E5, "M", "v"), + (0x1D4E6, "M", "w"), + (0x1D4E7, "M", "x"), + (0x1D4E8, "M", "y"), + (0x1D4E9, "M", "z"), + (0x1D4EA, "M", "a"), + (0x1D4EB, "M", "b"), + (0x1D4EC, "M", "c"), + (0x1D4ED, "M", "d"), + (0x1D4EE, "M", "e"), + (0x1D4EF, "M", "f"), + (0x1D4F0, "M", "g"), + (0x1D4F1, "M", "h"), + (0x1D4F2, "M", "i"), + (0x1D4F3, "M", "j"), + (0x1D4F4, "M", "k"), + (0x1D4F5, "M", "l"), + (0x1D4F6, "M", "m"), + (0x1D4F7, "M", "n"), + (0x1D4F8, "M", "o"), + (0x1D4F9, "M", "p"), + (0x1D4FA, "M", "q"), + (0x1D4FB, "M", "r"), + (0x1D4FC, "M", "s"), + (0x1D4FD, "M", "t"), + (0x1D4FE, "M", "u"), + (0x1D4FF, "M", "v"), + (0x1D500, "M", "w"), + (0x1D501, "M", "x"), + (0x1D502, "M", "y"), + (0x1D503, "M", "z"), + (0x1D504, "M", "a"), + (0x1D505, "M", "b"), + (0x1D506, "X"), + (0x1D507, "M", "d"), + (0x1D508, "M", "e"), + (0x1D509, "M", "f"), + (0x1D50A, "M", "g"), + (0x1D50B, "X"), + (0x1D50D, "M", "j"), + (0x1D50E, "M", "k"), + (0x1D50F, "M", "l"), + (0x1D510, "M", "m"), + (0x1D511, "M", "n"), + (0x1D512, "M", "o"), + (0x1D513, "M", "p"), + (0x1D514, "M", "q"), + (0x1D515, "X"), + (0x1D516, "M", "s"), + (0x1D517, "M", "t"), + (0x1D518, "M", "u"), + (0x1D519, "M", "v"), + (0x1D51A, "M", "w"), + (0x1D51B, "M", "x"), + (0x1D51C, "M", "y"), + (0x1D51D, "X"), + (0x1D51E, "M", "a"), + (0x1D51F, "M", "b"), + (0x1D520, "M", "c"), + (0x1D521, "M", "d"), + (0x1D522, "M", "e"), + (0x1D523, "M", "f"), + (0x1D524, "M", "g"), + (0x1D525, "M", "h"), + (0x1D526, "M", "i"), + (0x1D527, "M", "j"), + ] + + +def _seg_65() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D528, "M", "k"), + (0x1D529, "M", "l"), + (0x1D52A, "M", "m"), + (0x1D52B, "M", "n"), + (0x1D52C, "M", "o"), + (0x1D52D, "M", "p"), + (0x1D52E, "M", "q"), + (0x1D52F, "M", "r"), + (0x1D530, "M", "s"), + (0x1D531, "M", "t"), + (0x1D532, "M", "u"), + (0x1D533, "M", "v"), + (0x1D534, "M", "w"), + (0x1D535, "M", "x"), + (0x1D536, "M", "y"), + (0x1D537, "M", "z"), + (0x1D538, "M", "a"), + (0x1D539, "M", "b"), + (0x1D53A, "X"), + (0x1D53B, "M", "d"), + (0x1D53C, "M", "e"), + (0x1D53D, "M", "f"), + (0x1D53E, "M", "g"), + (0x1D53F, "X"), + (0x1D540, "M", "i"), + (0x1D541, "M", "j"), + (0x1D542, "M", "k"), + (0x1D543, "M", "l"), + (0x1D544, "M", "m"), + (0x1D545, "X"), + (0x1D546, "M", "o"), + (0x1D547, "X"), + (0x1D54A, "M", "s"), + (0x1D54B, "M", "t"), + (0x1D54C, "M", "u"), + (0x1D54D, "M", "v"), + (0x1D54E, "M", "w"), + (0x1D54F, "M", "x"), + (0x1D550, "M", "y"), + (0x1D551, "X"), + (0x1D552, "M", "a"), + (0x1D553, "M", "b"), + (0x1D554, "M", "c"), + (0x1D555, "M", "d"), + (0x1D556, "M", "e"), + (0x1D557, "M", "f"), + (0x1D558, "M", "g"), + (0x1D559, "M", "h"), + (0x1D55A, "M", "i"), + (0x1D55B, "M", "j"), + (0x1D55C, "M", "k"), + (0x1D55D, "M", "l"), + (0x1D55E, "M", "m"), + (0x1D55F, "M", "n"), + (0x1D560, "M", "o"), + (0x1D561, "M", "p"), + (0x1D562, "M", "q"), + (0x1D563, "M", "r"), + (0x1D564, "M", "s"), + (0x1D565, "M", "t"), + (0x1D566, "M", "u"), + (0x1D567, "M", "v"), + (0x1D568, "M", "w"), + (0x1D569, "M", "x"), + (0x1D56A, "M", "y"), + (0x1D56B, "M", "z"), + (0x1D56C, "M", "a"), + (0x1D56D, "M", "b"), + (0x1D56E, "M", "c"), + (0x1D56F, "M", "d"), + (0x1D570, "M", "e"), + (0x1D571, "M", "f"), + (0x1D572, "M", "g"), + (0x1D573, "M", "h"), + (0x1D574, "M", "i"), + (0x1D575, "M", "j"), + (0x1D576, "M", "k"), + (0x1D577, "M", "l"), + (0x1D578, "M", "m"), + (0x1D579, "M", "n"), + (0x1D57A, "M", "o"), + (0x1D57B, "M", "p"), + (0x1D57C, "M", "q"), + (0x1D57D, "M", "r"), + (0x1D57E, "M", "s"), + (0x1D57F, "M", "t"), + (0x1D580, "M", "u"), + (0x1D581, "M", "v"), + (0x1D582, "M", "w"), + (0x1D583, "M", "x"), + (0x1D584, "M", "y"), + (0x1D585, "M", "z"), + (0x1D586, "M", "a"), + (0x1D587, "M", "b"), + (0x1D588, "M", "c"), + (0x1D589, "M", "d"), + (0x1D58A, "M", "e"), + (0x1D58B, "M", "f"), + (0x1D58C, "M", "g"), + (0x1D58D, "M", "h"), + ] + + +def _seg_66() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D58E, "M", "i"), + (0x1D58F, "M", "j"), + (0x1D590, "M", "k"), + (0x1D591, "M", "l"), + (0x1D592, "M", "m"), + (0x1D593, "M", "n"), + (0x1D594, "M", "o"), + (0x1D595, "M", "p"), + (0x1D596, "M", "q"), + (0x1D597, "M", "r"), + (0x1D598, "M", "s"), + (0x1D599, "M", "t"), + (0x1D59A, "M", "u"), + (0x1D59B, "M", "v"), + (0x1D59C, "M", "w"), + (0x1D59D, "M", "x"), + (0x1D59E, "M", "y"), + (0x1D59F, "M", "z"), + (0x1D5A0, "M", "a"), + (0x1D5A1, "M", "b"), + (0x1D5A2, "M", "c"), + (0x1D5A3, "M", "d"), + (0x1D5A4, "M", "e"), + (0x1D5A5, "M", "f"), + (0x1D5A6, "M", "g"), + (0x1D5A7, "M", "h"), + (0x1D5A8, "M", "i"), + (0x1D5A9, "M", "j"), + (0x1D5AA, "M", "k"), + (0x1D5AB, "M", "l"), + (0x1D5AC, "M", "m"), + (0x1D5AD, "M", "n"), + (0x1D5AE, "M", "o"), + (0x1D5AF, "M", "p"), + (0x1D5B0, "M", "q"), + (0x1D5B1, "M", "r"), + (0x1D5B2, "M", "s"), + (0x1D5B3, "M", "t"), + (0x1D5B4, "M", "u"), + (0x1D5B5, "M", "v"), + (0x1D5B6, "M", "w"), + (0x1D5B7, "M", "x"), + (0x1D5B8, "M", "y"), + (0x1D5B9, "M", "z"), + (0x1D5BA, "M", "a"), + (0x1D5BB, "M", "b"), + (0x1D5BC, "M", "c"), + (0x1D5BD, "M", "d"), + (0x1D5BE, "M", "e"), + (0x1D5BF, "M", "f"), + (0x1D5C0, "M", "g"), + (0x1D5C1, "M", "h"), + (0x1D5C2, "M", "i"), + (0x1D5C3, "M", "j"), + (0x1D5C4, "M", "k"), + (0x1D5C5, "M", "l"), + (0x1D5C6, "M", "m"), + (0x1D5C7, "M", "n"), + (0x1D5C8, "M", "o"), + (0x1D5C9, "M", "p"), + (0x1D5CA, "M", "q"), + (0x1D5CB, "M", "r"), + (0x1D5CC, "M", "s"), + (0x1D5CD, "M", "t"), + (0x1D5CE, "M", "u"), + (0x1D5CF, "M", "v"), + (0x1D5D0, "M", "w"), + (0x1D5D1, "M", "x"), + (0x1D5D2, "M", "y"), + (0x1D5D3, "M", "z"), + (0x1D5D4, "M", "a"), + (0x1D5D5, "M", "b"), + (0x1D5D6, "M", "c"), + (0x1D5D7, "M", "d"), + (0x1D5D8, "M", "e"), + (0x1D5D9, "M", "f"), + (0x1D5DA, "M", "g"), + (0x1D5DB, "M", "h"), + (0x1D5DC, "M", "i"), + (0x1D5DD, "M", "j"), + (0x1D5DE, "M", "k"), + (0x1D5DF, "M", "l"), + (0x1D5E0, "M", "m"), + (0x1D5E1, "M", "n"), + (0x1D5E2, "M", "o"), + (0x1D5E3, "M", "p"), + (0x1D5E4, "M", "q"), + (0x1D5E5, "M", "r"), + (0x1D5E6, "M", "s"), + (0x1D5E7, "M", "t"), + (0x1D5E8, "M", "u"), + (0x1D5E9, "M", "v"), + (0x1D5EA, "M", "w"), + (0x1D5EB, "M", "x"), + (0x1D5EC, "M", "y"), + (0x1D5ED, "M", "z"), + (0x1D5EE, "M", "a"), + (0x1D5EF, "M", "b"), + (0x1D5F0, "M", "c"), + (0x1D5F1, "M", "d"), + ] + + +def _seg_67() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D5F2, "M", "e"), + (0x1D5F3, "M", "f"), + (0x1D5F4, "M", "g"), + (0x1D5F5, "M", "h"), + (0x1D5F6, "M", "i"), + (0x1D5F7, "M", "j"), + (0x1D5F8, "M", "k"), + (0x1D5F9, "M", "l"), + (0x1D5FA, "M", "m"), + (0x1D5FB, "M", "n"), + (0x1D5FC, "M", "o"), + (0x1D5FD, "M", "p"), + (0x1D5FE, "M", "q"), + (0x1D5FF, "M", "r"), + (0x1D600, "M", "s"), + (0x1D601, "M", "t"), + (0x1D602, "M", "u"), + (0x1D603, "M", "v"), + (0x1D604, "M", "w"), + (0x1D605, "M", "x"), + (0x1D606, "M", "y"), + (0x1D607, "M", "z"), + (0x1D608, "M", "a"), + (0x1D609, "M", "b"), + (0x1D60A, "M", "c"), + (0x1D60B, "M", "d"), + (0x1D60C, "M", "e"), + (0x1D60D, "M", "f"), + (0x1D60E, "M", "g"), + (0x1D60F, "M", "h"), + (0x1D610, "M", "i"), + (0x1D611, "M", "j"), + (0x1D612, "M", "k"), + (0x1D613, "M", "l"), + (0x1D614, "M", "m"), + (0x1D615, "M", "n"), + (0x1D616, "M", "o"), + (0x1D617, "M", "p"), + (0x1D618, "M", "q"), + (0x1D619, "M", "r"), + (0x1D61A, "M", "s"), + (0x1D61B, "M", "t"), + (0x1D61C, "M", "u"), + (0x1D61D, "M", "v"), + (0x1D61E, "M", "w"), + (0x1D61F, "M", "x"), + (0x1D620, "M", "y"), + (0x1D621, "M", "z"), + (0x1D622, "M", "a"), + (0x1D623, "M", "b"), + (0x1D624, "M", "c"), + (0x1D625, "M", "d"), + (0x1D626, "M", "e"), + (0x1D627, "M", "f"), + (0x1D628, "M", "g"), + (0x1D629, "M", "h"), + (0x1D62A, "M", "i"), + (0x1D62B, "M", "j"), + (0x1D62C, "M", "k"), + (0x1D62D, "M", "l"), + (0x1D62E, "M", "m"), + (0x1D62F, "M", "n"), + (0x1D630, "M", "o"), + (0x1D631, "M", "p"), + (0x1D632, "M", "q"), + (0x1D633, "M", "r"), + (0x1D634, "M", "s"), + (0x1D635, "M", "t"), + (0x1D636, "M", "u"), + (0x1D637, "M", "v"), + (0x1D638, "M", "w"), + (0x1D639, "M", "x"), + (0x1D63A, "M", "y"), + (0x1D63B, "M", "z"), + (0x1D63C, "M", "a"), + (0x1D63D, "M", "b"), + (0x1D63E, "M", "c"), + (0x1D63F, "M", "d"), + (0x1D640, "M", "e"), + (0x1D641, "M", "f"), + (0x1D642, "M", "g"), + (0x1D643, "M", "h"), + (0x1D644, "M", "i"), + (0x1D645, "M", "j"), + (0x1D646, "M", "k"), + (0x1D647, "M", "l"), + (0x1D648, "M", "m"), + (0x1D649, "M", "n"), + (0x1D64A, "M", "o"), + (0x1D64B, "M", "p"), + (0x1D64C, "M", "q"), + (0x1D64D, "M", "r"), + (0x1D64E, "M", "s"), + (0x1D64F, "M", "t"), + (0x1D650, "M", "u"), + (0x1D651, "M", "v"), + (0x1D652, "M", "w"), + (0x1D653, "M", "x"), + (0x1D654, "M", "y"), + (0x1D655, "M", "z"), + ] + + +def _seg_68() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D656, "M", "a"), + (0x1D657, "M", "b"), + (0x1D658, "M", "c"), + (0x1D659, "M", "d"), + (0x1D65A, "M", "e"), + (0x1D65B, "M", "f"), + (0x1D65C, "M", "g"), + (0x1D65D, "M", "h"), + (0x1D65E, "M", "i"), + (0x1D65F, "M", "j"), + (0x1D660, "M", "k"), + (0x1D661, "M", "l"), + (0x1D662, "M", "m"), + (0x1D663, "M", "n"), + (0x1D664, "M", "o"), + (0x1D665, "M", "p"), + (0x1D666, "M", "q"), + (0x1D667, "M", "r"), + (0x1D668, "M", "s"), + (0x1D669, "M", "t"), + (0x1D66A, "M", "u"), + (0x1D66B, "M", "v"), + (0x1D66C, "M", "w"), + (0x1D66D, "M", "x"), + (0x1D66E, "M", "y"), + (0x1D66F, "M", "z"), + (0x1D670, "M", "a"), + (0x1D671, "M", "b"), + (0x1D672, "M", "c"), + (0x1D673, "M", "d"), + (0x1D674, "M", "e"), + (0x1D675, "M", "f"), + (0x1D676, "M", "g"), + (0x1D677, "M", "h"), + (0x1D678, "M", "i"), + (0x1D679, "M", "j"), + (0x1D67A, "M", "k"), + (0x1D67B, "M", "l"), + (0x1D67C, "M", "m"), + (0x1D67D, "M", "n"), + (0x1D67E, "M", "o"), + (0x1D67F, "M", "p"), + (0x1D680, "M", "q"), + (0x1D681, "M", "r"), + (0x1D682, "M", "s"), + (0x1D683, "M", "t"), + (0x1D684, "M", "u"), + (0x1D685, "M", "v"), + (0x1D686, "M", "w"), + (0x1D687, "M", "x"), + (0x1D688, "M", "y"), + (0x1D689, "M", "z"), + (0x1D68A, "M", "a"), + (0x1D68B, "M", "b"), + (0x1D68C, "M", "c"), + (0x1D68D, "M", "d"), + (0x1D68E, "M", "e"), + (0x1D68F, "M", "f"), + (0x1D690, "M", "g"), + (0x1D691, "M", "h"), + (0x1D692, "M", "i"), + (0x1D693, "M", "j"), + (0x1D694, "M", "k"), + (0x1D695, "M", "l"), + (0x1D696, "M", "m"), + (0x1D697, "M", "n"), + (0x1D698, "M", "o"), + (0x1D699, "M", "p"), + (0x1D69A, "M", "q"), + (0x1D69B, "M", "r"), + (0x1D69C, "M", "s"), + (0x1D69D, "M", "t"), + (0x1D69E, "M", "u"), + (0x1D69F, "M", "v"), + (0x1D6A0, "M", "w"), + (0x1D6A1, "M", "x"), + (0x1D6A2, "M", "y"), + (0x1D6A3, "M", "z"), + (0x1D6A4, "M", "ı"), + (0x1D6A5, "M", "ȷ"), + (0x1D6A6, "X"), + (0x1D6A8, "M", "α"), + (0x1D6A9, "M", "β"), + (0x1D6AA, "M", "γ"), + (0x1D6AB, "M", "δ"), + (0x1D6AC, "M", "ε"), + (0x1D6AD, "M", "ζ"), + (0x1D6AE, "M", "η"), + (0x1D6AF, "M", "θ"), + (0x1D6B0, "M", "ι"), + (0x1D6B1, "M", "κ"), + (0x1D6B2, "M", "λ"), + (0x1D6B3, "M", "μ"), + (0x1D6B4, "M", "ν"), + (0x1D6B5, "M", "ξ"), + (0x1D6B6, "M", "ο"), + (0x1D6B7, "M", "π"), + (0x1D6B8, "M", "ρ"), + (0x1D6B9, "M", "θ"), + (0x1D6BA, "M", "σ"), + ] + + +def _seg_69() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D6BB, "M", "τ"), + (0x1D6BC, "M", "υ"), + (0x1D6BD, "M", "φ"), + (0x1D6BE, "M", "χ"), + (0x1D6BF, "M", "ψ"), + (0x1D6C0, "M", "ω"), + (0x1D6C1, "M", "∇"), + (0x1D6C2, "M", "α"), + (0x1D6C3, "M", "β"), + (0x1D6C4, "M", "γ"), + (0x1D6C5, "M", "δ"), + (0x1D6C6, "M", "ε"), + (0x1D6C7, "M", "ζ"), + (0x1D6C8, "M", "η"), + (0x1D6C9, "M", "θ"), + (0x1D6CA, "M", "ι"), + (0x1D6CB, "M", "κ"), + (0x1D6CC, "M", "λ"), + (0x1D6CD, "M", "μ"), + (0x1D6CE, "M", "ν"), + (0x1D6CF, "M", "ξ"), + (0x1D6D0, "M", "ο"), + (0x1D6D1, "M", "π"), + (0x1D6D2, "M", "ρ"), + (0x1D6D3, "M", "σ"), + (0x1D6D5, "M", "τ"), + (0x1D6D6, "M", "υ"), + (0x1D6D7, "M", "φ"), + (0x1D6D8, "M", "χ"), + (0x1D6D9, "M", "ψ"), + (0x1D6DA, "M", "ω"), + (0x1D6DB, "M", "∂"), + (0x1D6DC, "M", "ε"), + (0x1D6DD, "M", "θ"), + (0x1D6DE, "M", "κ"), + (0x1D6DF, "M", "φ"), + (0x1D6E0, "M", "ρ"), + (0x1D6E1, "M", "π"), + (0x1D6E2, "M", "α"), + (0x1D6E3, "M", "β"), + (0x1D6E4, "M", "γ"), + (0x1D6E5, "M", "δ"), + (0x1D6E6, "M", "ε"), + (0x1D6E7, "M", "ζ"), + (0x1D6E8, "M", "η"), + (0x1D6E9, "M", "θ"), + (0x1D6EA, "M", "ι"), + (0x1D6EB, "M", "κ"), + (0x1D6EC, "M", "λ"), + (0x1D6ED, "M", "μ"), + (0x1D6EE, "M", "ν"), + (0x1D6EF, "M", "ξ"), + (0x1D6F0, "M", "ο"), + (0x1D6F1, "M", "π"), + (0x1D6F2, "M", "ρ"), + (0x1D6F3, "M", "θ"), + (0x1D6F4, "M", "σ"), + (0x1D6F5, "M", "τ"), + (0x1D6F6, "M", "υ"), + (0x1D6F7, "M", "φ"), + (0x1D6F8, "M", "χ"), + (0x1D6F9, "M", "ψ"), + (0x1D6FA, "M", "ω"), + (0x1D6FB, "M", "∇"), + (0x1D6FC, "M", "α"), + (0x1D6FD, "M", "β"), + (0x1D6FE, "M", "γ"), + (0x1D6FF, "M", "δ"), + (0x1D700, "M", "ε"), + (0x1D701, "M", "ζ"), + (0x1D702, "M", "η"), + (0x1D703, "M", "θ"), + (0x1D704, "M", "ι"), + (0x1D705, "M", "κ"), + (0x1D706, "M", "λ"), + (0x1D707, "M", "μ"), + (0x1D708, "M", "ν"), + (0x1D709, "M", "ξ"), + (0x1D70A, "M", "ο"), + (0x1D70B, "M", "π"), + (0x1D70C, "M", "ρ"), + (0x1D70D, "M", "σ"), + (0x1D70F, "M", "τ"), + (0x1D710, "M", "υ"), + (0x1D711, "M", "φ"), + (0x1D712, "M", "χ"), + (0x1D713, "M", "ψ"), + (0x1D714, "M", "ω"), + (0x1D715, "M", "∂"), + (0x1D716, "M", "ε"), + (0x1D717, "M", "θ"), + (0x1D718, "M", "κ"), + (0x1D719, "M", "φ"), + (0x1D71A, "M", "ρ"), + (0x1D71B, "M", "π"), + (0x1D71C, "M", "α"), + (0x1D71D, "M", "β"), + (0x1D71E, "M", "γ"), + (0x1D71F, "M", "δ"), + (0x1D720, "M", "ε"), + ] + + +def _seg_70() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D721, "M", "ζ"), + (0x1D722, "M", "η"), + (0x1D723, "M", "θ"), + (0x1D724, "M", "ι"), + (0x1D725, "M", "κ"), + (0x1D726, "M", "λ"), + (0x1D727, "M", "μ"), + (0x1D728, "M", "ν"), + (0x1D729, "M", "ξ"), + (0x1D72A, "M", "ο"), + (0x1D72B, "M", "π"), + (0x1D72C, "M", "ρ"), + (0x1D72D, "M", "θ"), + (0x1D72E, "M", "σ"), + (0x1D72F, "M", "τ"), + (0x1D730, "M", "υ"), + (0x1D731, "M", "φ"), + (0x1D732, "M", "χ"), + (0x1D733, "M", "ψ"), + (0x1D734, "M", "ω"), + (0x1D735, "M", "∇"), + (0x1D736, "M", "α"), + (0x1D737, "M", "β"), + (0x1D738, "M", "γ"), + (0x1D739, "M", "δ"), + (0x1D73A, "M", "ε"), + (0x1D73B, "M", "ζ"), + (0x1D73C, "M", "η"), + (0x1D73D, "M", "θ"), + (0x1D73E, "M", "ι"), + (0x1D73F, "M", "κ"), + (0x1D740, "M", "λ"), + (0x1D741, "M", "μ"), + (0x1D742, "M", "ν"), + (0x1D743, "M", "ξ"), + (0x1D744, "M", "ο"), + (0x1D745, "M", "π"), + (0x1D746, "M", "ρ"), + (0x1D747, "M", "σ"), + (0x1D749, "M", "τ"), + (0x1D74A, "M", "υ"), + (0x1D74B, "M", "φ"), + (0x1D74C, "M", "χ"), + (0x1D74D, "M", "ψ"), + (0x1D74E, "M", "ω"), + (0x1D74F, "M", "∂"), + (0x1D750, "M", "ε"), + (0x1D751, "M", "θ"), + (0x1D752, "M", "κ"), + (0x1D753, "M", "φ"), + (0x1D754, "M", "ρ"), + (0x1D755, "M", "π"), + (0x1D756, "M", "α"), + (0x1D757, "M", "β"), + (0x1D758, "M", "γ"), + (0x1D759, "M", "δ"), + (0x1D75A, "M", "ε"), + (0x1D75B, "M", "ζ"), + (0x1D75C, "M", "η"), + (0x1D75D, "M", "θ"), + (0x1D75E, "M", "ι"), + (0x1D75F, "M", "κ"), + (0x1D760, "M", "λ"), + (0x1D761, "M", "μ"), + (0x1D762, "M", "ν"), + (0x1D763, "M", "ξ"), + (0x1D764, "M", "ο"), + (0x1D765, "M", "π"), + (0x1D766, "M", "ρ"), + (0x1D767, "M", "θ"), + (0x1D768, "M", "σ"), + (0x1D769, "M", "τ"), + (0x1D76A, "M", "υ"), + (0x1D76B, "M", "φ"), + (0x1D76C, "M", "χ"), + (0x1D76D, "M", "ψ"), + (0x1D76E, "M", "ω"), + (0x1D76F, "M", "∇"), + (0x1D770, "M", "α"), + (0x1D771, "M", "β"), + (0x1D772, "M", "γ"), + (0x1D773, "M", "δ"), + (0x1D774, "M", "ε"), + (0x1D775, "M", "ζ"), + (0x1D776, "M", "η"), + (0x1D777, "M", "θ"), + (0x1D778, "M", "ι"), + (0x1D779, "M", "κ"), + (0x1D77A, "M", "λ"), + (0x1D77B, "M", "μ"), + (0x1D77C, "M", "ν"), + (0x1D77D, "M", "ξ"), + (0x1D77E, "M", "ο"), + (0x1D77F, "M", "π"), + (0x1D780, "M", "ρ"), + (0x1D781, "M", "σ"), + (0x1D783, "M", "τ"), + (0x1D784, "M", "υ"), + (0x1D785, "M", "φ"), + (0x1D786, "M", "χ"), + ] + + +def _seg_71() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D787, "M", "ψ"), + (0x1D788, "M", "ω"), + (0x1D789, "M", "∂"), + (0x1D78A, "M", "ε"), + (0x1D78B, "M", "θ"), + (0x1D78C, "M", "κ"), + (0x1D78D, "M", "φ"), + (0x1D78E, "M", "ρ"), + (0x1D78F, "M", "π"), + (0x1D790, "M", "α"), + (0x1D791, "M", "β"), + (0x1D792, "M", "γ"), + (0x1D793, "M", "δ"), + (0x1D794, "M", "ε"), + (0x1D795, "M", "ζ"), + (0x1D796, "M", "η"), + (0x1D797, "M", "θ"), + (0x1D798, "M", "ι"), + (0x1D799, "M", "κ"), + (0x1D79A, "M", "λ"), + (0x1D79B, "M", "μ"), + (0x1D79C, "M", "ν"), + (0x1D79D, "M", "ξ"), + (0x1D79E, "M", "ο"), + (0x1D79F, "M", "π"), + (0x1D7A0, "M", "ρ"), + (0x1D7A1, "M", "θ"), + (0x1D7A2, "M", "σ"), + (0x1D7A3, "M", "τ"), + (0x1D7A4, "M", "υ"), + (0x1D7A5, "M", "φ"), + (0x1D7A6, "M", "χ"), + (0x1D7A7, "M", "ψ"), + (0x1D7A8, "M", "ω"), + (0x1D7A9, "M", "∇"), + (0x1D7AA, "M", "α"), + (0x1D7AB, "M", "β"), + (0x1D7AC, "M", "γ"), + (0x1D7AD, "M", "δ"), + (0x1D7AE, "M", "ε"), + (0x1D7AF, "M", "ζ"), + (0x1D7B0, "M", "η"), + (0x1D7B1, "M", "θ"), + (0x1D7B2, "M", "ι"), + (0x1D7B3, "M", "κ"), + (0x1D7B4, "M", "λ"), + (0x1D7B5, "M", "μ"), + (0x1D7B6, "M", "ν"), + (0x1D7B7, "M", "ξ"), + (0x1D7B8, "M", "ο"), + (0x1D7B9, "M", "π"), + (0x1D7BA, "M", "ρ"), + (0x1D7BB, "M", "σ"), + (0x1D7BD, "M", "τ"), + (0x1D7BE, "M", "υ"), + (0x1D7BF, "M", "φ"), + (0x1D7C0, "M", "χ"), + (0x1D7C1, "M", "ψ"), + (0x1D7C2, "M", "ω"), + (0x1D7C3, "M", "∂"), + (0x1D7C4, "M", "ε"), + (0x1D7C5, "M", "θ"), + (0x1D7C6, "M", "κ"), + (0x1D7C7, "M", "φ"), + (0x1D7C8, "M", "ρ"), + (0x1D7C9, "M", "π"), + (0x1D7CA, "M", "ϝ"), + (0x1D7CC, "X"), + (0x1D7CE, "M", "0"), + (0x1D7CF, "M", "1"), + (0x1D7D0, "M", "2"), + (0x1D7D1, "M", "3"), + (0x1D7D2, "M", "4"), + (0x1D7D3, "M", "5"), + (0x1D7D4, "M", "6"), + (0x1D7D5, "M", "7"), + (0x1D7D6, "M", "8"), + (0x1D7D7, "M", "9"), + (0x1D7D8, "M", "0"), + (0x1D7D9, "M", "1"), + (0x1D7DA, "M", "2"), + (0x1D7DB, "M", "3"), + (0x1D7DC, "M", "4"), + (0x1D7DD, "M", "5"), + (0x1D7DE, "M", "6"), + (0x1D7DF, "M", "7"), + (0x1D7E0, "M", "8"), + (0x1D7E1, "M", "9"), + (0x1D7E2, "M", "0"), + (0x1D7E3, "M", "1"), + (0x1D7E4, "M", "2"), + (0x1D7E5, "M", "3"), + (0x1D7E6, "M", "4"), + (0x1D7E7, "M", "5"), + (0x1D7E8, "M", "6"), + (0x1D7E9, "M", "7"), + (0x1D7EA, "M", "8"), + (0x1D7EB, "M", "9"), + (0x1D7EC, "M", "0"), + (0x1D7ED, "M", "1"), + ] + + +def _seg_72() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1D7EE, "M", "2"), + (0x1D7EF, "M", "3"), + (0x1D7F0, "M", "4"), + (0x1D7F1, "M", "5"), + (0x1D7F2, "M", "6"), + (0x1D7F3, "M", "7"), + (0x1D7F4, "M", "8"), + (0x1D7F5, "M", "9"), + (0x1D7F6, "M", "0"), + (0x1D7F7, "M", "1"), + (0x1D7F8, "M", "2"), + (0x1D7F9, "M", "3"), + (0x1D7FA, "M", "4"), + (0x1D7FB, "M", "5"), + (0x1D7FC, "M", "6"), + (0x1D7FD, "M", "7"), + (0x1D7FE, "M", "8"), + (0x1D7FF, "M", "9"), + (0x1D800, "V"), + (0x1DA8C, "X"), + (0x1DA9B, "V"), + (0x1DAA0, "X"), + (0x1DAA1, "V"), + (0x1DAB0, "X"), + (0x1DF00, "V"), + (0x1DF1F, "X"), + (0x1DF25, "V"), + (0x1DF2B, "X"), + (0x1E000, "V"), + (0x1E007, "X"), + (0x1E008, "V"), + (0x1E019, "X"), + (0x1E01B, "V"), + (0x1E022, "X"), + (0x1E023, "V"), + (0x1E025, "X"), + (0x1E026, "V"), + (0x1E02B, "X"), + (0x1E030, "M", "а"), + (0x1E031, "M", "б"), + (0x1E032, "M", "в"), + (0x1E033, "M", "г"), + (0x1E034, "M", "д"), + (0x1E035, "M", "е"), + (0x1E036, "M", "ж"), + (0x1E037, "M", "з"), + (0x1E038, "M", "и"), + (0x1E039, "M", "к"), + (0x1E03A, "M", "л"), + (0x1E03B, "M", "м"), + (0x1E03C, "M", "о"), + (0x1E03D, "M", "п"), + (0x1E03E, "M", "р"), + (0x1E03F, "M", "с"), + (0x1E040, "M", "т"), + (0x1E041, "M", "у"), + (0x1E042, "M", "ф"), + (0x1E043, "M", "х"), + (0x1E044, "M", "ц"), + (0x1E045, "M", "ч"), + (0x1E046, "M", "ш"), + (0x1E047, "M", "ы"), + (0x1E048, "M", "э"), + (0x1E049, "M", "ю"), + (0x1E04A, "M", "ꚉ"), + (0x1E04B, "M", "ә"), + (0x1E04C, "M", "і"), + (0x1E04D, "M", "ј"), + (0x1E04E, "M", "ө"), + (0x1E04F, "M", "ү"), + (0x1E050, "M", "ӏ"), + (0x1E051, "M", "а"), + (0x1E052, "M", "б"), + (0x1E053, "M", "в"), + (0x1E054, "M", "г"), + (0x1E055, "M", "д"), + (0x1E056, "M", "е"), + (0x1E057, "M", "ж"), + (0x1E058, "M", "з"), + (0x1E059, "M", "и"), + (0x1E05A, "M", "к"), + (0x1E05B, "M", "л"), + (0x1E05C, "M", "о"), + (0x1E05D, "M", "п"), + (0x1E05E, "M", "с"), + (0x1E05F, "M", "у"), + (0x1E060, "M", "ф"), + (0x1E061, "M", "х"), + (0x1E062, "M", "ц"), + (0x1E063, "M", "ч"), + (0x1E064, "M", "ш"), + (0x1E065, "M", "ъ"), + (0x1E066, "M", "ы"), + (0x1E067, "M", "ґ"), + (0x1E068, "M", "і"), + (0x1E069, "M", "ѕ"), + (0x1E06A, "M", "џ"), + (0x1E06B, "M", "ҫ"), + (0x1E06C, "M", "ꙑ"), + (0x1E06D, "M", "ұ"), + ] + + +def _seg_73() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1E06E, "X"), + (0x1E08F, "V"), + (0x1E090, "X"), + (0x1E100, "V"), + (0x1E12D, "X"), + (0x1E130, "V"), + (0x1E13E, "X"), + (0x1E140, "V"), + (0x1E14A, "X"), + (0x1E14E, "V"), + (0x1E150, "X"), + (0x1E290, "V"), + (0x1E2AF, "X"), + (0x1E2C0, "V"), + (0x1E2FA, "X"), + (0x1E2FF, "V"), + (0x1E300, "X"), + (0x1E4D0, "V"), + (0x1E4FA, "X"), + (0x1E5D0, "V"), + (0x1E5FB, "X"), + (0x1E5FF, "V"), + (0x1E600, "X"), + (0x1E7E0, "V"), + (0x1E7E7, "X"), + (0x1E7E8, "V"), + (0x1E7EC, "X"), + (0x1E7ED, "V"), + (0x1E7EF, "X"), + (0x1E7F0, "V"), + (0x1E7FF, "X"), + (0x1E800, "V"), + (0x1E8C5, "X"), + (0x1E8C7, "V"), + (0x1E8D7, "X"), + (0x1E900, "M", "𞤢"), + (0x1E901, "M", "𞤣"), + (0x1E902, "M", "𞤤"), + (0x1E903, "M", "𞤥"), + (0x1E904, "M", "𞤦"), + (0x1E905, "M", "𞤧"), + (0x1E906, "M", "𞤨"), + (0x1E907, "M", "𞤩"), + (0x1E908, "M", "𞤪"), + (0x1E909, "M", "𞤫"), + (0x1E90A, "M", "𞤬"), + (0x1E90B, "M", "𞤭"), + (0x1E90C, "M", "𞤮"), + (0x1E90D, "M", "𞤯"), + (0x1E90E, "M", "𞤰"), + (0x1E90F, "M", "𞤱"), + (0x1E910, "M", "𞤲"), + (0x1E911, "M", "𞤳"), + (0x1E912, "M", "𞤴"), + (0x1E913, "M", "𞤵"), + (0x1E914, "M", "𞤶"), + (0x1E915, "M", "𞤷"), + (0x1E916, "M", "𞤸"), + (0x1E917, "M", "𞤹"), + (0x1E918, "M", "𞤺"), + (0x1E919, "M", "𞤻"), + (0x1E91A, "M", "𞤼"), + (0x1E91B, "M", "𞤽"), + (0x1E91C, "M", "𞤾"), + (0x1E91D, "M", "𞤿"), + (0x1E91E, "M", "𞥀"), + (0x1E91F, "M", "𞥁"), + (0x1E920, "M", "𞥂"), + (0x1E921, "M", "𞥃"), + (0x1E922, "V"), + (0x1E94C, "X"), + (0x1E950, "V"), + (0x1E95A, "X"), + (0x1E95E, "V"), + (0x1E960, "X"), + (0x1EC71, "V"), + (0x1ECB5, "X"), + (0x1ED01, "V"), + (0x1ED3E, "X"), + (0x1EE00, "M", "ا"), + (0x1EE01, "M", "ب"), + (0x1EE02, "M", "ج"), + (0x1EE03, "M", "د"), + (0x1EE04, "X"), + (0x1EE05, "M", "و"), + (0x1EE06, "M", "ز"), + (0x1EE07, "M", "ح"), + (0x1EE08, "M", "ط"), + (0x1EE09, "M", "ي"), + (0x1EE0A, "M", "ك"), + (0x1EE0B, "M", "ل"), + (0x1EE0C, "M", "م"), + (0x1EE0D, "M", "ن"), + (0x1EE0E, "M", "س"), + (0x1EE0F, "M", "ع"), + (0x1EE10, "M", "ف"), + (0x1EE11, "M", "ص"), + (0x1EE12, "M", "ق"), + (0x1EE13, "M", "ر"), + (0x1EE14, "M", "ش"), + ] + + +def _seg_74() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1EE15, "M", "ت"), + (0x1EE16, "M", "ث"), + (0x1EE17, "M", "خ"), + (0x1EE18, "M", "ذ"), + (0x1EE19, "M", "ض"), + (0x1EE1A, "M", "ظ"), + (0x1EE1B, "M", "غ"), + (0x1EE1C, "M", "ٮ"), + (0x1EE1D, "M", "ں"), + (0x1EE1E, "M", "ڡ"), + (0x1EE1F, "M", "ٯ"), + (0x1EE20, "X"), + (0x1EE21, "M", "ب"), + (0x1EE22, "M", "ج"), + (0x1EE23, "X"), + (0x1EE24, "M", "ه"), + (0x1EE25, "X"), + (0x1EE27, "M", "ح"), + (0x1EE28, "X"), + (0x1EE29, "M", "ي"), + (0x1EE2A, "M", "ك"), + (0x1EE2B, "M", "ل"), + (0x1EE2C, "M", "م"), + (0x1EE2D, "M", "ن"), + (0x1EE2E, "M", "س"), + (0x1EE2F, "M", "ع"), + (0x1EE30, "M", "ف"), + (0x1EE31, "M", "ص"), + (0x1EE32, "M", "ق"), + (0x1EE33, "X"), + (0x1EE34, "M", "ش"), + (0x1EE35, "M", "ت"), + (0x1EE36, "M", "ث"), + (0x1EE37, "M", "خ"), + (0x1EE38, "X"), + (0x1EE39, "M", "ض"), + (0x1EE3A, "X"), + (0x1EE3B, "M", "غ"), + (0x1EE3C, "X"), + (0x1EE42, "M", "ج"), + (0x1EE43, "X"), + (0x1EE47, "M", "ح"), + (0x1EE48, "X"), + (0x1EE49, "M", "ي"), + (0x1EE4A, "X"), + (0x1EE4B, "M", "ل"), + (0x1EE4C, "X"), + (0x1EE4D, "M", "ن"), + (0x1EE4E, "M", "س"), + (0x1EE4F, "M", "ع"), + (0x1EE50, "X"), + (0x1EE51, "M", "ص"), + (0x1EE52, "M", "ق"), + (0x1EE53, "X"), + (0x1EE54, "M", "ش"), + (0x1EE55, "X"), + (0x1EE57, "M", "خ"), + (0x1EE58, "X"), + (0x1EE59, "M", "ض"), + (0x1EE5A, "X"), + (0x1EE5B, "M", "غ"), + (0x1EE5C, "X"), + (0x1EE5D, "M", "ں"), + (0x1EE5E, "X"), + (0x1EE5F, "M", "ٯ"), + (0x1EE60, "X"), + (0x1EE61, "M", "ب"), + (0x1EE62, "M", "ج"), + (0x1EE63, "X"), + (0x1EE64, "M", "ه"), + (0x1EE65, "X"), + (0x1EE67, "M", "ح"), + (0x1EE68, "M", "ط"), + (0x1EE69, "M", "ي"), + (0x1EE6A, "M", "ك"), + (0x1EE6B, "X"), + (0x1EE6C, "M", "م"), + (0x1EE6D, "M", "ن"), + (0x1EE6E, "M", "س"), + (0x1EE6F, "M", "ع"), + (0x1EE70, "M", "ف"), + (0x1EE71, "M", "ص"), + (0x1EE72, "M", "ق"), + (0x1EE73, "X"), + (0x1EE74, "M", "ش"), + (0x1EE75, "M", "ت"), + (0x1EE76, "M", "ث"), + (0x1EE77, "M", "خ"), + (0x1EE78, "X"), + (0x1EE79, "M", "ض"), + (0x1EE7A, "M", "ظ"), + (0x1EE7B, "M", "غ"), + (0x1EE7C, "M", "ٮ"), + (0x1EE7D, "X"), + (0x1EE7E, "M", "ڡ"), + (0x1EE7F, "X"), + (0x1EE80, "M", "ا"), + (0x1EE81, "M", "ب"), + (0x1EE82, "M", "ج"), + (0x1EE83, "M", "د"), + ] + + +def _seg_75() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1EE84, "M", "ه"), + (0x1EE85, "M", "و"), + (0x1EE86, "M", "ز"), + (0x1EE87, "M", "ح"), + (0x1EE88, "M", "ط"), + (0x1EE89, "M", "ي"), + (0x1EE8A, "X"), + (0x1EE8B, "M", "ل"), + (0x1EE8C, "M", "م"), + (0x1EE8D, "M", "ن"), + (0x1EE8E, "M", "س"), + (0x1EE8F, "M", "ع"), + (0x1EE90, "M", "ف"), + (0x1EE91, "M", "ص"), + (0x1EE92, "M", "ق"), + (0x1EE93, "M", "ر"), + (0x1EE94, "M", "ش"), + (0x1EE95, "M", "ت"), + (0x1EE96, "M", "ث"), + (0x1EE97, "M", "خ"), + (0x1EE98, "M", "ذ"), + (0x1EE99, "M", "ض"), + (0x1EE9A, "M", "ظ"), + (0x1EE9B, "M", "غ"), + (0x1EE9C, "X"), + (0x1EEA1, "M", "ب"), + (0x1EEA2, "M", "ج"), + (0x1EEA3, "M", "د"), + (0x1EEA4, "X"), + (0x1EEA5, "M", "و"), + (0x1EEA6, "M", "ز"), + (0x1EEA7, "M", "ح"), + (0x1EEA8, "M", "ط"), + (0x1EEA9, "M", "ي"), + (0x1EEAA, "X"), + (0x1EEAB, "M", "ل"), + (0x1EEAC, "M", "م"), + (0x1EEAD, "M", "ن"), + (0x1EEAE, "M", "س"), + (0x1EEAF, "M", "ع"), + (0x1EEB0, "M", "ف"), + (0x1EEB1, "M", "ص"), + (0x1EEB2, "M", "ق"), + (0x1EEB3, "M", "ر"), + (0x1EEB4, "M", "ش"), + (0x1EEB5, "M", "ت"), + (0x1EEB6, "M", "ث"), + (0x1EEB7, "M", "خ"), + (0x1EEB8, "M", "ذ"), + (0x1EEB9, "M", "ض"), + (0x1EEBA, "M", "ظ"), + (0x1EEBB, "M", "غ"), + (0x1EEBC, "X"), + (0x1EEF0, "V"), + (0x1EEF2, "X"), + (0x1F000, "V"), + (0x1F02C, "X"), + (0x1F030, "V"), + (0x1F094, "X"), + (0x1F0A0, "V"), + (0x1F0AF, "X"), + (0x1F0B1, "V"), + (0x1F0C0, "X"), + (0x1F0C1, "V"), + (0x1F0D0, "X"), + (0x1F0D1, "V"), + (0x1F0F6, "X"), + (0x1F101, "M", "0,"), + (0x1F102, "M", "1,"), + (0x1F103, "M", "2,"), + (0x1F104, "M", "3,"), + (0x1F105, "M", "4,"), + (0x1F106, "M", "5,"), + (0x1F107, "M", "6,"), + (0x1F108, "M", "7,"), + (0x1F109, "M", "8,"), + (0x1F10A, "M", "9,"), + (0x1F10B, "V"), + (0x1F110, "M", "(a)"), + (0x1F111, "M", "(b)"), + (0x1F112, "M", "(c)"), + (0x1F113, "M", "(d)"), + (0x1F114, "M", "(e)"), + (0x1F115, "M", "(f)"), + (0x1F116, "M", "(g)"), + (0x1F117, "M", "(h)"), + (0x1F118, "M", "(i)"), + (0x1F119, "M", "(j)"), + (0x1F11A, "M", "(k)"), + (0x1F11B, "M", "(l)"), + (0x1F11C, "M", "(m)"), + (0x1F11D, "M", "(n)"), + (0x1F11E, "M", "(o)"), + (0x1F11F, "M", "(p)"), + (0x1F120, "M", "(q)"), + (0x1F121, "M", "(r)"), + (0x1F122, "M", "(s)"), + (0x1F123, "M", "(t)"), + (0x1F124, "M", "(u)"), + (0x1F125, "M", "(v)"), + ] + + +def _seg_76() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1F126, "M", "(w)"), + (0x1F127, "M", "(x)"), + (0x1F128, "M", "(y)"), + (0x1F129, "M", "(z)"), + (0x1F12A, "M", "〔s〕"), + (0x1F12B, "M", "c"), + (0x1F12C, "M", "r"), + (0x1F12D, "M", "cd"), + (0x1F12E, "M", "wz"), + (0x1F12F, "V"), + (0x1F130, "M", "a"), + (0x1F131, "M", "b"), + (0x1F132, "M", "c"), + (0x1F133, "M", "d"), + (0x1F134, "M", "e"), + (0x1F135, "M", "f"), + (0x1F136, "M", "g"), + (0x1F137, "M", "h"), + (0x1F138, "M", "i"), + (0x1F139, "M", "j"), + (0x1F13A, "M", "k"), + (0x1F13B, "M", "l"), + (0x1F13C, "M", "m"), + (0x1F13D, "M", "n"), + (0x1F13E, "M", "o"), + (0x1F13F, "M", "p"), + (0x1F140, "M", "q"), + (0x1F141, "M", "r"), + (0x1F142, "M", "s"), + (0x1F143, "M", "t"), + (0x1F144, "M", "u"), + (0x1F145, "M", "v"), + (0x1F146, "M", "w"), + (0x1F147, "M", "x"), + (0x1F148, "M", "y"), + (0x1F149, "M", "z"), + (0x1F14A, "M", "hv"), + (0x1F14B, "M", "mv"), + (0x1F14C, "M", "sd"), + (0x1F14D, "M", "ss"), + (0x1F14E, "M", "ppv"), + (0x1F14F, "M", "wc"), + (0x1F150, "V"), + (0x1F16A, "M", "mc"), + (0x1F16B, "M", "md"), + (0x1F16C, "M", "mr"), + (0x1F16D, "V"), + (0x1F190, "M", "dj"), + (0x1F191, "V"), + (0x1F1AE, "X"), + (0x1F1E6, "V"), + (0x1F200, "M", "ほか"), + (0x1F201, "M", "ココ"), + (0x1F202, "M", "サ"), + (0x1F203, "X"), + (0x1F210, "M", "手"), + (0x1F211, "M", "字"), + (0x1F212, "M", "双"), + (0x1F213, "M", "デ"), + (0x1F214, "M", "二"), + (0x1F215, "M", "多"), + (0x1F216, "M", "解"), + (0x1F217, "M", "天"), + (0x1F218, "M", "交"), + (0x1F219, "M", "映"), + (0x1F21A, "M", "無"), + (0x1F21B, "M", "料"), + (0x1F21C, "M", "前"), + (0x1F21D, "M", "後"), + (0x1F21E, "M", "再"), + (0x1F21F, "M", "新"), + (0x1F220, "M", "初"), + (0x1F221, "M", "終"), + (0x1F222, "M", "生"), + (0x1F223, "M", "販"), + (0x1F224, "M", "声"), + (0x1F225, "M", "吹"), + (0x1F226, "M", "演"), + (0x1F227, "M", "投"), + (0x1F228, "M", "捕"), + (0x1F229, "M", "一"), + (0x1F22A, "M", "三"), + (0x1F22B, "M", "遊"), + (0x1F22C, "M", "左"), + (0x1F22D, "M", "中"), + (0x1F22E, "M", "右"), + (0x1F22F, "M", "指"), + (0x1F230, "M", "走"), + (0x1F231, "M", "打"), + (0x1F232, "M", "禁"), + (0x1F233, "M", "空"), + (0x1F234, "M", "合"), + (0x1F235, "M", "満"), + (0x1F236, "M", "有"), + (0x1F237, "M", "月"), + (0x1F238, "M", "申"), + (0x1F239, "M", "割"), + (0x1F23A, "M", "営"), + (0x1F23B, "M", "配"), + (0x1F23C, "X"), + ] + + +def _seg_77() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x1F240, "M", "〔本〕"), + (0x1F241, "M", "〔三〕"), + (0x1F242, "M", "〔二〕"), + (0x1F243, "M", "〔安〕"), + (0x1F244, "M", "〔点〕"), + (0x1F245, "M", "〔打〕"), + (0x1F246, "M", "〔盗〕"), + (0x1F247, "M", "〔勝〕"), + (0x1F248, "M", "〔敗〕"), + (0x1F249, "X"), + (0x1F250, "M", "得"), + (0x1F251, "M", "可"), + (0x1F252, "X"), + (0x1F260, "V"), + (0x1F266, "X"), + (0x1F300, "V"), + (0x1F6D8, "X"), + (0x1F6DC, "V"), + (0x1F6ED, "X"), + (0x1F6F0, "V"), + (0x1F6FD, "X"), + (0x1F700, "V"), + (0x1F777, "X"), + (0x1F77B, "V"), + (0x1F7DA, "X"), + (0x1F7E0, "V"), + (0x1F7EC, "X"), + (0x1F7F0, "V"), + (0x1F7F1, "X"), + (0x1F800, "V"), + (0x1F80C, "X"), + (0x1F810, "V"), + (0x1F848, "X"), + (0x1F850, "V"), + (0x1F85A, "X"), + (0x1F860, "V"), + (0x1F888, "X"), + (0x1F890, "V"), + (0x1F8AE, "X"), + (0x1F8B0, "V"), + (0x1F8BC, "X"), + (0x1F8C0, "V"), + (0x1F8C2, "X"), + (0x1F900, "V"), + (0x1FA54, "X"), + (0x1FA60, "V"), + (0x1FA6E, "X"), + (0x1FA70, "V"), + (0x1FA7D, "X"), + (0x1FA80, "V"), + (0x1FA8A, "X"), + (0x1FA8F, "V"), + (0x1FAC7, "X"), + (0x1FACE, "V"), + (0x1FADD, "X"), + (0x1FADF, "V"), + (0x1FAEA, "X"), + (0x1FAF0, "V"), + (0x1FAF9, "X"), + (0x1FB00, "V"), + (0x1FB93, "X"), + (0x1FB94, "V"), + (0x1FBF0, "M", "0"), + (0x1FBF1, "M", "1"), + (0x1FBF2, "M", "2"), + (0x1FBF3, "M", "3"), + (0x1FBF4, "M", "4"), + (0x1FBF5, "M", "5"), + (0x1FBF6, "M", "6"), + (0x1FBF7, "M", "7"), + (0x1FBF8, "M", "8"), + (0x1FBF9, "M", "9"), + (0x1FBFA, "X"), + (0x20000, "V"), + (0x2A6E0, "X"), + (0x2A700, "V"), + (0x2B73A, "X"), + (0x2B740, "V"), + (0x2B81E, "X"), + (0x2B820, "V"), + (0x2CEA2, "X"), + (0x2CEB0, "V"), + (0x2EBE1, "X"), + (0x2EBF0, "V"), + (0x2EE5E, "X"), + (0x2F800, "M", "丽"), + (0x2F801, "M", "丸"), + (0x2F802, "M", "乁"), + (0x2F803, "M", "𠄢"), + (0x2F804, "M", "你"), + (0x2F805, "M", "侮"), + (0x2F806, "M", "侻"), + (0x2F807, "M", "倂"), + (0x2F808, "M", "偺"), + (0x2F809, "M", "備"), + (0x2F80A, "M", "僧"), + (0x2F80B, "M", "像"), + (0x2F80C, "M", "㒞"), + (0x2F80D, "M", "𠘺"), + (0x2F80E, "M", "免"), + ] + + +def _seg_78() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F80F, "M", "兔"), + (0x2F810, "M", "兤"), + (0x2F811, "M", "具"), + (0x2F812, "M", "𠔜"), + (0x2F813, "M", "㒹"), + (0x2F814, "M", "內"), + (0x2F815, "M", "再"), + (0x2F816, "M", "𠕋"), + (0x2F817, "M", "冗"), + (0x2F818, "M", "冤"), + (0x2F819, "M", "仌"), + (0x2F81A, "M", "冬"), + (0x2F81B, "M", "况"), + (0x2F81C, "M", "𩇟"), + (0x2F81D, "M", "凵"), + (0x2F81E, "M", "刃"), + (0x2F81F, "M", "㓟"), + (0x2F820, "M", "刻"), + (0x2F821, "M", "剆"), + (0x2F822, "M", "割"), + (0x2F823, "M", "剷"), + (0x2F824, "M", "㔕"), + (0x2F825, "M", "勇"), + (0x2F826, "M", "勉"), + (0x2F827, "M", "勤"), + (0x2F828, "M", "勺"), + (0x2F829, "M", "包"), + (0x2F82A, "M", "匆"), + (0x2F82B, "M", "北"), + (0x2F82C, "M", "卉"), + (0x2F82D, "M", "卑"), + (0x2F82E, "M", "博"), + (0x2F82F, "M", "即"), + (0x2F830, "M", "卽"), + (0x2F831, "M", "卿"), + (0x2F834, "M", "𠨬"), + (0x2F835, "M", "灰"), + (0x2F836, "M", "及"), + (0x2F837, "M", "叟"), + (0x2F838, "M", "𠭣"), + (0x2F839, "M", "叫"), + (0x2F83A, "M", "叱"), + (0x2F83B, "M", "吆"), + (0x2F83C, "M", "咞"), + (0x2F83D, "M", "吸"), + (0x2F83E, "M", "呈"), + (0x2F83F, "M", "周"), + (0x2F840, "M", "咢"), + (0x2F841, "M", "哶"), + (0x2F842, "M", "唐"), + (0x2F843, "M", "啓"), + (0x2F844, "M", "啣"), + (0x2F845, "M", "善"), + (0x2F847, "M", "喙"), + (0x2F848, "M", "喫"), + (0x2F849, "M", "喳"), + (0x2F84A, "M", "嗂"), + (0x2F84B, "M", "圖"), + (0x2F84C, "M", "嘆"), + (0x2F84D, "M", "圗"), + (0x2F84E, "M", "噑"), + (0x2F84F, "M", "噴"), + (0x2F850, "M", "切"), + (0x2F851, "M", "壮"), + (0x2F852, "M", "城"), + (0x2F853, "M", "埴"), + (0x2F854, "M", "堍"), + (0x2F855, "M", "型"), + (0x2F856, "M", "堲"), + (0x2F857, "M", "報"), + (0x2F858, "M", "墬"), + (0x2F859, "M", "𡓤"), + (0x2F85A, "M", "売"), + (0x2F85B, "M", "壷"), + (0x2F85C, "M", "夆"), + (0x2F85D, "M", "多"), + (0x2F85E, "M", "夢"), + (0x2F85F, "M", "奢"), + (0x2F860, "M", "𡚨"), + (0x2F861, "M", "𡛪"), + (0x2F862, "M", "姬"), + (0x2F863, "M", "娛"), + (0x2F864, "M", "娧"), + (0x2F865, "M", "姘"), + (0x2F866, "M", "婦"), + (0x2F867, "M", "㛮"), + (0x2F868, "M", "㛼"), + (0x2F869, "M", "嬈"), + (0x2F86A, "M", "嬾"), + (0x2F86C, "M", "𡧈"), + (0x2F86D, "M", "寃"), + (0x2F86E, "M", "寘"), + (0x2F86F, "M", "寧"), + (0x2F870, "M", "寳"), + (0x2F871, "M", "𡬘"), + (0x2F872, "M", "寿"), + (0x2F873, "M", "将"), + (0x2F874, "M", "当"), + (0x2F875, "M", "尢"), + (0x2F876, "M", "㞁"), + ] + + +def _seg_79() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F877, "M", "屠"), + (0x2F878, "M", "屮"), + (0x2F879, "M", "峀"), + (0x2F87A, "M", "岍"), + (0x2F87B, "M", "𡷤"), + (0x2F87C, "M", "嵃"), + (0x2F87D, "M", "𡷦"), + (0x2F87E, "M", "嵮"), + (0x2F87F, "M", "嵫"), + (0x2F880, "M", "嵼"), + (0x2F881, "M", "巡"), + (0x2F882, "M", "巢"), + (0x2F883, "M", "㠯"), + (0x2F884, "M", "巽"), + (0x2F885, "M", "帨"), + (0x2F886, "M", "帽"), + (0x2F887, "M", "幩"), + (0x2F888, "M", "㡢"), + (0x2F889, "M", "𢆃"), + (0x2F88A, "M", "㡼"), + (0x2F88B, "M", "庰"), + (0x2F88C, "M", "庳"), + (0x2F88D, "M", "庶"), + (0x2F88E, "M", "廊"), + (0x2F88F, "M", "𪎒"), + (0x2F890, "M", "廾"), + (0x2F891, "M", "𢌱"), + (0x2F893, "M", "舁"), + (0x2F894, "M", "弢"), + (0x2F896, "M", "㣇"), + (0x2F897, "M", "𣊸"), + (0x2F898, "M", "𦇚"), + (0x2F899, "M", "形"), + (0x2F89A, "M", "彫"), + (0x2F89B, "M", "㣣"), + (0x2F89C, "M", "徚"), + (0x2F89D, "M", "忍"), + (0x2F89E, "M", "志"), + (0x2F89F, "M", "忹"), + (0x2F8A0, "M", "悁"), + (0x2F8A1, "M", "㤺"), + (0x2F8A2, "M", "㤜"), + (0x2F8A3, "M", "悔"), + (0x2F8A4, "M", "𢛔"), + (0x2F8A5, "M", "惇"), + (0x2F8A6, "M", "慈"), + (0x2F8A7, "M", "慌"), + (0x2F8A8, "M", "慎"), + (0x2F8A9, "M", "慌"), + (0x2F8AA, "M", "慺"), + (0x2F8AB, "M", "憎"), + (0x2F8AC, "M", "憲"), + (0x2F8AD, "M", "憤"), + (0x2F8AE, "M", "憯"), + (0x2F8AF, "M", "懞"), + (0x2F8B0, "M", "懲"), + (0x2F8B1, "M", "懶"), + (0x2F8B2, "M", "成"), + (0x2F8B3, "M", "戛"), + (0x2F8B4, "M", "扝"), + (0x2F8B5, "M", "抱"), + (0x2F8B6, "M", "拔"), + (0x2F8B7, "M", "捐"), + (0x2F8B8, "M", "𢬌"), + (0x2F8B9, "M", "挽"), + (0x2F8BA, "M", "拼"), + (0x2F8BB, "M", "捨"), + (0x2F8BC, "M", "掃"), + (0x2F8BD, "M", "揤"), + (0x2F8BE, "M", "𢯱"), + (0x2F8BF, "M", "搢"), + (0x2F8C0, "M", "揅"), + (0x2F8C1, "M", "掩"), + (0x2F8C2, "M", "㨮"), + (0x2F8C3, "M", "摩"), + (0x2F8C4, "M", "摾"), + (0x2F8C5, "M", "撝"), + (0x2F8C6, "M", "摷"), + (0x2F8C7, "M", "㩬"), + (0x2F8C8, "M", "敏"), + (0x2F8C9, "M", "敬"), + (0x2F8CA, "M", "𣀊"), + (0x2F8CB, "M", "旣"), + (0x2F8CC, "M", "書"), + (0x2F8CD, "M", "晉"), + (0x2F8CE, "M", "㬙"), + (0x2F8CF, "M", "暑"), + (0x2F8D0, "M", "㬈"), + (0x2F8D1, "M", "㫤"), + (0x2F8D2, "M", "冒"), + (0x2F8D3, "M", "冕"), + (0x2F8D4, "M", "最"), + (0x2F8D5, "M", "暜"), + (0x2F8D6, "M", "肭"), + (0x2F8D7, "M", "䏙"), + (0x2F8D8, "M", "朗"), + (0x2F8D9, "M", "望"), + (0x2F8DA, "M", "朡"), + (0x2F8DB, "M", "杞"), + (0x2F8DC, "M", "杓"), + ] + + +def _seg_80() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F8DD, "M", "𣏃"), + (0x2F8DE, "M", "㭉"), + (0x2F8DF, "M", "柺"), + (0x2F8E0, "M", "枅"), + (0x2F8E1, "M", "桒"), + (0x2F8E2, "M", "梅"), + (0x2F8E3, "M", "𣑭"), + (0x2F8E4, "M", "梎"), + (0x2F8E5, "M", "栟"), + (0x2F8E6, "M", "椔"), + (0x2F8E7, "M", "㮝"), + (0x2F8E8, "M", "楂"), + (0x2F8E9, "M", "榣"), + (0x2F8EA, "M", "槪"), + (0x2F8EB, "M", "檨"), + (0x2F8EC, "M", "𣚣"), + (0x2F8ED, "M", "櫛"), + (0x2F8EE, "M", "㰘"), + (0x2F8EF, "M", "次"), + (0x2F8F0, "M", "𣢧"), + (0x2F8F1, "M", "歔"), + (0x2F8F2, "M", "㱎"), + (0x2F8F3, "M", "歲"), + (0x2F8F4, "M", "殟"), + (0x2F8F5, "M", "殺"), + (0x2F8F6, "M", "殻"), + (0x2F8F7, "M", "𣪍"), + (0x2F8F8, "M", "𡴋"), + (0x2F8F9, "M", "𣫺"), + (0x2F8FA, "M", "汎"), + (0x2F8FB, "M", "𣲼"), + (0x2F8FC, "M", "沿"), + (0x2F8FD, "M", "泍"), + (0x2F8FE, "M", "汧"), + (0x2F8FF, "M", "洖"), + (0x2F900, "M", "派"), + (0x2F901, "M", "海"), + (0x2F902, "M", "流"), + (0x2F903, "M", "浩"), + (0x2F904, "M", "浸"), + (0x2F905, "M", "涅"), + (0x2F906, "M", "𣴞"), + (0x2F907, "M", "洴"), + (0x2F908, "M", "港"), + (0x2F909, "M", "湮"), + (0x2F90A, "M", "㴳"), + (0x2F90B, "M", "滋"), + (0x2F90C, "M", "滇"), + (0x2F90D, "M", "𣻑"), + (0x2F90E, "M", "淹"), + (0x2F90F, "M", "潮"), + (0x2F910, "M", "𣽞"), + (0x2F911, "M", "𣾎"), + (0x2F912, "M", "濆"), + (0x2F913, "M", "瀹"), + (0x2F914, "M", "瀞"), + (0x2F915, "M", "瀛"), + (0x2F916, "M", "㶖"), + (0x2F917, "M", "灊"), + (0x2F918, "M", "災"), + (0x2F919, "M", "灷"), + (0x2F91A, "M", "炭"), + (0x2F91B, "M", "𠔥"), + (0x2F91C, "M", "煅"), + (0x2F91D, "M", "𤉣"), + (0x2F91E, "M", "熜"), + (0x2F91F, "M", "𤎫"), + (0x2F920, "M", "爨"), + (0x2F921, "M", "爵"), + (0x2F922, "M", "牐"), + (0x2F923, "M", "𤘈"), + (0x2F924, "M", "犀"), + (0x2F925, "M", "犕"), + (0x2F926, "M", "𤜵"), + (0x2F927, "M", "𤠔"), + (0x2F928, "M", "獺"), + (0x2F929, "M", "王"), + (0x2F92A, "M", "㺬"), + (0x2F92B, "M", "玥"), + (0x2F92C, "M", "㺸"), + (0x2F92E, "M", "瑇"), + (0x2F92F, "M", "瑜"), + (0x2F930, "M", "瑱"), + (0x2F931, "M", "璅"), + (0x2F932, "M", "瓊"), + (0x2F933, "M", "㼛"), + (0x2F934, "M", "甤"), + (0x2F935, "M", "𤰶"), + (0x2F936, "M", "甾"), + (0x2F937, "M", "𤲒"), + (0x2F938, "M", "異"), + (0x2F939, "M", "𢆟"), + (0x2F93A, "M", "瘐"), + (0x2F93B, "M", "𤾡"), + (0x2F93C, "M", "𤾸"), + (0x2F93D, "M", "𥁄"), + (0x2F93E, "M", "㿼"), + (0x2F93F, "M", "䀈"), + (0x2F940, "M", "直"), + (0x2F941, "M", "𥃳"), + ] + + +def _seg_81() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F942, "M", "𥃲"), + (0x2F943, "M", "𥄙"), + (0x2F944, "M", "𥄳"), + (0x2F945, "M", "眞"), + (0x2F946, "M", "真"), + (0x2F948, "M", "睊"), + (0x2F949, "M", "䀹"), + (0x2F94A, "M", "瞋"), + (0x2F94B, "M", "䁆"), + (0x2F94C, "M", "䂖"), + (0x2F94D, "M", "𥐝"), + (0x2F94E, "M", "硎"), + (0x2F94F, "M", "碌"), + (0x2F950, "M", "磌"), + (0x2F951, "M", "䃣"), + (0x2F952, "M", "𥘦"), + (0x2F953, "M", "祖"), + (0x2F954, "M", "𥚚"), + (0x2F955, "M", "𥛅"), + (0x2F956, "M", "福"), + (0x2F957, "M", "秫"), + (0x2F958, "M", "䄯"), + (0x2F959, "M", "穀"), + (0x2F95A, "M", "穊"), + (0x2F95B, "M", "穏"), + (0x2F95C, "M", "𥥼"), + (0x2F95D, "M", "𥪧"), + (0x2F95F, "M", "竮"), + (0x2F960, "M", "䈂"), + (0x2F961, "M", "𥮫"), + (0x2F962, "M", "篆"), + (0x2F963, "M", "築"), + (0x2F964, "M", "䈧"), + (0x2F965, "M", "𥲀"), + (0x2F966, "M", "糒"), + (0x2F967, "M", "䊠"), + (0x2F968, "M", "糨"), + (0x2F969, "M", "糣"), + (0x2F96A, "M", "紀"), + (0x2F96B, "M", "𥾆"), + (0x2F96C, "M", "絣"), + (0x2F96D, "M", "䌁"), + (0x2F96E, "M", "緇"), + (0x2F96F, "M", "縂"), + (0x2F970, "M", "繅"), + (0x2F971, "M", "䌴"), + (0x2F972, "M", "𦈨"), + (0x2F973, "M", "𦉇"), + (0x2F974, "M", "䍙"), + (0x2F975, "M", "𦋙"), + (0x2F976, "M", "罺"), + (0x2F977, "M", "𦌾"), + (0x2F978, "M", "羕"), + (0x2F979, "M", "翺"), + (0x2F97A, "M", "者"), + (0x2F97B, "M", "𦓚"), + (0x2F97C, "M", "𦔣"), + (0x2F97D, "M", "聠"), + (0x2F97E, "M", "𦖨"), + (0x2F97F, "M", "聰"), + (0x2F980, "M", "𣍟"), + (0x2F981, "M", "䏕"), + (0x2F982, "M", "育"), + (0x2F983, "M", "脃"), + (0x2F984, "M", "䐋"), + (0x2F985, "M", "脾"), + (0x2F986, "M", "媵"), + (0x2F987, "M", "𦞧"), + (0x2F988, "M", "𦞵"), + (0x2F989, "M", "𣎓"), + (0x2F98A, "M", "𣎜"), + (0x2F98B, "M", "舁"), + (0x2F98C, "M", "舄"), + (0x2F98D, "M", "辞"), + (0x2F98E, "M", "䑫"), + (0x2F98F, "M", "芑"), + (0x2F990, "M", "芋"), + (0x2F991, "M", "芝"), + (0x2F992, "M", "劳"), + (0x2F993, "M", "花"), + (0x2F994, "M", "芳"), + (0x2F995, "M", "芽"), + (0x2F996, "M", "苦"), + (0x2F997, "M", "𦬼"), + (0x2F998, "M", "若"), + (0x2F999, "M", "茝"), + (0x2F99A, "M", "荣"), + (0x2F99B, "M", "莭"), + (0x2F99C, "M", "茣"), + (0x2F99D, "M", "莽"), + (0x2F99E, "M", "菧"), + (0x2F99F, "M", "著"), + (0x2F9A0, "M", "荓"), + (0x2F9A1, "M", "菊"), + (0x2F9A2, "M", "菌"), + (0x2F9A3, "M", "菜"), + (0x2F9A4, "M", "𦰶"), + (0x2F9A5, "M", "𦵫"), + (0x2F9A6, "M", "𦳕"), + (0x2F9A7, "M", "䔫"), + ] + + +def _seg_82() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2F9A8, "M", "蓱"), + (0x2F9A9, "M", "蓳"), + (0x2F9AA, "M", "蔖"), + (0x2F9AB, "M", "𧏊"), + (0x2F9AC, "M", "蕤"), + (0x2F9AD, "M", "𦼬"), + (0x2F9AE, "M", "䕝"), + (0x2F9AF, "M", "䕡"), + (0x2F9B0, "M", "𦾱"), + (0x2F9B1, "M", "𧃒"), + (0x2F9B2, "M", "䕫"), + (0x2F9B3, "M", "虐"), + (0x2F9B4, "M", "虜"), + (0x2F9B5, "M", "虧"), + (0x2F9B6, "M", "虩"), + (0x2F9B7, "M", "蚩"), + (0x2F9B8, "M", "蚈"), + (0x2F9B9, "M", "蜎"), + (0x2F9BA, "M", "蛢"), + (0x2F9BB, "M", "蝹"), + (0x2F9BC, "M", "蜨"), + (0x2F9BD, "M", "蝫"), + (0x2F9BE, "M", "螆"), + (0x2F9BF, "M", "䗗"), + (0x2F9C0, "M", "蟡"), + (0x2F9C1, "M", "蠁"), + (0x2F9C2, "M", "䗹"), + (0x2F9C3, "M", "衠"), + (0x2F9C4, "M", "衣"), + (0x2F9C5, "M", "𧙧"), + (0x2F9C6, "M", "裗"), + (0x2F9C7, "M", "裞"), + (0x2F9C8, "M", "䘵"), + (0x2F9C9, "M", "裺"), + (0x2F9CA, "M", "㒻"), + (0x2F9CB, "M", "𧢮"), + (0x2F9CC, "M", "𧥦"), + (0x2F9CD, "M", "䚾"), + (0x2F9CE, "M", "䛇"), + (0x2F9CF, "M", "誠"), + (0x2F9D0, "M", "諭"), + (0x2F9D1, "M", "變"), + (0x2F9D2, "M", "豕"), + (0x2F9D3, "M", "𧲨"), + (0x2F9D4, "M", "貫"), + (0x2F9D5, "M", "賁"), + (0x2F9D6, "M", "贛"), + (0x2F9D7, "M", "起"), + (0x2F9D8, "M", "𧼯"), + (0x2F9D9, "M", "𠠄"), + (0x2F9DA, "M", "跋"), + (0x2F9DB, "M", "趼"), + (0x2F9DC, "M", "跰"), + (0x2F9DD, "M", "𠣞"), + (0x2F9DE, "M", "軔"), + (0x2F9DF, "M", "輸"), + (0x2F9E0, "M", "𨗒"), + (0x2F9E1, "M", "𨗭"), + (0x2F9E2, "M", "邔"), + (0x2F9E3, "M", "郱"), + (0x2F9E4, "M", "鄑"), + (0x2F9E5, "M", "𨜮"), + (0x2F9E6, "M", "鄛"), + (0x2F9E7, "M", "鈸"), + (0x2F9E8, "M", "鋗"), + (0x2F9E9, "M", "鋘"), + (0x2F9EA, "M", "鉼"), + (0x2F9EB, "M", "鏹"), + (0x2F9EC, "M", "鐕"), + (0x2F9ED, "M", "𨯺"), + (0x2F9EE, "M", "開"), + (0x2F9EF, "M", "䦕"), + (0x2F9F0, "M", "閷"), + (0x2F9F1, "M", "𨵷"), + (0x2F9F2, "M", "䧦"), + (0x2F9F3, "M", "雃"), + (0x2F9F4, "M", "嶲"), + (0x2F9F5, "M", "霣"), + (0x2F9F6, "M", "𩅅"), + (0x2F9F7, "M", "𩈚"), + (0x2F9F8, "M", "䩮"), + (0x2F9F9, "M", "䩶"), + (0x2F9FA, "M", "韠"), + (0x2F9FB, "M", "𩐊"), + (0x2F9FC, "M", "䪲"), + (0x2F9FD, "M", "𩒖"), + (0x2F9FE, "M", "頋"), + (0x2FA00, "M", "頩"), + (0x2FA01, "M", "𩖶"), + (0x2FA02, "M", "飢"), + (0x2FA03, "M", "䬳"), + (0x2FA04, "M", "餩"), + (0x2FA05, "M", "馧"), + (0x2FA06, "M", "駂"), + (0x2FA07, "M", "駾"), + (0x2FA08, "M", "䯎"), + (0x2FA09, "M", "𩬰"), + (0x2FA0A, "M", "鬒"), + (0x2FA0B, "M", "鱀"), + (0x2FA0C, "M", "鳽"), + ] + + +def _seg_83() -> List[Union[Tuple[int, str], Tuple[int, str, str]]]: + return [ + (0x2FA0D, "M", "䳎"), + (0x2FA0E, "M", "䳭"), + (0x2FA0F, "M", "鵧"), + (0x2FA10, "M", "𪃎"), + (0x2FA11, "M", "䳸"), + (0x2FA12, "M", "𪄅"), + (0x2FA13, "M", "𪈎"), + (0x2FA14, "M", "𪊑"), + (0x2FA15, "M", "麻"), + (0x2FA16, "M", "䵖"), + (0x2FA17, "M", "黹"), + (0x2FA18, "M", "黾"), + (0x2FA19, "M", "鼅"), + (0x2FA1A, "M", "鼏"), + (0x2FA1B, "M", "鼖"), + (0x2FA1C, "M", "鼻"), + (0x2FA1D, "M", "𪘀"), + (0x2FA1E, "X"), + (0x30000, "V"), + (0x3134B, "X"), + (0x31350, "V"), + (0x323B0, "X"), + (0xE0100, "I"), + (0xE01F0, "X"), + ] + + +uts46data = tuple( + _seg_0() + + _seg_1() + + _seg_2() + + _seg_3() + + _seg_4() + + _seg_5() + + _seg_6() + + _seg_7() + + _seg_8() + + _seg_9() + + _seg_10() + + _seg_11() + + _seg_12() + + _seg_13() + + _seg_14() + + _seg_15() + + _seg_16() + + _seg_17() + + _seg_18() + + _seg_19() + + _seg_20() + + _seg_21() + + _seg_22() + + _seg_23() + + _seg_24() + + _seg_25() + + _seg_26() + + _seg_27() + + _seg_28() + + _seg_29() + + _seg_30() + + _seg_31() + + _seg_32() + + _seg_33() + + _seg_34() + + _seg_35() + + _seg_36() + + _seg_37() + + _seg_38() + + _seg_39() + + _seg_40() + + _seg_41() + + _seg_42() + + _seg_43() + + _seg_44() + + _seg_45() + + _seg_46() + + _seg_47() + + _seg_48() + + _seg_49() + + _seg_50() + + _seg_51() + + _seg_52() + + _seg_53() + + _seg_54() + + _seg_55() + + _seg_56() + + _seg_57() + + _seg_58() + + _seg_59() + + _seg_60() + + _seg_61() + + _seg_62() + + _seg_63() + + _seg_64() + + _seg_65() + + _seg_66() + + _seg_67() + + _seg_68() + + _seg_69() + + _seg_70() + + _seg_71() + + _seg_72() + + _seg_73() + + _seg_74() + + _seg_75() + + _seg_76() + + _seg_77() + + _seg_78() + + _seg_79() + + _seg_80() + + _seg_81() + + _seg_82() + + _seg_83() +) # type: Tuple[Union[Tuple[int, str], Tuple[int, str, str]], ...] diff --git a/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/METADATA b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/METADATA new file mode 100644 index 0000000..11ba9ff --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/METADATA @@ -0,0 +1,40 @@ +Metadata-Version: 2.1 +Name: ilorest +Version: 7.0.0.0 +Summary: HPE iLORest Tool +Home-page: https://github.com/HewlettPackard/python-redfish-utility +Author: Hewlett Packard Enterprise +Author-email: rajeevalochana.kallur@hpe.com +Keywords: Hewlett Packard Enterprise +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Communications +Requires-Dist: urllib3 >=1.26.2 +Requires-Dist: pyaes >=1.6.1 +Requires-Dist: colorama >=0.4.4 +Requires-Dist: jsonpointer >=2.0 +Requires-Dist: six >=1.15.0 +Requires-Dist: ply +Requires-Dist: requests +Requires-Dist: decorator >=4.4.2 +Requires-Dist: jsonpatch >=1.28 +Requires-Dist: jsonpath-rw >=1.4.0 +Requires-Dist: jsondiff >=1.2.0 +Requires-Dist: tabulate >=0.8.7 +Requires-Dist: prompt-toolkit +Requires-Dist: certifi >=2020.12.5 +Requires-Dist: wcwidth >=0.2.5 +Requires-Dist: pyudev +Requires-Dist: future +Requires-Dist: python-ilorest-library >=7.0.0.0 +Requires-Dist: setproctitle >=1.1.8 ; platform_system == "Linux" +Requires-Dist: pywin32 ; platform_system == "Windows" +Requires-Dist: enum ; python_version <= "2.7.19" +Requires-Dist: futures ; python_version <= "2.7.19" + diff --git a/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/RECORD b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/RECORD new file mode 100644 index 0000000..bb49051 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/RECORD @@ -0,0 +1,326 @@ +../../../bin/ilorest,sha256=PLP7vUiznKIPPNKauuYE1AkTyXlCYO0Ko25uS3mQ238,264 +../../../logging_config.json,sha256=H_vbfeKiyQPNLkCJ_p1iAcvfV_QlUWcq1qJqs2p-_EE,1171 +ilorest-7.0.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +ilorest-7.0.0.0.dist-info/METADATA,sha256=s851iAC0ePMC6Ns0aK9K6EB57roYOVmwGTzk5BWzPIM,1531 +ilorest-7.0.0.0.dist-info/RECORD,, +ilorest-7.0.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest-7.0.0.0.dist-info/WHEEL,sha256=DZajD4pwLWue70CAfc7YaxT1wLUciNBvN_TTcvXpltE,110 +ilorest-7.0.0.0.dist-info/entry_points.txt,sha256=hZK831ZpPnfSjuVPord6I0D4r2NEzv0XO7AFKKd9UqU,56 +ilorest-7.0.0.0.dist-info/top_level.txt,sha256=PBm3JtzoqCberFEbu0RbzOYwiAVP05Exdx9afhHPAfg,14 +ilorest/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/__pycache__/__init__.cpython-310.pyc,, +ilorest/__pycache__/cliutils.cpython-310.pyc,, +ilorest/__pycache__/debug_utils.cpython-310.pyc,, +ilorest/__pycache__/logging_config_path.cpython-310.pyc,, +ilorest/__pycache__/rdmc.cpython-310.pyc,, +ilorest/__pycache__/rdmc_base_classes.cpython-310.pyc,, +ilorest/__pycache__/rdmc_helper.cpython-310.pyc,, +ilorest/__pycache__/security_masking.cpython-310.pyc,, +ilorest/__pycache__/versioning.cpython-310.pyc,, +ilorest/chiflibrary/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/chiflibrary/__pycache__/__init__.cpython-310.pyc,, +ilorest/chiflibrary/arm/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/chiflibrary/arm/__pycache__/__init__.cpython-310.pyc,, +ilorest/chiflibrary/arm/ilorest_chif.so,sha256=6kW4rWEmFWELZOkb30ezYXbBPMxZCjqPGo9CdbGRU28,9423008 +ilorest/chiflibrary/ilorest_chif.dll,sha256=1PLq8d7u7on53n8FcLXDFu_rLYI5-XQtkB3AiMQAdUI,3580048 +ilorest/chiflibrary/ilorest_chif.so,sha256=TnyuB9O791U3Pa485Hm4Zn_yEDvRxfMzyLLhktyCz0o,9918784 +ilorest/cliutils.py,sha256=Rgcu2Iqu-PK3bSDpQgkisDnFIMsPGjuyw7y7aARFhkg,6837 +ilorest/config/__init__.py,sha256=RWSNi8PsptVoQ4E3CvFKPU0RBQ7gSRjKu1QsDG9U97Q,30 +ilorest/config/__pycache__/__init__.cpython-310.pyc,, +ilorest/config/__pycache__/config.cpython-310.pyc,, +ilorest/config/__pycache__/rdmc_config.cpython-310.pyc,, +ilorest/config/config.py,sha256=917hDTAOLONGV6iQtMG54kuZFhUs2fzIqNtiUDGwboA,4603 +ilorest/config/rdmc_config.py,sha256=BWGp0GJulWPLkl81DPOW6utF5M4zexCIF9wZ5E3VF1w,8356 +ilorest/debug_utils.py,sha256=9RRfnfqpcWUyFbuy5EEabmkWpkzLkZdgdEnLbirvOlw,15914 +ilorest/extensions/BIOS_COMMANDS/BiosDefaultsCommand.py,sha256=svj96UdwlANpHRDJVtBxwLbejRNSfpQg7Vr56FKM73Q,5944 +ilorest/extensions/BIOS_COMMANDS/BootOrderCommand.py,sha256=Of_PGsNUqE8y9UziGNBV4zEzJKwaQ27pHeE20wgOKiQ,30819 +ilorest/extensions/BIOS_COMMANDS/IscsiConfigCommand.py,sha256=UGQew-apGEFxVzoTo-X9SmfQ2TX2GOymzr5CRNy8ZqM,41079 +ilorest/extensions/BIOS_COMMANDS/SetPasswordCommand.py,sha256=rmVGOfuCo1w6is2ZmUEPySlW02eSFiJWYt4IuhYKJcg,8101 +ilorest/extensions/BIOS_COMMANDS/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/BIOS_COMMANDS/__pycache__/BiosDefaultsCommand.cpython-310.pyc,, +ilorest/extensions/BIOS_COMMANDS/__pycache__/BootOrderCommand.cpython-310.pyc,, +ilorest/extensions/BIOS_COMMANDS/__pycache__/IscsiConfigCommand.cpython-310.pyc,, +ilorest/extensions/BIOS_COMMANDS/__pycache__/SetPasswordCommand.cpython-310.pyc,, +ilorest/extensions/BIOS_COMMANDS/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/COMMANDS/CommitCommand.py,sha256=J0TGidmyDr6CfAWQ45Py5MgkSdyupPU4sfA47ifvlEs,4710 +ilorest/extensions/COMMANDS/DetectiLOCommand.py,sha256=5-qSebt-yqXrQsdyiEZpQp8qt87ke67qJlNRmmh3hYc,7944 +ilorest/extensions/COMMANDS/GetCommand.py,sha256=_Krup0OgHyera46nT7VmS1ndSBvwYnL79d4Eh46Z6yU,21015 +ilorest/extensions/COMMANDS/InfoCommand.py,sha256=ooEooTW1r8gs3ZMIfsZaqdTxmQOlx-ib3JcoE0csdts,6614 +ilorest/extensions/COMMANDS/ListCommand.py,sha256=6d7PMUkQgJTfzf9Pir14KKN0_k0ZvAHeUm_VS1-czBo,6696 +ilorest/extensions/COMMANDS/LoadCommand.py,sha256=hPIA8uZfiDmhwbICM6TAmnqfkWnK25F-e2Fmb7AOugo,20867 +ilorest/extensions/COMMANDS/LoginCommand.py,sha256=5FfXEnCxEzFo7mon-zfoVYPfk8VYo_HggE-6HUDk40g,23852 +ilorest/extensions/COMMANDS/LogoutCommand.py,sha256=ugSDQ82L2iYkHg2HotER3Bib2y19rmuRiDdZIhjIHi8,2486 +ilorest/extensions/COMMANDS/PendingChangesCommand.py,sha256=3vEasrpdHLpMulMd2JPX1bH5A8gjas5m4aInySEMKdE,7116 +ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/ExitCommand.py,sha256=La8VTu8qqmHflAn5cHik5KTffXphTFOW0N2Hju79cVg,2635 +ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/HelpCommand.py,sha256=QHH9qF8KCPDdQu1Ed-FmXn06TL5Vvug2Nwqc-FPySyo,4037 +ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/ExitCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/HelpCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/COMMANDS/ResultsCommand.py,sha256=BrVezaY0o0WtS490gLUZWKENa0UCAA1oK8l5D3zBxUk,5809 +ilorest/extensions/COMMANDS/SaveCommand.py,sha256=kYNkxqQ6bj1I0gg62S-ACCBlldbkLdXQdqaQJaUUUQI,16873 +ilorest/extensions/COMMANDS/SelectCommand.py,sha256=6E_x97WCMELf8Xp9vLWMELtSLtPDG927J_CUOD72wGE,5069 +ilorest/extensions/COMMANDS/SetCommand.py,sha256=7DlOvsdgIqr0LMfkClyZHxnv5GyCzJ-b49BqGU1-rAs,20664 +ilorest/extensions/COMMANDS/StatusCommand.py,sha256=o1R48UQDtyAMlbwR8tEwadk8FT9w_ToqIo9VWHsNc7g,8037 +ilorest/extensions/COMMANDS/TypesCommand.py,sha256=-uG2n1vUJLGJv30-LkjHxUaULY_eiRkWkj-pnL739Tg,3883 +ilorest/extensions/COMMANDS/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/COMMANDS/__pycache__/CommitCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/DetectiLOCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/GetCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/InfoCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/ListCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/LoadCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/LoginCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/LogoutCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/PendingChangesCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/ResultsCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/SaveCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/SelectCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/SetCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/StatusCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/TypesCommand.cpython-310.pyc,, +ilorest/extensions/COMMANDS/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/AdvancedPmmConfigCommand.py,sha256=_MbN5SP1ehHzFWUjVy24UgO22hStFVht2w8eoKHGtNY,18808 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ApplyPmemConfigCommand.py,sha256=gmQyD-z60tPvFsZZazE9z2WmFFc3b18IHe-VwNZXqu8,14398 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ClearPendingConfigCommand.py,sha256=1ChJAb_VDyoL94x9Njw31REpqcj8bVuwa0IbZrDFDKU,4827 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/DisplaySecurityStateCommand.py,sha256=5FaeXgJueiWojgMKwcf5V1g5XDUVpa4PcIYAPCAtfss,3996 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowPmemCommand.py,sha256=9n4_zCDmksNek0Ld7qqtGw55ddNuI2dUTxpMwS89SMw,17737 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowPmemPendingConfigCommand.py,sha256=ncRm7Dc2-wHpxovPA4H2yXDWK8nTjjau2uh3YpyvOjM,6303 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowRecommendedConfigCommand.py,sha256=i3Sx9FKMiNhi2xUIqWeWv6T72pd_9SY0apa2Y3BdvIg,7216 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/AdvancedPmmConfigCommand.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ApplyPmemConfigCommand.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ClearPendingConfigCommand.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/DisplaySecurityStateCommand.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowPmemCommand.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowPmemPendingConfigCommand.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowRecommendedConfigCommand.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/DisplayHelpers.py,sha256=HFeOk7PR1_upJw7DiQ7yt_9oIggkZWwax7RBW9HcYlA,7133 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/Mapper.py,sha256=B_Xb2VpBuRfW5AVqQd_B3szbpsNc8I1jAJlmfb34EjI,4326 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/MapperRenderers.py,sha256=-_SimQIsIrG2eUPJeulacu7x9_pwgliwQ_SxSr6A2SI,18668 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/PmemHelpers.py,sha256=aFjrhiKmGw6U8DGosep9E3yORx13GR5tFhsh4IYqOKw,4802 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/RestHelpers.py,sha256=_f935lAQG_wjLADx5pwUtMO6HYSVJH1gBq4K7r9sjmA,11361 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/DisplayHelpers.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/Mapper.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/MapperRenderers.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/PmemHelpers.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/RestHelpers.cpython-310.pyc,, +ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/RAW_COMMANDS/RawDeleteCommand.py,sha256=YyPKH1_XUyzlZFZg68OmSy8Set8xq2mMDnnyiD7tyv0,6927 +ilorest/extensions/RAW_COMMANDS/RawGetCommand.py,sha256=8OnhE4t1MVs6LnJXZZI5gAystihzJ-uyDA0DqzyKogc,10936 +ilorest/extensions/RAW_COMMANDS/RawHeadCommand.py,sha256=8-SioIss_02rZXr112ofFJL0bWZv-llC74kJkt2Hs9o,4728 +ilorest/extensions/RAW_COMMANDS/RawPatchCommand.py,sha256=xIxSpIzdUEn2lxzC4KKMi8PzyoKkXTIUhADtkPpCcVU,8965 +ilorest/extensions/RAW_COMMANDS/RawPostCommand.py,sha256=noWsPUSAG0o8sLkOt8_FI_qVSprOwN8uePU37JsWeqc,9664 +ilorest/extensions/RAW_COMMANDS/RawPutCommand.py,sha256=6XlDQpPhdoW9Z7oRWOirKzjs1HEIPknG7KxqcZG2RYI,9155 +ilorest/extensions/RAW_COMMANDS/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/RAW_COMMANDS/__pycache__/RawDeleteCommand.cpython-310.pyc,, +ilorest/extensions/RAW_COMMANDS/__pycache__/RawGetCommand.cpython-310.pyc,, +ilorest/extensions/RAW_COMMANDS/__pycache__/RawHeadCommand.cpython-310.pyc,, +ilorest/extensions/RAW_COMMANDS/__pycache__/RawPatchCommand.cpython-310.pyc,, +ilorest/extensions/RAW_COMMANDS/__pycache__/RawPostCommand.cpython-310.pyc,, +ilorest/extensions/RAW_COMMANDS/__pycache__/RawPutCommand.cpython-310.pyc,, +ilorest/extensions/RAW_COMMANDS/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/SMART_ARRAY_COMMANDS/ClearControllerConfigCommand.py,sha256=oQyRUERDeRbUWLmNq_jlo9aPUo2MKZzcv2k69cNpU0E,6710 +ilorest/extensions/SMART_ARRAY_COMMANDS/CreateVolumeCommand.py,sha256=sTBWpFeFJd--oktN9NMtr8QV7cCF0BtomzOh7RMaSnU,51544 +ilorest/extensions/SMART_ARRAY_COMMANDS/DeleteVolumeCommand.py,sha256=mbyn6Ln7VtnwkuJkzC6U_IWZR0jo_nlmXowgffRLn7I,18602 +ilorest/extensions/SMART_ARRAY_COMMANDS/DriveSanitizeCommand.py,sha256=O1HMG0C9TQrAIEIV10pJU9Fkbtc5ucBdpdZU2gV9BDs,25977 +ilorest/extensions/SMART_ARRAY_COMMANDS/FactoryResetControllerCommand.py,sha256=A5AaSt9jLwOzqLUcDi_5ymWRJYzP4K9OyeljQg0MO-U,9361 +ilorest/extensions/SMART_ARRAY_COMMANDS/StorageControllerCommand.py,sha256=p8qtXCQIPMd3Q7J7bJXls4eUWeyqyQj34zDz0dsNrEg,115762 +ilorest/extensions/SMART_ARRAY_COMMANDS/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/ClearControllerConfigCommand.cpython-310.pyc,, +ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/CreateVolumeCommand.cpython-310.pyc,, +ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/DeleteVolumeCommand.cpython-310.pyc,, +ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/DriveSanitizeCommand.cpython-310.pyc,, +ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/FactoryResetControllerCommand.cpython-310.pyc,, +ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/StorageControllerCommand.cpython-310.pyc,, +ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/SMART_NIC_COMMANDS/SmartNicCommand.py,sha256=g6gWnnCqpgaKyeWUrEyuG07MYEz-y0M5dim3dZpJHhE,30343 +ilorest/extensions/SMART_NIC_COMMANDS/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/SMART_NIC_COMMANDS/__pycache__/SmartNicCommand.cpython-310.pyc,, +ilorest/extensions/SMART_NIC_COMMANDS/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/__init__.py,sha256=3rsbSdxIc6lIarsPy7XQnDg8DIL6aTvAVMaq-LolXRQ,899 +ilorest/extensions/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/AHSdiagCommand.py,sha256=Wt_BUevii30PhNCeJ8bpscbf4RGHOkQAJQasb-5G2oE,16463 +ilorest/extensions/_hidden_commands/AutomaticTestingCommand.py,sha256=G1Nz1_dpLK3DIgc7phkxq9NxMyKWbVVj-zmpOgKjOWM,122529 +ilorest/extensions/_hidden_commands/ExportAllConfigCommand.py,sha256=L3R3gZCGLmIMZHooPOvBq9W9-AGGWAB5jZNO5oS0vJk,3569 +ilorest/extensions/_hidden_commands/GetInventoryCommand.py,sha256=9Myn7vjfu03cxK0udwyflACogZWJ3BVzz2dvLn53LYs,12647 +ilorest/extensions/_hidden_commands/HpGooeyCommand.py,sha256=JITcGsZhNAM_9xvRDwc_RtcX0uMAi2c3ZiqP7vxTC8M,40801 +ilorest/extensions/_hidden_commands/ISToolCommand.py,sha256=AI9QUDXDG5wqYYBhAJ-IS39YJcITZtHNiSv-pKvP9Tw,6502 +ilorest/extensions/_hidden_commands/MonolithCommand.py,sha256=FzgTVvv7Bg0ma_0wrUyCn3md7zcIPCzU2SOmeY6k8W8,4023 +ilorest/extensions/_hidden_commands/SMBiosCommand.py,sha256=f9olt0JchtfUEglSrRSi3QoiLEBvNiUn3uAsCTzZlfg,6753 +ilorest/extensions/_hidden_commands/SecurityStatusCommand.py,sha256=dLH0PZ_9iN5QOxZSo99GOCFYjCutoktxUohJ56B9BEw,6798 +ilorest/extensions/_hidden_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/_hidden_commands/__pycache__/AHSdiagCommand.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/__pycache__/AutomaticTestingCommand.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/__pycache__/ExportAllConfigCommand.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/__pycache__/GetInventoryCommand.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/__pycache__/HpGooeyCommand.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/__pycache__/ISToolCommand.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/__pycache__/MonolithCommand.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/__pycache__/SMBiosCommand.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/__pycache__/SecurityStatusCommand.cpython-310.pyc,, +ilorest/extensions/_hidden_commands/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/AppAccountCommand.py,sha256=txmffIVmyXwl5XfrHuLU_svv18dNJS7ULhYgMR1DEn0,34957 +ilorest/extensions/iLO_COMMANDS/CertificateCommand.py,sha256=p2r3sjkoIAosWmW1bejQCRi-93DLalZ3nau-jzVNXlI,57083 +ilorest/extensions/iLO_COMMANDS/ChannelInterfaceCommand.py,sha256=Kb9nbKxegOaQ6_UDaPwgr7LvY7ZfdzV5cdpwbDl9bBA,4358 +ilorest/extensions/iLO_COMMANDS/ClearRestApiStateCommand.py,sha256=wmKmlkvtqkA_5bf5V_geSFeguL7vAiWJaVYNY8P5JYE,4331 +ilorest/extensions/iLO_COMMANDS/ComputeOpsManagementCommand.py,sha256=hAl5FXeWRy4ScNMg4zCbXaIBUK6JAFSkzzWMgGZMh-I,147029 +ilorest/extensions/iLO_COMMANDS/DirectoryCommand.py,sha256=k44ai8_9aqXMcW3dstSoIsFogxxqpMNGJ4SWBQIv69s,38752 +ilorest/extensions/iLO_COMMANDS/DisableIloFunctionalityCommand.py,sha256=AEzYrIGl0b7mcHGTogxykJr4hmmAY_6E-mMKiTIHN5g,7201 +ilorest/extensions/iLO_COMMANDS/ESKMCommand.py,sha256=5be1-WdDpzSnZgXzeLDUBPElOV5NjeGKYxCEaP8RNFU,4119 +ilorest/extensions/iLO_COMMANDS/EthernetCommand.py,sha256=vxI4_VQvmpfSg5uSKGeefGnsNpcNoyjjIXn6L0zy3cM,60484 +ilorest/extensions/iLO_COMMANDS/FactoryDefaultsCommand.py,sha256=T0UXwnCITMwN_vkrSPPEwyQT_7V401ZnQaEBvCe2bbs,4364 +ilorest/extensions/iLO_COMMANDS/FirmwareIntegrityCheckCommand.py,sha256=YWz37yR4W8_tdNRndBc20HZmZZlvdZCioJvMVvfDxGk,6047 +ilorest/extensions/iLO_COMMANDS/FirmwareUpdateCommand.py,sha256=yVGpgz_pohTKeMGhErq9aznzg3kESSoRrUrNWADW3tY,12031 +ilorest/extensions/iLO_COMMANDS/IPProfilesCommand.py,sha256=amjTIzQ4kC5AB8T7GDcC1qxg_mFQ6XsSpQszToSNwh4,25536 +ilorest/extensions/iLO_COMMANDS/IloAccountsCommand.py,sha256=-n_Aesw55P99s2BcFZXwozPytT5pFQi-MZHmvY-BU4I,33192 +ilorest/extensions/iLO_COMMANDS/IloBackupRestoreCommand.py,sha256=XNj68Z6rDjASJjdkyg9CIQnMXBqmCqzIVI3XabRPu7c,11422 +ilorest/extensions/iLO_COMMANDS/IloFederationCommand.py,sha256=TV6kypUB7oGyF0ZqBjaCO7PY2ChzRtZQC8ePstOEfNQ,21571 +ilorest/extensions/iLO_COMMANDS/IloLicenseCommand.py,sha256=O5yBH8m68Eh6ZqC5u_aqtiGYnLHuj8I99DalnaXvf5E,11629 +ilorest/extensions/iLO_COMMANDS/IloResetCommand.py,sha256=eBaa6yj0CazGjgYs2eGinbTl6q5AMVqGxdfNz87k5mc,4368 +ilorest/extensions/iLO_COMMANDS/LogconfigCommand.py,sha256=ZGUi-lzmU6TY5rFlrGTOyNwEeyGxqB6FIdXpJILDZxA,38374 +ilorest/extensions/iLO_COMMANDS/OneButtonEraseCommand.py,sha256=4nx5oS9348tZJcmgFJQMYrFi8T-x8S1RitnanR-9TXQ,10970 +ilorest/extensions/iLO_COMMANDS/RebootCommand.py,sha256=yLFILZyMUjSsoKMHucyag7UU1DJiOmTUR6perlFeOws,10817 +ilorest/extensions/iLO_COMMANDS/SendTestCommand.py,sha256=LIwsGy2e7BfcXuDzPeNXuY_27T574JWtJB8-ETiAL6c,5255 +ilorest/extensions/iLO_COMMANDS/ServerCloneCommand.py,sha256=gVjdEjkGj-9vCzc49Pp6BmPeDnuXRTrgR7-y9P6uEHs,116013 +ilorest/extensions/iLO_COMMANDS/ServerConfigurationLockCommand.py,sha256=MGLk9vnUbPxnLjh9XYp6flR0Q40kkGGvpcaNCd4TDkw,11946 +ilorest/extensions/iLO_COMMANDS/ServerInfoCommand.py,sha256=6htgW9ct9pdV91ejMFVScWYllhHru8iihFiPWcb-xi4,55364 +ilorest/extensions/iLO_COMMANDS/ServerStateCommand.py,sha256=6N-RN5M8TjrKH-m4__ftlsMZ5siObtxXI2NautOr7gs,3492 +ilorest/extensions/iLO_COMMANDS/ServerlogsCommand.py,sha256=gEbmQLYiVJFwBo0wlrwvVaMvmKPvVeTabUfGkhASPRU,57706 +ilorest/extensions/iLO_COMMANDS/SetTwoFactorAuthenticationCommand.py,sha256=szF7k2wvXks_U3Skrbj0eozBclzpvDdmMXyxB--5k8Q,15395 +ilorest/extensions/iLO_COMMANDS/SigRecomputeCommand.py,sha256=JRwmOlRZppyfbzr3BkqrHFG181D5cb7K40N5IwdVBrE,3540 +ilorest/extensions/iLO_COMMANDS/SingleSignOnCommand.py,sha256=8G1QVrr1TUgpay8juPt5mbo18qK_tE7Tlu6BVrwf3h0,7232 +ilorest/extensions/iLO_COMMANDS/VirtualMediaCommand.py,sha256=EdB9t86MvdOIYPLHL_twdiRCVJkaWiCGEe6f8QkGysQ,16503 +ilorest/extensions/iLO_COMMANDS/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/iLO_COMMANDS/__pycache__/AppAccountCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/CertificateCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/ChannelInterfaceCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/ClearRestApiStateCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/ComputeOpsManagementCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/DirectoryCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/DisableIloFunctionalityCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/ESKMCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/EthernetCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/FactoryDefaultsCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/FirmwareIntegrityCheckCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/FirmwareUpdateCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/IPProfilesCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/IloAccountsCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/IloBackupRestoreCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/IloFederationCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/IloLicenseCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/IloResetCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/LogconfigCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/OneButtonEraseCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/RebootCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/SendTestCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/ServerCloneCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/ServerConfigurationLockCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/ServerInfoCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/ServerStateCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/ServerlogsCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/SetTwoFactorAuthenticationCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/SigRecomputeCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/SingleSignOnCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/VirtualMediaCommand.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/com_helpers/__init__.py,sha256=NjC5YIsmVXxSvGcCxULFdg3yttoi3vACJ8CkSAbpIw8,430 +ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/config_resolver.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/ilo_connector.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/network_configurator.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/progress_tracker.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/com_helpers/config_resolver.py,sha256=Q1KUwNP6Xkcq9V-Bn2NpcUZs1yrT6wjPLviXxPTtu2g,5945 +ilorest/extensions/iLO_COMMANDS/com_helpers/ilo_connector.py,sha256=XX5_WjwYXW1-f7ER39m2pp-HUzzazuVKDtI0gWTvWkM,8558 +ilorest/extensions/iLO_COMMANDS/com_helpers/network_configurator.py,sha256=yZkvVEmE3j4MMKaiuLV4-UikXSGwyKNszj1UloJvR4A,9572 +ilorest/extensions/iLO_COMMANDS/com_helpers/progress_tracker.py,sha256=J-y-8Tk3JAyoEg3nv4lZmW9u2TTXfIuvw-kcos7k2JI,3287 +ilorest/extensions/iLO_COMMANDS/data/InputTemplate.py,sha256=9QlHIW3VnzSw5rJ2yyInz-BOHMpFSPknKqS5lzkF818,1578 +ilorest/extensions/iLO_COMMANDS/data/__init__.py,sha256=ffkT9zzMfrTDZcngRQpum3EQB_zPs_ZUdGn3OkB2Xwo,36 +ilorest/extensions/iLO_COMMANDS/data/__pycache__/InputTemplate.cpython-310.pyc,, +ilorest/extensions/iLO_COMMANDS/data/__pycache__/__init__.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/DeleteComponentCommand.py,sha256=4aWxZO-f34vdocbJEA8v4etDlpBpEHTlkofs4XYF4kE,6160 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/DownloadComponentCommand.py,sha256=cunaJk5Ek1q5Ai0hbb6Egv4oa8IlFIo1L44_ZWV1P4I,8165 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/FwpkgCommand.py,sha256=VNN6AMF80BDnXQqJ-8zrgvtMBtyPGnUizTYx_Ns4D-M,32483 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/InstallSetCommand.py,sha256=s3bFatYA0Y4UFbxujBsMNJ-zP-_DgpZRiu_-IQWE0EI,23843 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/ListComponentCommand.py,sha256=-nv0lClwTk9X3pVegRE1Azos_LCz4Vd9zkm6nqiH630,4576 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/MaintenanceWindowCommand.py,sha256=-mwcLxQJnB_1j9IkXex3NLVh05kTsFRyQzYJmjDeAN8,12486 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/MakeInstallSetCommand.py,sha256=AeGuqvFK5-GwYpFiqtmhWuivlqqxz9hnJuJ_S8mCRB0,13167 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/UpdateTaskQueueCommand.py,sha256=CFYps3nivVYHf0BjnRxw-Y9JGe2HaoZh7kMSRx8iqtU,16480 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/UploadComponentCommand.py,sha256=LftdjEr2zNmKWC66LmCXOU1Qv71fDwHt3-DEJ5B7Sgs,34071 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/DeleteComponentCommand.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/DownloadComponentCommand.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/FwpkgCommand.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/InstallSetCommand.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/ListComponentCommand.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/MaintenanceWindowCommand.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/MakeInstallSetCommand.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/UpdateTaskQueueCommand.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/UploadComponentCommand.cpython-310.pyc,, +ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/__init__.cpython-310.pyc,, +ilorest/logging_config_path.py,sha256=BHbopU-MEoU9Kac6viEEX7sFp0h81P7BGZI334dIGbM,1050 +ilorest/rdmc.py,sha256=0wkcDpKVwpSmfpE1XPmGGIc-kIaAhTayG3WpZnP2ckU,52247 +ilorest/rdmc_base_classes.py,sha256=NoZj8fW7H8--2Jk8bk3sjomu9gf8w8IcXw8BAJUC2BQ,21499 +ilorest/rdmc_helper.py,sha256=sbS4pycbM0JATEUG3HAFUYzWvL6I9kRc4Q6HU9aE-L0,45253 +ilorest/security_masking.py,sha256=9D5BrLASpjTXZn3Ds0CdsRGLWRyshRE0mZbkVpcZCGg,23386 +ilorest/versioning.py,sha256=bUNIH5lUhefdirBDtDo-heXTuQumfHDFO0rKbJ2cTl4,865 +tests/pmemtests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tests/pmemtests/__pycache__/__init__.cpython-310.pyc,, +tests/pmemtests/__pycache__/conftest.cpython-310.pyc,, +tests/pmemtests/conftest.py,sha256=66bT2_cGlPmGiQ28N1jXe1ia7M_weILFEqNSCXTH8F8,3387 +tests/pmemtests/functional/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tests/pmemtests/functional/__pycache__/__init__.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/conftest.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/ilomock.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_all_flag.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_applydcpmmconfig.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_cleardcpmmpendingconfig.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_config_flag.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_device_flag.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_display_flag.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_logical_flag.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_showdcpmm.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_showdcpmmpendingconfig.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/test_summary_flag.cpython-310.pyc,, +tests/pmemtests/functional/__pycache__/utils.cpython-310.pyc,, +tests/pmemtests/functional/conftest.py,sha256=k-x_1iHYdxzyXdXif5oqLYE_vE4c71LkCRNizLV9_YY,7351 +tests/pmemtests/functional/ilomock.py,sha256=RVaI4wgM3t4c5DysABowPoTPY-4aewftKsajMWUDL_w,3229 +tests/pmemtests/functional/test_all_flag.py,sha256=3aoP3SpPotIRxE6OqZme8T1EN7gDtOebiiVv5UjUVcU,1364 +tests/pmemtests/functional/test_applydcpmmconfig.py,sha256=qBde_vSFsCwecPRYtOzD2J7ykeH55W3535PeWti-K4A,6504 +tests/pmemtests/functional/test_cleardcpmmpendingconfig.py,sha256=6S03RdG8SSIMzvlj5e2CdL0u3kc_wNrPq-0sMBM5mKA,2131 +tests/pmemtests/functional/test_config_flag.py,sha256=4OM08T9VjnTFxTYOkY7ocAyCW-wm-EiJeiM3NwN1rEU,3968 +tests/pmemtests/functional/test_device_flag.py,sha256=ZJ2N2gncAj0QJCCo4eVTr7oofsxUKGvWMKxcKiOpj2A,4632 +tests/pmemtests/functional/test_display_flag.py,sha256=T9hos6NksyBFieQwd30YYZS5nVCy9ihOUUtxArqukpQ,2573 +tests/pmemtests/functional/test_logical_flag.py,sha256=v-xKCRGbo6el6UzUQd62PfYOEjhQ0oW75i_mwbsUBFI,1796 +tests/pmemtests/functional/test_showdcpmm.py,sha256=Wr4odVffd4g7al5IdhUJH8LgRwtQT9vWelTJ3Sn3n8s,5746 +tests/pmemtests/functional/test_showdcpmmpendingconfig.py,sha256=-gBXzgVpAzSChkXE_wJ9zkripe1e7KfMM-xaojXAN-U,2786 +tests/pmemtests/functional/test_summary_flag.py,sha256=zGvBk2MspJJ-mOhpNoLZKSrSflJBfUPHMxaksc6eHsQ,1796 +tests/pmemtests/functional/utils.py,sha256=mtL8gZprd2fJ0AspzwxAWhe68a6uRmoGKa5-X2-AuTA,3359 +tests/pmemtests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tests/pmemtests/integration/__pycache__/__init__.cpython-310.pyc,, +tests/pmemtests/integration/__pycache__/conftest.cpython-310.pyc,, +tests/pmemtests/integration/__pycache__/ilocli.cpython-310.pyc,, +tests/pmemtests/integration/__pycache__/ilodebug.cpython-310.pyc,, +tests/pmemtests/integration/conftest.py,sha256=64jwzUefkm4kXd-5MZwfgwQNOINk-HwNfPZtfPAYBSg,4960 +tests/pmemtests/integration/ilocli.py,sha256=Td5khTodClRvUiKXVzBQXxX8erahhQaW8Y6MQQBa0Ic,2635 +tests/pmemtests/integration/ilodebug.py,sha256=ZE-1RXBqAFPG0h4oHtgtkTiuGJmyXcJ5jtzLiJTWRZc,1874 +tests/pmemtests/unit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +tests/pmemtests/unit/__pycache__/__init__.cpython-310.pyc,, +tests/pmemtests/unit/__pycache__/conftest.cpython-310.pyc,, +tests/pmemtests/unit/__pycache__/test_applydcpmmconfig.cpython-310.pyc,, +tests/pmemtests/unit/__pycache__/test_clear_pending_config_command.cpython-310.pyc,, +tests/pmemtests/unit/__pycache__/test_display_helpers.cpython-310.pyc,, +tests/pmemtests/unit/__pycache__/test_mapper_renderers.cpython-310.pyc,, +tests/pmemtests/unit/__pycache__/test_pmem_helpers.cpython-310.pyc,, +tests/pmemtests/unit/__pycache__/test_rest_helpers.cpython-310.pyc,, +tests/pmemtests/unit/__pycache__/test_show_pmem_commands.cpython-310.pyc,, +tests/pmemtests/unit/conftest.py,sha256=uFoPRv4LN2t_tVrTXVcHY_qcOmL_FKYXwWnxyWiaM9I,409 +tests/pmemtests/unit/test_applydcpmmconfig.py,sha256=K6zZ4mWte_X6kA8YPxQ78FaaKtcceDhE8mxVL2I-Xfw,18985 +tests/pmemtests/unit/test_clear_pending_config_command.py,sha256=qStxUaXd9n4eE8Y9uQum3raVImqaIEZUXHwV_ix6W2Y,7366 +tests/pmemtests/unit/test_display_helpers.py,sha256=J40sv49RBfWSAH6pe4m_0ouwRYafcVEHvrAUpIvf-NQ,6745 +tests/pmemtests/unit/test_mapper_renderers.py,sha256=5LTC-LCfcvCQRm4HQU_yehZkCrbZrU1mr7lBFlLtKq8,10684 +tests/pmemtests/unit/test_pmem_helpers.py,sha256=dxqvoOEYOHQSgBPHl7XTSGvwitBK1HtdIM6GJTkoRmo,6963 +tests/pmemtests/unit/test_rest_helpers.py,sha256=xUbm5aUk3zwfsZy9KUIC9_2t1dEbu-nZq7Kn4akSxUE,19419 +tests/pmemtests/unit/test_show_pmem_commands.py,sha256=XJqZWSqzT9ss34ViSdOlr5zs-QDaRGw5vyCf3XPknt4,12666 diff --git a/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/REQUESTED b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/WHEEL b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/WHEEL new file mode 100644 index 0000000..832be11 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.43.0) +Root-Is-Purelib: true +Tag: py2-none-any +Tag: py3-none-any + diff --git a/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/entry_points.txt b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/entry_points.txt new file mode 100644 index 0000000..3770bdf --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +ilorest = ilorest.rdmc:ilorestcommand diff --git a/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/top_level.txt b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/top_level.txt new file mode 100644 index 0000000..b872e13 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest-7.0.0.0.dist-info/top_level.txt @@ -0,0 +1,2 @@ +ilorest +tests diff --git a/venv/lib/python3.10/site-packages/ilorest/__init__.py b/venv/lib/python3.10/site-packages/ilorest/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..6f56cc4 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/__pycache__/cliutils.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/__pycache__/cliutils.cpython-310.pyc new file mode 100644 index 0000000..2e01ef1 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/__pycache__/cliutils.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/__pycache__/debug_utils.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/__pycache__/debug_utils.cpython-310.pyc new file mode 100644 index 0000000..fa98962 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/__pycache__/debug_utils.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/__pycache__/logging_config_path.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/__pycache__/logging_config_path.cpython-310.pyc new file mode 100644 index 0000000..c92d76a Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/__pycache__/logging_config_path.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/__pycache__/rdmc.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/__pycache__/rdmc.cpython-310.pyc new file mode 100644 index 0000000..845a088 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/__pycache__/rdmc.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/__pycache__/rdmc_base_classes.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/__pycache__/rdmc_base_classes.cpython-310.pyc new file mode 100644 index 0000000..357e5d1 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/__pycache__/rdmc_base_classes.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/__pycache__/rdmc_helper.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/__pycache__/rdmc_helper.cpython-310.pyc new file mode 100644 index 0000000..9e500eb Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/__pycache__/rdmc_helper.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/__pycache__/security_masking.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/__pycache__/security_masking.cpython-310.pyc new file mode 100644 index 0000000..64f9f5e Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/__pycache__/security_masking.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/__pycache__/versioning.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/__pycache__/versioning.cpython-310.pyc new file mode 100644 index 0000000..dc634cc Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/__pycache__/versioning.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/chiflibrary/__init__.py b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/chiflibrary/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1fb4a23 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/chiflibrary/arm/__init__.py b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/arm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/chiflibrary/arm/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/arm/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..e6efd55 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/arm/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/chiflibrary/arm/ilorest_chif.so b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/arm/ilorest_chif.so new file mode 100644 index 0000000..06bce1a Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/arm/ilorest_chif.so differ diff --git a/venv/lib/python3.10/site-packages/ilorest/chiflibrary/ilorest_chif.dll b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/ilorest_chif.dll new file mode 100644 index 0000000..9075f71 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/ilorest_chif.dll differ diff --git a/venv/lib/python3.10/site-packages/ilorest/chiflibrary/ilorest_chif.so b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/ilorest_chif.so new file mode 100644 index 0000000..562f0f6 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/chiflibrary/ilorest_chif.so differ diff --git a/venv/lib/python3.10/site-packages/ilorest/cliutils.py b/venv/lib/python3.10/site-packages/ilorest/cliutils.py new file mode 100644 index 0000000..0e738d3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/cliutils.py @@ -0,0 +1,246 @@ +### +# Copyright 2017 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Base implementation for cli interaction with Redfish interface""" + +# ---------Imports--------- + +import getpass +import os +import re +import subprocess +import sys + +try: + from rdmc_helper import UI +except ImportError: + from ilorest.rdmc_helper import UI + +if os.name == "nt": + import ctypes + from ctypes import windll, wintypes + +# ---------End of imports--------- + + +class CommandNotFoundException(Exception): + """Exception throw when Command is not found""" + + pass + + +class ResourceAllocationError(Exception): + """Exception throw when Command is not found""" + + pass + + +def get_user_config_dir(): + """Platform specific directory for user configuration. + + :returns: returns the user configuration directory. + :rtype: string + """ + if os.name == "nt": + try: + csidl_appdata = 26 + + shgetfolderpath = windll.shell32.SHGetFolderPathW + shgetfolderpath.argtypes = [ + wintypes.HWND, + ctypes.c_int, + wintypes.HANDLE, + wintypes.DWORD, + wintypes.LPCWSTR, + ] + + path_buf = ctypes.create_unicode_buffer(wintypes.MAX_PATH) + result = shgetfolderpath(0, csidl_appdata, 0, 0, path_buf) + + if result == 0: + return path_buf.value + except ImportError: + pass + + return os.environ["APPDATA"] + + return os.path.expanduser("~") + + +def is_exe(filename): + """Determine if filename is an executable. + + :param filename: the filename to examine. + :type filename: string + :returns: True if filename is executable False otherwise. + :rtype: boolean + """ + if sys.platform == "win32": + if os.path.exists(filename): + return True + + # windows uses PATHEXT to list valid executable extensions + pathext = os.environ["PATHEXT"].split(os.pathsep) + (_, ext) = os.path.splitext(filename) + + if ext in pathext: + return True + else: + if os.path.exists(filename) and os.access(filename, os.X_OK): + return True + + return False + + +def find_exe(filename, path=None): + """Search path for a executable (aka which) + + :param filename: the filename to search for. + :type filename: string. + :param path: the path(s) to search (default: os.environ['PATH']) + :param path: string separated with os.pathsep + :returns: string with full path to the file or None if not found. + :rtype: string or None. + """ + if path is None: + path = os.environ["PATH"] + + pathlist = path.split(os.pathsep) + + # handle fully qualified paths + if os.path.isfile(filename) and is_exe(filename): + return filename + + for innerpath in pathlist: + foundpath = os.path.join(innerpath, filename) + + if is_exe(foundpath): + return foundpath + + return None + + +def get_terminal_size(): + """Returns the rows and columns of the terminal as a tuple. + + :returns: the row and column count of the terminal + :rtype: tuple (cols, rows) + """ + _tuple = (80, 25) # default + + if os.name == "nt": + pass + else: + which_stty = find_exe("stty") + + if which_stty: + args = [which_stty, "size"] + try: + procs = subprocess.Popen(args, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + except OSError as excp: + raise ResourceAllocationError(str(excp)) + (stdout_s, _) = procs.communicate() + + _ = procs.wait() + # python3 change + if isinstance(stdout_s, bytes): + stdout_s = stdout_s.decode("utf-8") + if stdout_s and re.search(r"^\d+ \d+$", stdout_s): + rows, cols = stdout_s.split() + _tuple = (cols, rows) + + return _tuple + + +class CLI(object): + """Class for building command line interfaces.""" + + def __init__(self, verbosity=1, out=sys.stdout): + self._verbosity = verbosity + self._out = out + cols, rows = get_terminal_size() + self._cols = int(cols) + self._rows = int(rows) + + def verbosity(self, verbosity): + self._verbosity = verbosity + + def get_hrstr(self, character="-"): + """returns a string suitable for use as a horizontal rule. + + :param character: the character to use as the rule. (default -) + :type character: str. + """ + return "%s\n" % (character * self._cols) + + def printer(self, data, flush=True): + """printing wrapper + + :param data: data to be printed to the output stream + :type data: str. + :param flush: flush buffer - True, not flush output buffer - False + :type flush: boolean + """ + + UI(self._verbosity).printer(data, flush) + + def horizontalrule(self, character="-"): + """writes a horizontal rule to the file handle. + + :param character: the character to use as the rule. (default -) + :type character: str. + """ + + self.printer(self.get_hrstr(character=character)) + + def version(self, progname, version, extracontent): + """Prints a version string to fileh. + + :param progname: the name of the program. + :type progname: str. + :param version: the version of the program. + :type version str. + :param fileh: the file handle to write to. Default is sys.stdout. + :type fileh: file object + :returns: None + """ + tmp = "%s version %s\n%s" % (progname, version, extracontent) + self.printer(tmp) + self.horizontalrule() + + def prompt_password(self, msg, default=None): + """Convenient password prompting function + + :param msg: prompt text. + :type msg: str. + :param default: default value if user does not enter anything. + :type default: str. + :returns: string user entered, or default if nothing entered + """ + message = "%s : " % msg + + if default is not None: + message = "%s [%s] : " % (msg, default) + + i = getpass.getpass(message) + + if i is None or len(i) == 0: + i = default + + i = str(i) + + return i.strip() diff --git a/venv/lib/python3.10/site-packages/ilorest/config/__init__.py b/venv/lib/python3.10/site-packages/ilorest/config/__init__.py new file mode 100644 index 0000000..4886160 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/config/__init__.py @@ -0,0 +1 @@ +"""Config parsing for RDMC""" diff --git a/venv/lib/python3.10/site-packages/ilorest/config/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/config/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..91c8ebb Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/config/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/config/__pycache__/config.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/config/__pycache__/config.cpython-310.pyc new file mode 100644 index 0000000..8f094c7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/config/__pycache__/config.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/config/__pycache__/rdmc_config.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/config/__pycache__/rdmc_config.cpython-310.pyc new file mode 100644 index 0000000..de3815c Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/config/__pycache__/rdmc_config.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/config/config.py b/venv/lib/python3.10/site-packages/ilorest/config/config.py new file mode 100644 index 0000000..7edbcb8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/config/config.py @@ -0,0 +1,159 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Module for working with global configuration options.""" + +# ---------Imports--------- + +import logging +import os +import re + +import six +from six.moves import configparser + +# ---------End of imports--------- + + +# ---------Debug logger--------- + +LOGGER = logging.getLogger(__name__) + +# ---------End of debug logger--------- + + +class AutoConfigParser(object): + """Auto configuration parser. Properties starting with _ac__ are automatically + serialized to config file""" + + _config_pattern = re.compile(r"_ac__(?P.*)") + + def __init__(self, filename=None): + """Initialize AutoConfigParser + + :param filename: file name to be used for config loading. + :type filename: str. + + """ + self._sectionname = "globals" + self._configfile = filename + + @property + def configfile(self): + """The current configuration file location""" + return self._configfile + + def _get_ac_keys(self): + """Retrieve parse option keys""" + result = [] + for key in six.iterkeys(self.__dict__): + match = AutoConfigParser._config_pattern.search(key) + if match: + result.append(match.group("confkey")) + return result + + def _get(self, key): + """Retrieve parse option key + + :param key: key to retrieve. + :type key: str. + + """ + ackey = "_ac__%s" % key.replace("-", "_") + if ackey in self.__dict__: + return self.__dict__[ackey] + return None + + def _set(self, key, value): + """Set parse option key + + :param key: key to be set. + :type key: str. + :param value: value to be given to key. + :type value: str. + + """ + ackey = "_ac__%s" % key.replace("-", "_") + if ackey in self.__dict__: + self.__dict__[ackey] = value + return None + + def load(self, filename=None): + """Load configuration settings from the file filename, if filename + is None then the value from configfile property is used + + :param filename: file name to be used for config loading. + :type filename: str. + + """ + fname = self.configfile + if filename: + fname = filename + + if not fname or not os.path.isfile(fname): + return + + try: + config = configparser.RawConfigParser() + config.read(fname) + for key in self._get_ac_keys(): + configval = None + try: + configval = config.get(self._sectionname, key) + except configparser.NoOptionError: + # also try with - instead of _ + try: + configval = config.get(self._sectionname, key.replace("_", "-")) + except configparser.NoOptionError: + pass + + if configval: + ackey = "_ac__%s" % key + self.__dict__[ackey] = configval + except configparser.NoOptionError: + pass + except configparser.NoSectionError: + pass + + def save(self, filename=None): + """Save configuration settings from the file filename, if filename + is None then the value from configfile property is used + + :param filename: file name to be used for config saving. + :type filename: str. + + """ + fname = self.configfile + if filename: + fname = filename + + if fname: + return + + config = configparser.RawConfigParser() + try: + config.add_section(self._sectionname) + except configparser.DuplicateSectionError: + pass # ignored + + for key in self._get_ac_keys(): + ackey = "_ac__%s" % key + config.set(self._sectionname, key, str(self.__dict__[ackey])) + + fileh = open(self._configfile, "wb") + config.write(fileh) + fileh.close() diff --git a/venv/lib/python3.10/site-packages/ilorest/config/rdmc_config.py b/venv/lib/python3.10/site-packages/ilorest/config/rdmc_config.py new file mode 100644 index 0000000..9758c15 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/config/rdmc_config.py @@ -0,0 +1,288 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Rdmc config""" + +import os +import json + +try: + from config.config import AutoConfigParser + from logging_config_path import get_logging_config_path +except ImportError: + from ilorest.config.config import AutoConfigParser + from ilorest.logging_config_path import get_logging_config_path + + +class RdmcConfig(AutoConfigParser): + """Rdmc config class for loading and parsing the .conf file global configuration options. + Uses the AutoConfigParser.""" + + def _load_logdir_from_json(self): + """Load logdir from logging configuration JSON file. + + :returns: logdir path from JSON config, or current directory as fallback + :rtype: str + """ + config_path = get_logging_config_path() + try: + if os.path.isfile(config_path): + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + handlers = config.get("handlers", {}) + file_handler = handlers.get("file", {}) + filename = file_handler.get("filename", "") + if "%(logdir)s" in filename: + logdir = file_handler.get("filename").split("%(logdir)s")[0].rstrip("/\\") + return logdir if logdir else os.getcwd() + else: + # If filename does not contain %(logdir)s, extract directory from filename + return os.path.dirname(filename) if os.path.dirname(filename) else os.getcwd() + except Exception: + pass + + # Fallback to current working directory if any issues + return os.getcwd() + + def __init__(self, filename=None): + """Initialize RdmcConfig + + :param filename: file name to be used for Rdmcconfig loading. + :type filename: str + + """ + AutoConfigParser.__init__(self, filename=filename) + self._sectionname = "redfish" + self._configfile = filename + self._ac__logdir = self._load_logdir_from_json() + self._ac__cache = True + self._ac__url = "" + self._ac__username = "" + self._ac__password = "" + self._ac__sslcert = "" + self._ac__commit = "" + self._ac__format = "" + self._ac__cachedir = "" + self._ac__savefile = "" + self._ac__loadfile = "" + self._ac__user_cert = "" + self._ac__user_root_ca_key = "" + self._ac__user_root_ca_password = "" + + @property + def configfile(self): + """The current configuration file""" + return self._configfile + + @configfile.setter + def configfile(self, config_file): + """Set the current configuration file + + :param config_file: file name to be used for Rmcconfig loading. + :type config_file: str + """ + self._configfile = config_file + + @property + def logdir(self): + """Get the current log directory""" + return self._get("logdir") + + @logdir.setter + def logdir(self, value): + """Set the current log directory + :param value: current working directory for logging + :type value: str + """ + return self._set("logdir", value) + + @property + def cache(self): + """Get the config file cache status""" + + if isinstance(self._get("cache"), bool): + return self._get("cache") + + return self._get("cache").lower() in ("yes", "true", "t", "1") + + @cache.setter + def cache(self, value): + """Get the config file cache status + + :param value: status of config file cache + :type value: bool + """ + return self._set("cache", value) + + @property + def url(self): + """Get the config file URL""" + url = self._get("url") + url = url[:-1] if url.endswith("/") else url + + return url + + @url.setter + def url(self, value): + """Set the config file URL + + :param value: URL path for the config file + :type value: str + """ + return self._set("url", value) + + @property + def username(self): + """Get the config file user name""" + return self._get("username") + + @username.setter + def username(self, value): + """Set the config file user name + + :param value: user name for config file + :type value: str + """ + return self._set("username", value) + + @property + def password(self): + """Get the config file password""" + return self._get("password") + + @password.setter + def password(self, value): + """Set the config file password + + :param value: password for config file + :type value: str + """ + return self._set("password", value) + + @property + def commit(self): + """Get the config file commit status""" + return self._get("commit") + + @commit.setter + def commit(self, value): + """Set the config file commit status + + :param value: commit status + :type value: str + """ + return self._set("commit", value) + + @property + def format(self): + """Get the config file default format""" + return self._get("format") + + @format.setter + def format(self, value): + """Set the config file default format + + :param value: set the config file format + :type value: str + """ + return self._set("format", value) + + @property + def cachedir(self): + """Get the config file cache directory""" + return self._get("cachedir") + + @cachedir.setter + def cachedir(self, value): + """Set the config file cache directory + + :param value: config file cache directory + :type value: str + """ + return self._set("cachedir", value) + + @property + def defaultsavefilename(self): + """Get the config file default save name""" + return self._get("savefile") + + @defaultsavefilename.setter + def defaultsavefilename(self, value): + """Set the config file default save name + + :param value: config file save name + :type value: str + """ + return self._set("savefile", value) + + @property + def defaultloadfilename(self): + """Get the config file default load name""" + return self._get("loadfile") + + @defaultloadfilename.setter + def defaultloadfilename(self, value): + """Set the config file default load name + + :param value: name of config file to load by default + :type value: str + """ + return self._set("loadfile", value) + + @property + def proxy(self): + """Get proxy value to be set for communication""" + return self._get("proxy") + + @proxy.setter + def proxy(self, value): + """Set proxy value for communication""" + return self._set("proxy", value) + + @property + def ssl_cert(self): + """Get proxy value to be set for communication""" + return self._get("sslcert") + + @ssl_cert.setter + def ssl_cert(self, value): + """Set proxy value for communication""" + return self._set("sslcert", value) + + @property + def user_cert(self): + return self._get("usercert") + + @user_cert.setter + def user_cert(self, value): + return self._set("usercert", value) + + @property + def user_root_ca_key(self): + return self._get("user_root_ca_key") + + @user_root_ca_key.setter + def user_root_ca_key(self, value): + return self._set("user_root_ca_key", value) + + @property + def user_root_ca_password(self): + return self._get("user_root_ca_password") + + @user_root_ca_password.setter + def user_root_ca_password(self, value): + return self._set("user_root_ca_password", value) diff --git a/venv/lib/python3.10/site-packages/ilorest/debug_utils.py b/venv/lib/python3.10/site-packages/ilorest/debug_utils.py new file mode 100644 index 0000000..4c528ac --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/debug_utils.py @@ -0,0 +1,475 @@ +### +# Copyright 2016-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Debug utilities for iLOrest""" + +import json +import os +import logging + +try: + from logging_config_path import get_logging_config_path +except ModuleNotFoundError: + from ilorest.logging_config_path import get_logging_config_path + + +def is_debug_enabled(config_path=None): + """ + Check if debug logging is enabled by reading the logging configuration file. + + Args: + config_path (str, optional): Path to the logging config file. + Defaults to the standard config location. + + Returns: + bool: True if debug logging is enabled, False otherwise. + """ + if config_path is None: + config_path = get_logging_config_path() + + try: + # Check if config file exists + if not os.path.isfile(config_path): + # If config file doesn't exist, assume debug is disabled + return False + + # Load the configuration file + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + # Check the root logger level first + loggers = config.get("loggers", {}) + root_logger = loggers.get("", {}) + root_level = root_logger.get("level", "INFO").upper() + + # If root logger is set to DEBUG, debug is enabled + if root_level == "DEBUG": + return True + + # Check if any specific loggers have DEBUG level + for logger_name, logger_config in loggers.items(): + if logger_name == "": # Skip root logger, already checked + continue + level = logger_config.get("level", "INFO").upper() + if level == "DEBUG": + return True + + # Check handlers for DEBUG level + handlers = config.get("handlers", {}) + for handler_name, handler_config in handlers.items(): + level = handler_config.get("level", "INFO").upper() + if level == "DEBUG": + return True + + # If no DEBUG level found anywhere, debug is disabled + return False + + except (FileNotFoundError, json.JSONDecodeError, KeyError, ValueError): + # If there's any error reading the config, assume debug is disabled + return False + except Exception: + # For any other unexpected error, assume debug is disabled + return False + + +def get_current_log_level(logger_name="", config_path=None): + """ + Get the current log level for a specific logger from the configuration file. + + Args: + logger_name (str): Name of the logger. Empty string for root logger. + config_path (str, optional): Path to the logging config file. + + Returns: + str: The log level (e.g., 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'). + Returns 'INFO' as default if not found. + """ + if config_path is None: + config_path = get_logging_config_path() + + try: + # Check if config file exists + if not os.path.isfile(config_path): + return "INFO" + + # Load the configuration file + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + # Get the specific logger configuration + loggers = config.get("loggers", {}) + logger_config = loggers.get(logger_name, {}) + + # Return the level, defaulting to INFO + return logger_config.get("level", "INFO").upper() + + except Exception: + # For any error, return INFO as default + return "INFO" + + +def set_debug_level_in_config(enable_debug=True, config_path=None): + """ + Update the logging configuration file to enable or disable debug logging. + + Args: + enable_debug (bool): True to enable debug, False to disable. + config_path (str, optional): Path to the logging config file. + + Returns: + bool: True if successful, False otherwise. + """ + if config_path is None: + config_path = get_logging_config_path() + + try: + # Check if config file exists + if not os.path.isfile(config_path): + return False + + # Load the configuration file + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + # Set the desired level + new_level = "DEBUG" if enable_debug else "INFO" + + # Update root logger level + if "loggers" not in config: + config["loggers"] = {} + if "" not in config["loggers"]: + config["loggers"][""] = {} + + config["loggers"][""]["level"] = new_level + + # Update all other loggers to the same level + for logger_name in config["loggers"]: + config["loggers"][logger_name]["level"] = new_level + + # Update handlers to the same level + if "handlers" in config: + for handler_name in config["handlers"]: + config["handlers"][handler_name]["level"] = new_level + + # Write the updated configuration back to file + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2) + + return True + + except PermissionError as e: + logging.warning("Administrator privileges are required to save the configuration") + return False + except Exception as e: + logging.debug(f"Unexpected error in set_debug_level_in_config: {e}") + return False + + +def set_file_handlers_level(root_logger=None, level=logging.INFO): + """Set level for any FileHandler-like handlers on the provided logger. + + Args: + root_logger (logging.Logger): Logger instance to operate on. If None, uses logging.getLogger(). + level (int): Level to set for file handlers. + Returns: + int: Number of handlers updated. + """ + updated = 0 + try: + if root_logger is None: + root_logger = logging.getLogger() + + for h in list(root_logger.handlers): + try: + if isinstance(h, logging.FileHandler) or getattr(h, "baseFilename", None): + h.setLevel(level) + updated += 1 + except Exception: + pass + return updated + except Exception: + return 0 + + +def set_named_handler_level_if_exists(root_logger=None, name="logconfig_stdout", level=logging.INFO): + """Set level on a named handler only if it exists; do not create handlers. + + Args: + root_logger (logging.Logger): Logger to operate on (defaults to root logger). + name (str): Handler name to look for. + level (int): Level to set. + Returns: + bool: True if handler level was updated, False otherwise. + """ + try: + if root_logger is None: + root_logger = logging.getLogger() + + for h in list(root_logger.handlers): + if getattr(h, "name", None) == name: + try: + h.setLevel(level) + return True + except Exception: + return False + return False + except Exception: + return False + + +def update_config_handlers_levels(config_path=None, level_str="INFO", handler_names=None): + """Update handler levels in the JSON logging configuration file. + + Args: + config_path (str): Path to the logging_config.json file. + level_str (str): Level string to set (e.g., 'DEBUG', 'INFO'). + handler_names (iterable|None): If provided, only update these handler names; otherwise update all handlers. + + Returns: + bool: True if update succeeded, False otherwise. + """ + if config_path is None: + config_path = get_logging_config_path() + + try: + if not os.path.isfile(config_path): + return False + + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + if "handlers" not in config: + config["handlers"] = {} + + for hname in list(config["handlers"].keys()): + if handler_names is None or hname in handler_names: + try: + config["handlers"][hname]["level"] = level_str + except Exception: + pass + + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2) + + return True + except PermissionError as e: + logging.warning("Administrator privileges are required to save the configuration") + return False + except Exception as e: + logging.debug(f"Unexpected error in update_config_handlers_levels: {e}") + return False + + +def update_config_loggers_levels(config_path=None, level_str="INFO"): + """Update all logger levels in the JSON logging configuration file. + + Args: + config_path (str): Path to the logging_config.json file. + level_str (str): Level string to set for loggers. + + Returns: + bool: True if update succeeded, False otherwise. + """ + if config_path is None: + config_path = get_logging_config_path() + + try: + if not os.path.isfile(config_path): + return False + + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + if "loggers" not in config: + config["loggers"] = {} + + # Set root logger + if "" not in config["loggers"]: + config["loggers"][""] = {} + config["loggers"][""]["level"] = level_str + + # Set all other loggers to the same level + for lname in list(config["loggers"].keys()): + try: + config["loggers"][lname]["level"] = level_str + except Exception: + pass + + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2) + + return True + except PermissionError as e: + logging.warning("Administrator privileges are required to save the configuration") + return False + except Exception as e: + logging.debug(f"Unexpected error in update_config_loggers_levels: {e}") + return False + + +def set_runtime_logger_levels(root_logger=None, runtime_level=logging.INFO, set_module_logger=None): + """Set runtime logger and handler levels consistently. + + Args: + root_logger (logging.Logger): Logger instance to operate on. Defaults to root logger. + runtime_level (int): Logging level (e.g., logging.DEBUG). + set_module_logger (logging.Logger|None): Optional module-level logger to set (e.g., rdmc helper LOGGER). + + Returns: + bool: True if operations completed (best-effort), False on failure. + """ + try: + if root_logger is None: + root_logger = logging.getLogger() + + root_logger.setLevel(runtime_level) + + # Update named stdout handler if present + set_named_handler_level_if_exists(root_logger, "logconfig_stdout", runtime_level) + + # Update file handlers level + set_file_handlers_level(root_logger, runtime_level) + + if set_module_logger is not None: + try: + set_module_logger.setLevel(runtime_level) + except Exception: + pass + + return True + except Exception: + return False + + +def update_config_file_rotation_settings(config_path=None, max_bytes=None, backup_count=None): + """Update file rotation settings for RotatingFileHandler in the JSON logging configuration file. + + Args: + config_path (str): Path to the logging_config.json file. + max_bytes (int|None): Maximum size of log file in bytes before rotation. + backup_count (int|None): Number of backup files to keep. + + Returns: + bool: True if update succeeded, False otherwise. + """ + if config_path is None: + config_path = get_logging_config_path() + + try: + if not os.path.isfile(config_path): + return False + + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + if "handlers" not in config: + config["handlers"] = {} + + # Update rotation settings for RotatingFileHandler instances + updated = False + for handler_name, handler_config in config["handlers"].items(): + if not isinstance(handler_config, dict): + continue + + handler_class = handler_config.get("class", "") + if "RotatingFileHandler" in handler_class: + if max_bytes is not None: + handler_config["maxBytes"] = max_bytes + updated = True + + if backup_count is not None: + handler_config["backupCount"] = backup_count + updated = True + + if updated: + with open(config_path, "w", encoding="utf-8") as f: + json.dump(config, f, indent=2) + + return updated + except PermissionError as e: + logging.warning("Administrator privileges are required to save the configuration") + return False + except Exception as e: + logging.debug(f"Unexpected error in update_config_file_rotation_settings: {e}") + return False + + +def get_config_file_rotation_settings(config_path=None): + """Get current file rotation settings from the JSON logging configuration file. + + Args: + config_path (str): Path to the logging_config.json file. + + Returns: + dict: Dictionary containing rotation settings for each RotatingFileHandler. + Format: {handler_name: {'maxBytes': value, 'backupCount': value}} + """ + if config_path is None: + config_path = get_logging_config_path() + + rotation_settings = {} + + try: + if not os.path.isfile(config_path): + return rotation_settings + + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + handlers = config.get("handlers", {}) + for handler_name, handler_config in handlers.items(): + if not isinstance(handler_config, dict): + continue + + handler_class = handler_config.get("class", "") + if "RotatingFileHandler" in handler_class: + rotation_settings[handler_name] = { + "maxBytes": handler_config.get("maxBytes"), + "backupCount": handler_config.get("backupCount"), + } + + return rotation_settings + except Exception: + return rotation_settings + + +def validate_rotation_parameters(max_bytes=None, backup_count=None): + """Validate file rotation parameters. + + Args: + max_bytes (int|None): Maximum size of log file in bytes before rotation. + backup_count (int|None): Number of backup files to keep. + + Returns: + tuple: (is_valid, error_message) where is_valid is bool and error_message is str or None. + """ + try: + if max_bytes is not None: + if not isinstance(max_bytes, int): + return False, "log_file_size must be an integer" + if max_bytes <= 0: + return False, "log_file_size must be a positive integer" + + if backup_count is not None: + if not isinstance(backup_count, int): + return False, "backup_count must be an integer" + if backup_count < 0: + return False, "backup_count must be a non-negative integer" + + return True, None + except Exception: + return False, "Invalid rotation parameters" diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/BiosDefaultsCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/BiosDefaultsCommand.py new file mode 100644 index 0000000..9901a5f --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/BiosDefaultsCommand.py @@ -0,0 +1,156 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""BiosDefaultsCommand for rdmc""" + +try: + from rdmc_helper import Encryption, InvalidCommandLineErrorOPTS, ReturnCodes +except ImportError: + from ilorest.rdmc_helper import Encryption, InvalidCommandLineErrorOPTS, ReturnCodes + + +class BiosDefaultsCommand: + """Set BIOS settings back to default for the server that is currently + logged in""" + + def __init__(self): + self.ident = { + "name": "biosdefaults", + "usage": None, + "description": "Run to set the currently" + " logged in server's Bios. type settings to defaults\n\texample: " + "biosdefaults\n\n\tRun to set the currently logged in server's " + "Bios. type settings to user defaults\n\texample: biosdefaults " + "--userdefaults\n\n\tRun to set the currently logged in server " + "to manufacturing defaults, including boot order and secure boot." + "\n\texample: biosdefaults --manufacturingdefaults", + "summary": "Set the currently logged in server to default BIOS settings.", + "aliases": [], + "auxcommands": ["SetCommand", "RebootCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main BIOS defaults worker function""" + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.defaultsvalidation(options) + + if options.manufdefaults: + self.rdmc.ui.printer("Resetting BIOS attributes and settings to manufacturing defaults.\n") + elif options.userdefaults: + self.rdmc.ui.printer("Resetting BIOS attributes and settings to user defaults.\n") + + put_path = self.rdmc.app.typepath.defs.biospath + body = None + + if self.rdmc.app.typepath.defs.isgen10 and not options.manufdefaults: + bodydict = self.rdmc.app.get_handler(self.rdmc.app.typepath.defs.biospath, service=True, silent=True).dict + + for item in bodydict["Actions"]: + if "ResetBios" in item: + action = item.split("#")[-1] + path = bodydict["Actions"][item]["target"] + break + + body = {"Action": action} + + if options.userdefaults: + body["ResetType"] = "default.user" + else: + body["ResetType"] = "default" + + self.rdmc.app.post_handler(path, body) + else: + if options.userdefaults: + body = {"BaseConfig": "default.user"} + elif not options.manufdefaults: + body = {"BaseConfig": "default"} + if body: + self.rdmc.app.put_handler( + put_path + "/settings", + body=body, + optionalpassword=options.biospassword, + ) + + if not body and options.manufdefaults: + setstring = "RestoreManufacturingDefaults=Yes --selector=HpBios. --commit" + if options.reboot: + setstring += " --reboot=%s" % options.reboot + + self.auxcommands["set"].run(setstring) + + elif options.reboot: + self.auxcommands["reboot"].run(options.reboot) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def defaultsvalidation(self, options): + """BIOS defaults method validation function""" + self.cmdbase.login_select_validation(self, options) + + if options.encode: + options.biospassword = Encryption.decode_credentials(options.biospassword) + if isinstance(options.biospassword, bytes): + options.biospassword = options.biospassword.decode("utf-8") + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--reboot", + dest="reboot", + help="Use this flag to perform a reboot command function after" + " completion of operations. For help with parameters and" + " descriptions regarding the reboot flag, run help reboot.", + default=None, + ) + customparser.add_argument( + "--userdefaults", + dest="userdefaults", + action="store_true", + help="Sets bios to user defaults instead of manufacturing defaults.", + default=False, + ) + customparser.add_argument( + "--manufacturingdefaults", + dest="manufdefaults", + action="store_true", + help="Reset all configuration settings to manufacturing defaults, " "including boot order and secure boot.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/BootOrderCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/BootOrderCommand.py new file mode 100644 index 0000000..7be53c2 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/BootOrderCommand.py @@ -0,0 +1,746 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""BootOrder Command for rdmc""" + +import ast +import copy +import fnmatch +from functools import reduce + +import six + +try: + from rdmc_helper import ( + UI, + BootOrderMissingEntriesError, + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidOrNothingChangedSettingsError, + ReturnCodes, + ) +except: + from ilorest.rdmc_helper import ( + UI, + BootOrderMissingEntriesError, + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidOrNothingChangedSettingsError, + ReturnCodes, + ) + + +class BootOrderCommand: + """Changes the boot order for the server that is currently logged in""" + + def __init__(self): + self.ident = { + "name": "bootorder", + "usage": None, + "description": "Run without arguments for current boot order and one time boot options." + "\n\texample: bootorder\n\n\tTo set the persistent boot order pick items " + 'from\n\tthe "Current Persistent Boot Order" section.\n\t' + "example: bootorder [5,4,3,2,1] --commit\n\n\tSetting partial" + " boot order is also supported.\n\tMissing entries are " + "concatenated at the end.\n\texample: bootorder [5] --commit\n\n\t" + "You can also set the boot order using partial string matching.\n\t" + "example: bootorder NIC.*v4 HD* Generic.USB.1.1 --commit\n\n\tThis will set " + "All v4 NICs first, followed by all hard drives,\n\tfollowed by Generic.USB.1.1. " + "Everything not listed will be\n\tadded to the end of the boot order." + '\n\n\tTo set one time boot entry pick items from the\n\t"' + 'Continuous and one time boot uefi options" section.\n\t' + "example: bootorder --onetimeboot=Hdd\n\n\tTo set continuous" + ' boot entry pick items from the\n\t"Continuous and one time ' + 'boot uefi options" section.\n\texample: bootorder --' + "continuousboot=Utilities --commit\n\n\tDisable either " + "continuous or one time boot options.\n\texample: bootorder " + "--disablebootflag --commit\n\n\t" + "Changing Secure Boot Keys:\n\tTo manage secure boot keys use" + " the --securebootkeys flag.\n\tTo delete all keys.\n\n\texample:" + " bootorder --securebootkeys=deletepk\n\tFor all possibilities" + " see the --securebootkeys flag \n\tin the options list.\n\n\t" + "NOTE: pick ONETIMEBOOT and " + 'CONTINUOUS items from "Continuous\n\tand one time boot ' + 'options" section. Items in this list represent\n\ta ' + '"clustered" view of the "Continuous and one time boot uefi' + '\n\toptions" section. Example: choosing Pxe will try to Pxe' + ' boot\n\tcapable devices in the order found in the "' + 'Continuous and one\n\ttime boot options".\n\n\t', + "summary": "Displays and sets the current boot order.", + "aliases": [], + "auxcommands": [ + "GetCommand", + "SelectCommand", + "SetCommand", + "RebootCommand", + ], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main boot order worker function""" + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.bootordervalidation(options) + + if options.secureboot: + self.secureboothelper(options.secureboot) + + if options.reboot: + self.auxcommands["reboot"].run(options.reboot) + + return ReturnCodes.SUCCESS + + self.rdmc.app.select(selector="HpeServerBootSettings.", path_refresh=True) + + props = self.rdmc.app.getprops(skipnonsetting=False) + + for prop in props: + bootname = prop.get("Name") + + self.rdmc.ui.printer("bootname is : {0}\n\n".format(bootname)) + + if self.rdmc.app.typepath.defs.isgen10: + if "current" in bootname.lower() or "pending" in bootname.lower(): + try: + bootpath = prop.get("@odata.id") + except: + bootpath = prop.get("links") + else: + bootpath = "/rest/v1/systems/1/bios/boot" + + self.rdmc.ui.printer("bootpath is : {0}\n\n".format(bootpath)) + + bootsources = self.rdmc.app.get_handler(bootpath, service=True, silent=True).dict["BootSources"] + + bootoverride = None + self.auxcommands["select"].selectfunction("HpeBios.") + try: + bootmode = self.auxcommands["get"].getworkerfunction("BootMode", options, results=True, uselist=True) + except: + bootmode = dict() + bootmode["BootMode"] = "Uefi" + + self.auxcommands["select"].selectfunction("ComputerSystem.") + onetimebootsettings = self.auxcommands["get"].getworkerfunction( + ["Boot/" + self.rdmc.app.typepath.defs.bootoverridetargettype], + options, + results=True, + uselist=True, + ) + + bootstatus = self.auxcommands["get"].getworkerfunction( + ["Boot/BootSourceOverrideEnabled"], + options, + results=True, + uselist=True, + ) + + targetstatus = self.auxcommands["get"].getworkerfunction( + ["Boot/BootSourceOverrideTarget"], + options, + results=True, + uselist=True, + ) + + uefitargetstatus = self.auxcommands["get"].getworkerfunction( + ["Boot/UefiTargetBootSourceOverride"], + options, + results=True, + uselist=True, + ) + + currentsettings = self.rdmc.app.get_handler(self.rdmc.app.typepath.defs.systempath, service=True, silent=True) + + if bootmode and bootmode[0].get("BootMode", None) == "Uefi": + if self.rdmc.app.typepath.defs.isgen9: + uefionetimebootsettings = self.auxcommands["get"].getworkerfunction( + ["Boot/UefiTargetBootSourceOverrideSupported"], + options, + results=True, + uselist=True, + ) + + else: + # Gen 10 + bootsettings = self.auxcommands["get"].getworkerfunction( + ["Boot"], + options, + results=True, + uselist=True, + )[0]["Boot"] + finaluefi = [] + if "UefiTargetBootSourceOverride@Redfish.AllowableValues" in bootsettings: + uefionetimebootsettings = bootsettings["UefiTargetBootSourceOverride@Redfish.AllowableValues"] + + for setting in uefionetimebootsettings: + for source in bootsources: + if "UEFIDevicePath" in source and source["UEFIDevicePath"].endswith(setting): + finaluefi.append(source["StructuredBootString"]) + continue + + uefionetimebootsettings = {"Boot": {"UefiTargetBootSourceOverrideSupported": finaluefi}} + else: + uefionetimebootsettings = None + + if options.onetimeboot is None and options.continuousboot is None and not options.disablebootflag: + self.auxcommands["select"].selectfunction("HpeServerBootSettings.") + bootsettings = self.auxcommands["get"].getworkerfunction( + "PersistentBootConfigOrder", options, results=True, uselist=True + ) + + if not args: + if not options.json: + self.print_out_boot_order( + bootsettings, + onetimebootsettings, + uefionetimebootsettings, + bootmode, + bootsources, + bootstatus, + targetstatus, + ) + elif options.json: + UI().print_out_json(bootsettings) + elif len(args) == 1 and args[0][0] == "[": + bootlist = args[0][1:-1].split(",") + currentlist = bootsettings[0]["PersistentBootConfigOrder"] + + if not isinstance(currentlist, list): + templist = ast.literal_eval(currentlist[1:-1]) + currentlist = [n.strip() for n in templist] + + removallist = copy.deepcopy(currentlist) + + if len(bootlist) > len(currentlist): + raise InvalidCommandLineError("Number of entries is greater than the current boot order length.") + else: + newlist = "[" + + for value, _ in enumerate(bootlist): + try: + newlist += currentlist[int(bootlist[value]) - 1] + except: + raise InvalidCommandLineError( + "Invalid entry " + "number passed to bootorder. Please \n" + " run bootorder without arguments" + " for possible boot \n order numbers. " + ) + + removallist.remove(currentlist[int(bootlist[value]) - 1]) + + if removallist: + newlist += "," + + if not removallist: + newlist += "]" + else: + for value, _ in enumerate(removallist): + newlist += removallist[value] + + if not value == len(removallist) - 1: + newlist += "," + + newlist += "]" + + if options.biospassword: + newlist += " --biospassword " + options.biospassword + + if options.reboot: + newlist += " --commit --reboot " + options.reboot + elif options.commit: + newlist += " --commit" + + self.auxcommands["set"].run("PersistentBootConfigOrder=" + newlist) + else: + currlist = bootsettings[0]["PersistentBootConfigOrder"] + if not isinstance(currlist, list): + templist = ast.literal_eval(currlist[1:-1]) + currlist = [n.strip() for n in templist] + remlist = copy.deepcopy(currlist) + if len(args) > len(currlist): + raise InvalidCommandLineError("Number of entries is " "greater than the current boot order length.") + newlist = [] + for arg in args: + argmatch = [val for val in remlist if fnmatch.fnmatch(val.lower(), arg.lower())] + if not argmatch and not options.ime: + raise InvalidCommandLineError( + "Invalid entry passed: " + "{0}. Please run bootorder to check for possible " + "values and reevaluate.\n".format(arg) + ) + if argmatch: + newlist.extend(argmatch) + _ = [remlist.remove(val) for val in newlist if val in remlist] + newlist.extend(remlist) + strlist = "[" + concatlist = reduce((lambda x, y: x + "," + y), newlist) + strlist = strlist + concatlist + "]" + + if options.biospassword: + strlist += " --biospassword " + options.biospassword + if options.reboot: + strlist += " --commit --reboot " + options.reboot + elif options.commit: + strlist += " --commit" + self.auxcommands["set"].run("PersistentBootConfigOrder=" + strlist) + else: + if options.onetimeboot is not None: + entry = options.onetimeboot + + if not bootstatus[0]["Boot"]["BootSourceOverrideEnabled"] == "Once": + bootoverride = " Boot/BootSourceOverrideEnabled=Once" + elif options.continuousboot is not None: + entry = options.continuousboot + + if not bootstatus[0]["Boot"]["BootSourceOverrideEnabled"] == "Continuous": + bootoverride = " Boot/BootSourceOverrideEnabled=Continuous" + else: + entry = "JacksBootOption" + if not bootstatus[0]["Boot"]["BootSourceOverrideEnabled"] == "Disabled": + if currentsettings.dict["Boot"]["BootSourceOverrideEnabled"] == "Disabled": + bootoverride = "Boot/BootSourceOverrideTarget=None" " Boot/BootSourceOverrideEnabled=Disabled" + else: + bootoverride = "Boot/BootSourceOverrideTarget=None" + + newlist = "" + + if entry.lower() in ( + item.lower() + for item in onetimebootsettings[0]["Boot"][self.rdmc.app.typepath.defs.bootoverridetargettype] + ): + if entry and isinstance(entry, six.string_types): + entry = entry.upper() + + entry = self.searchcasestring( + entry, + onetimebootsettings[0]["Boot"][self.rdmc.app.typepath.defs.bootoverridetargettype], + ) + + if not entry == targetstatus[0]["Boot"]["BootSourceOverrideTarget"]: + newlist += " Boot/BootSourceOverrideTarget=" + entry + + if bootoverride: + newlist += bootoverride + + if options.biospassword and newlist: + newlist += " --biospassword " + options.biospassword + + if options.reboot and newlist: + newlist += " --commit --reboot " + options.reboot + elif options.commit and newlist: + newlist += " --commit" + + if newlist: + self.auxcommands["set"].run(newlist) + else: + raise InvalidOrNothingChangedSettingsError("Entry is the current boot setting.") + elif ( + uefionetimebootsettings + and uefionetimebootsettings["Boot"]["UefiTargetBootSourceOverrideSupported"] + and entry in (item for item in uefionetimebootsettings["Boot"]["UefiTargetBootSourceOverrideSupported"]) + ): + if entry and isinstance(entry, six.string_types): + entry = entry.upper() + + entry = self.searchcasestring( + entry, + uefionetimebootsettings["Boot"]["UefiTargetBootSourceOverrideSupported"], + ) + try: + # gen10 + allowable_vals = next( + iter( + self.auxcommands["get"].getworkerfunction( + ["Boot/UefiTargetBootSourceOverride@Redfish.AllowableValues"], + options, + results=True, + uselist=True, + ) + ), + {}, + )["Boot"]["UefiTargetBootSourceOverride@Redfish.AllowableValues"] + for source in bootsources: + if source["StructuredBootString"].upper() == entry.upper(): + for val in allowable_vals: + if "UEFIDevicePath" in source and source["UEFIDevicePath"].endswith(val): + entry = val + break + except KeyError: + pass + + if not entry == uefitargetstatus["Boot"]["UefiTargetBootSourceOverride"]: + newlist += " Boot/UefiTargetBootSourceOverride=" + entry + elif not targetstatus["Boot"]["BootSourceOverrideTarget"] == "UefiTarget": + newlist += " Boot/BootSourceOverrideTarget=UefiTarget" + + if bootoverride: + if self.rdmc.app.typepath.defs.isgen9 and newlist: + if not bootoverride.split("=")[-1] == bootstatus["Boot"]["BootSourceOverrideEnabled"]: + # Preemptively set UefiTargetBootSourceOverride so iLO 4 doesn't complain + self.rdmc.app.patch_handler( + self.rdmc.app.typepath.defs.systempath, + {"Boot": {"UefiTargetBootSourceOverride": entry}}, + silent=True, + service=True, + ) + self.rdmc.app.select(selector=self.rdmc.app.selector, path_refresh=True) + newlist = "" + newlist += bootoverride + else: + newlist += bootoverride + + if options.reboot and newlist: + newlist += " --commit --reboot " + options.reboot + elif options.commit and newlist: + newlist += " --commit" + + if newlist: + try: + self.auxcommands["set"].run(newlist) + except InvalidOrNothingChangedSettingsError: + if self.rdmc.app.typepath.defs.isgen9: + pass + else: + raise + else: + raise InvalidOrNothingChangedSettingsError("Entry is the " "current boot setting.\n") + elif options.disablebootflag: + if bootoverride: + newlist += bootoverride + + if options.reboot: + newlist += " --commit --reboot " + options.reboot + elif options.commit and newlist: + newlist += " --commit" + + if newlist: + self.auxcommands["set"].run(newlist) + else: + raise InvalidOrNothingChangedSettingsError("Entry is the " "current boot setting.\n") + else: + raise InvalidCommandLineError( + "Invalid entry passed for one" + " time boot. Please run boot \n order without" + " arguments to view available options.\n" + ) + + self.cmdbase.logout_routine(self, options) + + # Return code + return ReturnCodes.SUCCESS + + def searchcasestring(self, entry, content): + """Helper function for retrieving correct case for value + + :param entry: entry to correlate case + :type entry: string. + :param content: list of items + :type content: list. + """ + for item in content: + if entry.upper() == item.upper(): + return item + + def secureboothelper(self, securebootoption): + """Helper function for secure boot function + + :param securebootoption: option passed in for secure boot + :type securebootoption: string. + """ + actionlist = ["defaultkeys", "deletekeys", "deletepk"] + + if not securebootoption.lower() in actionlist: + raise InvalidCommandLineError("%s is not a valid option for " "the securebootkeys flag." % securebootoption) + + if securebootoption == actionlist[0]: + action = "ResetAllKeysToDefault" + elif securebootoption == actionlist[1]: + action = "DeleteAllKeys" + elif securebootoption == actionlist[2]: + action = "DeletePK" + + results = self.rdmc.app.select(selector=self.rdmc.app.typepath.defs.hpsecureboot) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + results = results.resp.dict + + try: + for item in results["Actions"]: + if "ResetKeys" in item: + path = results["Actions"][item]["target"] + break + + body = {"ResetKeysType": action} + self.rdmc.app.post_handler(path, body) + except: + if securebootoption == actionlist[0]: + self.auxcommands["select"].selectfunction(self.rdmc.app.typepath.defs.hpsecureboot) + self.auxcommands["set"].run("ResetToDefaultKeys=True --commit") + elif securebootoption == actionlist[1]: + self.auxcommands["select"].selectfunction(self.rdmc.app.typepath.defs.hpsecureboot) + self.auxcommands["set"].run("ResetAllKeys=True --commit") + else: + self.rdmc.ui.warn("DeletePK option is not available on Gen9.\n") + + def print_out_boot_order( + self, + content, + onetimecontent, + uefionetimecontent, + bootmode, + bootsources, + bootstatus, + targetstatus, + ): + """Convert content to human readable and print out to std.out + + :param content: current content + :type content: string. + :param onetimecontent: list of one time boot entries + :type onetimecontent: list. + :param uefionetimecontent: list of uefi one time boot entries + :type uefionetimecontent: list. + :param bootmode: current system boot mode + :type bootmode: string. + :param bootsources: current systems boot sources + :type bootsources: list. + """ + if content is None: + raise BootOrderMissingEntriesError("No entries found in " "current boot order.\n\n") + else: + self.print_boot_helper(content[0], "\nCurrent Persistent Boot " "Order:", bootsources=bootsources) + + bootstatusval = bootstatus[0]["Boot"]["BootSourceOverrideEnabled"] + boottoval = targetstatus[0]["Boot"]["BootSourceOverrideTarget"] + if bootstatusval == "Continuous": + self.rdmc.ui.printer("Current continuous boot: {0}\n\n".format(boottoval)) + elif bootstatusval == "Once": + self.rdmc.ui.printer("Current one time boot: {0}\n\n".format(boottoval)) + + if onetimecontent is None: + raise BootOrderMissingEntriesError("No entries found for one time boot options.\n\n") + else: + self.print_boot_helper(onetimecontent[0]["Boot"], "Continuous and one time boot options:") + + if bootmode and any([bootmode[0].get(boot, None) == "Uefi" for boot in bootmode[0]]): + if uefionetimecontent is None: + self.rdmc.ui.printer("Continuous and one time boot uefi options:\n") + self.rdmc.ui.printer( + "No entries found for one-time UEFI options or boot source mode " "is not set to UEFI." + ) + else: + if isinstance(uefionetimecontent, dict): + self.print_boot_helper( + uefionetimecontent["Boot"], + "Continuous and one time boot uefi options:", + bootsources=bootsources, + ) + else: + self.print_boot_helper( + uefionetimecontent[0]["Boot"], + "Continuous and one time boot uefi options:", + bootsources=bootsources, + ) + + def print_boot_helper(self, content, outstring, indent=0, bootsources=None): + """Print boot helper + + :param content: current content + :type content: string. + :param outstring: output string + :type outstring: string. + :param indent: indent format + :type indent: string. + :param bootsources: current systems boot sources + :type bootsources: list. + """ + for _, value in list(content.items()): + self.rdmc.ui.printer("\t" * indent + outstring) + + if isinstance(value, list): + count = 1 + + for item in value: + self.rdmc.ui.printer("\n") + + if not item: + item = str("null") + + # if isinstance(item, six.string_types): + bootstring = False + try: + for source in bootsources: + if item == source["StructuredBootString"]: + self.rdmc.ui.printer( + "\t" * indent + + str(count) + + ". " + + str(item) + + " (" + + str(source["BootString"]) + + ")" + ) + bootstring = True + break + + if not bootstring: + self.rdmc.ui.printer("\t" * indent + str(count) + ". " + str(item)) + except: + self.rdmc.ui.printer("\t" * indent + str(count) + ". " + str(item)) + + count += 1 + # else: + # self.print_boot_helper(item, indent+1) + + self.rdmc.ui.printer("\n\n") + + def bootordervalidation(self, options): + """Boot order method validation function + + :param options: command line options + :type options: list. + """ + inputline = list() + + if self.rdmc.config.commit.lower() == "true": + options.commit = True + + self.cmdbase.login_select_validation(self, options) + + if inputline: + self.lobobj.loginfunction(inputline) + + if options.encode: + options.biospassword = Encryption.decode_credentials(options.biospassword) + if isinstance(options.biospassword, bytes): + options.biospassword = options.biospassword.decode("utf-8") + + @staticmethod + def options_argument_group(parser): + """Additional argument + + :param parser: The parser to add the removeprivs option group to + :type parser: ArgumentParser/OptionParser + """ + + parser.add_argument_group( + "GLOBAL OPTION:", + "Option(s) are available " "for all arguments within the scope of this command.", + ) + + parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + self.options_argument_group(customparser) + + customparser.add_argument( + "--onetimeboot", + dest="onetimeboot", + help="Use this flag to configure a one-time boot option." + " Using this flag will prioritize the provided boot source" + " only on the very next time the server is booted.", + default=None, + ) + customparser.add_argument( + "--continuousboot", + dest="continuousboot", + help="Use this flag to enable a continuous boot option. Using" + " this flag will cause the system to boot to the selected" + " device every time the system boots.", + default=None, + ) + customparser.add_argument( + "--disablebootflag", + dest="disablebootflag", + action="store_true", + help="Use this to disable either continuous or one-time boot modes.", + default=None, + ) + customparser.add_argument( + "--securebootkeys", + dest="secureboot", + help="Use this flag to perform actions on secure boot keys." + "Possible values include defaultkeys: resets all keys to default," + "deletekeys: deletes all keys, deletepk: deletes all product keys.", + default=False, + ) + customparser.add_argument( + "--commit", + dest="commit", + action="store_true", + help="Use this flag when you are ready to commit all pending" + " changes. Note that some changes made in this way will be updated" + " instantly, while others will be reflected the next time the" + " server is started.", + default=None, + ) + customparser.add_argument( + "--reboot", + dest="reboot", + help="Use this flag to perform a reboot command function after" + " completion of operations. For help with parameters and" + " descriptions regarding the reboot flag, run help reboot.", + default=None, + ) + customparser.add_argument( + "--ignorematcherror", + dest="ime", + action="store_true", + help="Use this flag when you want to run multiple matches and " + "not throw an error in case there are no matches found for given " + "expression.", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/IscsiConfigCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/IscsiConfigCommand.py new file mode 100644 index 0000000..a6e0d31 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/IscsiConfigCommand.py @@ -0,0 +1,978 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""IscsiConfig Command for rdmc""" + +import json +import re +import time + +import redfish.ris + +try: + from rdmc_helper import ( + BootOrderMissingEntriesError, + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NicMissingOrConfigurationError, + ReturnCodes, + ) +except: + from ilorest.rdmc_helper import ( + BootOrderMissingEntriesError, + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NicMissingOrConfigurationError, + ReturnCodes, + ) + + +class IscsiConfigCommand: + """Changes the iscsi configuration for the server that is currently logged in""" + + def __init__(self): + self.ident = { + "name": "iscsiconfig", + "usage": None, + "description": "Run without" + " arguments for available NIC sources for iSCSI" + " configuration.\n\texample: iscsiconfig\n\n\tDisplay" + " the current iSCSI configuration:\n\texample: " + "iscsiconfig --list\n\n\tSaving current iSCSI " + "configuration to a file:\n\texample: iscsiconfig " + "--list -f output.txt\n\n\tLoading iSCSI " + "configurations from a file:\n\texample: iscsiconfig " + "--modify output.txt\n\n\tIn order to add a NIC " + 'source to an iSCSI boot attempt you must run \n\t"' + 'iscsiconfig" without any paramters. This will ' + "display a list of NIC\n\tsources which are currently" + " present in the system.\n\tAdding an iSCSI boot " + "attempt:\n\texample: iscsiconfig --add 1\n\n\tIn " + "order to delete an iSCSI boot attempt you must run" + '\n\t"iscsiconfig --list" to view the currently ' + "configured attempts.\n\tOnce you find the attempt " + "you want to delete simply pass the attempt\n\tnumber" + " to the iSCSI delete function.\n\tDeleting an iSCSI " + "boot attempt:\n\texample: iscsiconfig --delete 1", + "summary": "Displays and configures the current iscsi settings.", + "aliases": [], + "auxcommands": [ + "GetCommand", + "SetCommand", + "SelectCommand", + "RebootCommand", + "LogoutCommand", + ], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main iscsi configuration worker function + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.iscsiconfigurationvalidation(options) + + if self.rdmc.app.typepath.defs.isgen10: + iscsipath = self.gencompatpaths(selector="HpeiSCSISoftwareInitiator.", rel=False) + if "/settings" not in iscsipath: + iscsisettingspath = iscsipath + "settings" + else: + iscsisettingspath = iscsipath + bootpath = self.gencompatpaths(selector="HpeServerBootSettings.") + bootpath = bootpath.replace("/settings", "") + else: + # TODO: update gencompats to handle the nesting of these links within the gen 9 version. + if self.rdmc.app.typepath.defs.biospath[-1] == "/": + iscsipath = self.rdmc.app.typepath.defs.biospath + "iScsi/" + iscsisettingspath = self.rdmc.app.typepath.defs.biospath + "iScsi/settings/" + bootpath = self.rdmc.app.typepath.defs.biospath + "Boot/" + else: + iscsipath = self.rdmc.app.typepath.defs.biospath + "/iScsi" + iscsisettingspath = self.rdmc.app.typepath.defs.biospath + "/iScsi/settings" + bootpath = self.rdmc.app.typepath.defs.biospath + "/Boot" + + if options.list: + self.listoptionhelper(options, iscsipath, iscsisettingspath, bootpath) + elif options.modify: + self.modifyoptionhelper(options, iscsisettingspath) + elif options.add: + self.addoptionhelper(options, iscsipath, iscsisettingspath, bootpath) + elif options.delete: + self.deleteoptionhelper(options, iscsisettingspath) + elif not args: + self.defaultsoptionhelper(options, iscsipath, bootpath) + else: + if len(args) < 2: + self.iscsiconfigurationvalidation(options) + else: + raise InvalidCommandLineError( + "Invalid number of parameters. " "Iscsi configuration takes a maximum of 1 parameter." + ) + + if options.reboot: + self.auxcommands["reboot"].run(options.reboot) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def gencompatpaths(self, selector=None, rel=False): + """Helper function for finding gen compatible paths + + :param selector: the type selection for the get operation + :type selector: str. + :param rel: flag to tell select function to reload selected instance + :type rel: boolean. + :returns: returns urls + """ + + self.rdmc.app.select(selector=selector, path_refresh=rel) + props = self.rdmc.app.getprops(skipnonsetting=False) + for prop in props: + name = prop.get("Name") + if "current" in name.lower() or "pending" in name.lower(): + try: + path = prop.get("@odata.id") + except: + self.rdmc.ui.error("URI path could not be found.") + return path + + def get_enabled_only(self): + enable_disable = dict() + final_enabled_list = list() + + bios = "/redfish/v1/systems/1/bios/" + bios_dict = self.rdmc.app.get_handler(bios, silent=True, service=True).dict + if "Attributes" in bios_dict: + bios_dict_attributes = bios_dict["Attributes"].items() + else: + bios_dict_attributes = bios_dict.items() + for attr, val in bios_dict_attributes: + if "NetworkBoot" in str(val): + enable_disable[attr] = val + # Enabled NIC only listing + for k, v in enable_disable.items(): + final_enabled_list.append(k) + + return final_enabled_list + + def addoptionhelper(self, options, iscsipath, iscsisettingspath, bootpath): + """Helper function to add option for iscsi + + :param options: command line options + :type options: list. + :param iscsipath: current iscsi path + :type iscsipath: str. + :param iscsisettingspath: current iscsi settings path + :type iscsisettingspath: str. + :param bootpath: current boot path + :type bootpath: str. + """ + devicealloc = list() + self.auxcommands["select"].selectfunction("HpeBiosMapping.") + pcisettingsmap = self.auxcommands["get"].getworkerfunction( + "BiosPciSettingsMappings", options, results=True, uselist=True + ) + + for item in pcisettingsmap[0]["BiosPciSettingsMappings"]: + if "Associations" in item: + if ( + "EmbNicEnable" in item["Associations"] + or "EmbNicConfig" in item["Associations"] + or "OcpAEnable" in item["Associations"] + ): + _ = [devicealloc.append(x) for x in item["Subinstances"]] + + if ( + re.match("FlexLom[0-9]+Enable", item["Associations"][0]) + or re.match("PciSlot[0-9]+Enable", item["Associations"][0]) + or re.match("Slot[0-9]+NicBoot[0-9]+", item["Associations"][0]) + ): + _ = [devicealloc.append(x) for x in item["Subinstances"]] + + foundlocation = False + iscsibootsources = self.rawdatahandler(action="GET", silent=True, jsonflag=True, path=iscsisettingspath) + count = 0 + attemptinstancenumber = self.bootattemptcounter(iscsibootsources[self.rdmc.app.typepath.defs.iscsisource]) + if self.rdmc.app.typepath.defs.isgen10: + newpcilist = [] + self.auxcommands["select"].selectfunction("HpeServerPciDeviceCollection") + pcideviceslist = next( + iter(self.auxcommands["get"].getworkerfunction("Members", options, results=True, uselist=False)), + None, + ) + for device in pcideviceslist["Members"]: + newpcilist.append(self.rdmc.app.get_handler(device["@odata.id"], silent=True).dict) + pcideviceslist = newpcilist + else: + self.auxcommands["select"].selectfunction(["Collection."]) + pcideviceslist = next( + iter( + self.auxcommands["get"].getworkerfunction( + "Items", + options, + results=True, + uselist=False, + filtervals=("MemberType", "HpServerPciDevice.*"), + ) + ), + None, + )["Items"] + + self.pcidevicehelper(devicealloc, iscsipath, bootpath, pcideviceslist) + + devicealloc_final = self.make_final_list(devicealloc, pcideviceslist) + + print_flag = False + iscsi_list = self.listoptionhelper(options, iscsipath, iscsisettingspath, bootpath, print_flag) + refined_list = list() + for s in iscsi_list: + for key, val in s.items(): + if key != "Not Added": + for k, v in val.items(): + refined_list.append(v["iSCSINicSource"]) + enabled_only_bootsource = self.get_enabled_only() + refined_devicealloc = list() + for d in devicealloc: + if "PreBootNetwork" in d["Associations"][0]: + nic = d["Associations"][1] + else: + nic = d["Associations"][0] + if "Storage" in nic: + continue + if nic in enabled_only_bootsource: + refined_devicealloc.append(d) + + for item in iscsibootsources[self.rdmc.app.typepath.defs.iscsisource]: + try: + if not item[self.rdmc.app.typepath.defs.iscsiattemptinstance]: + nicsourcedata = refined_devicealloc[int(options.add) - 1]["Associations"] + if "PreBootNetwork" in nicsourcedata[0]: + nic_getting_added = nicsourcedata[1] + else: + nic_getting_added = nicsourcedata[0] + if nic_getting_added in refined_list: + self.rdmc.ui.printer("Warning: This NIC is already added to existing attempt\n") + iscsibootsources[self.rdmc.app.typepath.defs.iscsisource][count]["iSCSINicSource"] = ( + nicsourcedata[1] if isinstance(nicsourcedata[0], dict) else nicsourcedata[0] + ) + + iscsibootsources[self.rdmc.app.typepath.defs.iscsisource][count][ + self.rdmc.app.typepath.defs.iscsiattemptinstance + ] = attemptinstancenumber + iscsibootsources[self.rdmc.app.typepath.defs.iscsisource][count][ + self.rdmc.app.typepath.defs.iscsiattemptname + ] = str(attemptinstancenumber) + foundlocation = True + break + except Exception: + raise NicMissingOrConfigurationError("Invalid input value for configuring NIC.") + count += 1 + + if foundlocation: + self.rdmc.app.patch_handler( + iscsisettingspath, + iscsibootsources, + optionalpassword=options.biospassword, + ) + else: + raise NicMissingOrConfigurationError("Failed to add NIC. All NICs" " have already been configured.") + + def make_final_list(self, devicealloc, pcideviceslist): + final_devicealloc = [] + for item in devicealloc: + if isinstance(item["Associations"][0], dict): + listval = 1 + else: + listval = 0 + if "Storage" not in item["Associations"][listval]: + for pcidevice in pcideviceslist: + # if item["CorrelatableID"] == pcidevice["UEFIDevicePath"]: + final_devicealloc.append(item) + return final_devicealloc + + def bootattemptcounter(self, bootsources): + """Helper function to count the current boot entries for iscsi + + :param bootsources: current iscsi boot sources + :type bootsources: list. + """ + size = 0 + count = list() + + for item in bootsources: + size += 1 + + if item[self.rdmc.app.typepath.defs.iscsiattemptinstance]: + count.append(int(item[self.rdmc.app.typepath.defs.iscsiattemptinstance])) + + if size == len(count): + raise NicMissingOrConfigurationError("Failed to add NIC. All " "NICs have already been configured.") + + count.sort(key=None, reverse=False) + + if len(count) > 0: + iterate = 0 + + for i in range(1, size + 1, 1): + if iterate < len(count) and i == count[iterate]: + iterate += 1 + else: + return iterate + 1 + else: + return int(1) + + def deleteoptionhelper(self, options, iscsisettingspath): + """Helper function to delete option for iscsi + + :param options: command line options + :type options: list. + :param iscsisettingspath: current iscsi settings path + :type iscsisettingspath: string. + """ + patch = None + + self.auxcommands["select"].selectfunction("HpBaseConfigs.") + contents = self.rdmc.app.getprops(selector="BaseConfigs") + + for content in contents: + for key in content["BaseConfigs"][0]["default"]: + if key == self.rdmc.app.typepath.defs.iscsisource: + patch = content["BaseConfigs"][0]["default"][key] + + if not patch: + raise NicMissingOrConfigurationError("Could not access Base Configurations.") + + self.validateinput(options=options, deleteoption=True) + + foundlocation = False + iscsibootsources = self.rawdatahandler(action="GET", silent=True, jsonflag=False, path=iscsisettingspath) + holdetag = iscsibootsources.getheader("etag") + iscsibootsources = json.loads(iscsibootsources.read) + + try: + count = 0 + for item in iscsibootsources[self.rdmc.app.typepath.defs.iscsisource]: + if item[self.rdmc.app.typepath.defs.iscsiattemptinstance] == int(options.delete): + iscsibootsources[self.rdmc.app.typepath.defs.iscsisource][count] = patch[count] + foundlocation = True + + count += 1 + except Exception: + raise NicMissingOrConfigurationError( + "The NIC targeted for delete" " does not exist. The request for " "delete could not be completed." + ) + + if foundlocation: + self.rdmc.app.put_handler( + iscsisettingspath, + iscsibootsources, + optionalpassword=options.biospassword, + headers={"if-Match": holdetag}, + ) + self.rdmc.app.get_handler(iscsisettingspath, silent=True) + else: + raise NicMissingOrConfigurationError("The given attempt instance does not exist.") + + def listoptionhelper(self, options, iscsipath, iscsisettingspath, bootpath, print_flag=True): + """Helper function to list options for iscsi + + :param options: command line options + :type options: list. + :param iscsipath: current iscsi path + :type iscsipath: str. + :param iscsisettingspath: current iscsi settings path + :type iscsisettingspath: str. + :param bootpath: current boot path + :type bootpath: str. + """ + self.auxcommands["select"].selectfunction("HpeBiosMapping.") + pcisettingsmap = self.auxcommands["get"].getworkerfunction( + "BiosPciSettingsMappings", options, results=True, uselist=True + ) + + devicealloc = list() + for item in pcisettingsmap[0]["BiosPciSettingsMappings"]: + if "Associations" in item: + if ( + "EmbNicEnable" in item["Associations"] + or "EmbNicConfig" in item["Associations"] + or "OcpAEnable" in item["Associations"] + ): + _ = [devicealloc.append(x) for x in item["Subinstances"]] + + if ( + re.match("FlexLom[0-9]+Enable", item["Associations"][0]) + or re.match("PciSlot[0-9]+Enable", item["Associations"][0]) + or re.match("Slot[0-9]+NicBoot[0-9]+", item["Associations"][0]) + ): + _ = [devicealloc.append(x) for x in item["Subinstances"]] + + if self.rdmc.app.typepath.defs.isgen10: + newpcilist = [] + + self.auxcommands["select"].selectfunction("HpeServerPciDeviceCollection") + pcideviceslist = next( + iter(self.auxcommands["get"].getworkerfunction("Members", options, results=True, uselist=False)), + None, + ) + + for device in pcideviceslist["Members"]: + newpcilist.append(self.rdmc.app.get_handler(device["@odata.id"], silent=True).dict) + + pcideviceslist = newpcilist + else: + self.auxcommands["select"].selectfunction(["Collection."]) + pcideviceslist = next( + iter( + self.auxcommands["get"].getworkerfunction( + "Items", + options, + results=True, + uselist=False, + filtervals=("MemberType", "HpServerPciDevice.*"), + ) + ), + None, + )["Items"] + i = 0 + iscsibootsources = "" + while i < 4: + self.auxcommands["select"].selectfunction("HpiSCSISoftwareInitiator.") + iscsibootsources = self.rawdatahandler(action="GET", silent=True, jsonflag=True, path=iscsisettingspath) + if not ("error" in iscsibootsources): + break + time.sleep(10) + i = i + 1 + self.rdmc.ui.printer("Retrying ...\n\n") + structeredlist = list() + + self.pcidevicehelper(devicealloc, iscsipath, bootpath, pcideviceslist) + if not ("error" in iscsibootsources): + total_attempts = [] + for item in iscsibootsources[self.rdmc.app.typepath.defs.iscsisource]: + if item["iSCSINicSource"]: + iscsi_attempt = item[self.rdmc.app.typepath.defs.iscsiattemptinstance] + for device in devicealloc: + it = device["CorrelatableID"].split(",") + del it[-1] + it = ",".join(it) + listval = 0 + if not isinstance(device["Associations"][0], list): + listval = 1 if isinstance(device["Associations"][0], dict) else 0 + + if item["iSCSINicSource"] == device["Associations"][listval]: + for pcidevice in pcideviceslist: + if "Storage" in pcidevice["DeviceType"]: + continue + pcid = pcidevice["UEFIDevicePath"].split(",") + del pcid[-1] + pcid = ",".join(pcid) + # using total_attempts to avoid duplicate entries + if ( + device["CorrelatableID"] == pcidevice["UEFIDevicePath"] + or ("Embedded" not in pcidevice["DeviceType"] and (pcid == it)) + ) and iscsi_attempt not in total_attempts: + total_attempts.append(iscsi_attempt) + inputstring = ( + pcidevice["DeviceType"] + + " " + + str(pcidevice["DeviceInstance"]) + + " Port " + + str(device["Subinstance"]) + + " : " + + pcidevice.get("Name", "").strip() + ) + structeredlist.append({inputstring: {str("Attempt " + str(iscsi_attempt)): item}}) + + else: + structeredlist.append({"Not Added": {}}) + + try: + if iscsibootsources is None: + raise BootOrderMissingEntriesError("No entries found for iscsi boot sources \n\n") + elif "error" in iscsibootsources: + raise BootOrderMissingEntriesError( + "/redfish/v1/systems/1/bios/oem/hpe/iscsi/settings URI seems to be not reachable even after" + " 4 retry attempts , kindly retry the command after some time \n\n" + ) + elif not options.filename and print_flag: + self.print_iscsi_config_helper(structeredlist, "Current iSCSI Attempts: \n") + except Exception as excp: + raise excp + + if structeredlist is None: + self.rdmc.ui.error("No entries found for iscsi boot sources.\n\n") + elif options.filename: + output = json.dumps(structeredlist, indent=2, cls=redfish.ris.JSONEncoder, sort_keys=True) + filehndl = open(options.filename[0], "w") + filehndl.write(output) + filehndl.close() + + self.rdmc.ui.printer("Results written out to '%s'\n" % options.filename[0]) + return structeredlist + + def defaultsoptionhelper(self, options, iscsipath, bootpath): + """Helper function for default options for iscsi + + :param options: command line options + :type options: list. + :param iscsipath: current iscsi path + :type iscsipath: str. + :param bootpath: current boot path + :type bootpath: str. + """ + self.auxcommands["select"].selectfunction("HpBiosMapping.") + pcisettingsmap = self.auxcommands["get"].getworkerfunction( + "BiosPciSettingsMappings", options, results=True, uselist=True + ) + + devicealloc = list() + for item in pcisettingsmap[0]["BiosPciSettingsMappings"]: + if "Associations" in item: + # self.rdmc.ui.printer("Assoc 1 : %s\n" % (item["Associations"])) + # self.rdmc.ui.printer("Sub 1 : %s\n" % (item["Subinstances"])) + if ( + "EmbNicEnable" in item["Associations"] + or "EmbNicConfig" in item["Associations"] + or "OcpAEnable" in item["Associations"] + ): + _ = [devicealloc.append(x) for x in item["Subinstances"]] + + try: + if ( + re.match("FlexLom[0-9]+Enable", item["Associations"][0]) + or re.match("PciSlot[0-9]+Enable", item["Associations"][0]) + or re.match("Slot[0-9]+NicBoot[0-9]+", item["Associations"][0]) + ): + _ = [devicealloc.append(x) for x in item["Subinstances"]] + except IndexError: + pass + # self.rdmc.ui.printer("Device alloc : %s\n" % devicealloc) + + if self.rdmc.app.typepath.defs.isgen10: + newpcilist = [] + self.auxcommands["select"].selectfunction("HpeServerPciDeviceCollection") + pcideviceslist = self.auxcommands["get"].getworkerfunction("Members", options, results=True, uselist=False) + pcideviceslist = pcideviceslist[0] + for device in pcideviceslist["Members"]: + newpcilist.append(self.rdmc.app.get_handler(device["@odata.id"], silent=True).dict) + pcideviceslist = newpcilist + else: + self.auxcommands["select"].selectfunction(["Collection."]) + pcideviceslist = self.auxcommands["get"].getworkerfunction( + "Items", + options, + results=True, + uselist=False, + filtervals=("MemberType", "HpServerPciDevice.*"), + )[0]["Items"] + + self.auxcommands["select"].selectfunction(self.rdmc.app.typepath.defs.hpiscsisoftwareinitiatortype) + iscsiinitiatorname = self.auxcommands["get"].getworkerfunction( + "iSCSIInitiatorName", options, results=True, uselist=True + ) + + disabledlist = self.pcidevicehelper(devicealloc, iscsipath, bootpath, pcideviceslist) + + self.print_out_iscsi_configuration(iscsiinitiatorname[0], devicealloc, pcideviceslist) + + if disabledlist: + self.print_out_iscsi_configuration(iscsiinitiatorname[0], disabledlist, pcideviceslist, disabled=True) + + def modifyoptionhelper(self, options, iscsisettingspath): + """Helper function to modify options for iscsi + + :param options: command line options + :type options: list. + :param iscsisettingspath: current iscsi settings path + :type iscsisettingspath: str. + """ + try: + inputfile = open(options.modify, "r") + contentsholder = json.loads(inputfile.read()) + except Exception as excp: + raise InvalidCommandLineError("%s" % excp) + + iscsibootsources = self.rawdatahandler(action="GET", silent=True, jsonflag=True, path=iscsisettingspath) + + count = 0 + resultsdict = list() + + for item in contentsholder: + for entry in item.values(): + enteredsection = False + + for key, value in entry.items(): + enteredsection = True + resultsdict.append( + self.modifyfunctionhelper( + key, + value, + iscsibootsources[self.rdmc.app.typepath.defs.iscsisource], + ) + ) + + if not enteredsection: + resultsdict.append(iscsibootsources[self.rdmc.app.typepath.defs.iscsisource][count]) + + count += 1 + + contentsholder = {self.rdmc.app.typepath.defs.iscsisource: resultsdict} + + self.rdmc.app.patch_handler(iscsisettingspath, contentsholder, optionalpassword=options.biospassword) + self.rdmc.app.get_handler(iscsisettingspath, silent=True) + self.rdmc.ui.printer("Please reboot the server for changes to take effect\n") + + def modifyfunctionhelper(self, key, value, bootsources): + """Helper function to modify the entries for iscsi + + :param key: key to be used for attempt + :type key: string. + :param value: value to apply to attempt + :type value: str. + :param bootsources: current boot sources + :type bootsources: list. + """ + foundoption = False + + for bootsource in bootsources: + if bootsource[self.rdmc.app.typepath.defs.iscsiattemptinstance] == int(key[-1:]): + foundoption = True + break + else: + return value + if foundoption: + return value + + def pcidevicehelper(self, devicealloc, iscsipath, bootpath, pcideviceslist=None, options=None): + """Helper function to check for extra pci devices / identify disabled devices + + :param devicealloc: list of devices allocated + :type devicealloc: list. + :param iscsipath: current iscsi path + :type iscsipath: str. + :param bootpath: current boot path + :type bootpath: str. + :param pcideviceslist: current pci device list + :type pcideviceslist: list. + :param options: command line options + :type options: list. + """ + if not pcideviceslist: + if self.rdmc.app.typepath.defs.isgen10: + newpcilist = [] + self.auxcommands["select"].selectfunction("HpeServerPciDeviceCollection") + pcideviceslist = next( + iter(self.auxcommands["get"].getworkerfunction("Members", options, results=True, uselist=False)), + None, + ) + + for device in pcideviceslist["Members"]: + newpcilist.append(self.rdmc.app.get_handler(device["@odata.id"], silent=True).dict) + + pcideviceslist = newpcilist + else: + self.auxcommands["select"].selectfunction(["Collection."]) + pcideviceslist = next( + iter( + self.auxcommands["get"].getworkerfunction( + "Items", + options, + results=True, + uselist=False, + filtervals=("MemberType", "HpServerPciDevice.*"), + ) + ), + None, + )["Items"] + try: + self.rawdatahandler(action="GET", silent=True, jsonflag=True, path=iscsipath)["iSCSINicSources"] + except: + raise NicMissingOrConfigurationError("No iSCSI nic sources available.") + + _ = [x["UEFIDevicePath"] for x in pcideviceslist] + removal = list() + + bios = self.rawdatahandler(action="GET", silent=True, jsonflag=True, path=bootpath) + + for item in devicealloc: + if isinstance(item["Associations"][0], dict): + if "PreBootNetwork" in list(item["Associations"][0].keys()): + if item["Associations"] and item["Associations"][0]["PreBootNetwork"] in list(bios.keys()): + if bios[item["Associations"][0]["PreBootNetwork"]] == "Disabled": + removal.append(item) + else: + if item["Associations"] and item["Associations"][0] in list(bios.keys()): + if bios[item["Associations"][0]] == "Disabled": + removal.append(item) + + _ = [devicealloc.remove(x) for x in removal] + + return removal + + def print_out_iscsi_configuration(self, iscsiinitiatorname, devicealloc, pcideviceslist, disabled=False): + """Convert content to human readable and print out to std.out + + :param iscsiinitiatorname: iscsi initiator name + :type iscsiinitiatorname: str. + :param devicealloc: list of devices allocated + :type devicealloc: list. + :param pcideviceslist: current pci device list + :type pcideviceslist: list. + :param disabled: command line options + :type disabled: boolean. + """ + try: + if iscsiinitiatorname is None: + BootOrderMissingEntriesError("No entry found for the iscsi initiator name.\n\n") + elif disabled: + pass + else: + self.print_iscsi_config_helper(iscsiinitiatorname["iSCSIInitiatorName"], "\nIscsi Initiator Name: ") + except Exception as excp: + raise excp + + try: + final_device_list = list() + enable_val = list() + + bios = "/redfish/v1/systems/1/bios/" + bios_dict = self.rdmc.app.get_handler(bios, silent=True, service=True).dict + if "Attributes" in bios_dict: + bios_dict_items = bios_dict["Attributes"].items() + else: + bios_dict_items = bios_dict.items() + for attr, val in bios_dict_items: + if "NetworkBoot" in str(val): + enable_val.append(attr.lower().strip()) + + # before comparing with pcideviceslist filtering enabled nics only + devicealloc = [ + val + for val in devicealloc + if (len(val["Associations"]) == 2 and val["Associations"][1].lower() in enable_val) + ] + + if devicealloc and pcideviceslist: + count = 0 + output_data = [] + for pcidevice in pcideviceslist: + if "storage" in pcidevice["DeviceType"].lower(): + continue + + pcid = pcidevice["UEFIDevicePath"].split(",") + del pcid[-1] + pcid = ",".join(pcid) + + for item in devicealloc: + it = item["CorrelatableID"].split(",") + del it[-1] + it = ",".join(it) + + # Match the correlatable ID from the + # PCI device & allocated device + if pcid == it: + # Format the device information + device = ( + pcidevice["DeviceType"] + + " " + + str(pcidevice["DeviceInstance"]) + + " Port " + + str(item["Subinstance"]) + ) + # final_device_list using to + # avoid the duplicate entries + if device not in final_device_list: + final_device_list.append(device) + # Add the device name or + # structured name to the dictionary + if "Name" not in pcidevice: + name = pcidevice.get( + "StructuredName", + "", + ).strip() + else: + name = pcidevice.get("Name", "").strip() + + count = count + 1 + output_data.append(f"{[count]} {device} : {name}\n") + + if output_data: + # display output data + status = "Disabled" if disabled else "Available" + self.rdmc.ui.printer(f"\n{status} iSCSI Boot Network Interfaces: \n") + [self.rdmc.ui.printer(i) for i in output_data] + print() # For a blank line, after the output + else: + self.rdmc.ui.printer("No iSCSI Boot Network Interfaces Available.\n\n") + + else: + raise BootOrderMissingEntriesError("No entries found for" " iscsi configurations devices.\n") + except Exception as excp: + raise excp + + def print_iscsi_config_helper(self, content, outstring, indent=0): + """Print iscsi configuration helper + + :param content: current content to be output + :type content: string. + :param outstring: current output string + :type outstring: str. + :param indent: current iscsi settings path + :type indent: str. + """ + self.rdmc.ui.printer("\t" * indent + outstring) + + if content: + self.rdmc.ui.print_out_json(content) + else: + self.rdmc.ui.error("\t" * indent + "No entries currently configured.\n") + + self.rdmc.ui.printer("\n\n") + + def rawdatahandler(self, action="None", path=None, silent=True, jsonflag=False): + """Helper function to get and put the raw data + + :param action: current rest action + :type action: list. + :param path: current path + :type path: str. + :param silent: flag to determine silent mode + :type silent: boolean. + :param jsonflag: flag to determine json output + :type jsonflag: boolean. + """ + if action == "GET": + rawdata = self.rdmc.app.get_handler(get_path=path, silent=silent) + + if jsonflag is True: + rawdata = json.loads(rawdata.read) + + return rawdata + + def validateinput(self, deviceallocsize=None, options=None, deleteoption=False): + """Helper function to validate that the input is correct + + :param deviceallocsize: current device allocated size + :type deviceallocsize: str. + :param options: command line options + :type options: list. + :param deleteoption: flag to delete option + :type deleteoption: boolean. + """ + if deviceallocsize: + try: + if int(options.add) - 1 >= deviceallocsize: + raise NicMissingOrConfigurationError("Please verify the " "given input value for configuring NIC.") + except Exception: + raise NicMissingOrConfigurationError("Please verify the " "given input value for configuring NIC.") + if deleteoption: + try: + if int(options.delete) == 0: + raise NicMissingOrConfigurationError( + "Invalid input value." "Please give valid attempt for instance values." + ) + except Exception: + raise NicMissingOrConfigurationError( + "Invalid input value." "Please give valid attempt for instance values." + ) + + def iscsiconfigurationvalidation(self, options): + """iscsi configuration method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + if options.encode: + options.biospassword = Encryption.decode_credentials(options.biospassword) + if isinstance(options.biospassword, bytes): + options.biospassword = options.biospassword.decode("utf-8") + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag if you wish to use a different" + " filename than the default one. The default filename is" + " ilorest.json.", + action="append", + default=None, + ) + customparser.add_argument( + "--add", + dest="add", + help="Use this iSCSI configuration option to add an iSCSI" " configuration option.", + default=None, + ) + customparser.add_argument( + "--delete", + dest="delete", + help="Use this iSCSI configuration option to delete an iSCSI" " configuration option.", + default=None, + ) + customparser.add_argument( + "--modify", + dest="modify", + help="Use this iSCSI configuration option to modify an iSCSI" " configuration option.", + default=None, + ) + customparser.add_argument( + "--list", + dest="list", + action="store_true", + help="Use this iSCSI configuration option to list the details" " of the different iSCSI configurations.", + default=None, + ) + customparser.add_argument( + "--reboot", + dest="reboot", + help="Use this flag to perform a reboot command function after" + " completion of operations. For help with parameters and" + " descriptions regarding the reboot flag, run help reboot.", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/SetPasswordCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/SetPasswordCommand.py new file mode 100644 index 0000000..528bad6 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/SetPasswordCommand.py @@ -0,0 +1,225 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""SetPassword Command for rdmc""" + +try: + from rdmc_helper import ( + Encryption, + InvalidCommandLineErrorOPTS, + ReturnCodes, + UnableToDecodeError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + Encryption, + InvalidCommandLineErrorOPTS, + ReturnCodes, + UnableToDecodeError, + ) + + +class SetPasswordCommand: + """Set password class command""" + + def __init__(self): + self.ident = { + "name": "setpassword", + "usage": None, + "description": "Sets the admin password and power-on password\n" + "setpassword --newpassword --currentpassword [OPTIONS]\n\n\t" + "Setting the admin password with no previous password set." + "\n\texample: setpassword --newpassword testnew --currentpassword None\n\n\tSetting the admin " + "password back to nothing.\n\texample: setpassword --newpassword None --currentpassword testnew " + "\n\n\tSetting the power on password.\n\texample: setpassword" + " --newpassword testnew --currentpassword None --poweron\n\tNote: " + "if it is empty password, send None as above.", + "summary": "Sets the admin password and power-on password", + "aliases": [], + "auxcommands": [ + "LoginCommand", + "SetCommand", + "SelectCommand", + "CommitCommand", + "RebootCommand", + "LogoutCommand", + ], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main set password worker function + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.setpasswordvalidation(options) + + # if not args: + # self.rdmc.ui.printer('Please input the current password.\n') + # tempoldpass = getpass.getpass() + + # if tempoldpass and tempoldpass != '\r': + # tempoldpass = tempoldpass + # else: + # tempoldpass = '""' + + # self.rdmc.ui.printer('Please input the new password.\n') + # tempnewpass = getpass.getpass() + + # if tempnewpass and tempnewpass != '\r': + # tempnewpass = tempnewpass + # else: + # tempnewpass = '""' + # args.extend([tempnewpass, tempoldpass]) + + # if len(args) < 2: + # raise InvalidCommandLineError("Please pass both new password and old password.") + + args = list() + args.append(options.newpassword) + args.append(options.currentpassword) + count = 0 + for arg in args: + if arg: + if arg.lower() == "none" or arg.lower() == "null" or arg is None: + args[count] = "" + elif len(arg) > 2: + if ('"' in arg[0] and '"' in arg[-1]) or ("'" in arg[0] and "'" in arg[-1]): + args[count] = arg[1:-1] + elif len(arg) == 2: + if (arg[0] == '"' and arg[1] == '"') or (arg[0] == "'" and arg[1] == "'"): + args[count] = "" + count += 1 + + if options.encode: + _args = [] + for arg in args: + try: + arg = Encryption.decode_credentials(arg) + if isinstance(arg, bytes): + arg = arg.decode("utf-8") + _args.append(arg) + except UnableToDecodeError: + _args.append(arg) + args = _args + if self.rdmc.app.typepath.defs.isgen10: + bodydict = self.rdmc.app.get_handler(self.rdmc.app.typepath.defs.biospath, service=True, silent=True).dict + + for item in bodydict["Actions"]: + if "ChangePassword" in item: + path = bodydict["Actions"][item]["target"] + break + + if options.poweron: + body = { + "PasswordName": "User", + "OldPassword": args[1], + "NewPassword": args[0], + } + else: + body = { + "PasswordName": "Administrator", + "OldPassword": args[1], + "NewPassword": args[0], + } + + self.rdmc.app.post_handler(path, body) + else: + if options.poweron: + self.auxcommands["select"].run("HpBios.") + self.auxcommands["set"].run("PowerOnPassword=%s OldPowerOnPassword=%s" % (args[0], args[1])) + self.auxcommands["commit"].run("") + else: + self.auxcommands["select"].run("HpBios.") + self.auxcommands["set"].run("AdminPassword=%s OldAdminPassword=%s" % (args[0], args[1])) + self.auxcommands["commit"].run("") + self.rdmc.ui.printer( + "\nThe session will now be terminated.\n" + " login again with updated credentials in order to continue.\n" + ) + self.auxcommands["logout"].run("") + + if options: + if options.reboot: + self.auxcommands["reboot"].run(options.reboot) + + self.cmdbase.logout_routine(self, options) + return ReturnCodes.SUCCESS + + def setpasswordvalidation(self, options): + """Results method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--currentpassword", + dest="currentpassword", + help="Use this flag to provide current password.", + required=True, + ) + + customparser.add_argument( + "--newpassword", + dest="newpassword", + help="Use this flag to provide new password.", + required=True, + ) + + customparser.add_argument( + "--reboot", + dest="reboot", + help="Use this flag to perform a reboot command function after " + "completion of operations. 'REBOOT' is a replaceable parameter " + "that can have multiple values. For help with parameters and " + "descriptions regarding the reboot flag, run help reboot.", + default=None, + ) + customparser.add_argument( + "--poweron", + dest="poweron", + action="store_true", + help="""Use this flag to set power on password instead""", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/BiosDefaultsCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/BiosDefaultsCommand.cpython-310.pyc new file mode 100644 index 0000000..08b26da Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/BiosDefaultsCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/BootOrderCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/BootOrderCommand.cpython-310.pyc new file mode 100644 index 0000000..670ce4d Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/BootOrderCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/IscsiConfigCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/IscsiConfigCommand.cpython-310.pyc new file mode 100644 index 0000000..e1fde02 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/IscsiConfigCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/SetPasswordCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/SetPasswordCommand.cpython-310.pyc new file mode 100644 index 0000000..9720483 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/SetPasswordCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..f6124ed Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/BIOS_COMMANDS/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/CommitCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/CommitCommand.py new file mode 100644 index 0000000..9a9f821 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/CommitCommand.py @@ -0,0 +1,140 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Commit Command for RDMC""" + +from redfish.ris.rmc_helper import NothingSelectedError + +try: + from rdmc_helper import ( + FailureDuringCommitError, + InvalidCommandLineErrorOPTS, + NoChangesFoundOrMadeError, + NoCurrentSessionEstablished, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + FailureDuringCommitError, + InvalidCommandLineErrorOPTS, + NoChangesFoundOrMadeError, + NoCurrentSessionEstablished, + ReturnCodes, + ) + + +class CommitCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "commit", + "usage": None, + "description": "commit [OPTIONS]\n\n\tRun to apply all changes made during" + " the current session\n\texample: commit", + "summary": "Applies all the changes made during the current session.", + "aliases": [], + "auxcommands": ["LogoutCommand", "RebootCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def commitfunction(self, options=None): + """Main commit worker function + + :param options: command line options + :type options: list. + """ + self.commitvalidation() + + self.rdmc.ui.printer("Committing changes...\n") + + if options: + if options.biospassword: + self.rdmc.app.current_client.bios_password = options.biospassword + try: + failure = False + commit_opp = self.rdmc.app.commit() + for path in commit_opp: + if self.rdmc.opts.verbose: + self.rdmc.ui.printer("Changes are being made to path: %s\n" % path) + if next(commit_opp): + failure = True + except NothingSelectedError: + raise NoChangesFoundOrMadeError("No changes found or made during commit operation.") + else: + if failure: + raise FailureDuringCommitError( + "One or more types failed to commit. Run the " + "status command to see uncommitted data. " + "if you wish to discard failed changes refresh the " + "type using select with the --refresh flag." + ) + + if options.reboot: + self.auxcommands["reboot"].run(options.reboot) + self.auxcommands["logout"].run("") + + def run(self, line, help_disp=False): + """Wrapper function for commit main function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.commitfunction(options) + + # Return code + return ReturnCodes.SUCCESS + + def commitvalidation(self): + """Commit method validation function""" + + try: + _ = self.rdmc.app.current_client + except: + raise NoCurrentSessionEstablished("Please login and make setting" " changes before using commit command.") + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + customparser.add_argument( + "--reboot", + dest="reboot", + help="Use this flag to perform a reboot command function after" + " completion of operations. For help with parameters and" + " descriptions regarding the reboot flag, run help reboot.", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/DetectiLOCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/DetectiLOCommand.py new file mode 100644 index 0000000..8e4b238 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/DetectiLOCommand.py @@ -0,0 +1,212 @@ +### +# Copyright 2016-2024 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""DetectiLO Command for RDMC""" +from redfish.hpilo.vnichpilo import AppAccount +from redfish.rest.connections import ChifDriverMissingOrNotFound, VnicNotEnabledError +import requests + +try: + from rdmc_helper import ( + UI, + InvalidCommandLineErrorOPTS, + ReturnCodes, + GenBeforeLoginError, + VnicExistsError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + UI, + InvalidCommandLineErrorOPTS, + ReturnCodes, + GenBeforeLoginError, + VnicExistsError, + ) + + +class DetectiLOCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "detectilo", + "usage": None, + "description": "Provides the iLO generation of the currently logged-in server.\n\n" + "If --ignore_session is specified, it retrieves the iLO generation of the IP mentioned in --url\n" + "or attempts to determine the iLO generation of the current server.", + "summary": "Retrieves the iLO generation of the server.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main get worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if not options.ignore_session and not options.url: + client = self.detectilovalidation(options) + if client: + if not options.json: + self.rdmc.ui.printer("Using the current logged in session information.\n") + if not any(substring in client.base_url for substring in ("blobstore", "16.1.15.1")): + options.url = client.base_url.split("/")[-1] + + if options.url: + if not options.json: + self.rdmc.ui.printer("Detecting iLO for remote device %s\n" % options.url) + retcode = self.detect_remote_ilotype(options) + else: + if not options.json: + self.rdmc.ui.printer("Detecting iLO for this server.\n") + retcode = self.detect_local_ilotype(options) + return retcode + + def detectilovalidation(self, options): + """detectilo validation function + + :param options: command line options + :type options: list. + """ + return self.rdmc.login_select_validation(self, options) + + def detect_remote_ilotype(self, options): + try: + path = "https://" + options.url + "/redfish/v1" + data = requests.get(path, verify=False) + json_data = data.json() + Oem_Hpe = json_data["Oem"]["Hpe"] + if "ManagerType" in Oem_Hpe["Manager"][0]: + ilo_ver = int(Oem_Hpe["Manager"][0]["ManagerType"].split(" ")[1]) + else: + manager_data = Oem_Hpe["Moniker"] + ilo_ver = int(manager_data["PRODGEN"].split(" ")[1]) + except Exception as excp: + raise GenBeforeLoginError( + "An error occurred while retrieving the iLO generation. " + "Please ensure that the virtual NIC is enabled for iLO7 based " + "servers, or that the CHIF driver is installed for iLO5 " + "and iLO6 based servers.\n" + ) + + if options.json: + content = {"iLOType": ilo_ver} + UI().print_out_json(content) + else: + self.rdmc.ui.printer("iLO Type: %s\n" % ilo_ver) + + return ReturnCodes.SUCCESS + + def detect_local_ilotype(self, options): + app_obj = AppAccount(log_dir=self.rdmc.log_dir) + security_state_dict = { + 0: "Error occurred while fetching the security state", + 1: "Factory", + 2: "Wipe", + 3: "Production", + 4: "HighSecurity", + 5: "FIPS", + 6: "SuiteB", + } + try: + ilo_ver, sec_state = self.rdmc.app.getilover_beforelogin(app_obj) + if options.json: + contents = {"iLOType": str(ilo_ver)} + if ilo_ver < 7: + contents["SecurityState"] = security_state_dict[sec_state] + UI().print_out_json(contents) + else: + self.rdmc.ui.printer("iLO Type: %s\n" % ilo_ver) + if ilo_ver < 7: + self.rdmc.ui.printer("Security State: %s\n" % security_state_dict[sec_state]) + return ReturnCodes.SUCCESS + except ChifDriverMissingOrNotFound: + if options.json: + contents = {"iLOType": "5_6"} + UI().print_out_json(contents) + else: + self.rdmc.ui.printer("iLO Type: Unable to find iLO generation\n") + raise + except VnicNotEnabledError: + ilo_ver = 7 + if options.json: + contents = {"iLOType": str(ilo_ver)} + UI().print_out_json(contents) + else: + self.rdmc.ui.printer("iLO Type: %s\n" % ilo_ver) + raise VnicExistsError( + "Unable to access iLO using virtual NIC. " + "Please ensure virtual NIC is enabled in iLO. " + "Ensure that virtual NIC in the host OS is " + "configured properly. Refer to documentation for more information.\n" + ) + except Exception: + raise GenBeforeLoginError( + "An error occurred while retrieving the iLO generation. " + "Please ensure that the virtual NIC is enabled for iLO7 based " + "servers, or that the CHIF driver is installed for iLO5 and iLO6 " + "based servers.\n" + ) + + def definearguments(self, customparser): + """Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + + customparser.add_argument( + "--ignore_session", + dest="ignore_session", + action="store_true", + help="Optionally include this flag if you wish to ignore any" + " session that has been logged in, in order to perform detectilo", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/GetCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/GetCommand.py new file mode 100644 index 0000000..797ecbf --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/GetCommand.py @@ -0,0 +1,512 @@ +### +# Copyright 2016-2023 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Get Command for RDMC""" +from collections import OrderedDict + +import six +import logging + +import redfish.ris +from redfish.ris.utils import iterateandclear + +try: + from rdmc_helper import ( + UI, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + UI, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +try: + from rdmc_helper import HARDCODEDLIST +except ImportError: + from ilorest.rdmc_helper import HARDCODEDLIST + +LOGGER = logging.getLogger(__name__) + + +class GetCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "get", + "usage": None, + "description": "To retrieve all" + " the properties run without arguments. \n\t*Note*: " + "a type will need to be selected or this will return an " + "error.\n\texample: get\n\n\tTo retrieve multiple " + "properties use the following example\n\texample: " + "get Temperatures/ReadingCelsius Fans/Name --selector=Thermal." + "\n\n\tTo change output style format provide" + " the json flag\n\texample: get --json", + "summary": "Displays the current value(s) of a " "property(ies) within a selected type.", + "aliases": [], + "auxcommands": ["LogoutCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main get worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if getattr(options, "json"): + self.rdmc.json = True + + selector = options.selector.lower() if options.selector else self.rdmc.app.selector.lower() + + if "securityservice" in selector: + self.cmdbase.login_validation(self, options) + else: + self.getvalidation(options) + + filtr = (None, None) + if options.filter: + try: + if (str(options.filter)[0] == str(options.filter)[-1]) and str(options.filter).startswith(("'", '"')): + options.filter = options.filter[1:-1] + + (sel, val) = options.filter.split("=") + filtr = (sel.strip(), val.strip()) + + except (InvalidCommandLineError, SystemExit): + raise InvalidCommandLineError("Invalid filter" " parameter format [filter_attribute]=[filter_value]") + + self.getworkerfunction( + args, + options, + results=None, + uselist=True, + filtervals=filtr, + readonly=options.noreadonly, + ) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def getworkerfunction( + self, + args, + options, + readonly=False, + filtervals=(None, None), + results=None, + uselist=False, + ): + """main get worker function + + :param args: command line arguments + :type args: list. + :param options: command line options + :type options: list. + :param line: command line input + :type line: string. + :param readonly: remove readonly properties + :type readonly: bool + :param filtervals: filter key value pair (Key,Val) + :type filtervals: tuple + :param results: current results collected + :type results: string. + :param uselist: use reserved properties list to filter results + :type uselist: boolean. + """ + nocontent = set() + instances = None + arg = None + + # For rest redfish compatibility of bios. + if hasattr(options, "selector") and options.selector: + self.rdmc.app.selector = options.selector + args = [args] if args and isinstance(args, six.string_types) else args + if self.rdmc.app.selector and "." not in self.rdmc.app.selector: + self.rdmc.app.selector = self.rdmc.app.selector + "." + args = ( + [ + ( + "Attributes/" + arg + if self.rdmc.app.selector + and self.rdmc.app.selector.lower().startswith("bios.") + and not ( + arg.lower() in ["id", "name", "attributeregistry", "attributes"] or "@odata" in arg.lower() + ) + else arg + ) + for arg in args + ] + if args + else args + ) + if filtervals[0]: + instances = self.rdmc.app.select(selector=self.rdmc.app.selector, fltrvals=filtervals) + + try: + if "securityservice" in self.rdmc.app.selector.lower(): + url = "/redfish/v1/Managers/1/SecurityService/" + LOGGER.info("Fetching SecurityService data from: %s", url) + contents = self.rdmc.app.get_handler(url, service=True, silent=True).dict + security_contents = [] + if not args and not options.json: + LOGGER.debug("Printing human-readable security service data.") + UI().print_out_human_readable(contents) + elif options and options.json and contents: + LOGGER.debug("Printing JSON security service data.") + UI().print_out_json(contents) + else: + attr = args[0] + contents_lower = {k.lower(): v for k, v in contents.items()} + security_contents.append({attr: contents_lower[attr.lower()]}) + if security_contents: + UI().print_out_human_readable(security_contents) + elif ( + "componentintegrity" + in self.rdmc.app.selector.lower() + # or "bios." in self.rdmc.app.selector.lower() + ): + url = "" + if "componentintegrity" in self.rdmc.app.selector.lower(): + url = "/redfish/v1/ComponentIntegrity/?$expand=." + # elif "bios." in self.rdmc.app.selector.lower(): + # url = "/redfish/v1/systems/1/bios/?$expand=." + + LOGGER.info("Fetching %s data from: %s", self.rdmc.app.selector, url) + contents = self.get_content(url, args, uselist, options) + else: + LOGGER.debug("Handling general selector case.") + skipnonsettingflag = True + if "networkadapter" in self.rdmc.app.selector.lower(): + skipnonsettingflag = False + + if "selector" in options: + LOGGER.info("Fetching properties with selector: %s", options.selector) + contents = self.rdmc.app.getprops( + props=args, + remread=readonly, + selector=options.selector, + nocontent=nocontent, + insts=instances, + skipnonsetting=skipnonsettingflag, + ) + + if "networkadapter" in self.rdmc.app.selector.lower(): + unique_adapters = {} + for entry in contents: + # Remove keys @odata.id & Id if it contains "settings" + adapter_id = entry.get("Id", "") + odata_id = entry.get("@odata.id", "") + if "settings" in odata_id.lower() or "settings" in adapter_id.lower(): + continue + # Preventing deduplicates by Id + if adapter_id: + unique_adapters[adapter_id] = entry + + if unique_adapters: + contents = list(unique_adapters.values()) + + if ( + contents + and "logentrycollection" in self.rdmc.app.selector.lower() + and "@odata.id" in args + and not uselist + ): + # as per documentation + # collection of LogEntry resource instances + log_contents = self.rdmc.app.getprops( + props=["entries/@odata.id"], + remread=readonly, + selector="logservice", + nocontent=nocontent, + insts=instances, + skipnonsetting=skipnonsettingflag, + ) + if log_contents: + log_contents = [ + val["Entries"] + for val in log_contents + if "Entries" in val and "@odata.id" in val["Entries"] + ] + + if log_contents: + contents.extend(log_contents) + # Removing duplicates + contents = list({val["@odata.id"]: val for val in contents}.values()) + + else: + LOGGER.info("Fetching properties without selector.") + contents = self.rdmc.app.getprops( + props=args, remread=readonly, nocontent=nocontent, insts=instances + ) + uselist = False if readonly else uselist + except redfish.ris.rmc_helper.EmptyRaiseForEAFP: + LOGGER.error("EmptyRaiseForEAFP exception encountered, retrying getprops without remread.") + contents = self.rdmc.app.getprops(props=args, nocontent=nocontent) + for ind, content in enumerate(contents): + if "bios." in self.rdmc.app.selector.lower() and "Attributes" in list(content.keys()): + content.update(content["Attributes"]) + del content["Attributes"] + LOGGER.debug("Flattened BIOS attributes in response.") + contents[ind] = OrderedDict(sorted(list(content.items()), key=lambda x: x[0])) + if len(args) > 0 and "Members" in args[0]: + uselist = False + if uselist: + for item in contents: + self.removereserved(item) + if results: + LOGGER.info("Returning retrieved contents.") + return contents + + contents = contents[0] if len(contents) == 1 else contents + + if options and options.json and contents: + LOGGER.debug("Printing output in JSON format.") + UI().print_out_json(contents) + elif contents: + LOGGER.debug("Printing output in human-readable format.") + UI().print_out_human_readable(contents) + else: + try: + if nocontent or not any(next(iter(contents))): + raise Exception() + except Exception: + strtoprint = ", ".join(str(val) for val in nocontent) + if not strtoprint and arg: + strtoprint = arg + LOGGER.error("No contents found for entry: %s", strtoprint) + raise NoContentsFoundForOperationError("No get contents found for entry: %s" % strtoprint) + else: + LOGGER.error("No contents found for selected type.") + raise NoContentsFoundForOperationError("No get contents found for " "selected type.") + if options.logout: + LOGGER.info("Logging out.") + self.auxcommands["logout"].run("") + + def get_content(self, url, args, uselist, options): + """Get data from url""" + contents = self.rdmc.app.get_handler(url, service=True, silent=True).dict + if uselist: + # for get command removing @odata.* properties + args = [arg.lower() for arg in args if "@odata." not in arg.lower()] + else: + args = [arg.lower() for arg in args] + + if contents and not args: + contents = [contents] + elif contents and args: + collect_data = [] + for key, value in contents.items(): + # Collect top-level keys based on args + if any(key.lower() == arg for arg in args): + collect_data.append({key: value}) + + if "attributes" == key.lower() and isinstance(value, dict) and "attributes" not in args: + attr_vals = {k.lower(): v for k, v in value.items()} + val_dict = {arg: attr_vals[arg.lower()] for arg in args if arg.lower() in attr_vals} + if val_dict: + collect_data.append(val_dict) + + if key == "Members" and isinstance(value, list): + for member in value: + # Loop through each dict of Members + m_dict = {k.lower(): v for k, v in member.items()} + val_dict = {arg: m_dict[arg.lower()] for arg in args if arg.lower() in m_dict} + if val_dict: + collect_data.append(val_dict) + + # collect_data = [dict(t) for t in {tuple(d.items()) for d in collect_data}] + contents = collect_data + else: + contents = [] + + return contents + + def find_key_recursive(self, data, target_key): + """Recursively searches for a key in a nested dictionary (case-insensitive) and returns its value(s).""" + target_key_lower = target_key.lower() # Convert target_key to lowercase for case-insensitive matching + + if isinstance(data, dict): + for key, value in data.items(): + if key.lower() == target_key_lower: # Convert dictionary key to lowercase before comparison + yield value # Use yield to return multiple matches + elif isinstance(value, (dict, list)): + yield from self.find_key_recursive(value, target_key) + + elif isinstance(data, list): + for item in data: + yield from self.find_key_recursive(item, target_key) + + def removereserved(self, entry): + """function to remove reserved properties + :param entry: dictionary to remove reserved properties from + :type entry: dict. + """ + + for key, val in list(entry.items()): + if key.lower() in HARDCODEDLIST or "@odata" in key.lower(): + del entry[key] + elif isinstance(val, list): + for item in entry[key]: + if isinstance(item, dict) and item: + self.removereserved(item) + if all([True if not test else False for test in entry[key]]): + del entry[key] + elif isinstance(val, dict): + self.removereserved(val) + if all([True if not test else False for test in entry[key]]): + del entry[key] + + def checktoprint(self, options, contents, nocontent, arg): + """function to decide what/how to print + :param options: list of options + :type options: list. + :param contents: dictionary value returned by getprops. + :type contents: dict. + :param nocontent: props not found are added to the list. + :type nocontent: list. + :param arg: string of args + :type arg: string + """ + if options and options.json and contents: + self.rdmc.ui.print_out_json(contents) + elif contents: + self.rdmc.ui.print_out_human_readable(contents) + else: + try: + if nocontent or not any(next(iter(contents))): + raise Exception() + except Exception: + strtoprint = ", ".join(str(val) for val in nocontent) + if not strtoprint and arg: + strtoprint = arg + raise NoContentsFoundForOperationError("No get contents " "found for entry: %s" % strtoprint) + else: + raise NoContentsFoundForOperationError("No get contents " "found for selected type.") + + def collectandclear(self, contents, key, values): + """function to find and remove unneeded values from contents dictionary + :param contents: dictionary value returned by getprops + :type contents: dict. + :param key: string of keys + :type key: string. + :param values: list of values + :type values: list. + """ + clearcontent = contents[0][key] + if isinstance(clearcontent, dict): + keyslist = list(clearcontent.keys()) + else: + keyslist = [clearcontent] + clearedlist = keyslist + for arg in values: + for keys in keyslist: + if str(keys).lower() == str(arg).lower(): + clearedlist.remove(arg) + contents = iterateandclear(contents, clearedlist) + return contents + + def getvalidation(self, options): + """get method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--selector", + dest="selector", + help="Optionally include this flag to select a type to run" + " the current command on. Use this flag when you wish to" + " select a type without entering another command, or if you" + " wish to work with a type that is different from the one" + " you currently have selected.", + default=None, + ) + + customparser.add_argument( + "--filter", + dest="filter", + help="Optionally set a filter value for a filter attribute." + " This uses the provided filter for the currently selected" + " type. Note: Use this flag to narrow down your results. For" + " example, selecting a common type might return multiple" + " objects that are all of that type. If you want to modify" + " the properties of only one of those objects, use the filter" + " flag to narrow down results based on properties." + "\t\t\t\t\t Usage: --filter [ATTRIBUTE]=[VALUE]", + default=None, + ) + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + customparser.add_argument( + "--noreadonly", + dest="noreadonly", + action="store_true", + help="Optionally include this flag if you wish to only show" + " properties that are not read-only. This is useful to see what " + "is configurable with the selected type(s).", + default=False, + ) + customparser.add_argument( + "--refresh", + dest="ref", + action="store_true", + help="Optionally reload the data of selected type and clear " "patches from current selection.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/InfoCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/InfoCommand.py new file mode 100644 index 0000000..e1898f8 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/InfoCommand.py @@ -0,0 +1,183 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Info Command for RDMC""" + +try: + from rdmc_helper import ( + InfoMissingEntriesError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InfoMissingEntriesError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + +try: + from rdmc_helper import HARDCODEDLIST +except: + from ilorest.rdmc_helper import HARDCODEDLIST + + +class InfoCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "info", + "usage": None, + "description": "Displays detailed " + "information about a property within a selected type" + "\n\texample: info property\n\n\tDisplays detailed " + "information for several properties\n\twithin a selected " + "type\n\texample: info property property/sub-property property\n\n\t" + "Run without arguments to display properties \n\tthat " + "are available for info command\n\texample: info", + "summary": "Displays detailed information about a property within a selected type.", + "aliases": [], + "auxcommands": [], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, autotest=False, help_disp=False): + """Main info worker function + + :param line: command line input + :type line: string. + :param autotest: flag to determine if running automatictesting + :type autotest: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.infovalidation(options) + + if args: + for item in args: + if self.rdmc.app.selector.lower().startswith("bios.") and "attributes" not in item.lower(): + if not (item.lower() in HARDCODEDLIST or "@odata" in item.lower()): + item = "Attributes/" + item + + outdata = self.rdmc.app.info(props=item, dumpjson=options.json, latestschema=options.latestschema) + + if autotest: + return outdata + if outdata and options.json: + self.rdmc.ui.print_out_json(outdata) + elif outdata: + self.rdmc.ui.printer(outdata) + + if not outdata: + raise InfoMissingEntriesError("There are no valid " "entries for info in the current instance.") + else: + if len(args) > 1 and not item == args[-1]: + self.rdmc.ui.printer("\n************************************" "**************\n") + else: + results = set() + instances = self.rdmc.app.select() + for instance in instances: + currdict = instance.resp.dict + currdict = ( + currdict["Attributes"] + if instance.maj_type.startswith(self.rdmc.app.typepath.defs.biostype) + and currdict.get("Attributes", None) + else currdict + ) + results.update([key for key in currdict if key not in HARDCODEDLIST and "@odata" not in key.lower()]) + + if results and autotest: + return results + elif results: + self.rdmc.ui.printer("Info options:\n") + for item in results: + self.rdmc.ui.printer("%s\n" % item) + else: + raise InfoMissingEntriesError( + "No info items available for this selected type." " Try running with the --latestschema flag." + ) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def infovalidation(self, options): + """Info method validation function + + :param options: command line options + :type options: list. + """ + + if self.rdmc.opts.latestschema: + options.latestschema = True + + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--selector", + dest="selector", + help="Optionally include this flag to select a type to run" + " the current command on. Use this flag when you wish to" + " select a type without entering another command, or if you" + " wish to work with a type that is different from the one" + " you currently have selected.", + default=None, + ) + + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + customparser.add_argument( + "--latestschema", + dest="latestschema", + action="store_true", + help="Optionally use the latest schema instead of the one " + "requested by the file. Note: May cause errors in some data " + "retrieval due to difference in schema versions.", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/ListCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/ListCommand.py new file mode 100644 index 0000000..0daa4e9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/ListCommand.py @@ -0,0 +1,180 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""List Command for RDMC""" + +try: + from rdmc_helper import ( + UI, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + UI, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class ListCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "list", + "usage": None, + "description": "Displays the current values of the " + "properties within\n\ta selected type including" + " reserved properties\n\texample: list\n\n\tNOTE: If " + "you wish to not list all the reserved properties\n\t " + " run the get command instead", + "summary": "Displays the current value(s) of a" + " property(ies) within a selected type including" + " reserved properties.", + "aliases": ["ls"], + "auxcommands": ["GetCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Wrapper function for main list function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + selector = options.selector.lower() if options.selector else self.rdmc.app.selector.lower() + + if "securityservice" in selector: + self.cmdbase.login_validation(self, options) + else: + self.listvalidation(options) + + fvals = (None, None) + + if options.filter: + try: + if (str(options.filter)[0] == str(options.filter)[-1]) and str(options.filter).startswith(("'", '"')): + options.filter = options.filter[1:-1] + + (sel, val) = options.filter.split("=") + fvals = (sel.strip(), val.strip()) + except: + raise InvalidCommandLineError("Invalid filter" " parameter format [filter_attribute]=[filter_value]") + if options.selector: + if "storagecontroller." in options.selector.lower(): + options.selector = "storagecontrollercollection" + self.rdmc.app.selector = options.selector + elif self.rdmc.app.selector: + options.selector = self.rdmc.app.selector + if "securityservice" in options.selector.lower(): + url = "/redfish/v1/Managers/1/SecurityService/" + contents = self.rdmc.app.get_handler(url, service=True, silent=True).dict + security_contents = [] + + if options and options.json and contents: + UI().print_out_json(contents) + elif not args: + UI().print_out_human_readable(contents) + else: + attr = args[0] + contents_lower = {k.lower(): v for k, v in contents.items()} + security_contents.append({attr: contents_lower[attr.lower()]}) + if security_contents: + UI().print_out_human_readable(security_contents) + else: + self.auxcommands["get"].getworkerfunction(args, options, filtervals=fvals, uselist=False) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def listvalidation(self, options): + """List data validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--selector", + dest="selector", + help="Optionally include this flag to select a type to run" + " the current command on. Use this flag when you wish to" + " select a type without entering another command, or if you" + " wish to work with a type that is different from the one" + " you currently have selected.", + default=None, + ) + + customparser.add_argument( + "--filter", + dest="filter", + help="Optionally set a filter value for a filter attribute." + " This uses the provided filter for the currently selected" + " type. Note: Use this flag to narrow down your results. For" + " example, selecting a common type might return multiple" + " objects that are all of that type. If you want to modify" + " the properties of only one of those objects, use the filter" + " flag to narrow down results based on properties." + "\t\t\t\t\t Usage: --filter [ATTRIBUTE]=[VALUE]", + default=None, + ) + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + customparser.add_argument( + "--refresh", + dest="ref", + action="store_true", + help="Optionally reload the data of selected type and clear " "patches from current selection.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/LoadCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/LoadCommand.py new file mode 100644 index 0000000..202b77f --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/LoadCommand.py @@ -0,0 +1,569 @@ +### +# Copyright 2016-2023 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Load Command for RDMC""" + +import json +import os +import shlex +import subprocess +import sys +from datetime import datetime + +from six.moves import queue + +import redfish.ris +from redfish.ris.rmc_helper import LoadSkipSettingError + +try: + from rdmc_helper import ( + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + InvalidMSCfileInputError, + MultipleServerConfigError, + NoChangesFoundOrMadeError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + InvalidMSCfileInputError, + MultipleServerConfigError, + NoChangesFoundOrMadeError, + ReturnCodes, + ) + +try: + from rdmc_helper import HARDCODEDLIST +except: + from ilorest.rdmc_helper import HARDCODEDLIST + +# default file name +__filename__ = "ilorest.json" + + +class LoadCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "load", + "usage": None, + "description": "Run to load the default configuration" + " file\n\texample: load\n\n\tLoad configuration file from a " + "different file\n\tif any property values have changed, the " + "changes are committed and the user is logged out of the server" + "\n\n\texample: load -f output.json\n\n\tLoad configurations to " + "multiple servers\n\texample: load -m mpfilename.txt -f output." + "json\n\n\tNote: multiple server file format (1 server per new " + "line)\n\t--url -u admin -p password\n\t--url" + " -u admin -p password\n\t--url -u admin -p password", + "summary": "Loads the server configuration settings from a file.", + "aliases": [], + "auxcommands": ["CommitCommand", "SelectCommand", "RawPatchCommand"], + } + self.filenames = None + self.mpfilename = None + self.queue = queue.Queue() + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def securebootremovereadonly(self, tmp): + templist = [ + "SecureBootCurrentBoot", + "@odata.etag", + "Id", + "AutoNeg", + "FQDN", + "FullDuplex", + "HostName", + "IPv6Addresses", + "IPv6DefaultGateway", + "LinkStatus", + "MaxIPv6StaticAddresses", + "Name", + "NameServers", + "PermanentMACAddress", + "ConfigurationSettings", + "InterfaceType", + "NICSupportsIPv6", + "DomainName", + "IPV6", + "DNSServers", + "MACAddress", + "VLAN", + "SpeedMbps", + "DateTime", + ] + remove_data = self.rdmc.app.removereadonlyprops(tmp, False, True, templist) + return remove_data + + def run(self, line, help_disp=False): + """Main load worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.loadvalidation(options) + returnvalue = False + + if options.mpfilename: + self.rdmc.ui.printer("Loading configuration for multiple servers...\n") + else: + self.rdmc.ui.printer("Loading configuration...\n") + + for files in self.filenames: + if not os.path.isfile(files): + raise InvalidFileInputError( + "File '%s' doesn't exist. Please create file by running save command." % files + ) + if options.encryption: + with open(files, "rb") as myfile: + data = myfile.read() + data = Encryption().decrypt_file(data, options.encryption) + loadcontents = json.loads(data) + else: + try: + with open(files, "r") as myfile: + loadcontents = json.load(myfile) + except: + raise InvalidFileFormattingError("Invalid file formatting found in file %s" % files) + + if options.mpfilename: + mfile = options.mpfilename + outputdir = None + + if options.outdirectory: + outputdir = options.outdirectory + + if self.runmpfunc(mpfile=mfile, lfile=files, outputdir=outputdir): + return ReturnCodes.SUCCESS + else: + raise MultipleServerConfigError("One or more servers failed to load given configuration.") + + results = False + eth_conf = False + validation_errs = [] + + for loadcontent in loadcontents: + for content, loaddict in loadcontent.items(): + inputlist = list() + + if content == "Comments": + continue + + if "EthernetInterface" in content: + if options.force_network_config: + import platform + import tempfile + + patchpath = "/redfish/v1/Managers/1/EthernetInterfaces/1/" + + try: + payload = loadcontent[content][patchpath] + except KeyError: + continue + + payload = loadcontent[content][patchpath] + remove_odata = self.securebootremovereadonly(payload) + payload.update(remove_odata) + if "StaticNameServers" in payload: + payload["StaticNameServers"] = [ + item for item in payload["StaticNameServers"] if item != "::" + ] + tempdir = "/tmp" if platform.system() == "Darwin" else tempfile.gettempdir() + temp_file = os.path.join(tempdir, "temp_patch.json") + out_file = open(temp_file, "w") + patch_payload = {patchpath: payload} + json.dump(patch_payload, out_file, indent=6) + out_file.close() + self.auxcommands["rawpatch"].run(temp_file + " --service") + os.remove(temp_file) + eth_conf = True + continue + else: + self.rdmc.ui.printer( + "Skipping network configurations as" " --force_network_config is not included.\n" + ) + continue + + inputlist.append(content) + if options.biospassword: + inputlist.extend(["--biospassword", options.biospassword]) + + self.auxcommands["select"].selectfunction(inputlist) + if self.rdmc.app.selector.lower() not in content.lower(): + raise InvalidCommandLineError("Selector not found.\n") + + try: + for _, items in loaddict.items(): + remove_odata = self.securebootremovereadonly(items) + items.update(remove_odata) + try: + if self.rdmc.app.loadset( + seldict=items, + latestschema=options.latestschema, + uniqueoverride=options.uniqueoverride, + ): + results = True + except LoadSkipSettingError: + returnvalue = True + results = True + except: + raise + except redfish.ris.ValidationError as excp: + errs = excp.get_errors() + validation_errs.append({self.rdmc.app.selector: errs}) + except: + raise + + try: + if results: + self.auxcommands["commit"].commitfunction(options=options) + reboot_needed = False + for n in inputlist: + n = n.lower() + if "bios" in n or "boot" in n or "iscsi" in n: + reboot_needed = True + if reboot_needed: + self.rdmc.ui.printer("Reboot is required for settings to take effect.\n") + except NoChangesFoundOrMadeError as excp: + if returnvalue: + pass + else: + raise excp + + if validation_errs: + for validation_err in validation_errs: + for err_type in validation_err: + self.rdmc.ui.error("Validation error(s) in type %s:\n" % err_type) + for err in validation_err[err_type]: + if isinstance(err, redfish.ris.RegistryValidationError): + self.rdmc.ui.error(err.message) + try: + if err.reg: + self.rdmc.ui.error(str(err.sel)) + except: + pass + raise redfish.ris.ValidationError() + + if not results: + if eth_conf: + continue + else: + self.rdmc.ui.printer("No differences found from current configuration.\n") + + # Return code + if returnvalue: + return ReturnCodes.LOAD_SKIP_SETTING_ERROR + + return ReturnCodes.SUCCESS + + def loadvalidation(self, options): + """Load method validation function + + :param options: command line options + :type options: list. + """ + + if self.rdmc.opts.latestschema: + options.latestschema = True + + try: + self.cmdbase.login_validation(self, options) + except Exception: + if options.mpfilename: + pass + else: + raise + + # filename validations and checks + if options.filename: + self.filenames = options.filename + elif self.rdmc.config: + if self.rdmc.config.defaultloadfilename: + self.filenames = [self.rdmc.config.defaultloadfilename] + + if not self.filenames: + self.filenames = [__filename__] + + def verify_file(self, filedata, inputfile): + """Function used to handle oddly named files and convert to JSON + + :param filedata: input file data + :type filedata: string. + :param inputfile: current input file + :type inputfile: string. + """ + try: + tempholder = json.loads(filedata) + return tempholder + except: + raise InvalidFileFormattingError("Invalid file formatting found in file %s" % inputfile) + + def get_current_selector(self, path=None): + """Returns current selected content minus hard coded list + + :param path: current path + :type path: string. + """ + contents = self.rdmc.app.monolith.path[path] + + if not contents: + contents = list() + + for content in contents: + for k in list(content.keys()): + if k.lower() in HARDCODEDLIST or "@odata" in k.lower(): + del content[k] + + return contents + + def runmpfunc(self, mpfile=None, lfile=None, outputdir=None): + """Main worker function for multi file command + + :param mpfile: configuration file + :type mpfile: string. + :param lfile: custom file name + :type lfile: string. + :param outputdir: custom output directory + :type outputdir: string. + """ + # self.logoutobj.run("") + data = self.validatempfile(mpfile=mpfile, lfile=lfile) + + if not data: + return False + + processes = [] + finalreturncode = True + outputform = "%Y-%m-%d-%H-%M-%S" + + if outputdir: + if outputdir.endswith(('"', "'")) and outputdir.startswith(('"', "'")): + outputdir = outputdir[1:-1] + + if not os.path.isdir(outputdir): + self.rdmc.ui.error("The give output folder path does not exist.\n") + raise InvalidCommandLineErrorOPTS("") + + dirpath = outputdir + else: + dirpath = os.getcwd() + + dirname = "%s_%s" % (datetime.now().strftime(outputform), "MSClogs") + createdir = os.path.join(dirpath, dirname) + os.mkdir(createdir) + + oofile = open(os.path.join(createdir, "CompleteOutputfile.txt"), "w+") + self.rdmc.ui.printer("Create multiple processes to load configuration " "concurrently to all servers...\n") + + while True: + if not self.queue.empty(): + line = self.queue.get() + else: + break + + finput = "\n" + "Output for " + line[line.index("--url") + 1] + ": \n\n" + urlvar = line[line.index("--url") + 1] + + if "python" in os.path.basename(sys.executable.lower()): + # If we are running from source we have to add the python file to the command + listargforsubprocess = [sys.executable, sys.argv[0]] + line + else: + listargforsubprocess = [sys.executable] + line + + if os.name != "nt": + listargforsubprocess = " ".join(listargforsubprocess) + + urlfilename = urlvar.split("//")[-1] + logfile = open(os.path.join(createdir, urlfilename + ".txt"), "w+") + pinput = subprocess.Popen(listargforsubprocess, shell=True, stdout=logfile, stderr=logfile) + + processes.append((pinput, finput, urlfilename, logfile)) + + for pinput, finput, urlfilename, logfile in processes: + pinput.wait() + returncode = pinput.returncode + finalreturncode = finalreturncode and not returncode + + logfile.close() + logfile = open(os.path.join(createdir, urlfilename + ".txt"), "r+") + oofile.write(finput + str(logfile.read())) + oofile.write("-x+x-" * 16) + logfile.close() + + if returncode == 0: + self.rdmc.ui.printer("Loading Configuration for {} : SUCCESS\n".format(urlfilename)) + else: + self.rdmc.ui.error("Loading Configuration for {} : FAILED\n".format(urlfilename)) + self.rdmc.ui.error( + "ILOREST return code : {}.\nFor more details please check " + "{}.txt under {} directory.\n".format(returncode, urlfilename, createdir) + ) + + oofile.close() + + if finalreturncode: + self.rdmc.ui.printer("All servers have been successfully configured.\n") + + return finalreturncode + + def validatempfile(self, mpfile=None, lfile=None): + """Validate temporary file + + :param mpfile: configuration file + :type mpfile: string. + :param lfile: custom file name + :type lfile: string. + """ + self.rdmc.ui.printer("Checking given server information...\n") + + if not mpfile: + return False + + if not os.path.isfile(mpfile): + raise InvalidFileInputError( + "File '%s' doesn't exist, please " "create file by running save command." % mpfile + ) + + try: + with open(mpfile, "r") as myfile: + data = list() + cmdtorun = ["load"] + cmdargs = ["-f", str(lfile)] + globalargs = ["-v", "--nocache"] + + while True: + line = myfile.readline() + + if not line: + break + + if line.endswith(os.linesep): + line.rstrip(os.linesep) + + args = shlex.split(line, posix=False) + + if len(args) < 5: + self.rdmc.ui.error("Incomplete data in input file: {}\n".format(line)) + raise InvalidMSCfileInputError("Please verify the " "contents of the %s file" % mpfile) + else: + linelist = globalargs + cmdtorun + args + cmdargs + line = str(line).replace("\n", "") + self.queue.put(linelist) + data.append(linelist) + except Exception as excp: + raise excp + + if data: + return data + + return False + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag if you wish to use a different" + " filename than the default one. The default filename is" + " %s." % __filename__, + action="append", + default=None, + ) + customparser.add_argument( + "-m", + "--multiprocessing", + dest="mpfilename", + help="""use the provided filename to obtain data""", + default=None, + ) + customparser.add_argument( + "--outputdirectory", + dest="outdirectory", + help="""use the provided directory to output data for multiple server configuration""", + default=None, + ) + customparser.add_argument( + "--latestschema", + dest="latestschema", + action="store_true", + help="Optionally use the latest schema instead of the one " + "requested by the file. Note: May cause errors in some data " + "retrieval due to difference in schema versions.", + default=None, + ) + customparser.add_argument( + "--uniqueoverride", + dest="uniqueoverride", + action="store_true", + help="Override the measures stopping the tool from writing " "over items that are system unique.", + default=False, + ) + customparser.add_argument( + "--encryption", + dest="encryption", + help="Optionally include this flag to encrypt/decrypt a file " "using the key provided.", + default=None, + ) + customparser.add_argument( + "--reboot", + dest="reboot", + help="Use this flag to perform a reboot command function after" + " completion of operations. For help with parameters and" + " descriptions regarding the reboot flag, run help reboot.", + default=None, + ) + customparser.add_argument( + "--force_network_config", + dest="force_network_config", + help="Use this flag to force set network configuration." + "Network settings will be skipped if the flag is not included.", + action="store_true", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/LoginCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/LoginCommand.py new file mode 100644 index 0000000..5e36f67 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/LoginCommand.py @@ -0,0 +1,582 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Login Command for RDMC""" + +import getpass +import os +import socket +import redfish.ris +from redfish.hpilo.vnichpilo import AppAccount +from redfish.rest.connections import ChifDriverMissingOrNotFound, VnicNotEnabledError +import requests +from requests.exceptions import SSLError, RequestException +import platform +from argparse import SUPPRESS + +try: + from rdmc_helper import ( + Encryption, + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + PathUnavailableError, + ReturnCodes, + UsernamePasswordRequiredError, + VnicExistsError, + VnicLoginError, + InactiveAppAccountTokenError, + NoAppAccountError, + GenBeforeLoginError, + AppAccountExistsError, + ) +except ModuleNotFoundError: + from ilorest.rdmc_helper import ( + ReturnCodes, + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + PathUnavailableError, + Encryption, + UsernamePasswordRequiredError, + VnicExistsError, + VnicLoginError, + InactiveAppAccountTokenError, + NoAppAccountError, + GenBeforeLoginError, + AppAccountExistsError, + ) + +from redfish.rest.v1 import ServerDownOrUnreachableError + + +class LoginCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "login", + "usage": None, + "description": "To login remotely run using iLO url and iLO credentials" + "\n\texample: login -u " + "-p \n\n\tTo login on a local server run without " + "arguments\n\texample: login" + "\n\n\tTo login through VNIC run using --force_vnic and iLO credentials " + "\n\texample: login --force_vnic -u -p " + "\n\nLogin using OTP can be done in 2 ways." + "\n\n\t To login implicitly, use the tag --wait_for_otp." + "\n\t\texample: login -u -p --wait_for_otp" + "\n\n\n\t To login explicitly, use the tag -o/--otp and enter OTP after." + "\n\t\texample: login -u -p -o " + "\n\n\tNOTE: A [URL] can be specified with " + "an IPv4, IPv6, or hostname address.", + "summary": "Connects to a server, establishes a secure session," " and discovers data from iLO.", + "aliases": [], + "auxcommands": ["LogoutCommand"], + "cert_data": {}, + } + self.cmdbase = None + self.rdmc = None + self.url = None + self.username = None + self.password = None + self.sessionid = None + self.biospassword = None + self.auxcommands = dict() + self.cert_data = dict() + self.login_otp = None + self.ilo_gen = 6 + + def run(self, line, help_disp=False): + """wrapper function for main login function + + :param line: command line input + :type line: string. + :param help_disp: flag to determine to display or not + :type help_disp: boolean + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + self.loginfunction(line) + + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + + if not self.rdmc.app.monolith._visited_urls: + self.auxcommands["logout"].run("") + raise PathUnavailableError("The path specified by the --path flag is unavailable.") + except Exception: + raise + + # Return code + return ReturnCodes.SUCCESS + + def perform_login(self, options, skipbuild, user_ca_cert_data, args, app_obj): + + ilo_ver = self.ilo_gen + if ilo_ver < 7 or (ilo_ver >= 7 and (options.usechif or options.noapptoken or args)): + login_response = self.rdmc.app.login( + username=self.username, + password=self.password, + sessionid=self.sessionid, + base_url=self.url, + path=options.path, + skipbuild=skipbuild, + includelogs=options.includelogs, + biospassword=self.biospassword, + is_redfish=self.rdmc.opts.is_redfish, + proxy=self.rdmc.opts.proxy, + user_ca_cert_data=user_ca_cert_data, + json_out=self.rdmc.json, + login_otp=self.login_otp, + log_dir=self.rdmc.log_dir, + ) + else: + if options.hostappname or options.hostappid or options.salt: + if not (options.hostappname and options.hostappid and options.salt): + raise InvalidCommandLineError("Please enter all the necessary host application information.\n") + + # Code to check if vnic is enabled. + vnic_enabled = self.rdmc.app.vexists(app_obj) + if not vnic_enabled: + raise VnicExistsError( + "Unable to access iLO using virtual NIC. " + "Please ensure virtual NIC is enabled in iLO. " + "Ensure that virtual NIC in the host OS is " + "configured properly. Refer to documentation for more information.\n" + ) + + # To check if appaccount exists. If not, error out. + try: + apptoken_exists = self.rdmc.app.token_exists(app_obj) + except Exception as excp: + raise AppAccountExistsError("Error occurred while checking if app account exists.\n") + + # If appaccount doesn't exist + if not apptoken_exists: + raise NoAppAccountError( + "The app account is not available for this host application. " + "Please retry using the --no_app_account option or generate " + "an application account using the appaccount create command.\n" + ) + + try: + # Function will log in, retrieve session id, call build monolith, .save() + login_response = self.rdmc.app.vnic_login( + app_obj=app_obj, + path=options.path, + skipbuild=skipbuild, + includelogs=options.includelogs, + json_out=self.rdmc.json, + base_url=self.url, + username=self.username, + password=self.password, + log_dir=self.rdmc.log_dir, + login_otp=self.login_otp, + is_redfish=self.rdmc.opts.is_redfish, + proxy=self.rdmc.opts.proxy, + user_ca_cert_data=user_ca_cert_data, + biospassword=self.biospassword, + sessionid=self.sessionid, + ) + except redfish.hpilo.vnichpilo.InactiveAppAccountTokenError: + LOGGER.error("Login failed due to an inactive or expired App Account token.") + raise InactiveAppAccountTokenError("Login failed due to an inactive or expired App Account token.") + except Exception as excp: + error_msg = """Error occurred while performing login using app account. + Check if application account exists using 'appaccount details' command. + Otherwise, try with --no_app_account option.\n""" + LOGGER.error(error_msg) + raise VnicLoginError(error_msg) + + if options.session_id and login_response and hasattr(login_response, "_auth_key"): + self.rdmc.ui.printer(f"\nSessionID:{login_response._auth_key}\n\n") + + def loginfunction(self, line, skipbuild=None, json_out=False): + """Main worker function for login class + + :param line: entered command line + :type line: list. + :param skipbuild: flag to determine if monolith should be build + :type skipbuild: boolean. + :param json_out: flag to determine if json output neededd + :type skipbuild: boolean. + """ + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineError("Invalid command line arguments") + + if platform.system() == "Darwin": + # Mac OS + app_obj = None + self.ilo_gen = 6 + else: + app_obj = AppAccount( + appname=options.hostappname, + appid=options.hostappid, + salt=options.salt, + username=options.user, + password=options.password, + log_dir=self.rdmc.log_dir, + ) + + self.ilo_gen = self.get_ilover_beforelogin(args=args, app_obj=app_obj, options=options) + + self.loginvalidation(options=options, args=args, app_obj=app_obj) + + # if proxy server provided in command line as --useproxy, it will be used, + # otherwise it will the environment variable setting. + # else proxy will be set as None. + if self.rdmc.opts.proxy: + _ = self.rdmc.opts.proxy + elif "https_proxy" in os.environ and os.environ["https_proxy"]: + _ = os.environ["https_proxy"] + elif "http_proxy" in os.environ and os.environ["http_proxy"]: + _ = os.environ["http_proxy"] + else: + _ = self.rdmc.config.proxy + + no_bundle = False + + if getattr(options, "ca_cert_bundle", False): + user_ca_cert_data = {"ca_certs": options.ca_cert_bundle} + else: + user_ca_cert_data = {} + if getattr(options, "user_certificate", False): + no_bundle = True + user_ca_cert_data.update({"cert_file": options.user_certificate}) + if getattr(options, "user_root_ca_key", False): + no_bundle = True + user_ca_cert_data.update({"key_file": options.user_root_ca_key}) + if getattr(options, "user_root_ca_password", False): + no_bundle = True + user_ca_cert_data.update({"key_password": options.user_root_ca_password}) + + if not no_bundle: + if hasattr(user_ca_cert_data, "ca_certs"): + user_ca_cert_data.pop("ca_certs") + + try: + if getattr(options, "force_vnic", False): + self.rdmc.ui.printer("\nAttempt to login with Vnic...\n") + try: + sock = socket.create_connection((args[0], 443)) + if sock: + sock.close + except: + pass + + self.sessionid = options.sessionid + self.login_otp = options.login_otp + + if self.url and "blobstore" in self.url and (options.waitforOTP or options.login_otp): + options.waitforOTP = None + options.login_otp = None + self.rdmc.ui.printer( + "Warning: For local inband mode, TFA is not supported, options --wait_for_otp " + "and --otp will be ignored\n" + ) + + if options.waitforOTP: + try: + self.perform_login(options, skipbuild, user_ca_cert_data, args, app_obj) + except redfish.rest.connections.OneTimePasscodeError: + self.rdmc.ui.printer("One Time Passcode Sent to registered email.\n") + ans = input("Enter OTP: ") + self.login_otp = ans + self.perform_login(options, skipbuild, user_ca_cert_data, args, app_obj) + else: + self.perform_login(options, skipbuild, user_ca_cert_data, args, app_obj) + except ServerDownOrUnreachableError as excp: + self.rdmc.ui.printer("The following error occurred during login: '%s'\n" % str(excp.__class__.__name__)) + + self.username = None + self.password = None + + # Warning for cache enabled, since we save session in plain text + if not self.rdmc.encoding: + self.rdmc.ui.warn("Cache is activated. Session keys are stored in plaintext.") + + if self.rdmc.opts.debug: + self.rdmc.ui.warn("Logger is activated. Logging is stored in plaintext.") + + if options.selector: + try: + self.rdmc.app.select(selector=options.selector) + + if self.rdmc.opts.verbose: + self.rdmc.ui.printer(("Selected option: '%s'\n" % options.selector)) + except Exception as excp: + raise redfish.ris.InstanceNotFoundError(excp) + + def loginvalidation(self, options, args, app_obj): + """Login helper function for login validations + + :param options: command line options + :type options: list. + :param args: command line arguments + :type args: list. + """ + # Fill user name/password from config file + if not options.user: + options.user = self.rdmc.config.username + if not options.password: + options.password = self.rdmc.config.password + if not hasattr(options, "user_certificate"): + options.user_certificate = self.rdmc.config.user_cert + if not hasattr(options, "user_root_ca_key"): + options.user_root_ca_key = self.rdmc.config.user_root_ca_key + if not hasattr(options, "user_root_ca_password"): + options.user_root_ca_password = self.rdmc.config.user_root_ca_password + + ilo_ver = self.ilo_gen + + if ( + options.user + and not options.password + and ( + not hasattr(options, "user_certificate") + or not hasattr(options, "user_root_ca_key") + or hasattr(options, "user_root_ca_password") + ) + ): + # Option for interactive entry of password + tempinput = getpass.getpass().rstrip() + if tempinput: + options.password = tempinput + else: + raise InvalidCommandLineError("Empty or invalid password was entered.") + + if options.user: + self.username = options.user + + if options.password: + self.password = options.password + + if options.encode: + self.username = Encryption.decode_credentials(self.username).decode("utf-8") + self.password = Encryption.decode_credentials(self.password).decode("utf-8") + + if options.biospassword: + self.biospassword = options.biospassword + + if ilo_ver >= 7: + if not args: + if self.username and self.password and not options.noapptoken and not options.usechif: + options.noapptoken = True + if options.noapptoken: + if ( + not self.username or not self.password + ): # Add another check to see if username and password is present in environment variables + raise UsernamePasswordRequiredError( + "Please enter username and password in order to login without app account.\n" + ) + options.force_vnic = True + + # Assignment of url in case no url is entered + if getattr(options, "force_vnic", False): + if not (getattr(options, "ca_cert_bundle", False) or getattr(options, "user_certificate", False)): + if not (self.username and self.password) and not options.sessionid: + raise UsernamePasswordRequiredError("Please provide credentials to login with virtual NIC.\n") + self.url = "https://16.1.15.1" + else: + if ilo_ver >= 7 and not options.usechif: + # TO DO + # self.url = self.rdmc.app.GetIPAddress() + self.url = "https://16.1.15.1" + else: + self.url = "blobstore://." + + if args: + # Any argument should be treated as an URL + self.url = args[0] + + # Verify that URL is properly formatted for https:// + if "https://" not in self.url: + self.url = "https://" + self.url + + if not ( + hasattr(options, "user_certificate") + or hasattr(options, "user_root_ca_key") + or hasattr(options, "user_root_ca_password") + ): + if not (options.user and options.password): + raise InvalidCommandLineError("Empty username or password was entered.") + else: + # Check to see if there is a URL in config file + if self.rdmc.config.url: + self.url = self.rdmc.config.url + + def get_ilover_beforelogin(self, args, app_obj, options): + """ + Attempts to retrieve the iLO version before login. + + :param args: List of command-line arguments; expects iLO IP/hostname at index 0. + :param app_obj: App object for fallback retrieval if args is empty. + :param options: Options object, may include flags like 'force_vnic' or 'usechif'. + :return: iLO version as an integer. + :raises: GenBeforeLoginError, VnicExistsError, ChifDriverMissingOrNotFound, VnicNotEnabledError + """ + ip = path = None # Initialize to avoid UnboundLocalError + try: + if args: + ip = args[0] + path = f"https://{ip}/redfish/v1" + + response = requests.get(path, verify=False, timeout=10) + response.raise_for_status() + + try: + json_data = response.json() + oem_data = json_data.get("Oem", {}).get("Hpe") or json_data.get("Oem", {}).get("Hp") + if not oem_data: + raise KeyError("Oem->Hpe or Oem->Hp block missing in response.") + + manager = oem_data.get("Manager", [{}])[0] + manager_type = manager.get("ManagerType") + + if manager_type: + ilo_ver = int(manager_type.split(" ")[1]) + else: + moniker = oem_data.get("Moniker", {}) + prodgen = moniker.get("PRODGEN") + if not prodgen: + raise KeyError("PRODGEN key not found in Moniker.") + ilo_ver = int(prodgen.split(" ")[1]) + except (KeyError, ValueError, IndexError) as e: + raise GenBeforeLoginError( + f"Failed to parse iLO version from response. Error: {e}\n" + "Ensure the iLO firmware is Redfish-compliant and responds correctly.\n" + ) + else: + ilo_ver, _ = self.rdmc.app.getilover_beforelogin(app_obj) + + return ilo_ver + + except ChifDriverMissingOrNotFound: + if getattr(options, "force_vnic", False): + return 5 + raise + + except VnicNotEnabledError: + if getattr(options, "usechif", False): + return 7 + raise VnicExistsError( + "Unable to access iLO using virtual NIC.\n" + "Please ensure that virtual NIC is enabled in iLO and " + "properly configured in the host OS.\n" + "Refer to the documentation for setup instructions.\n" + ) + + except SSLError: + raise GenBeforeLoginError( + f"SSL Error: Unable to connect to the server at {ip or '[unknown IP]'}.\n" + "Please verify the iLO IP and try accessing the GUI.\n" + ) + + except RequestException as e: + raise GenBeforeLoginError( + f"Connection Error: Could not retrieve data from {path or '[unknown path]'}.\n" f"Details: {e}\n" + ) + + except Exception as e: + raise GenBeforeLoginError( + f"An unexpected error occurred while retrieving the iLO version.\n" + f"Details: {e}\n" + "Verify if virtual NIC is enabled for iLO7 or that the CHIF driver " + "is installed for iLO5 and iLO6 based servers.\n" + ) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + + def remove_argument(parser, arg): + for action in parser._actions: + opts = action.option_strings + if (opts and opts[0] == arg) or action.dest == arg: + parser._remove_action(action) + break + + for action in parser._action_groups: + for group_action in action._group_actions: + opts = group_action.option_strings + if (opts and opts[0] == arg) or group_action.dest == arg: + action._group_actions.remove(group_action) + return + + if not customparser: + return + + customparser.add_argument( + "--wait_for_otp", + dest="waitforOTP", + help="Optionally include this flag to implicitly wait for OTP.", + action="store_true", + default=None, + ) + self.cmdbase.add_login_arguments_group(customparser) + remove_argument(customparser, "url") + customparser.add_argument( + "--selector", + dest="selector", + help="Optionally include this flag to select a type to run" + " the current command on. Use this flag when you wish to" + " select a type without entering another command, or if you" + " wish to work with a type that is different from the one" + " you currently have selected.", + default=None, + ) + customparser.add_argument( + "--no_app_token", + "--no_app_account", + dest="noapptoken", + help="Include this parameter in order to login to iLO7 and above with credentials and not app account.", + default=None, + action="store_true", + ) + customparser.add_argument("--hostappid", dest="hostappid", help="Parameter to specify hostappid", default=None) + customparser.add_argument( + "--hostappname", dest="hostappname", help="Parameter to specify hostappname", default=None + ) + customparser.add_argument( + "--salt", dest="salt", help="Parameter to specify application owned salt", default=None + ) + customparser.add_argument( + "--use_chif", + dest="usechif", + help="Include this parameter in order to operate iLO7 and above using chif.", + default=None, + action="store_true", + ) + customparser.add_argument( + "--show_session_id", + "-s", + dest="session_id", + help=SUPPRESS, + default=None, + action="store_true", + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/LogoutCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/LogoutCommand.py new file mode 100644 index 0000000..cc7b999 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/LogoutCommand.py @@ -0,0 +1,79 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Logout Command for RDMC""" + +try: + from rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes +except ImportError: + from ilorest.rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes + + +class LogoutCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "logout", + "usage": None, + "description": "Run to end the current session and disconnect" " from the server\n\tExample: logout", + "summary": "Ends the current session and disconnects from the server.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + + def logoutfunction(self, line): + """Main logout worker function + + :param line: command line input + :type line: string. + """ + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.rdmc.app.logout("") + + def run(self, line, help_disp=False): + """Wrapper function for main logout function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + self.rdmc.ui.printer("Logging session out.\n") + self.logoutfunction(line) + + # Return code + return ReturnCodes.SUCCESS + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/PendingChangesCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/PendingChangesCommand.py new file mode 100644 index 0000000..5ced8f1 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/PendingChangesCommand.py @@ -0,0 +1,201 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Results Command for rdmc""" + +import copy +import json +import sys + +import jsondiff + +try: + from rdmc_helper import HARDCODEDLIST +except: + from ilorest.rdmc_helper import HARDCODEDLIST +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class PendingChangesCommand: + """PendingChanges class command""" + + def __init__(self): + self.ident = { + "name": "pending", + "usage": None, + "description": "Run to show pending committed changes " + "that will be applied after a reboot.\n\texample: pending", + "summary": "Show the pending changes that will be applied on reboot.", + "aliases": [], + "auxcommands": [], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Show pending changes of settings objects + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("Pending command does not take any arguments.") + self.pendingvalidation(options) + + self.pendingfunction() + sys.stdout.write("\n") + self.cmdbase.logout_routine(self, options) + return ReturnCodes.SUCCESS + + def pendingfunction(self): + """Main pending command worker function""" + settingsuri = [] + ignorekeys = ["@odata.id", "@odata.etag", "@redfish.settings", "oem"] + ignoreuri = [str("hpsut*")] + ignorekeys.extend(HARDCODEDLIST) + + resourcedir = self.rdmc.app.get_handler(self.rdmc.app.monolith._resourcedir, service=True, silent=True) + + for resource in resourcedir.dict["Instances"]: + if (resource["@odata.id"].split("/").__len__() - 1) > 4: + splitstr = resource["@odata.id"].split("/")[5] + for element in ignoreuri: + if "/settings" in resource["@odata.id"] and not self.wildcard_str_match(element, splitstr): + settingsuri.append(resource["@odata.id"]) + + self.rdmc.ui.printer("Current Pending Changes:\n") + + for uri in settingsuri: + diffprint = {} + baseuri = uri.split("settings")[0] + + base = self.rdmc.app.get_handler(baseuri, service=True, silent=True) + settings = self.rdmc.app.get_handler(uri, service=True, silent=True) + + typestring = self.rdmc.app.monolith.typepath.defs.typestring + currenttype = ".".join(base.dict[typestring].split("#")[-1].split(".")[:-1]) + + differences = json.loads(jsondiff.diff(base.dict, settings.dict, syntax="symmetric", dump=True)) + + diffprint = self.recursdict(differences, ignorekeys) + + self.rdmc.ui.printer("\n%s:" % currenttype) + if not diffprint: + self.rdmc.ui.printer("\nNo pending changes found.\n") + else: + self.rdmc.ui.pretty_human_readable(diffprint) + + def wildcard_str_match(self, first, second): + """ + Recursive function for determining match between two strings. Accounts + for wildcard characters + + :param first: comparison string (may contain '*' or '?' for wildcards) + :param type: str (unicode) + :param second: string value to be compared (must not contain '*' or '?') + :param type: str (unicode) + """ + + if not first and not second: + return True + if len(first) > 1 and first[0] == "*" and not second: + return False + if (len(first) > 1 and first[0] == "?") or (first and second and first[0] == second[0]): + return self.wildcard_str_match(first[1:], second[1:]) + if first and first[0] == "*": + return self.wildcard_str_match(first[1:], second) or self.wildcard_str_match(first, second[1:]) + + return False + + def recursdict(self, diff, ignorekeys): + """Recursively get dict ready for printing + + :param diff: diff dict + :type options: dict. + """ + diffprint = copy.deepcopy(diff) + for item in diff: + if item.lower() in ignorekeys: + diffprint.pop(item) + elif item == "$delete": + for ditem in diff[item]: + if isinstance(diff[item], list): + continue + if isinstance(diff[item][ditem], dict): + diffprint[item].pop(ditem) + if ditem.lower() in ignorekeys or ditem.isdigit(): + continue + else: + diffprint.update({"removed": ditem}) + diffprint.pop(item) + elif item == "$insert": + for ditem in diff[item]: + del diffprint[item][diffprint[item].index(ditem)] + diffprint.update({"changed index position": ditem[1]}) + diffprint.pop(item) + elif isinstance(diff[item], dict): + diffprint[item] = self.recursdict(diff[item], ignorekeys) + + elif isinstance(diff[item], list): + diffprint.update({item: {"Current": diff[item][0], "Pending": diff[item][1]}}) + else: + continue + + return diffprint + + def pendingvalidation(self, options): + """Pending method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/ExitCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/ExitCommand.py new file mode 100644 index 0000000..942343a --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/ExitCommand.py @@ -0,0 +1,80 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Exit Command for rdmc""" + +import sys + +try: + from rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes +except ImportError: + from ilorest.rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes + + +class ExitCommand: + """Exit class to handle exiting from interactive mode""" + + def __init__(self): + self.ident = { + "name": "exit", + "usage": None, + "description": "Run to exit from the interactive shell\n\texample: exit", + "summary": "Exits from the interactive shell.", + "aliases": ["quit"], + "auxcommands": ["LogoutCommand"], + } + # self.rdmc = rdmcObj + # self.logoutobj = rdmcObj.commands_dict["LogoutCommand"](rdmcObj) + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """If an argument is present, print help else exit + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (_, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if not args or not line: + self.auxcommands["logout"].run("") + + # System exit + sys.exit(ReturnCodes.SUCCESS) + else: + self.rdmc.ui.error("Exit command does not take any parameters.\n") + raise InvalidCommandLineErrorOPTS("Invalid command line arguments.") + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/HelpCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/HelpCommand.py new file mode 100644 index 0000000..a74ced4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/HelpCommand.py @@ -0,0 +1,113 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Help Command for RDMC""" + +try: + from rdmc_base_classes import RdmcCommandBase, RdmcOptionParser +except ImportError: + from ilorest.rdmc_base_classes import RdmcCommandBase, RdmcOptionParser +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class HelpCommand(RdmcCommandBase): + """Constructor""" + + def __init__(self): + self.ident = { + "name": "help", + "usage": None, + "description": "help [COMMAND]\n\tFor more detailed command descriptions" + " use the help command feature\n\texample: help login\n", + "summary": "Displays command line syntax and help menus for individual commands." " Example: help login\n", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Wrapper function for help main function + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if not args or not line: + RdmcOptionParser().print_help() + if self.rdmc: + cmddict = self.rdmc.get_commands() + sorted_keys = sorted(list(cmddict.keys())) + + for key in sorted_keys: + if key[0] == "_": + continue + else: + self.rdmc.ui.printer("\n%s\n" % key) + for cmd in cmddict[key]: + self.rdmc.ui.printer( + "%-25s - %s\n" + % ( + self.rdmc.commands_dict[cmd].ident["name"], + self.rdmc.commands_dict[cmd].ident["summary"], + ) + ) + else: + if self.rdmc: + cmddict = self.rdmc.get_commands() + sorted_keys = list(cmddict.keys()) + + for key in sorted_keys: + for cmd in cmddict[key]: + cmd_s = cmd.split("Command") + cmd_s = cmd_s[0] + if args[0].lower() == cmd_s.lower(): + self.rdmc.ui.printer(self.rdmc.commands_dict[cmd].ident["description"] + "\n") + return ReturnCodes.SUCCESS + raise InvalidCommandLineError("Command '%s' not found." % args[0]) + # Return code + return ReturnCodes.SUCCESS + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/ExitCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/ExitCommand.cpython-310.pyc new file mode 100644 index 0000000..08bb4e5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/ExitCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/HelpCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/HelpCommand.cpython-310.pyc new file mode 100644 index 0000000..05c33da Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/HelpCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..b19776f Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/REQUIREDCOMMANDS/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/ResultsCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/ResultsCommand.py new file mode 100644 index 0000000..31f04dc --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/ResultsCommand.py @@ -0,0 +1,156 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Results Command for rdmc""" + +from redfish.ris.resp_handler import ResponseHandler +from redfish.ris.rmc_helper import EmptyRaiseForEAFP + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class ResultsCommand: + """Monolith class command""" + + def __init__(self): + self.ident = { + "name": "results", + "usage": None, + "description": "Run to show the results of the last" " changes after a server reboot.\n\texample: results", + "summary": "Show the results of changes which require a server reboot.", + "aliases": [], + "auxcommands": ["LoginCommand", "SelectCommand"], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Gather results of latest BIOS change + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("Results command does not take any arguments.") + self.resultsvalidation(options) + results = {} + if self.rdmc.app.typepath.defs.biospath[-1] == "/": + iscsipath = self.rdmc.app.typepath.defs.biospath + "iScsi/" + bootpath = self.rdmc.app.typepath.defs.biospath + "Boot/" + else: + iscsipath = self.rdmc.app.typepath.defs.biospath + "/iScsi" + bootpath = self.rdmc.app.typepath.defs.biospath + "/Boot" + + try: + self.auxcommands["select"].selectfunction("SmartStorageConfig") + smartarray = self.rdmc.app.getprops() + sapaths = [path["@odata.id"].split("settings")[0] for path in smartarray] + except: + sapaths = None + + biosresults = self.rdmc.app.get_handler(self.rdmc.app.typepath.defs.biospath, service=True, silent=True) + iscsiresults = self.rdmc.app.get_handler(iscsipath, service=True, silent=True) + bootsresults = self.rdmc.app.get_handler(bootpath, service=True, silent=True) + saresults = [] + if sapaths: + saresults = [self.rdmc.app.get_handler(path, service=True, silent=True) for path in sapaths] + try: + results.update({"Bios:": biosresults.dict[self.rdmc.app.typepath.defs.biossettingsstring]["Messages"]}) + except Exception: + results.update({"Bios:": None}) + + try: + results.update({"Iscsi:": iscsiresults.dict[self.rdmc.app.typepath.defs.biossettingsstring]["Messages"]}) + except: + results.update({"Iscsi:": None}) + + try: + results.update({"Boot:": bootsresults.dict[self.rdmc.app.typepath.defs.biossettingsstring]["Messages"]}) + except: + results.update({"Boot:": None}) + try: + for result in saresults: + loc = "SmartArray" + if saresults.index(result) > 0: + loc += " %d:" % saresults.index(result) + else: + loc += ":" + results.update({loc: result.dict[self.rdmc.app.typepath.defs.biossettingsstring]["Messages"]}) + except: + results.update({"SmartArray:": None}) + + self.rdmc.ui.printer("Results of the previous reboot changes:\n\n") + + for result in results: + self.rdmc.ui.printer("%s\n" % result) + try: + for msg in results[result]: + _ = ResponseHandler( + self.rdmc.app.validationmanager, + self.rdmc.app.typepath.defs.messageregistrytype, + ).message_handler(response_data=msg, message_text="", verbosity=0, dl_reg=False) + pass + except EmptyRaiseForEAFP as exp: + raise EmptyRaiseForEAFP(exp) + except Exception: + self.rdmc.ui.error("No messages found for %s.\n" % result[:-1]) + + self.cmdbase.logout_routine(self, options) + return ReturnCodes.SUCCESS + + def resultsvalidation(self, options): + """Results method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/SaveCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/SaveCommand.py new file mode 100644 index 0000000..55eb3f3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/SaveCommand.py @@ -0,0 +1,424 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Save Command for RDMC""" + +import json +from collections import OrderedDict + +import redfish.ris + +try: + from rdmc_helper import ( + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + ReturnCodes, + iLORisCorruptionError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + ReturnCodes, + iLORisCorruptionError, + ) + +# default file name +__filename__ = "ilorest.json" + + +class SaveCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "save", + "usage": None, + "description": "Run to save a selected type to a file" + "\n\texample: save --selector HpBios.\n\n\tChange the default " + "output filename\n\texample: save --selector HpBios. -f " + "output.json\n\n\tTo save multiple types in one file\n\texample: " + "save --multisave Bios.,ComputerSystem.", + "summary": "Saves the selected type's settings to a file.", + "aliases": [], + "auxcommands": ["SelectCommand"], + } + self.filename = __filename__ + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main save worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if options.multisave: + options.selector = "" + if ( + hasattr(options, "selector") + and options.selector is not None + and "hpbaseconfigs" not in options.selector.lower() + and "hpebaseconfigs" not in options.selector.lower() + and "bios" not in options.selector.lower() + ): + self.savevalidation(options) + else: + # filename validations and checks + self.cmdbase.login_validation(self, options) + self.filename = None + if options.filename and len(options.filename) > 1: + raise InvalidCommandLineError("Save command doesn't support multiple filenames.") + elif options.filename: + self.filename = options.filename[0] + elif self.rdmc.config: + if self.rdmc.config.defaultsavefilename: + self.filename = self.rdmc.config.defaultsavefilename + + if not self.filename: + self.filename = __filename__ + + # if args: + # raise InvalidCommandLineError("Save command takes no arguments.") + + self.rdmc.ui.printer("Saving configuration...\n") + if options.filter: + try: + if (str(options.filter)[0] == str(options.filter)[-1]) and str(options.filter).startswith(("'", '"')): + options.filter = options.filter[1:-1] + + (sel, val) = options.filter.split("=") + sel = sel.strip() + val = val.strip() + except: + raise InvalidCommandLineError("Invalid filter" " parameter format [filter_attribute]=[filter_value]") + + instances = self.rdmc.app.select( + selector=self.rdmc.app.selector, + fltrvals=(sel, val), + path_refresh=True, + ) + contents = self.saveworkerfunction(options, instances=instances) + else: + contents = self.saveworkerfunction(options) + + if options.multisave: + for select in options.multisave: + try: + self.auxcommands["select"].run(select) + except: + pass + contents += self.saveworkerfunction(options) + + if not contents: + raise redfish.ris.NothingSelectedError + else: + # TODO: Maybe add this to the command. Not sure we use it elsewhere in the lib + contents = self.add_save_file_header(contents) + + if options.encryption: + with open(self.filename, "wb") as outfile: + outfile.write( + Encryption().encrypt_file( + json.dumps(contents, indent=2, cls=redfish.ris.JSONEncoder), + options.encryption, + ) + ) + else: + with open(self.filename, "w") as outfile: + outfile.write(json.dumps(contents, indent=2, cls=redfish.ris.JSONEncoder, sort_keys=True)) + self.rdmc.ui.printer("Configuration saved to: %s\n" % self.filename) + + self.cmdbase.logout_routine(self, options) + + # Return code + return ReturnCodes.SUCCESS + + def saveworkerfunction(self, options, instances=None): + """Returns the currently selected type for saving + + :param instances: list of instances from select to save + :type instances: list. + """ + try: + if ( + options.selector is not None + and ( + "bios" in options.selector.lower() + or "hpbaseconfigs" in options.selector.lower() + or "hpebaseconfigs" in options.selector.lower() + ) + ) and (options.multisave is None or options.multisave == ""): + raise KeyError + content = self.rdmc.app.getprops(insts=instances) + try: + contents = [{val[self.rdmc.app.typepath.defs.hrefstring]: val} for val in content] + except KeyError: + try: + contents = [{val["links"]["self"][self.rdmc.app.typepath.defs.hrefstring]: val} for val in content] + except KeyError: + raise iLORisCorruptionError( + "iLO Database seems to be corrupted. Please check. Reboot the server to " "restore\n" + ) + except Exception: + config_path = None + contents = list() + if options.selector is not None: + if options.selector.lower() == "bios." or "bios.v" in options.selector.lower(): + config_path = ["/redfish/v1/systems/1/bios/settings/"] + elif options.selector.lower() == "bios": + config_path = [ + "/redfish/v1/systems/1/bios/settings/", + "/redfish/v1/systems/1/bios/mappings/", + "/redfish/v1/systems/1/bios/oem/hpe/mappings/", + ] + elif "hpbaseconfigs" in options.selector.lower() or "hpebaseconfigs" in options.selector.lower(): + config_path = [ + "/redfish/v1/systems/1/bios/oem/hpe/baseconfigs/", + "/redfish/v1/systems/1/bios/oem/hpe/nvmeof/baseconfigs/", + "/redfish/v1/systems/1/bios/oem/hpe/iscsi/baseconfigs/", + "/redfish/v1/systems/1/bios/oem/hpe/tlsconfig/baseconfigs/", + "/redfish/v1/systems/1/bios/oem/hpe/serverconfiglock/baseconfigs/", + "/redfish/v1/systems/1/bios/oem/hpe/kmsconfig/baseconfigs/", + "/redfish/v1/systems/1/bios/oem/hpe/boot/baseconfigs/", + "/redfish/v1/systems/1/bios/baseconfigs/", + "/redfish/v1/systems/1/bios/nvmeof/baseconfigs/", + "/redfish/v1/systems/1/bios/iscsi/baseconfigs/", + "/redfish/v1/systems/1/bios/tlsconfig/baseconfigs/", + "/redfish/v1/systems/1/bios/serverconfiglock/baseconfigs/", + "/redfish/v1/systems/1/bios/kmsconfig/baseconfigs/", + "/redfish/v1/systems/1/bios/boot/baseconfigs/", + ] + elif "hpebiosmapping" in options.selector.lower(): + config_path = [ + "/redfish/v1/systems/1/bios/mappings", + "/redfish/v1/systems/1/bios/oem/hpe/mappings/", + ] + result = None + for b in config_path: + try: + result = self.rdmc.app.get_handler(b, silent=True, service=True) + except: + pass + if result is not None: + if result.status == 200: + d = {b: result.dict} + contents.append(d) + + type_string = self.rdmc.app.typepath.defs.typestring + + templist = list() + srnum_list = list() + + for content in contents: + typeselector = None + pathselector = None + + for path, values in content.items(): + # if "Managers/1/EthernetInterfaces/1" not in path: + for dictentry in list(values.keys()): + if dictentry == type_string: + typeselector = values[dictentry] + pathselector = path + del values[dictentry] + if dictentry in [ + "IPv4Addresses", + "IPv6Addresses", + "IPv6AddressPolicyTable", + "MACAddress", + "StaticNameServers", + # "AutoNeg", + # "FullDuplex", + "SpeedMbps", + ]: + del values[dictentry] + if dictentry in [ + "IPv6StaticAddresses", + "IPv6StaticDefaultGateways", + "IPv4StaticAddresses", + ]: + if values[dictentry]: + del values[dictentry] + if dictentry in ["IPv4", "IPv6", "DHCPv6", "DHCPv4"]: + if "DNSServers" in values[dictentry]: + del values[dictentry]["DNSServers"] + del values["Oem"]["Hpe"][dictentry]["DNSServers"] + if "UseDNSServers" in values[dictentry]: + del values[dictentry]["UseDNSServers"] + del values["Oem"]["Hpe"][dictentry]["UseDNSServers"] + + if values: + skip = False + if "SerialNumber" in values and values["SerialNumber"] is not None and values["SerialNumber"] != "": + if values["SerialNumber"] in srnum_list: + skip = True + else: + srnum_list.append(values["SerialNumber"]) + tempcontents = dict() + + if not skip: + if typeselector and pathselector: + tempcontents[typeselector] = {pathselector: values} + else: + raise InvalidFileFormattingError("Missing path or selector in input file.") + + templist.append(tempcontents) + + return templist + + def nested_sort(self, data): + """Helper function to sort all dictionary key:value pairs + + :param data: dictionary to sort + :type data: dict. + """ + + for key, value in data.items(): + if isinstance(value, dict): + data[key] = self.nested_sort(value) + + data = OrderedDict(sorted(list(data.items()), key=lambda x: x[0])) + + return data + + def savevalidation(self, options): + """Save method validation function + + :param options: command line options + :type options: list. + """ + + if options.multisave: + options.multisave = options.multisave.replace('"', "").replace("'", "") + options.multisave = options.multisave.replace(" ", "").split(",") + if not len(options.multisave) >= 1: + raise InvalidCommandLineError("Invalid number of types in multisave option.") + options.selector = options.multisave[0] + options.multisave = options.multisave[1:] + + self.cmdbase.login_select_validation(self, options) + + # filename validations and checks + self.filename = None + + if options.filename and len(options.filename) > 1: + raise InvalidCommandLineError("Save command doesn't support multiple filenames.") + elif options.filename: + self.filename = options.filename[0] + elif self.rdmc.config: + if self.rdmc.config.defaultsavefilename: + self.filename = self.rdmc.config.defaultsavefilename + + if not self.filename: + self.filename = __filename__ + + def add_save_file_header(self, contents): + """Helper function to retrieve the comments for save file + + :param contents: current save contents + :type contents: list. + """ + templist = list() + + headers = self.rdmc.app.create_save_header() + templist.append(headers) + + for content in contents: + templist.append(content) + + return templist + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag if you wish to use a different filename than the default one. " + "The default filename is %s." % __filename__, + action="append", + default=None, + ) + + customparser.add_argument( + "--selector", + dest="selector", + help="Optionally include this flag to select a type to run the current command on. " + "Use this flag when you wish to select a type without entering another command, " + "or if you wish to work with a type that is different from the one currently " + "selected.", + default=None, + # required=True, + ) + customparser.add_argument( + "--multisave", + dest="multisave", + help="Optionally include this flag to save multiple types to a single file. " + "Overrides the currently selected type.\n\t Usage: --multisave type1.,type2.,type3.", + default="", + ) + customparser.add_argument( + "--filter", + dest="filter", + help="Optionally set a filter value for a filter attribute. This uses the provided " + "filter for the currently selected type. Note: Use this flag to narrow down your " + "results. For example, selecting a common type might return multiple objects that " + "are all of that type. If you want to modify the properties of only one of those " + "objects, use the filter flag to narrow down results based on properties." + "\n\t Usage: --filter [ATTRIBUTE]=[VALUE]", + default=None, + ) + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the displayed output to " + "JSON format. Preserving the JSON data structure makes the information easier to " + "parse.", + default=False, + ) + customparser.add_argument( + "--encryption", + dest="encryption", + help="Optionally include this flag to encrypt/decrypt a file using the key provided.", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/SelectCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/SelectCommand.py new file mode 100644 index 0000000..0b7a799 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/SelectCommand.py @@ -0,0 +1,143 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Select Command for RDMC""" + +from redfish.ris import NothingSelectedError + +try: + from rdmc_helper import LOGGER, InvalidCommandLineErrorOPTS, ReturnCodes +except ImportError: + from ilorest.rdmc_helper import LOGGER, InvalidCommandLineErrorOPTS, ReturnCodes + + +class SelectCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "select", + "usage": None, + "description": "Selects the Redfish/HpRest type to be used.\nIn order to " + "remove the need of including the version while selecting you" + " can simply enter the type name until the first period\n\t" + "example: select HpBios.\n" + "Run without an argument to " + "display the currently selected type\n\texample: select", + "summary": "Selects the object type to be used.", + "aliases": ["sel"], + "auxcommands": ["LoginCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def selectfunction(self, line): + """Main select worker function + + :param line: command line input + :type line: string. + """ + + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.selectvalidation(options) + + if args: + if options.ref: + LOGGER.warning("Patches from current selection will be cleared.") + selector = args[0] + selections = self.rdmc.app.select(selector=selector, path_refresh=options.ref) + + if "storage" in selector.lower(): + storage_url = "/redfish/v1/Systems/1/Storage/" + list_storageid = self.rdmc.app.get_handler(storage_url, silent=True, service=True).dict["Members"] + for val in list_storageid: + storageid_fromurl = val["@odata.id"] + ctr = self.rdmc.app.get_handler(storageid_fromurl, silent=True, service=True).dict + if "Controllers" in ctr: + ctr_url = ctr["Controllers"]["@odata.id"] + self.rdmc.app.get_handler(ctr_url, silent=True, service=True).dict + if self.rdmc.opts.verbose and selections: + templist = list() + self.rdmc.ui.printer("Selected option(s): ") + + for item in selections: + if item.type not in templist: + templist.append(item.type) + + self.rdmc.ui.printer("%s\n" % ", ".join(map(str, templist))) + + else: + selector = self.rdmc.app.selector + + if selector: + sellist = [sel for sel in self.rdmc.app.monolith.typesadded if selector.lower() in sel.lower()] + self.rdmc.ui.printer("Current selection: ") + self.rdmc.ui.printer("%s\n" % ", ".join(map(str, sellist))) + else: + raise NothingSelectedError + + self.cmdbase.logout_routine(self, options) + + def selectvalidation(self, options): + """Select data validation function + + :param options: command line options + :type options: list. + """ + + self.cmdbase.login_select_validation(self, options) + + def run(self, line, help_disp=False): + """Wrapper function for main select function + + :param line: entered command line + :type line: list. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + self.selectfunction(line) + + # Return code + return ReturnCodes.SUCCESS + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--refresh", + dest="ref", + action="store_true", + help="Optionally reload the data of selected type and clear " "patches from current selection.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/SetCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/SetCommand.py new file mode 100644 index 0000000..c34dcd0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/SetCommand.py @@ -0,0 +1,472 @@ +### +# Copyright 2016-2023 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Set Command for RDMC""" +import json +import os +import sys + +import redfish.ris + +from redfish.rest.connections import SecurityStateError + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidOrNothingChangedSettingsError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidOrNothingChangedSettingsError, + ) + +from redfish.ris.rmc_helper import NothingSelectedError + + +class SetCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "set", + "usage": None, + "description": "Setting a " + "single level property example:\n\tset property=value\n\n\t" + "Setting multiple single level properties example:\n\tset " + "property=value property=value property=value\n\n\t" + "Setting a multi level property example:\n\tset property/" + "subproperty=value", + "summary": "Changes the value of a property within the" " currently selected type.", + "aliases": [], + "auxcommands": [ + "CommitCommand", + "RebootCommand", + "RawPatchCommand", + "SelectCommand", + "LogoutCommand", + ], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def setfunction(self, line, skipprint=False): + """Main set worker function + + :param line: command line input + :type line: string. + :param skipprint: boolean to determine output + :type skipprint: boolean. + """ + + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if not self.rdmc.interactive and not self.rdmc.app.cache: + raise InvalidCommandLineError("The 'set' command is not useful in " "non-interactive and non-cache modes.") + + self.setvalidation(options) + ilover = self.rdmc.app.getiloversion() + if ilover >= 7.000 and ("HighSecurity" in args[0] or "Production" in args[0]): + self.rdmc.ui.error( + "HighSecurity and Production modes are not supported on iLO 7 , Please use SecureStandard instead \n" + ) + raise SecurityStateError( + "HighSecurity and Production modes are not supported on iLO 7 , Please use SecureStandard instead" + ) + else: + if "SecureStandard" in args[0] and ilover < 7.000: + self.rdmc.ui.error( + "SecureStandard mode is not supported on iLO 6 and below , Please use HighSecurity instead \n" + ) + raise SecurityStateError( + "SecureStandard mode is not supported on iLO 6 and below , Please use HighSecurity instead \n" + ) + _ = args[0].find("HighSecurity") + if ilover >= 7.000: + _ = args[0].find("SecureStandard") + fsel = None + fval = None + if args: + if "ethernetinterface" in self.rdmc.app.selector.lower() and "ipv4addresses" in args[0].lower(): + raise InvalidCommandLineError( + "For security reasons this request is discarded. " + "Use `rawpatch` or the `load --force_network_config` command " + "to modify the IP configuration of the ilO.\n" + ) + if options.filter: + try: + (fsel, fval) = str(options.filter).strip("'\" ").split("=") + (fsel, fval) = (fsel.strip(), fval.strip()) + except: + raise InvalidCommandLineError( + "Invalid filter" " parameter format [filter_attribute]=[filter_value]" + ) + + if any([s.lower().startswith("adminpassword=") for s in args]) and not any( + [s.lower().startswith("oldadminpassword=") for s in args] + ): + raise InvalidCommandLineError( + "'OldAdminPassword' must also " + "be set with the current password \nwhen " + "changing 'AdminPassword' for security reasons." + ) + count = 0 + for arg in args: + if arg: + if len(arg) > 2: + if ('"' in arg[0] and '"' in arg[-1]) or ("'" in arg[0] and "'" in arg[-1]): + args[count] = arg[1:-1] + elif len(arg) == 2: + if (arg[0] == '"' and arg[1] == '"') or (arg[0] == "'" and arg[1] == "'"): + args[count] = None + count += 1 + if not self.rdmc.app.selector: + raise NothingSelectedError + if "." not in self.rdmc.app.selector: + self.rdmc.app.selector = self.rdmc.app.selector + "." + if self.rdmc.app.selector: + if self.rdmc.app.selector.lower().startswith("bios."): + if "attributes" not in arg.lower(): + arg = "Attributes/" + arg + + try: + (sel, val) = arg.split("=") + sel = sel.strip().lower() + val = val.strip("\"'") + + if val.lower() == "true" or val.lower() == "false": + val = val.lower() in ("yes", "true", "t", "1") + except: + raise InvalidCommandLineError("Invalid set parameter format. [Key]=[Value]") + + newargs = list() + + if "/" in sel and "/" not in str(val): + newargs = sel.split("/") + elif "/" in sel: + items = arg.split("=", 1) + newargs = items[0].split("/") + if not isinstance(val, bool): + if val: + if val[0] == "[" and val[-1] == "]": + val = val[1:-1].split(",") + payload = {newargs[-1]: val} if newargs else {sel: val} + if newargs: + for key in newargs[:-1][::-1]: + payload = {key: payload} + try: + contents = self.rdmc.app.loadset( + seldict=payload, + latestschema=options.latestschema, + fltrvals=(fsel, fval), + uniqueoverride=options.uniqueoverride, + ) + if not contents: + if not sel.lower() == "oldadminpassword": + raise InvalidOrNothingChangedSettingsError( + "Nothing changed " + "for attribute '%s'.\nPlease check if the attribute is Oem/Hpe Attribute or Read-only " + "or System Unique property or the value trying to set is same or invalid" % sel + ) + elif contents == "No entries found": + raise InvalidOrNothingChangedSettingsError( + "No " "entries found in the current " "selection for the setting '%s'." % sel + ) + elif contents == "reverting": + self.rdmc.ui.error("Removing previous patch and returning to the " "original value.\n") + else: + for content in contents: + self.rdmc.ui.printer("Added the following patch:\n") + self.rdmc.ui.print_out_json(content) + + except redfish.ris.ValidationError as excp: + errs = excp.get_errors() + + for err in errs: + if err.sel and err.sel.lower() == "adminpassword": + types = self.rdmc.app.monolith.types + + for item in types: + for instance in types[item]["Instances"]: + if "hpbios." in instance.maj_type.lower(): + _ = [ + instance.patches.remove(patch) + for patch in instance.patches + if patch.patch[0]["path"] == "/OldAdminPassword" + ] + + if isinstance(err, redfish.ris.RegistryValidationError): + self.rdmc.ui.printer(err.message) + + raise redfish.ris.ValidationError(excp) + + if options.commit: + self.auxcommands["commit"].commitfunction(options) + + if options.reboot and not options.commit: + self.auxcommands["reboot"].run(options.reboot) + + if options.logout: + self.auxcommands["logout"].run("") + + else: + raise InvalidCommandLineError("Missing parameters for 'set' command.\n") + + def patchfunction(self, line): + """Main set worker function + :param line: command line input + :type line: string. + :param skipprint: boolean to determine output + :type skipprint: boolean. + """ + import platform + import tempfile + + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if not self.rdmc.interactive and not self.rdmc.app.cache: + raise InvalidCommandLineError("The 'set' command is not useful in " "non-interactive and non-cache modes.") + + _ = self.rdmc.app.selector + self.setvalidation(options) + if args: + count = 0 + for arg in args: + if arg: + if len(arg) > 2: + if ('"' in arg[0] and '"' in arg[-1]) or ("'" in arg[0] and "'" in arg[-1]): + args[count] = arg[1:-1] + elif len(arg) == 2: + if (arg[0] == '"' and arg[1] == '"') or (arg[0] == "'" and arg[1] == "'"): + args[count] = None + count += 1 + if not self.rdmc.app.selector: + raise NothingSelectedError + if "." not in self.rdmc.app.selector: + self.rdmc.app.selector = self.rdmc.app.selector + "." + if self.rdmc.app.selector: + if self.rdmc.app.selector.lower().startswith("bios."): + if "attributes" not in arg.lower(): + arg = "Attributes/" + arg + + try: + (sel, val) = arg.split("=") + sel = sel.strip() + val = val.strip("\"'") + + if val.lower() == "true" or val.lower() == "false": + val = val.lower() in ("yes", "true", "t", "1") + except: + raise InvalidCommandLineError("Invalid set parameter format. [Key]=[Value]") + + newargs = list() + + if "/" in sel and "/" not in str(val): + newargs = sel.split("/") + elif "/" in sel: + items = arg.split("=", 1) + newargs = items[0].split("/") + + if not isinstance(val, bool): + if val: + if val[0] == "[" and val[-1] == "]": + val = val[1:-1].split(",") + if val.isdigit(): + val = int(val) + patch_data = dict() + payload = {newargs[-1]: val} if newargs else {sel: val} + if "drive" in self.rdmc.app.selector.lower(): + tempdir = "/tmp" if platform.system() == "Darwin" else tempfile.gettempdir() + temp_file = os.path.join(tempdir, "temp_patch.json") + out_file = open(temp_file, "w") + if getattr(options, "filter", None): + filt_arr = options.filter.split("=") + patchpath = filt_arr[-1] + else: + sys.stdout.write("Please provide the actual drive to modify using filter option\n") + return + patch_payload = {patchpath: payload} + json.dump(patch_payload, out_file, indent=6) + out_file.close() + sys.stdout.write("payload %s \n" % patch_payload) + self.auxcommands["rawpatch"].run(temp_file + " --service") + os.remove(temp_file) + elif newargs: + for key in newargs[:-1][::-1]: + if key == "PowerControl": + payload = {key: [payload]} + else: + payload = {key: payload} + patch_data.update(payload) + if "Oem" in key: + if ( + "managernetworkprotocol." in self.rdmc.app.selector + or "thermal." in self.rdmc.app.selector + ): + tempdir = "/tmp" if platform.system() == "Darwin" else tempfile.gettempdir() + temp_file = os.path.join(tempdir, "temp_patch.json") + out_file = open(temp_file, "w") + if "managernetworkprotocol." in self.rdmc.app.selector: + patchpath = self.rdmc.app.typepath.defs.managerpath + "NetworkProtocol/" + elif "thermal." in self.rdmc.app.selector: + patchpath = "/redfish/v1/Chassis/1/Thermal/" + patch_payload = {patchpath: payload} + json.dump(patch_payload, out_file, indent=6) + out_file.close() + sys.stdout.write("payload %s \n" % patch_payload) + self.auxcommands["rawpatch"].run(temp_file + " --service") + self.auxcommands["select"].run(self.rdmc.app.selector + " --refresh") + os.remove(temp_file) + if "PowerControl" in payload.keys(): + import platform + import tempfile + + tempdir = "/tmp" if platform.system() == "Darwin" else tempfile.gettempdir() + temp_file = os.path.join(tempdir, "temp_patch.json") + out_file = open(temp_file, "w") + patch_path = "/redfish/v1/Chassis/1/Power" + patch_payload = {patch_path: payload} + json.dump(patch_payload, out_file, indent=6) + out_file.close() + self.auxcommands["rawpatch"].run(temp_file + " --service") + os.remove(temp_file) + + def run(self, line, skipprint=False, help_disp=False): + """Main set function + + :param line: command line input + :type line: string. + :param skipprint: boolean to determine output + :type skipprint: boolean. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + if ( + ("Oem/Hpe/EnhancedDownloadPerformanceEnabled" in line[0]) + or ("Oem/Hpe/ThermalConfiguration" in line[0]) + or ("Oem/Hpe/FanPercentMinimum" in line[0]) + or ("PowerControl" in line[0]) + or (self.rdmc.app.selector is not None and "drive" in self.rdmc.app.selector.lower()) + ): + self.patchfunction(line) + else: + try: + self.setfunction(line, skipprint=skipprint) + except SecurityStateError: + return ReturnCodes.V1_SECURITY_STATE_ERROR + # Return code + return ReturnCodes.SUCCESS + + def setvalidation(self, options): + """Set data validation function""" + + if self.rdmc.opts.latestschema: + options.latestschema = True + if self.rdmc.config.commit.lower() == "true": + options.commit = True + try: + self.cmdbase.login_select_validation(self, options) + except redfish.ris.NothingSelectedError: + raise redfish.ris.NothingSelectedSetError("") + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--selector", + dest="selector", + help="Optionally include this flag to select a type to run" + " the current command on. Use this flag when you wish to" + " select a type without entering another command, or if you" + " wish to work with a type that is different from the one" + " you currently have selected.", + default=None, + ) + + customparser.add_argument( + "--filter", + dest="filter", + help="Optionally set a filter value for a filter attribute." + " This uses the provided filter for the currently selected" + " type. Note: Use this flag to narrow down your results. For" + " example, selecting a common type might return multiple" + " objects that are all of that type. If you want to modify" + " the properties of only one of those objects, use the filter" + " flag to narrow down results based on properties." + "\t\t\t\t\t Usage: --filter [ATTRIBUTE]=[VALUE]", + default=None, + ) + customparser.add_argument( + "--commit", + dest="commit", + action="store_true", + help="Use this flag when you are ready to commit all pending" + " changes. Note that some changes made in this way will be updated" + " instantly, while others will be reflected the next time the" + " server is started.", + default=None, + ) + customparser.add_argument( + "--reboot", + dest="reboot", + help="Use this flag to perform a reboot command function after" + " completion of operations. For help with parameters and" + " descriptions regarding the reboot flag, run help reboot.", + default=None, + ) + customparser.add_argument( + "--latestschema", + dest="latestschema", + action="store_true", + help="Optionally use the latest schema instead of the one " + "requested by the file. Note: May cause errors in some data " + "retrieval due to difference in schema versions.", + default=None, + ) + customparser.add_argument( + "--uniqueoverride", + dest="uniqueoverride", + action="store_true", + help="Override the measures stopping the tool from writing " "over items that are system unique.", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/StatusCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/StatusCommand.py new file mode 100644 index 0000000..ab96226 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/StatusCommand.py @@ -0,0 +1,203 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Status Command for RDMC""" + +from functools import reduce + +from redfish.ris.utils import merge_dict + +try: + from rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes +except ImportError: + from ilorest.rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes + + +class StatusCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "status", + "usage": None, + "description": "Run to display all pending changes within" + " the currently\n\tselected type that need to be" + " committed\n\texample: status", + "summary": "Displays all pending changes within a selected type" " that need to be committed.", + "aliases": [], + "auxcommands": ["SelectCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main status worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.statusvalidation(options) + contents = self.rdmc.app.status() + selector = self.rdmc.app.selector + + if contents and options.json: + self.jsonout(contents) + elif contents: + self.outputpatches(contents, selector) + else: + self.rdmc.ui.printer("No changes found\n") + + # Return code + return ReturnCodes.SUCCESS + + def jsonout(self, contents): + """Helper function to print json output of patches + + :param contents: contents for the selection + :type contents: string. + """ + self.rdmc.ui.printer("Current changes found:\n") + createdict = lambda y, x: {x: y} + totdict = {} + for item in contents: + for keypath, value in item.items(): + path = keypath.split("(")[1].strip("()") + cont = {} + totdict[path] = cont + for content in value: + val = ( + ["List Manipulation"] + if content["op"] == "move" + else [content["value"].strip("\"'")] if len(content["value"]) else [""] + ) + cont = reduce( + createdict, + reversed([path] + content["path"].strip("/").split("/") + val), + ) + merge_dict(totdict, cont) + self.rdmc.ui.print_out_json(totdict) + + def outputpatches(self, contents, selector): + """Helper function for status for use in patches + + :param contents: contents for the selection + :type contents: string. + :param selector: type selected + :type selector: string. + """ + self.rdmc.ui.printer("Current changes found:\n") + for item in contents: + moveoperation = "" + for key, value in item.items(): + if selector and key.lower().startswith(selector.lower()): + self.rdmc.ui.printer("%s (Currently selected)\n" % key) + else: + self.rdmc.ui.printer("%s\n" % key) + + for content in value: + try: + if content["op"] == "move": + moveoperation = "/".join(content["path"].split("/")[1:-1]) + continue + except: + if content[0]["op"] == "move": + moveoperation = "/".join(content[0]["path"].split("/")[1:-1]) + continue + try: + if isinstance(content[0]["value"], int): + self.rdmc.ui.printer("\t%s=%s" % (content[0]["path"][1:], content[0]["value"])) + elif not isinstance(content[0]["value"], bool) and not len(content[0]["value"]) == 0: + if content[0]["value"][0] == '"' and content[0]["value"][-1] == '"': + self.rdmc.ui.printer( + "\t%s=%s" + % ( + content[0]["path"][1:], + content[0]["value"][1:-1], + ) + ) + else: + self.rdmc.ui.printer("\t%s=%s" % (content[0]["path"][1:], content[0]["value"])) + else: + output = content[0]["value"] + + if not isinstance(output, bool): + if len(output) == 0: + output = '""' + + self.rdmc.ui.printer("\t%s=%s" % (content[0]["path"][1:], output)) + except: + if isinstance(content["value"], int): + self.rdmc.ui.printer("\t%s=%s" % (content["path"][1:], content["value"])) + elif ( + content["value"] + and not isinstance(content["value"], bool) + and not len(content["value"]) == 0 + ): + if content["value"][0] == '"' and content["value"][-1] == '"': + self.rdmc.ui.printer("\t%s=%s" % (content["path"][1:], content["value"])) + else: + self.rdmc.ui.printer("\t%s=%s" % (content["path"][1:], content["value"])) + else: + output = content["value"] + + if output and not isinstance(output, bool): + if len(output) == 0: + output = '""' + + self.rdmc.ui.printer("\t%s=%s" % (content["path"][1:], output)) + self.rdmc.ui.printer("\n") + if moveoperation: + self.rdmc.ui.printer("\t%s=List Manipulation\n" % moveoperation) + + def statusvalidation(self, options): + """Status method validation function""" + + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/TypesCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/TypesCommand.py new file mode 100644 index 0000000..c11c110 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/TypesCommand.py @@ -0,0 +1,125 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Types Command for RDMC""" + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class TypesCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "types", + "usage": None, + "description": "Run to display currently " "available selectable types\n\tExample: types", + "summary": "Displays all selectable types within the currently logged in server.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def typesfunction(self, line, returntypes=False): + """Main types worker function + + :param line: command line input + :type line: string. + :param returntypes: flag to determine if types should be printed + :type returntypes: boolean. + """ + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.typesvalidation(options) + + if not args: + typeslist = list() + typeslist = sorted(set(self.rdmc.app.types(options.fulltypes))) + + if not returntypes: + self.rdmc.ui.printer("Type options:\n") + + for item in typeslist: + self.rdmc.ui.printer("%s\n" % item) + else: + return typeslist + else: + raise InvalidCommandLineError("The 'types' command does not take any arguments.") + + self.cmdbase.logout_routine(self, options) + + def run(self, line, help_disp=False): + """Wrapper function for types main function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + self.typesfunction(line) + + # Return code + return ReturnCodes.SUCCESS + + def typesvalidation(self, options): + """types method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--fulltypes", + dest="fulltypes", + action="store_true", + help="Optionally include this flag if you would prefer to " + "return the full type name instead of the simplified versions" + " (Redfish only option).", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/CommitCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/CommitCommand.cpython-310.pyc new file mode 100644 index 0000000..0e97bb7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/CommitCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/DetectiLOCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/DetectiLOCommand.cpython-310.pyc new file mode 100644 index 0000000..34f7153 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/DetectiLOCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/GetCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/GetCommand.cpython-310.pyc new file mode 100644 index 0000000..05ff597 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/GetCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/InfoCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/InfoCommand.cpython-310.pyc new file mode 100644 index 0000000..cecbb2a Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/InfoCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/ListCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/ListCommand.cpython-310.pyc new file mode 100644 index 0000000..3870bf0 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/ListCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/LoadCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/LoadCommand.cpython-310.pyc new file mode 100644 index 0000000..0441327 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/LoadCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/LoginCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/LoginCommand.cpython-310.pyc new file mode 100644 index 0000000..90c894e Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/LoginCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/LogoutCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/LogoutCommand.cpython-310.pyc new file mode 100644 index 0000000..c66af3b Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/LogoutCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/PendingChangesCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/PendingChangesCommand.cpython-310.pyc new file mode 100644 index 0000000..9bd9aa9 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/PendingChangesCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/ResultsCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/ResultsCommand.cpython-310.pyc new file mode 100644 index 0000000..9bf55ff Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/ResultsCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/SaveCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/SaveCommand.cpython-310.pyc new file mode 100644 index 0000000..f137bff Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/SaveCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/SelectCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/SelectCommand.cpython-310.pyc new file mode 100644 index 0000000..281d150 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/SelectCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/SetCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/SetCommand.cpython-310.pyc new file mode 100644 index 0000000..0a56958 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/SetCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/StatusCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/StatusCommand.cpython-310.pyc new file mode 100644 index 0000000..8a34d65 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/StatusCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/TypesCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/TypesCommand.cpython-310.pyc new file mode 100644 index 0000000..db5e3ba Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/TypesCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..890aed4 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/COMMANDS/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/AdvancedPmmConfigCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/AdvancedPmmConfigCommand.py new file mode 100644 index 0000000..6922a6d --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/AdvancedPmmConfigCommand.py @@ -0,0 +1,453 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Command to apply specified configuration to PMM""" +from __future__ import absolute_import # verify if python3 libs can handle + +from argparse import Action +from copy import deepcopy + +try: + from rdmc_helper import ( + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + LOGGER, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + ) + +from .lib.RestHelpers import RestHelpers + + +class _ParseOptionsList(Action): + def __init__(self, option_strings, dest, nargs, **kwargs): + super(_ParseOptionsList, self).__init__(option_strings, dest, nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_strings): + """ + Callback to parse a comma-separated list into an array. + Then store the array in the option's destination + """ + try: + setattr(namespace, self.dest, next(iter(values)).split(",")) + except: + raise InvalidCommandLineError("Values in a list must be separated by a comma.") + + +class AdvancedPmmConfigCommand: + """ + Command to apply specified configuration to PMM + """ + + def __init__(self): + self.ident = { + "name": "provisionpmm", + "usage": None, + "description": "Applies specified configuration to PMM.\n" "\texample: provisionpmm -m 50 -i On -pid 1,2", + "summary": "Applies specified configuration to PMM.", + "aliases": ["provisionpmm"], + "auxcommands": [ + "ShowPmemPendingConfigCommand", + "ClearPendingConfigCommand", + ], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self._rest_helpers = RestHelpers(rdmcObject=self.rdmc) + + def run(self, line, help_disp=False): + """ + Wrapper function for new command main function + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + LOGGER.info("PMM: %s", self.ident["name"]) + + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineError("Failed to parse options") + + if args: + self.validate_args(args, options) + self.validate_options(options) + # Raise exception if server is in POST + if self._rest_helpers.in_post(): + raise NoContentsFoundForOperationError( + "Unable to retrieve resources - " "server might be in POST or powered off" + ) + self.configure_pmm(options) + + return ReturnCodes.SUCCESS + + @staticmethod + def validate_args(args, options): + """ + Produces relevant error messages when unwanted + extra arguments are specified with flags. + """ + error_message = ( + "Chosen flag is invalid. Use the '-m | --memory-mode'" " flag to specify the size in percentage." + ) + some_flag = options.memorymode or options.interleave or options.proc or options.force + + case_a = options.force and not options.memorymode and not options.interleave and not options.proc + case_b = options.interleave and not options.memorymode and not options.force and not options.proc + case_c = options.proc and not options.memorymode and not options.force and not options.interleave + case_d = options.memorymode and not options.proc and not options.interleave and not options.force + + if case_a or case_b or case_c: + raise InvalidCommandLineError(error_message) + if case_d or (some_flag not in args): + raise InvalidCommandLineError("Chosen flag is Invalid") + + def validate_options(self, options): + """ + Validate whether options specified by user are valid or not + :param options: options specified by the user with the 'provisionpmm' command + :type options: instance of OptionParser class + """ + some_flag = options.memorymode or options.interleave or options.proc or options.force + if not some_flag: + raise InvalidCommandLineError("No flag specified.\n\nUsage: " + self.ident["usage"]) + + # If the memory mode option value is not valid. + resp = self._rest_helpers.retrieve_model(self.rdmc) + model = resp["Model"] + + if "Gen10 Plus" in model: + if not (options.memorymode == 0 or options.memorymode == 100): + raise InvalidCommandLineError("Specify the correct value (0 or 100)" " to configure PMM") + else: + if options.memorymode < 0 or options.memorymode > 100: + raise InvalidCommandLineError("Specify the correct value (1-100)" " to configure PMM") + if options.interleave: + if options.interleave.lower() not in ["on", "off"]: + raise InvalidCommandLineError( + "Specify the correct value to set interleave" + " state of persistent memory regions\n\n" + self.ident["usage"] + ) + options.interleave = options.interleave.lower() + + if options.proc and not options.memorymode and not options.interleave: + raise InvalidCommandLineError( + "Use '-m|--memory-mode' flag to specify the size in percentage" + " or '-i|--pmem-interleave' flag to set interleave" + " state of persistent memory regions" + ) + + # If not 100% memorymode, -i flag is mandatory to interleave memroy regions. + if 0 <= options.memorymode < 100 and not options.interleave: + raise InvalidCommandLineError( + "Use '-i | --pmem-interleave' flag to" " specify the interleave state of persistent" " memory regions" + ) + if options.proc: + for proc_id in options.proc: + if not proc_id.isdigit(): + raise InvalidCommandLineError("Specify the correct processor id") + + def definearguments(self, customparser): + """ + Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-m", + "--memory-mode", + dest="memorymode", + type=int, + help="Percentage of the total capacity to configure all PMMs to MemoryMode." + " This flag is optional. If not specified," + " by default configuration will be applied with 0%% Volatile." + " To interleave persistent memory regions, use this flag in conjunction with" + " the --pmem-interleave flag. To configure all the PMMs on specific processor," + " use this flag in conjunction with the --proc flag.", + default=0, + ) + + customparser.add_argument( + "-i", + "--pmem-interleave", + dest="interleave", + help="Indicates whether the persistent memory regions should be interleaved or not.", + default=None, + ) + + customparser.add_argument( + "-pid", + "--proc", + nargs=1, + action=_ParseOptionsList, + dest="proc", + help="To apply selected configuration on specific processors, " + "supply a comma-separated list of Processor IDs (without spaces) where " + "p=processor id. This flag is optional. If not specified, " + "by default configuration will be applied to all the Processors.", + ) + + customparser.add_argument( + "-f", + "--force", + action="store_true", + dest="force", + help="Allow the user to force the configuration " "by automatically accepting any prompts.", + default=False, + ) + + @staticmethod + def warn_existing_chunks_and_tasks(self, memory_chunk_tasks, memory_chunks): + """ + Checks for existing Memory Chunks and Pending Configuration Task resources on + a server where a user is trying to selected configuration + :param memory_chunk_tasks: Pending Configuration Tasks + :type memory_chunk_tasks: list + :memory_chunks: Memory Chunks in the existing configuration + :type memory_chunks: list + :returns: None + """ + # If Memory Chunks exist, display Existing configuration warning + if memory_chunks: + self.rdmc.ui.warn( + "Existing configuration found. Proceeding with applying a new " + "configuration will result in overwriting the current configuration and " + "cause data loss.\n" + ) + # If Pending Configuration Tasks exist, display warning + if memory_chunk_tasks: + self.rdmc.ui.printer( + "Warning: Pending configuration tasks found. Proceeding with applying " + "a new configuration will result in overwriting the pending " + "configuration tasks.\n" + ) + # Raise a NoChangesFoundOrMade exception when either of the above conditions exist + if memory_chunks or memory_chunk_tasks: + # Line feed for proper formatting + raise NoChangesFoundOrMadeError( + "\nFound one or more of Existing Configuration or " + "Pending Configuration Tasks. Please use the " + "'--force | -f' flag with the same command to " + "approve these changes." + ) + return None + + def delete_existing_chunks_and_tasks(self, memory_chunk_tasks, memory_chunks): + """ + Delete existing Memory Chunks and Pending Configuration Tasks + :param memory_chunk_tasks: Pending Configuration Tasks + :type memory_chunk_tasks: list + :memory_chunks: Memory Chunks in the existing configuration + :type memory_chunks: list + :returns: None + """ + # Delete any pending configuration tasks + if memory_chunk_tasks: + self.auxcommands["clearpmmpendingconfig"].delete_tasks(memory_chunk_tasks) + # Delete any existing configuration + if memory_chunks: + for chunk in memory_chunks: + data_id = chunk.get("@odata.id") + resp = self._rest_helpers.delete_resource(data_id) + if not resp: + raise NoChangesFoundOrMadeError("Error occurred while deleting " "existing configuration") + return None + + @staticmethod + def get_valid_processor_list(input_proc_list, domain_members): + """ + Function to check processors specified by user are valid or not + :param domain_members: Memory Resources + :type input_proc_list: List + :type domain_members: Dict + :returns: List of valid processors in string format (ex: [PROC1MemoryDomain]) + """ + proc_list = [] + index = 0 + invalid_proc_list = [] + # Create list with valid processors based on 'Id' attribute. + for member in domain_members: + proc_list.append(member["Id"]) + + for index, proc_id in enumerate(input_proc_list): + temp_proc_id = "PROC" + str(proc_id) + "MemoryDomain" + if temp_proc_id not in proc_list: + invalid_proc_list.append(proc_id) + else: + input_proc_list[index] = temp_proc_id + + # Raise execption if given proc id is not exist. + if invalid_proc_list: + if len(invalid_proc_list) == 1: + raise NoChangesFoundOrMadeError( + "Specified processor number {proc_list} is invalid".format(proc_list=invalid_proc_list[0]) + ) + else: + raise NoChangesFoundOrMadeError( + "Specified processor numbers {proc_list} are invalid".format(proc_list=",".join(invalid_proc_list)) + ) + + return input_proc_list + + @staticmethod + def filter_memory_chunks(memory_chunks, domain_members, proc_list): + """ + Filter memory chunks based on specified processor list + :param memory_chunks: list of all memory chunks + :param domain_members: dict of memory resources + :param proc list: List of specified processors + :Retrun: List of memory chunks corresponding to specified processors + """ + all_chunks = list() + for member in domain_members: + proc_id = member["Id"] + if not proc_list or (proc_id in proc_list): + data_id = member.get("MemoryChunks").get("@odata.id") + all_chunks += [chunk for chunk in memory_chunks if data_id in chunk.get("@odata.id")] + + return all_chunks + + @staticmethod + def get_post_data(config_data, interleavable_memory_sets): + """ + Create post body based on config data + :param config_data: dict with user input + :type config_data: dict + :param interleavable_memory_sets: List of interleavable sets + :type interleavable_memory_sets: List + :returns: Returns post data + """ + + body = { + "AddressRangeType": "PMEM", + "InterleaveSets": [], + "Oem": {"Hpe": {"MemoryChunkSizePercentage": 100 - config_data["size"]}}, + } + # Get the list of interleave sets based on the configuration + if config_data["pmeminterleave"] == "on" or config_data["size"] == 100: + # If persistent memory regions are interleaved or if it's 100% MemoryMode, + # choose the list with maximum entries. + interleave_sets = [max(interleavable_memory_sets, key=lambda x: len(x["MemorySet"]))] + else: + # If persistent memory regions are not interleaved, + # choose all the lists with exactly one entry. + interleave_sets = [il_set for il_set in interleavable_memory_sets if len(il_set["MemorySet"]) == 1] + + # Using in-place update to change the interleave sets format. + # Replace 'MemorySet' with 'Memory' for each MemorySet in interleave_sets. + for index, interleavableset in enumerate(interleave_sets): + interleave_sets[index] = [ + {"Memory": {"@odata.id": str(memory_region["@odata.id"])}} + for memory_region in interleavableset["MemorySet"] + ] + + # Create post body for each interleave sets from post body template. + post_data = [] + for interleaveset in interleave_sets: + current_body = deepcopy(body) + current_body["InterleaveSets"] = interleaveset + post_data.append(current_body) + + return post_data + + def configure_pmm(self, options): + """ + Applies selected configuration to the PMMs + :param options: options specified by user + :returns: None + """ + # Retrieve Memory Chunks and Task Resources from server. + ( + task_members, + domain_members, + memory_chunks, + ) = self._rest_helpers.retrieve_task_members_and_mem_domains() + + # Filter Task Resources to include only Pending Configuration Tasks. + memory_chunk_tasks = self._rest_helpers.filter_task_members(task_members) + + if not domain_members: + raise NoContentsFoundForOperationError( + "Failed to retrieve Memory Domain Resources, please check if persistent memory is present/configured" + ) + + # Dict with input config data + config_data = { + "size": options.memorymode, + "pmeminterleave": options.interleave, + "proc": options.proc, + } + # Check for given processor id list is valid or not. + if options.proc: + config_data["proc"] = self.get_valid_processor_list(config_data["proc"], domain_members) + memory_chunks = self.filter_memory_chunks(memory_chunks, domain_members, config_data["proc"]) + + # In case of 100% Volatile, Interleaving memory regions is not applicable. + if ( + config_data["size"] == 100 + and config_data["pmeminterleave"] + and config_data["pmeminterleave"].lower() == "on" + ): + raise InvalidCommandLineError( + "Selected configuration is invalid. " "Interleaving not supported in 100% volatile." + ) + + if options.force: + self.delete_existing_chunks_and_tasks(memory_chunk_tasks, memory_chunks) + else: + self.warn_existing_chunks_and_tasks(self, memory_chunk_tasks, memory_chunks) + + for member in domain_members: + proc_id = member["Id"] + # If given proc list is not empty, applies configuration to selected processors + if not config_data["proc"] or (proc_id in config_data["proc"]): + path = member["MemoryChunks"].get("@odata.id") + data = self.get_post_data(config_data, member["InterleavableMemorySets"]) + for body in data: + resp = self._rest_helpers.post_resource(path, body) + if resp is None: + raise NoChangesFoundOrMadeError("Error occurred while applying configuration") + + # Display warning + self.rdmc.ui.printer("\nConfiguration changes require reboot to take effect.\n") + + # Display pending configuration + self.auxcommands["showpmmpendingconfig"].show_pending_config(type("MyOptions", (object,), dict(json=False))) + return None diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ApplyPmemConfigCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ApplyPmemConfigCommand.py new file mode 100644 index 0000000..fd7ba0a --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ApplyPmemConfigCommand.py @@ -0,0 +1,357 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Command to apply a pre-defined configuration to PMM""" +from __future__ import absolute_import + +from copy import deepcopy + +try: + from rdmc_helper import ( + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + LOGGER, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + ) + +from .lib.DisplayHelpers import DisplayHelpers +from .lib.RestHelpers import RestHelpers + + +class ApplyPmemConfigCommand: + """ + Command to apply a pre-defined configuration to PMM + """ + + # Available configIDs + config_ids = [ + { + "name": "MemoryMode", + "description": "Configure all the PMMs to 100% Memory Mode.", + "totalPmemPercentage": 0, + "pmemIntereleaved": False, + }, + { + "name": "PmemInterleaved", + "description": "Configure all PMMs to 100% Persistent. " + "Interleave the Persistent memory regions within each processor.", + "totalPmemPercentage": 100, + "pmemIntereleaved": True, + }, + { + "name": "PmemNotInterleaved", + "description": "Configure all PMMs to 100% Persistent. " + "The Persistent memory regions are not interleaved.", + "totalPmemPercentage": 100, + "pmemIntereleaved": False, + }, + ] + + def __init__(self): + self.ident = { + "name": "applypmmconfig", + "usage": None, + "description": "Applies a pre-defined configuration to PMM\n" + "\texample: applypmmconfig --pmmconfig MemoryMode\n", + "summary": "Applies a pre-defined configuration to PMM.", + "aliases": [], + "auxcommands": [ + "ShowPmemPendingConfigCommand", + "ClearPendingConfigCommand", + ], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + self._display_helpers = DisplayHelpers() + + def definearguments(self, customparser): + """ + Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-C", + "--pmmconfig", + action="store", + type=str, + dest="config", + help="Specify one of the pre-defined configIDs to apply" " to all Persistent Memory Modules.", + default=None, + ) + + customparser.add_argument( + "-L", + "--list", + action="store_true", + dest="list", + help="Display a list of available pre-defined configIDs" " along with a brief description.", + default=False, + ) + + customparser.add_argument( + "-f", + "--force", + action="store_true", + dest="force", + help="Allow the user to force the configuration " "by automatically accepting any prompts.", + default=False, + ) + + def run(self, line, help_disp=False): + """ + Wrapper function for new command main function + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + LOGGER.info("PMM Apply Pre-Defined Configuration: %s", self.ident["name"]) + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineError("Failed to parse options") + if args: + self.validate_args(options) + self.validate_options(options) + self.apply_pmm_config(options) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + @staticmethod + def validate_args(options): + """ + Produces relevant error messages when unwanted extra arguments are specified with flags. + """ + if options.config: + raise InvalidCommandLineError("'config | -C' expects a single argument") + else: + raise InvalidCommandLineError("Chosen flag doesn't expect additional arguments") + + def validate_options(self, options): + """ + Validate whether options specified by user are valid. + :param options: options specified by the user. + :type options: instance of OptionParser class. + """ + # Set of valid configIDs + valid_config_ids = {config_id.get("name").lower() for config_id in self.config_ids} + + if not options.config and not options.list and not options.force: + raise InvalidCommandLineError("No flag specified.\n\nUsage: " + self.ident["usage"]) + if not options.config and options.force: + raise InvalidCommandLineError("'--force | -f' flag mandatorily requires the " "'--pmmconfig | -C' flag.") + if options.config and options.list: + raise InvalidCommandLineError("Only one of '--pmmconfig | -C' or '--list | -L' " "may be specified") + # Check whether the user entered configID is valid + if options.config and not options.config.lower() in valid_config_ids: + raise InvalidCommandLineError( + "Invalid configID.\nTo view a list of pre-defined " "configIDs, use 'applypmmconfig --list'" + ) + + def apply_pmm_config(self, options): + """ + Calls relevant functions based on the flags specified by the user. + :param options: options specified by the user. + :type options: instance of OptionParser class. + """ + if options.list: + self.show_predefined_config_options() + elif options.config: + # Raise exception if server is in POST + if RestHelpers(rdmcObject=self.rdmc).in_post(): + raise NoContentsFoundForOperationError( + "Unable to retrieve resources - " "server might be in POST or powered off" + ) + self.apply_predefined_config(options) + + def show_predefined_config_options(self): + """ + Display the available pre-defined configIDs that the user can choose from to + apply to their Persistent memory Modules. + """ + self.rdmc.ui.printer("\nAvailable Configurations:\n\n") + for config_id in ApplyPmemConfigCommand.config_ids: + self.rdmc.ui.printer(config_id.get("name") + "\n") + self.rdmc.ui.printer("\t" + config_id.get("description") + "\n") + self.rdmc.ui.printer("\n") + + def warn_existing_chunks_and_tasks(self, memory_chunk_tasks, memory_chunks): + """ + Checks for existing Memory Chunks and Pending Configuration Task resources on + a server where a user is trying to apply a pre-defined configuration + :param memory_chunk_tasks: Pending Configuration Tasks + :type memory_chunk_tasks: list + :memory_chunks: Memory Chunks in the existing configuration + :type memory_chunks: list + :returns: None + """ + # If Memory Chunks exist, display Existing configuration warning + if memory_chunks: + self.rdmc.ui.printer( + "Warning: Existing configuration found. Proceeding with applying a new " + "configuration will result in overwriting the current configuration and " + "cause data loss.\n" + ) + # If Pending Configuration Tasks exist, display warning + if memory_chunk_tasks: + self.rdmc.ui.printer( + "Warning: Pending configuration tasks found. Proceeding with applying " + "a new configuration will result in overwriting the pending " + "configuration tasks.\n" + ) + # Raise a NoChangesFoundOrMade exception when either of the above conditions exist + if memory_chunks or memory_chunk_tasks: + # Line feed for proper formatting + raise NoChangesFoundOrMadeError( + "\nFound one or more of Existing Configuration or " + "Pending Configuration Tasks. Please use the " + "'--force | -f' flag with the same command to " + "approve these changes." + ) + return None + + def delete_existing_chunks_and_tasks(self, memory_chunk_tasks, memory_chunks): + """ + Delete existing Memory Chunks and Pending Configuration Tasks + :param memory_chunk_tasks: Pending Configuration Tasks + :type memory_chunk_tasks: list + :memory_chunks: Memory Chunks in the existing configuration + :type memory_chunks: list + :returns: None + """ + # Delete any pending configuration tasks + if memory_chunk_tasks: + self.auxcommands["clearpmmpendingconfig"].delete_tasks(memory_chunk_tasks) + # Delete any existing configuration + if memory_chunks: + for chunk in memory_chunks: + data_id = chunk.get("@odata.id") + resp = RestHelpers(rdmcObject=self.rdmc).delete_resource(data_id) + if not resp: + raise NoChangesFoundOrMadeError("Error occured while deleting " "existing configuration") + return None + + @staticmethod + def get_post_data(config_data, interleavable_memory_sets): + """ + Create post body based on config data + :param config_data: An entry of self.config_iD's list + :type config_data: dict + :param interleavable_memory_sets: List of interleavable sets + :type interleavable_memory_sets: List + :returns: Returns post data + """ + + body = { + "AddressRangeType": "PMEM", + "InterleaveSets": [], + "Oem": {"Hpe": {"MemoryChunkSizePercentage": config_data["totalPmemPercentage"]}}, + } + # Get the list of interleave sets based on the configuration + if config_data["pmemIntereleaved"] or config_data["totalPmemPercentage"] == 0: + # If pmem regions are interleaved or if it MemoryMode, choose the list with maximum entries. + interleave_sets = [max(interleavable_memory_sets, key=lambda x: len(x["MemorySet"]))] + else: + # If pmem regions are NOT interleaved, choose all the lists with exactly one entry. + interleave_sets = [il_set for il_set in interleavable_memory_sets if len(il_set["MemorySet"]) == 1] + + # Using in-place update to change the interleave sets format. + # Replace 'MemorySet' with 'Memory' for each MemorySet in interleave_sets. + # Get '@odata.id' for each memory region from original interleave_sets and add this to new structure. + for index, interleavableset in enumerate(interleave_sets): + interleave_sets[index] = [ + {"Memory": {"@odata.id": str(memory_region["@odata.id"])}} + for memory_region in interleavableset["MemorySet"] + ] + + # Create post body for each interleave sets from post body template. + post_data = [] + for interleaveset in interleave_sets: + current_body = deepcopy(body) + current_body["InterleaveSets"] = interleaveset + post_data.append(current_body) + + return post_data + + def apply_predefined_config(self, options): + """ + Apply the selected pre-defined configuration to Persistent Memory Modules. + :param options: option specified by the user. + :returns: None + """ + # Retrieve Memory Chunks and Task Resources from server + (task_members, domain_members, memory_chunks) = RestHelpers( + rdmcObject=self.rdmc + ).retrieve_task_members_and_mem_domains() + + # Filter Task Resources to include only Pending Configuration Tasks + memory_chunk_tasks = RestHelpers(rdmcObject=self.rdmc).filter_task_members(task_members) + + if options.force: + self.delete_existing_chunks_and_tasks(memory_chunk_tasks, memory_chunks) + else: + self.warn_existing_chunks_and_tasks(memory_chunk_tasks, memory_chunks) + + if not domain_members: + raise NoContentsFoundForOperationError("Failed to retrive Memory Domain Resources") + + # Get the user specified configID + config_data = next( + (config_id for config_id in self.config_ids if config_id.get("name").lower() == options.config.lower()), + None, + ) + + for proc in domain_members: + path = proc["MemoryChunks"].get("@odata.id") + data = self.get_post_data(config_data, proc["InterleavableMemorySets"]) + for body in data: + resp = RestHelpers(rdmcObject=self.rdmc).post_resource(path, body) + if resp is None: + raise NoChangesFoundOrMadeError("Error occured while applying configuration") + + # display warning + self.rdmc.ui.printer("Configuration changes require reboot to take effect.\n") + + # display pending configuration + self.auxcommands["showpmmpendingconfig"].show_pending_config(type("MyOptions", (object,), dict(json=False))) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ClearPendingConfigCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ClearPendingConfigCommand.py new file mode 100644 index 0000000..0635343 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ClearPendingConfigCommand.py @@ -0,0 +1,134 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Command to clear pending tasks""" +from __future__ import absolute_import + +try: + from rdmc_helper import ( + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + NoChangesFoundOrMadeError, + LOGGER, + ) + +from .lib.RestHelpers import RestHelpers + + +class ClearPendingConfigCommand: + """Command to clear pending config tasks""" + + def __init__(self): + self.ident = { + "name": "clearpmmpendingconfig", + "usage": None, + "description": "Clear pmm pending config tasks\n" "\texample: clearpmmpendingconfig", + "summary": "Clear pending config tasks", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def get_memory_chunk_tasks(self): + """ + Function to retrieve Memory Chunk Tasks + :returns: Retrieved Memory Chunk Tasks + """ + # Retrieving tasks members + tasks = RestHelpers(rdmcObject=self.rdmc).retrieve_task_members() + if tasks: + # Filtering out Memory Chunk Tasks + memory_chunk_tasks = RestHelpers(rdmcObject=self.rdmc).filter_task_members(tasks) + if memory_chunk_tasks: + return memory_chunk_tasks + self.rdmc.ui.printer("No pending configuration tasks found.\n\n") + return [] + + def delete_tasks(self, memory_chunk_tasks, verbose=False): + """ + Function to delete pending configuration tasks + :param memory_chunk_tasks: Pending confguration tasks. + :type memory_chunk_tasks: list + :param verbose: Toggles verbose mode, which print task IDs as + individual tasks are deleted. + :type verbose: Boolean + :returns: None + """ + for task in memory_chunk_tasks: + data_id = task.get("@odata.id") + task_id = task.get("Id") + resp = RestHelpers(rdmcObject=self.rdmc).delete_resource(data_id) + if resp: + if verbose: + self.rdmc.ui.printer("Deleted Task #{}".format(task_id) + "\n") + else: + raise NoChangesFoundOrMadeError("Error occured while deleting " "task #{}".format(task_id)) + return None + + def run(self, line, help_disp=False): + """ + Wrapper function for new command main function + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + LOGGER.info("Clear Pending Configuration: %s", self.ident["name"]) + # pylint: disable=unused-variable + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineError("Failed to parse options") + if args: + raise InvalidCommandLineError("Chosen flag doesn't expect additional arguments") + # Raise exception if server is in POST + if RestHelpers(rdmcObject=self.rdmc).in_post(): + raise NoContentsFoundForOperationError( + "Unable to retrieve resources - " "server might be in POST or powered off" + ) + memory_chunk_tasks = self.get_memory_chunk_tasks() + self.delete_tasks(memory_chunk_tasks, verbose=True) + + return ReturnCodes.SUCCESS + + def definearguments(self, customparser): + """ + Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/DisplaySecurityStateCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/DisplaySecurityStateCommand.py new file mode 100644 index 0000000..f3c28b4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/DisplaySecurityStateCommand.py @@ -0,0 +1,123 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- + +"""Command to Display Security State of Dimms""" + +# ---------Imports--------- +from __future__ import absolute_import + +from tabulate import tabulate + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ) + +from .lib.RestHelpers import RestHelpers + +# ---------End of imports--------- + + +class DisplaySecurityStateCommand: + """Command to Display Security State of Dimms""" + + def __init__(self): + self.ident = { + "name": "pmmsecuritystate", + "usage": None, + "description": "Displaying the Security state of dimms\n\texample: pmmsecuritystate", + "summary": "Displaying the Security state of dimms.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self._rest_helpers = None + + def display_SecurityState(self): + resp = self._rest_helpers.retrieve_pmem_location() + + pmemLocation = [] + for item in resp["Members"]: + if item["MemoryType"] == "IntelOptane": + pmemLocation.append(item["Name"]) + + securityState = [] + headers = ["Location", "State"] + for i in pmemLocation: + path = "/redfish/v1/Systems/1/Memory/" + i + resp = self._rest_helpers.retrieve_security_state(path) + state = resp["SecurityState"] + securityState.append(state) + + merged_list = tuple(zip(pmemLocation, securityState)) + print(tabulate(merged_list, headers, tablefmt="psql")) + + def run(self, line, help_disp=False): + """ + Wrapper function for new command main function + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineError("Failed to parse options") + if args: + raise InvalidCommandLineError("Chosen flag doesn't expect additional arguments") + + # Raise exception if server is in POST + self._rest_helpers = RestHelpers(rdmcObject=self.rdmc) + if self._rest_helpers.in_post(): + raise NoContentsFoundForOperationError( + "Unable to retrieve resources - " "server might be in POST or powered off" + ) + + self.display_SecurityState() + + return ReturnCodes.SUCCESS + + def definearguments(self, customparser): + """Wrapper function for smbios command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowPmemCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowPmemCommand.py new file mode 100644 index 0000000..e48cc83 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowPmemCommand.py @@ -0,0 +1,455 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Command to display information about Persistent Memory modules""" +from __future__ import absolute_import, division + +import re +from argparse import Action +from enum import Enum + +try: + from rdmc_helper import ( + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + LOGGER, + ) + +from .lib.DisplayHelpers import DisplayHelpers, OutputFormats +from .lib.Mapper import Mapper +from .lib.MapperRenderers import MappingTable +from .lib.PmemHelpers import PmemHelpers +from .lib.RestHelpers import RestHelpers + + +class _Parse_Options_List(Action): + def __init__(self, option_strings, dest, nargs, **kwargs): + super(_Parse_Options_List, self).__init__(option_strings, dest, nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_strings): + """ + Callback to parse a comma-separated list into an array. + Then store the array in the option's destination + """ + try: + setattr(namespace, self.dest, next(iter(values)).split(",")) + except: + raise InvalidCommandLineError("Values in a list must be separated by a comma.") + + +class DefaultAttributes(Enum): + """ + Enum class containing default display attributes for various flags + """ + + device = ["Location", "Capacity", "Status", "DIMMStatus", "Life", "FWVersion"] + config = ["Location", "VolatileSize", "PmemSize", "PmemInterleaved"] + summary = ["TotalCapacity", "TotalVolatileSize", "TotalPmemSize"] + logical = ["PmemSize", "DimmIds"] + + +def get_default_attributes(flag): + """ + This method returns default attributes to be displayed + :param flag: name of flag + :type flag: string + :return: list of default attributes for the passed flag + """ + if flag == "device": + return list(DefaultAttributes.device.value) + if flag == "config": + return list(DefaultAttributes.config.value) + if flag == "logical": + return list(DefaultAttributes.logical.value) + if flag == "summary": + return list(DefaultAttributes.summary.value) + return [] + + +class ShowPmemCommand: + """Command to display information about Persistent Memory modules""" + + def __init__(self): + self.ident = { + "name": "showpmm", + "usage": None, + "description": "Display information about " "Persistent Memory modules \n\texample: showpmm --device", + "summary": "Display information about Persistent Memory modules.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self._display_helpers = DisplayHelpers() + self._mapper = Mapper() + self._pmem_helpers = PmemHelpers() + + def show_pmem_modules(self, options): + """ + Command to display information about Persistent Memory modules + :param options: command options + :type options: options. + """ + # Retrieving memory collection resources + if options.logical or options.config: + memory, domain_members, all_chunks = RestHelpers(rdmcObject=self.rdmc).retrieve_mem_and_mem_domains() + if not domain_members: + raise NoContentsFoundForOperationError("Failed to retrieve Memory " "Domain resources") + else: + memory = RestHelpers(rdmcObject=self.rdmc).retrieve_memory_resources() + + if memory: + memory_members = memory.get("Members") + else: + raise NoContentsFoundForOperationError("Failed to retrieve Memory Resources") + + # Get dimm ids of all memory objects + member_dimm_ids = set() + for member in memory_members: + member_dimm_ids.add(member.get("DeviceLocator")) + + # Filtering Persistent Memory members + pmem_members, pmem_dimm_ids = self._pmem_helpers.get_pmem_members(memory_members) + if not pmem_members: + raise NoContentsFoundForOperationError("No Persistent Memory Modules found") + + parsed_dimm_ids = list() + if options.dimm: + parsed_dimm_ids = self._pmem_helpers.parse_dimm_id(options.dimm) + + for dimm_id in parsed_dimm_ids: + if dimm_id not in member_dimm_ids: + raise InvalidCommandLineError("One or more of the specified " "DIMM ID(s) are invalid") + elif dimm_id not in pmem_dimm_ids: + raise InvalidCommandLineError( + "One or more of the specified DIMM ID(s) " "are not Persistent Memory Modules" + ) + + # Creating a list of persistent memory members according to specified DIMM Ids + selected_pmem_members = list() + if parsed_dimm_ids: + for pmem_member in pmem_members: + if pmem_member.get("DeviceLocator") in parsed_dimm_ids: + selected_pmem_members.append(pmem_member) + else: + selected_pmem_members = pmem_members + + # Call 'show_pmem_module_device()' when either the user specifies '--device' flag + # or specifies no flag at all + if ( + not options.device and not options.config and not options.logical and not options.summary + ) or options.device: + self.show_pmem_module_device(selected_pmem_members, options) + + elif options.config: + self.show_pmem_module_configuration(selected_pmem_members, all_chunks, options) + + elif options.summary: + self.show_pmem_module_summary(selected_pmem_members, options) + + elif options.logical: + if not all_chunks: + self.rdmc.ui.printer("No Persistent Memory regions found\n\n") + return + self.show_persistent_interleave_sets(selected_pmem_members, all_chunks, options) + + def generate_display_output(self, members, options, flag, mapping_table, **resources): + """ + :param members: list of members returned as a result of GET request + :type members: list of dictionaries + :param options: command options + :type options: options + :param mapping_table: Mapping table to be used to extract attributes + :param flag: name of flag + :type flag: string + :return: filtered list of dictionaries to be sent to Display Helpers for output + """ + display_output_list = list() + # use default attributes for now + display_attributes = get_default_attributes(flag) + + for member in members: + temp_dict = self._mapper.get_multiple_attributes( + member, display_attributes, mapping_table, output_as_json=options.json, **resources + ) + display_output_list.append(temp_dict) + return display_output_list + + def show_pmem_module_device(self, selected_pmem_members, options): + """ + Command to display information about DIMMs when the + '--device' | '-D' flag is specified + :param selected_pmem_members: pmem members to be displayed + :type selected_pmem_members: list + :param options: command options + :type options: options + """ + # generating the data to be printed + display_output = self.generate_display_output( + selected_pmem_members, options, "device", MappingTable.device.value + ) + # Displaying output based on --json flag + if options.json: + self._display_helpers.display_data(display_output, OutputFormats.json) + else: + self._display_helpers.display_data(display_output, OutputFormats.table) + + def show_pmem_module_configuration(self, selected_pmem_members, all_chunks, options=None): + """ + Command to display information about DIMMs when the + '--pmmconfig' | '-C' flag is specified + :param selected_pmem_members: pmem members to be displayed + :type selected_pmem_members: list + :param all_chunks: list of memory chunks + :type all_chunks: list + :param options: command options + :type options: options + """ + # generating the data to be printed + display_output = self.generate_display_output( + selected_pmem_members, + options, + "config", + MappingTable.config.value, + chunks=all_chunks, + ) + # Displaying output based on --json flag + if options.json: + self._display_helpers.display_data(display_output, OutputFormats.json) + else: + self._display_helpers.display_data(display_output, OutputFormats.table) + + def show_persistent_interleave_sets(self, selected_pmem_members, all_chunks, options=None): + """ + Command to display information about the Persistent interleave + regions among the Persistent Memory Modules when the '--logical' | '-L' flag is + specified + :param selected_pmem_members: list of memory members retrieved via GET request + :type selected_pmem_members: list + :param all_chunks: list of memory chunks + :type all_chunks: list + :param options: command options + :type options: options + """ + # generating the data to be printed + display_output = self.generate_display_output( + all_chunks, + options, + "logical", + MappingTable.logical.value, + memory=selected_pmem_members, + ) + # Displaying output based on --json flag + if options.json: + self._display_helpers.display_data(display_output, OutputFormats.json) + else: + self.rdmc.ui.printer("\nInterleave Persistent Memory regions\n") + self._display_helpers.display_data(display_output, OutputFormats.table) + + def show_pmem_module_summary(self, selected_pmem_members, options=None): + """ + Command to display a summary of the current Persistent Memory + configuration when the '--summary' | '-M' flag is specified + :param selected_pmem_members: list of memory members retrieved via GET request + :type selected_pmem_members: list + :param options: command options + :type options: options + """ + # getting default attributes for --summary flag + attribute_list = get_default_attributes("summary") + # generating the data to be printed + display_output = self._mapper.get_multiple_attributes( + selected_pmem_members, + attribute_list, + MappingTable.summary.value, + options.json, + ) + # Displaying output based on --json flag + if options.json: + self._display_helpers.display_data(display_output, OutputFormats.json) + else: + self._display_helpers.print_properties([display_output]) + + def run(self, line, help_disp=False): + """ + Wrapper function for new command main function + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + LOGGER.info("PMM: %s", self.ident["name"]) + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineError("Failed to parse options") + + self.rdmc.login_select_validation(self, options) + + if args: + self.validate_args(options) + self.validate_show_pmem_options(options) + # Raise exception if server is in POST + if RestHelpers(rdmcObject=self.rdmc).in_post(): + raise NoContentsFoundForOperationError( + "Unable to retrieve resources - " "server might be in POST or powered off" + ) + self.show_pmem_modules(options) + + return ReturnCodes.SUCCESS + + @staticmethod + def validate_args(options): + """ + Produces relevant error messages when unwanted extra arguments are specified with flags + """ + some_flag = options.device or options.config or options.logical or options.summary + if options.logical or options.summary: + raise InvalidCommandLineError("Chosen flag doesn't expect additional arguments") + elif (options.device or options.config or not some_flag) and not options.dimm: + raise InvalidCommandLineError("Use the '--dimm | -I' flag to filter by DIMM IDs") + elif (options.device or options.config or not some_flag) and options.dimm: + raise InvalidCommandLineError("Values in a list must be comma-separated " "(no spaces)") + + @staticmethod + def validate_show_pmem_options(options): + """ + Validate whether options specified by user are valid + :param options: options specified by the user with the 'showpmm' command + :type options: instance of OptionParser class + """ + # Usage/Error strings + usage_multiple_flags = ( + "Only one of '--device | -D', '--pmmconfig | -C', " "'--logical | -L' or '--summary | -M' may be specified" + ) + # usage_all_display = "Only one of '--all | -a' or '--display | -d' may be specified\n" + usage_dimm_flag = ( + "'--dimm | -I' can only be specified with either the " + "'--device | -D' or '--pmmconfig | -C' flag\n" + " or without any flag" + ) + error_dimm_format = "DIMM IDs should be of the form 'ProcessorNumber@SlotNumber'" + error_dimm_range = "One or more of the specified DIMM ID(s) are invalid" + + views = [options.device, options.config, options.logical, options.summary] + + if (options.logical or options.summary) and options.dimm: + raise InvalidCommandLineError(usage_dimm_flag) + + if views.count(False) < 3: + raise InvalidCommandLineError(usage_multiple_flags) + + if options.dimm: + for dimm_id in options.dimm: + # Regex match test for expressions of type 'num1@num2' + if not re.match(r"^\d+\@\d+$", dimm_id): + raise InvalidCommandLineError(error_dimm_format) + # Accepts expressions from '1@1' to '9@19' + if not re.match(r"^[1-9]\@([1-9]|[1]\d)$", dimm_id): + raise InvalidCommandLineError(error_dimm_range) + + def definearguments(self, customparser): + """ + Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-j", + "--json", + action="store_true", + dest="json", + help="Optionally include this flag to change the output to JSON format.", + default=False, + ) + + customparser.add_argument( + "-D", + "--device", + action="store_true", + default=False, + dest="device", + help="Show information about the physical persistent memory modules." + " Default view shows information about all PMMs." + " To filter DIMMs, use this flag in conjunction with" + " the --dimm flag.", + ) + + customparser.add_argument( + "-C", + "--pmmconfig", + action="store_true", + default=False, + dest="config", + help="Show the current configuration of the persistent memory modules." + " Default view shows information about all PMMs." + " To filter DIMMs, use this flag in conjunction with" + " the --dimm flag.", + ) + + customparser.add_argument( + "-L", + "--logical", + action="store_true", + default=False, + dest="logical", + help="Show the Persistent Memory Regions.", + ) + + customparser.add_argument( + "-M", + "--summary", + action="store_true", + default=False, + dest="summary", + help="Show the summary of the persistent memory resources.", + ) + + customparser.add_argument( + "-I", + "--dimm", + type=str, + action=_Parse_Options_List, + metavar="IDLIST", + nargs=1, + dest="dimm", + help=" To view specific devices, supply a comma-separated list" + " of DIMM IDs in the format P@S (without spaces)," + " where P=processor and S=slot. Example: '1@1,1@12'", + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowPmemPendingConfigCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowPmemPendingConfigCommand.py new file mode 100644 index 0000000..a56251a --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowPmemPendingConfigCommand.py @@ -0,0 +1,170 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Command to show the pending configuration for PMM""" + +from __future__ import absolute_import + +try: + from rdmc_helper import ( + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + LOGGER, + NoContentsFoundForOperationError, + ) + +from .lib.DisplayHelpers import DisplayHelpers, OutputFormats +from .lib.Mapper import Mapper +from .lib.MapperRenderers import MappingTable +from .lib.PmemHelpers import PmemHelpers +from .lib.RestHelpers import RestHelpers + + +class ShowPmemPendingConfigCommand: + """ + Command to show the pending configuration for PMM + """ + + def __init__(self): + self.ident = { + "name": "showpmmpendingconfig", + "usage": None, + "description": "Shows the pending tasks for configuring PMM\n" "\texample: showpmmpendingconfig --json", + "summary": "Shows the pending configuration for PMM.", + "aliases": [], + "auxcommands": [], + } + self._mapper = Mapper() + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self._display_helpers = DisplayHelpers() + self._pmem_helpers = PmemHelpers() + + def definearguments(self, customparser): + """ + Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-j", + "--json", + action="store_true", + dest="json", + help="Optionally include this flag to change the output to JSON format.", + default=False, + ) + + def run(self, line, help_disp=False): + """ + Wrapper function for new command main function + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + LOGGER.info("PMM Pending Configuration: %s", self.ident["name"]) + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineError("Failed to parse options") + if args: + raise InvalidCommandLineError("Chosen command or flag doesn't expect additional arguments") + # Raise exception if server is in POST + if RestHelpers(rdmcObject=self.rdmc).in_post(): + raise NoContentsFoundForOperationError( + "Unable to retrieve resources - " "server might be in POST or powered off" + ) + self.show_pending_config(options) + + return ReturnCodes.SUCCESS + + def show_pending_config(self, options): + """ + Command to display information about pending Persistent Memory Configuration + :param options: command options + :type options: options. + """ + # retrieving task members + task_members = RestHelpers(rdmcObject=self.rdmc).retrieve_task_members() + + # filtering task members + filtered_task_members = RestHelpers(rdmcObject=self.rdmc).filter_task_members(task_members) + if not filtered_task_members: + self.rdmc.ui.printer("No pending configuration tasks found.\n\n") + return None + + # retrieving memory members + memory = RestHelpers(rdmcObject=self.rdmc).retrieve_memory_resources() + if memory: + memory_members = memory.get("Members") + else: + raise NoContentsFoundForOperationError("Failed to retrieve Memory Resources") + + attributes = ["Operation", "PmemSize", "VolatileSize", "DimmIds"] + display_output = list() + for task in filtered_task_members: + # finding operation of task + operation = self._mapper.get_single_attribute(task, "Operation", MappingTable.tasks.value, True) + # displaying existing configuration for DELETE operation + if operation.get("Operation", "") == "DELETE": + target_uri = task.get("Payload").get("TargetUri") + data = RestHelpers(rdmcObject=self.rdmc).get_resource(target_uri) + table = MappingTable.delete_task.value + else: + task_type = self._mapper.get_single_attribute(task, "Type", MappingTable.tasks.value, True) + task_type = task_type.get("Type", "") + if task_type != "PMEM": + self.rdmc.ui.warn("Unsupported interleave set type found: " + task_type) + continue + data = task + table = MappingTable.tasks.value + + task_output = self._mapper.get_multiple_attributes( + data, + attributes, + table, + output_as_json=options.json, + memory=memory_members, + ) + display_output.append(task_output) + + if options.json: + self._display_helpers.display_data(display_output, OutputFormats.json) + else: + self._display_helpers.display_data(display_output, OutputFormats.table) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowRecommendedConfigCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowRecommendedConfigCommand.py new file mode 100644 index 0000000..c634618 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/ShowRecommendedConfigCommand.py @@ -0,0 +1,193 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Command to show recommended configurations""" +from __future__ import absolute_import, division + +from collections import OrderedDict + +try: + from rdmc_helper import ( + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + LOGGER, + ) + +from .lib.DisplayHelpers import DisplayHelpers, OutputFormats +from .lib.Mapper import Mapper +from .lib.MapperRenderers import MappingTable +from .lib.PmemHelpers import PmemHelpers +from .lib.RestHelpers import RestHelpers + + +class ShowRecommendedConfigCommand: + """Show recommended configurations""" + + def __init__(self): + self.ident = { + "name": "showrecommendedpmmconfig", + "usage": None, + "description": "Show recommended configurations\n" "\texample: showrecommendedpmmconfig", + "summary": "Show Recommended Configuration", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self._display_helpers = DisplayHelpers() + self._mapper = Mapper() + self._pmem_helpers = PmemHelpers() + + def definearguments(self, customparser): + """ + Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-j", + "--json", + action="store_true", + dest="json", + help="Optionally include this flag to change the output to JSON format.", + default=False, + ) + + def run(self, line, help_disp=False): + """ + Wrapper function for new command main function + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + LOGGER.info("Show Recommended Configuration: %s", self.ident["name"]) + try: + (_, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineError("Failed to parse options") + if args: + raise InvalidCommandLineError("Chosen command doesn't expect additional arguments") + # Raise exception if server is in POST + if RestHelpers(rdmcObject=self.rdmc).in_post(): + raise NoContentsFoundForOperationError( + "Unable to retrieve resources - " "server might be in POST or powered off" + ) + self.show_recommended_config() + + return ReturnCodes.SUCCESS + + def show_recommended_config(self): + """ + Show recommended pmm configuration + """ + members = RestHelpers(rdmcObject=self.rdmc).retrieve_memory_resources().get("Members") + + if not members: + raise NoContentsFoundForOperationError("Failed to retrieve memory resources") + + # Retreving dimms + pmem_members = self._pmem_helpers.get_pmem_members(members)[0] + + if not pmem_members: + raise NoContentsFoundForOperationError("No Persistent Memory Modules found") + + # Retreving dram dimms + dram_members = self._pmem_helpers.get_non_aep_members(members)[0] + + if not dram_members: + raise NoContentsFoundForOperationError("No DRAM DIMMs found") + + # retrieving Total Capacity of PMEM dimms + attr = self._mapper.get_single_attribute(pmem_members, "TotalCapacity", MappingTable.summary.value, True) + pmem_size = attr.get("TotalCapacity", {}).get("Value", 0) + + # retrieving Total Capacity of DRAM dimms + dram_size = self._mapper.get_single_attribute(dram_members, "TotalCapacity", MappingTable.summary.value, True) + dram_size = dram_size.get("TotalCapacity", {}).get("Value", 0) + + display_output = list() + recommended_config = list() + + # Add AppDirect Mode + temp_dict = OrderedDict() + temp_dict["MemoryModeTotalSize"] = 0 + temp_dict["PmemTotalSize"] = pmem_size + temp_dict["CacheRatio"] = None + recommended_config.append(temp_dict) + + # Add Memory Mode + temp_dict = OrderedDict() + temp_dict["MemoryModeTotalSize"] = pmem_size + temp_dict["PmemTotalSize"] = 0 + temp_dict["CacheRatio"] = pmem_size / dram_size + recommended_config.append(temp_dict) + + # Add Mixed Mode (BPS doesn't support it) + if "Gen10 Plus" not in RestHelpers(rdmcObject=self.rdmc).retrieve_model(self.rdmc)["Model"]: + stepsize = 32 + appdirect_size = 0 + step = 1 + while appdirect_size < pmem_size: + appdirect_size = step * stepsize * len(pmem_members) + memorymode_size = pmem_size - appdirect_size + cache_ratio = memorymode_size / dram_size + if 2 <= cache_ratio <= 16: + temp_dict = OrderedDict() + temp_dict["MemoryModeTotalSize"] = memorymode_size + temp_dict["PmemTotalSize"] = appdirect_size + temp_dict["CacheRatio"] = cache_ratio + recommended_config.append(temp_dict) + step += 1 + + # Sorting based on MemoryModeTotalSize + recommended_config = sorted(recommended_config, key=lambda x: x["MemoryModeTotalSize"]) + + # Adding units and formating cache ratio + for output in recommended_config: + output["MemoryModeTotalSize"] = "%d GB" % output["MemoryModeTotalSize"] + output["PmemTotalSize"] = "%d GB" % output["PmemTotalSize"] + if output["CacheRatio"] is None: + output["CacheRatio"] = "N/A" + else: + output["CacheRatio"] = "1:%.1f" % output["CacheRatio"] + display_output.append(self._pmem_helpers.json_to_text(output)[0]) + + # Display output in table format + self._display_helpers.display_data(display_output, OutputFormats.table) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/AdvancedPmmConfigCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/AdvancedPmmConfigCommand.cpython-310.pyc new file mode 100644 index 0000000..bd2ea67 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/AdvancedPmmConfigCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ApplyPmemConfigCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ApplyPmemConfigCommand.cpython-310.pyc new file mode 100644 index 0000000..e3b33ae Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ApplyPmemConfigCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ClearPendingConfigCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ClearPendingConfigCommand.cpython-310.pyc new file mode 100644 index 0000000..dc44801 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ClearPendingConfigCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/DisplaySecurityStateCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/DisplaySecurityStateCommand.cpython-310.pyc new file mode 100644 index 0000000..7bbc296 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/DisplaySecurityStateCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowPmemCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowPmemCommand.cpython-310.pyc new file mode 100644 index 0000000..61ef44b Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowPmemCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowPmemPendingConfigCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowPmemPendingConfigCommand.cpython-310.pyc new file mode 100644 index 0000000..52362f6 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowPmemPendingConfigCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowRecommendedConfigCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowRecommendedConfigCommand.cpython-310.pyc new file mode 100644 index 0000000..dc6141e Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/ShowRecommendedConfigCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..46b366b Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/DisplayHelpers.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/DisplayHelpers.py new file mode 100644 index 0000000..8649a9c --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/DisplayHelpers.py @@ -0,0 +1,187 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""There are the helper functions for displaying data on the console""" +from __future__ import absolute_import + +from enum import Enum + +from tabulate import tabulate + +try: + from rdmc_helper import LOGGER, UI +except ImportError: + from ilorest.rdmc_helper import LOGGER, UI + + +class OutputFormats(Enum): + """Enum class representing output formats""" + + table = "table" + list = "list" + json = "json" + + +class DisplayHelpers(object): + """Helper functions for printing data to display on the console""" + + output_dict = dict() + + def __init__(self, width=20): + self.output_dict[OutputFormats.table] = self.print_table + self.output_dict[OutputFormats.list] = self.print_list + self.output_dict[OutputFormats.json] = self.output_to_json + self.max_width = width + self._ui = UI() + + def display_data(self, data_to_display, output_format, unique_id_property_name=None): + """ + Wrapper function for displaying data in the proper format: table, list, json, etc. + :param data_to_display: The data to display. Each object represents a row in a table + or an item in a list. + :type data_to_display: array of objects + :param output_format: Specifies which output format to use + :type output_format: enum + :param unique_id_property_name: Specifies the property which acts as the identifier + :type unique_id_property_name: string + """ + if data_to_display is None or data_to_display == []: + LOGGER.info("DCPMEM: Empty data") + return None + if output_format not in self.output_dict: + LOGGER.info("Incorrect output format") + return None + self.output_dict[output_format](data_to_display, unique_id_property_name) + return None + + def format_data(self, data, truncate=False): + """ + Function to identify header properties and convert Redfish property names to friendly names. + It also truncate strings whenever required + :param data: The data from which header has to be identified. + :type data: array of objects + :param truncate: Specifies whether truncation of strings is to be done or not + :type truncate: Boolean + :return:2 arrays. First array is an array of properties. + Second array is an array of objects. + Each object is an array containing value of properties at corresponding index. + """ + table = [] + for item in data: + row = [":".join(x.split(":")[1:]).strip() for x in item.split("\n") if len(x.split(":")) >= 2] + table.append(row) + headers = [x.split(":")[0].strip() for x in data[0].split("\n") if len(x.split(":")) == 2] + not_found = [x for x in data[0].split("\n") if len(x.split(":")) < 2] + self._ui.printer("\n".join(not_found)) + if not truncate: + return headers, table + truncated_headers = [self.truncate_lengthy(str(x), self.max_width) for x in headers] + truncated_data = [[self.truncate_lengthy(str(x), self.max_width) for x in row] for row in table] + return truncated_headers, truncated_data + + # pylint: disable=unused-argument + def print_table(self, table_data, property_id=None): + """ + This function prints data in table format + :param table_data: data to be printed in table format + :type array of objects + :param property_id: Specifies the property which acts as the identifier + :type string + """ + headers, data = self.format_data(table_data, True) + self._ui.printer("\n") + self._ui.printer(tabulate(data, headers, tablefmt="plain")) + self._ui.printer("\n\n") + return + + def print_list(self, list_data, property_id=None): + """ + This function prints data in list format + :param list_data: data to be printed in list format + :type array of objects + :param property_id: Specifies the property which acts as the identifier + :type string + """ + + headers, data = self.format_data(list_data) + flag = 0 + counter = 0 + if property_id is None or property_id not in headers: + flag = 1 + + if flag == 0: + property_id_index = headers.index(property_id) + del headers[property_id_index] + + for item in data: + if flag == 0: + self._ui.printer("--- " + property_id + ": " + str(item[property_id_index] + " ---")) + item.remove(item[property_id_index]) + else: + counter += 1 + self._ui.printer("--- " + str(counter) + " ---") + for prop in enumerate(headers): + self._ui.printer("\n" + headers[prop[0]] + ": " + str(item[prop[0]])) + self._ui.printer("\n\n") + return + + def print_properties(self, data): + """ + This function prints the data in list format without any header + :param data:data to be printed without header + :type array of string + """ + if not data: + self._ui.printer("\n") + return + headers, data = self.format_data(data) + for item in data: + for prop in enumerate(headers): + self._ui.printer("\n" + headers[prop[0]] + ": " + str(item[prop[0]])) + self._ui.printer("\n") + self._ui.printer("\n") + return + + # pylint: disable=unused-argument + def output_to_json(self, json_data, property_id=None): + """ + This function prints data in json format + :param json_data: data to be printed in json format + :type json_data: array of objects + :param property_id: Specifies the property which acts as the identifier + :type property_id: string + """ + self._ui.printer("\n") + self._ui.print_out_json(json_data) + self._ui.printer("\n") + return + + @staticmethod + def truncate_lengthy(stringin, max_length): + """Truncate lengthy strings to a maximum size + :param stringin: string to truncate + :type stringin: string + :param max_length: maximum allowed length of a string + :type max_length: int + :return: return the truncated string + :rtype: string + """ + if stringin: + if len(stringin) > max_length: + return stringin[: (max_length - 2)] + ".." + return stringin + return "" diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/Mapper.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/Mapper.py new file mode 100644 index 0000000..ddd3443 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/Mapper.py @@ -0,0 +1,96 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""There are helper functions used to get required data using mapping table""" +from __future__ import absolute_import + +from jsonpointer import resolve_pointer + + +class Mapper(object): + """Helper functions for getting data""" + + def get_multiple_attributes(self, data, attributes_list, mapping_table, output_as_json=False, **resources): + """ + Method used to get data for multiple attributes + :param data: json from which data is to be extracted + :param attributes_list: list of attributes to be extracted + :param mapping_table: table used to extract data + :param output_as_json: specifies whether the output should be in json or not + :param resources: additional resources required to compute attributes + :return: Either string or json containing extracted information for all the attributes + """ + # loop over the list of attribute names provided + if output_as_json: + output = {} + for attribute in attributes_list: + output.update( + self.get_single_attribute( + data, + attribute, + mapping_table, + output_as_json=True, + resources=resources, + ) + ) + else: + output = "" + for attribute in attributes_list: + output += "\n" + self.get_single_attribute(data, attribute, mapping_table, resources=resources) + return output + + @staticmethod + def get_single_attribute(data, attribute_name, mapping_table, output_as_json=False, resources=None): + """ + Method used to get data for single attribute + :param data: json from which data is to be extracted + :param attribute_name: attribute to be extracted + :param mapping_table: table used to extract data + :param output_as_json: specifies whether the output should be in json or not + :param resources: additional resources required to compute attributes + :return: Either string or json containing extracted information for single attribute + """ + output = {} if output_as_json else "No match found for '{}'".format(attribute_name) + # finding the attribute in the mapping table + mapping = mapping_table.get(attribute_name, None) + if mapping: + # attribute exists in mapping table + resolved_data = resolve_pointer(data, mapping["path"], None) + + if resolved_data is not None: + # attribute_name is present in data + if "compute" in mapping: + resolved_data = mapping["compute"](data=resolved_data, resources=resources) + value = resolved_data + + if output_as_json: + if "renderJSON" in mapping: + # calling the rendering function to generate the JSON output + value = mapping["renderJSON"](value) + if isinstance(value, bytes): + value = value.decode("utf-8") + output = {attribute_name: value} + else: + if "renderText" in mapping: + # calling the rendering function to generate the TEXT output + value = mapping["renderText"](value) + if isinstance(value, bytes): + value = value.decode("utf-8") + output = "{}: {}".format(attribute_name, value) + + # attribute does not exist in mapping table or value is None + return output diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/MapperRenderers.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/MapperRenderers.py new file mode 100644 index 0000000..57208b1 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/MapperRenderers.py @@ -0,0 +1,478 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""These are helper functions used to render different attributes""" +from __future__ import absolute_import, division + +from enum import Enum + +from .Mapper import Mapper +from .PmemHelpers import PmemHelpers + + +class MapperRenderers(object): + """Methods used to render different attributes""" + + @staticmethod + def format_num(num): + """ + Method to convert any number to GiB + Note: The result produced by the unit conversion is in GiB. + However, it is labeled as GB to be consistent with other products. + :param num: any number + :return: formatted number in string format + """ + return str(PmemHelpers.py3_round(num / 1024, 2)) + " GB" + + @staticmethod + def format_num_json(num): + """ + Method to convert any number to GiB in json format + Note: The result produced by the unit conversion is in GiB. + However, it is labeled as GB to be consistent with other products. + :param num: any number + :return: formatted number in json format + """ + return {"Units": "GB", "Value": PmemHelpers.py3_round(num / 1024, 2)} + + @staticmethod + def format_percentage(num): + """ + Method to append % symbol + :param num: any number + :return: formatted percentage in string format + """ + return "{}%".format(num) + + @staticmethod + def format_percentage_json(num): + """ + Method to get formatted percentage + :param num: any number + :return: formatted percentage in json format + """ + return {"Units": "%", "Value": num} + + @staticmethod + def find_pmem_interleaved(**kwargs): + """ + Method to find whether specified member is interleaved or not + :param kwargs: + data (dict): all the Memory REST data for the individual Memory to check whether + interleaved or not + resources (dict): a dict containing all required REST resources; the 'chunks' keyword + containing all MemoryChunk data is required for this method + :return: Yes if interleaved, No if not interleaved, N/A otherwise + """ + member = kwargs.get("data", {}) + chunks = kwargs.get("resources").get("chunks", []) + data_id = member.get("@odata.id", "") + for chunk in chunks: + interleave_sets = chunk.get("InterleaveSets") + if not interleave_sets: + continue + for mem_set in interleave_sets: + if PmemHelpers.compare_id(data_id, mem_set.get("Memory").get("@odata.id")): + if len(interleave_sets) > 1: + return "Yes" + return "No" + return "N/A" + + @staticmethod + def calculate_total_capacity(**kwargs): + """ + Method to calculate total capacity + :param kwargs: + data (list): all the memory members for which total capacity is to be calculated + :return: total capacity in MiB + """ + members = kwargs.get("data", []) + total = 0 + if members: + for member in members: + capacity = Mapper.get_single_attribute( + member, "Capacity", MappingTable.device.value, output_as_json=True + ) + total += capacity.get("Capacity", {}).get("Value", 0) + # returning value in MiB + return total * 1024 + + @staticmethod + def calculate_total_pmem(**kwargs): + """ + Method to calculate total pmem size + :param kwargs: + data (list): all the memory members for which total pmem size is to be calculated + :return: total pmem size in MiB + """ + members = kwargs.get("data", []) + total = 0 + if members: + for member in members: + capacity = Mapper.get_single_attribute( + member, "PmemSize", MappingTable.device.value, output_as_json=True + ) + total += capacity.get("PmemSize", {}).get("Value", 0) + # returning value in MiB + return total * 1024 + + @staticmethod + def calculate_total_volatile(**kwargs): + """ + Method to calculate total volatile size + :param kwargs: + data (list): all the members for which total volatile size is to be calculated + :return: total volatile size in MiB + """ + members = kwargs.get("data", []) + total = 0 + if members: + for member in members: + capacity = Mapper.get_single_attribute( + member, "VolatileSize", MappingTable.device.value, output_as_json=True + ) + total += capacity.get("VolatileSize", {}).get("Value", 0) + # returning value in MiB + return total * 1024 + + @staticmethod + def find_dimm_ids(**kwargs): + """ + Method to find DimmIds present in given chunk + :param kwargs: + data (dict): all the MemoryChunk REST data for the individual Memory chunk of which + dimmids is to be found + resources (dict): a dict containing all required REST resources; the 'memory' keyword + containing all memory resources is required for this method + :return: string containing dimmids + """ + member = kwargs.get("data", {}) + memory_members = kwargs.get("resources").get("memory", []) + interleave_sets = member.get("InterleaveSets", []) + locations = [] + for interleave_set in interleave_sets: + for pmem in memory_members: + if PmemHelpers.compare_id(interleave_set.get("Memory").get("@odata.id"), pmem.get("@odata.id")): + location = Mapper.get_single_attribute( + pmem, "Location", MappingTable.device.value, output_as_json=True + ) + locations.append(location.get("Location", "")) + return PmemHelpers.location_format_converter(locations)[0] + + @staticmethod + def map_operation(**kwargs): + """ + Method to map HTTP operations to user-friendly names + :param kwargs: + data (str): HTTP operation to be mapped to user-friendly name + :return: mapped operation + """ + operation = kwargs.get("data", "") + operation_mapper = {"POST": "CREATE"} + return operation_mapper.get(operation, operation) + + @staticmethod + def calculate_task_pmem_size(**kwargs): + """ + Method to calculate pmem size for a task + :param kwargs: + data (dict): all the Task REST data for the individual task of which pmem size + is to be calculated + resources (dict): a dict containing all required REST resources; the 'memory' keyword + containing all memory resources is required for this method + :return: pmem size of task in MiB + """ + size = 0 + task = kwargs.get("data", {}) + # finding memory chunk size + memory_chunk_size = Mapper.get_single_attribute( + task, "MemoryChunkSize", MappingTable.tasks.value, output_as_json=True + ) + memory_chunk_size = memory_chunk_size.get("MemoryChunkSize", {}).get("Value", None) + if memory_chunk_size is not None: + size = memory_chunk_size + else: + memory_members = kwargs.get("resources", {}).get("memory", []) + interleave_sets = task.get("Payload").get("JsonBody").get("InterleaveSets", []) + selected_members = [] + for interleave_set in interleave_sets: + for pmem in memory_members: + if PmemHelpers.compare_id( + interleave_set.get("Memory").get("@odata.id"), + pmem.get("@odata.id"), + ): + selected_members.append(pmem) + # finding total capacity + total_capacity = Mapper.get_single_attribute( + selected_members, + "TotalCapacity", + MappingTable.summary.value, + output_as_json=True, + ) + total_capacity = total_capacity.get("TotalCapacity", {}).get("Value", 0) + # finding memory chunk size percentage + memory_chunk_size_percentage = Mapper.get_single_attribute( + task, + "MemoryChunkSizePercentage", + MappingTable.tasks.value, + output_as_json=True, + ) + memory_chunk_size_percentage = memory_chunk_size_percentage.get("MemoryChunkSizePercentage", {}).get( + "Value", None + ) + if memory_chunk_size_percentage is not None: + size = total_capacity * memory_chunk_size_percentage / 100 + # returning value in MiB + return size * 1024 + + @staticmethod + def calculate_task_volatile_size(**kwargs): + """ + Method to calculate volatile size for a task + :param kwargs: + data (dict): all the Task REST data for the individual task of which volatile size + is to be calculated + resources (dict): a dict containing all required REST resources; the 'memory' keyword + containing all memory resources is required for this method + :return: volatile size of task in MiB + """ + task = kwargs.get("data", {}) + memory_members = kwargs.get("resources", {}).get("memory", []) + interleave_sets = task.get("Payload").get("JsonBody").get("InterleaveSets", []) + selected_members = [] + for interleave_set in interleave_sets: + for pmem in memory_members: + if PmemHelpers.compare_id(interleave_set.get("Memory").get("@odata.id"), pmem.get("@odata.id")): + selected_members.append(pmem) + # finding total capacity + total_capacity = Mapper.get_single_attribute( + selected_members, + "TotalCapacity", + MappingTable.summary.value, + output_as_json=True, + ) + total_capacity = total_capacity.get("TotalCapacity", {}).get("Value", 0) + volatile_size = total_capacity + # finding memory chunk size + memory_chunk_size = Mapper.get_single_attribute( + task, "MemoryChunkSize", MappingTable.tasks.value, output_as_json=True + ) + memory_chunk_size = memory_chunk_size.get("MemoryChunkSize", {}).get("Value", None) + if memory_chunk_size is not None: + size = memory_chunk_size + volatile_size = total_capacity - size + else: + # finding memory chunk size percentage + memory_chunk_size_percentage = Mapper.get_single_attribute( + task, + "MemoryChunkSizePercentage", + MappingTable.tasks.value, + output_as_json=True, + ) + memory_chunk_size_percentage = memory_chunk_size_percentage.get("MemoryChunkSizePercentage", {}).get( + "Value", None + ) + if memory_chunk_size_percentage is not None: + size = total_capacity * memory_chunk_size_percentage / 100 + volatile_size = total_capacity - size + # returning value in MiB + return volatile_size * 1024 + + @staticmethod + def calculate_chunk_volatile_size(**kwargs): + """ + Method to calculate volatile size for a chunk + :param kwargs: + data (dict): all the Task REST data for the individual chunk of which volatile size + is to be calculated + resources (dict): a dict containing all required REST resources; the 'memory' keyword + containing all memory resources is required for this method + :return: volatile size of chunk in MiB + """ + chunk = kwargs.get("data", {}) + memory_members = kwargs.get("resources", {}).get("memory", []) + interleave_sets = chunk.get("InterleaveSets", []) + selected_members = [] + for interleave_set in interleave_sets: + for pmem in memory_members: + if PmemHelpers.compare_id(interleave_set.get("Memory").get("@odata.id"), pmem.get("@odata.id")): + selected_members.append(pmem) + # finding total capacity + total_capacity = Mapper.get_single_attribute( + selected_members, + "TotalCapacity", + MappingTable.summary.value, + output_as_json=True, + ) + total_capacity = total_capacity.get("TotalCapacity", {}).get("Value", 0) + volatile_size = total_capacity + # finding memory chunk size + memory_chunk_size = Mapper.get_single_attribute( + chunk, "PmemSize", MappingTable.delete_task.value, output_as_json=True + ) + memory_chunk_size = memory_chunk_size.get("PmemSize", {}).get("Value", None) + if memory_chunk_size is not None: + volatile_size = total_capacity - memory_chunk_size + + # returning value in MiB + return volatile_size * 1024 + + +DEVICE_MAPPING_TABLE = { + "PmemSize": { + "path": "/PersistentRegionSizeLimitMiB", + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, + "Capacity": { + "path": "/CapacityMiB", + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, + "DIMMStatus": {"path": "/Oem/Hpe/DIMMStatus", "renderJSON": lambda data: data}, + "FWVersion": {"path": "/FirmwareRevision", "renderJSON": lambda data: data}, + "Life": { + "path": "/Oem/Hpe/PredictedMediaLifeLeftPercent", + "renderText": MapperRenderers.format_percentage, + "renderJSON": MapperRenderers.format_percentage_json, + }, + "Location": {"path": "/DeviceLocator", "renderJSON": lambda data: data}, + "Status": {"path": "/Status/Health", "renderJSON": lambda data: data}, + "VolatileSize": { + "path": "/VolatileRegionSizeLimitMiB", + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, +} + +CONFIG_MAPPING_TABLE = { + "PmemInterleaved": {"path": "", "compute": MapperRenderers.find_pmem_interleaved}, + "PmemSize": { + "path": "/PersistentRegionSizeLimitMiB", + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, + "Location": { + "path": "/DeviceLocator", + "renderJSON": lambda data: data.encode("ascii", "ignore"), + }, + "VolatileSize": { + "path": "/VolatileRegionSizeLimitMiB", + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, +} + +SUMMARY_MAPPING_TABLE = { + "TotalCapacity": { + "path": "", + "compute": MapperRenderers.calculate_total_capacity, + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, + "TotalPmemSize": { + "path": "", + "compute": MapperRenderers.calculate_total_pmem, + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, + "TotalVolatileSize": { + "path": "", + "compute": MapperRenderers.calculate_total_volatile, + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, +} + +LOGICAL_MAPPING_TABLE = { + "PmemSize": { + "path": "/MemoryChunkSizeMiB", + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, + "DimmIds": { + "path": "", + "compute": MapperRenderers.find_dimm_ids, + "renderJSON": lambda data: data, + }, +} + +TASK_MAPPING_TABLE = { + "DimmIds": { + "path": "/Payload/JsonBody", + "compute": MapperRenderers.find_dimm_ids, + "renderJSON": lambda data: data, + }, + "MemoryChunkSize": { + "path": "/Payload/JsonBody/MemoryChunkSizeMiB", + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, + "MemoryChunkSizePercentage": { + "path": "/Payload/JsonBody/Oem/Hpe/MemoryChunkSizePercentage", + "renderText": MapperRenderers.format_percentage, + "renderJSON": MapperRenderers.format_percentage_json, + }, + "PmemSize": { + "path": "", + "compute": MapperRenderers.calculate_task_pmem_size, + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, + "Operation": { + "path": "/Payload/HttpOperation", + "compute": MapperRenderers.map_operation, + }, + "Type": {"path": "/Payload/JsonBody/AddressRangeType"}, + "VolatileSize": { + "path": "", + "compute": MapperRenderers.calculate_task_volatile_size, + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, +} + +DELETE_TASK_MAPPING_TABLE = { + "DimmIds": { + "path": "", + "compute": MapperRenderers.find_dimm_ids, + "renderJSON": lambda data: data, + }, + "Operation": {"path": "", "compute": lambda **kwargs: "DELETE"}, + "PmemSize": { + "path": "/MemoryChunkSizeMiB", + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, + "VolatileSize": { + "path": "", + "compute": MapperRenderers.calculate_chunk_volatile_size, + "renderText": MapperRenderers.format_num, + "renderJSON": MapperRenderers.format_num_json, + }, +} + + +class MappingTable(Enum): + """Enum class representing mapping tables""" + + device = DEVICE_MAPPING_TABLE + summary = SUMMARY_MAPPING_TABLE + config = CONFIG_MAPPING_TABLE + logical = LOGICAL_MAPPING_TABLE + tasks = TASK_MAPPING_TABLE + delete_task = DELETE_TASK_MAPPING_TABLE diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/PmemHelpers.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/PmemHelpers.py new file mode 100644 index 0000000..0dee0cf --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/PmemHelpers.py @@ -0,0 +1,132 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""This module contains common helper functions used by several pmem commands""" + + +class PmemHelpers(object): + """ + Class containing common helper functions used by several pmem commands + """ + + @staticmethod + def py3_round(number, precision): + """ + Rounds numbers in accordance with the Python 3 round specification + :param number: number to be rounded + :type number: floating point number + :param precision: Number of decimal places the number should be rounded off to + :type precision: integer + :return: rounded off number + """ + if abs(round(number) - number) == 0.5: + return 2.0 * round(number / 2.0, precision) + return round(number, precision) + + @staticmethod + def parse_dimm_id(dimm_id_list): + """ + Converts DIMM IDs from the 'X@Y' format + to the 'PROC X DIMM Y' format + :param dimm_id_list: DIMM IDs in the 'X@Y' format + :type dimm_id_list: list + :return: list of DIMM IDs in the 'PROC X DIMM Y' format + """ + parsed_list = list() + for dimm_id in dimm_id_list: + temp = dimm_id.split("@") + parsed_list.append("PROC " + temp[0] + " DIMM " + temp[1]) + return parsed_list + + @staticmethod + def get_pmem_members(memory_members): + """ + Filters persistent memory members from memory resources + :param memory_members: members of memory collection resource + :type memory_members: list of members + :returns: list of persistent memory members if found else empty list + """ + base_module_type = "PMM" + pmem_members = list() + pmem_dimm_id = set() + for member in memory_members: + memory_type = member.get("Oem").get("Hpe").get("BaseModuleType") + if memory_type == base_module_type: + pmem_members.append(member) + pmem_dimm_id.add(member.get("DeviceLocator")) + return pmem_members, pmem_dimm_id + + @staticmethod + def get_non_aep_members(memory_members): + """ + Filters dram memory members from memory resources + :param memory_members: members of memory collection resource + :type memory_members: list of members + :returns: list of dram memory members if found else empty list + """ + base_module_type = "PMM" + dram_members = list() + dram_dimm_id = set() + for member in memory_members: + memory_type = member.get("Oem").get("Hpe").get("BaseModuleType") + if memory_type != base_module_type: + dram_members.append(member) + dram_dimm_id.add(member.get("DeviceLocator")) + return dram_members, dram_dimm_id + + @staticmethod + def json_to_text(dictionary): + """ + Converts json to string format + :param dictionary: json to be converted + :return: list containing the string + """ + output = "" + for key, value in dictionary.items(): + item = "\n" + key + ":" + str(value) + output += item + return [output] + + @staticmethod + def location_format_converter(location_list): + """ + Converts location format from 'PROC X DIMM Y' to 'X@Y' + :param location_list: list of locations of the format 'PROC X DIMM Y' + :type location_list: list of strings + :returns: string of locations in the 'X@Y' format (comma separated) + """ + converted_str = "" + for location in location_list: + temp = location.split(" ") + converted_str += temp[1] + "@" + temp[3] + if location is not location_list[-1]: + converted_str += ", " + return converted_str, ("PROC " + converted_str[0]) + + @staticmethod + def compare_id(id1, id2): + """ + Compares two ids + :param id1: first id to be compared + :param id2: second id to be compared + :return: True if ids are same else False + """ + if id1[-1] == "/": + id1 = id1[:-1] + if id2[-1] == "/": + id2 = id2[:-1] + return id1 == id2 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/RestHelpers.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/RestHelpers.py new file mode 100644 index 0000000..66d1ca3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/RestHelpers.py @@ -0,0 +1,268 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""This is the helper class with functions that manipulate REST data""" +from __future__ import absolute_import # check if python3 supported + +import json +import multiprocessing +from multiprocessing.dummy import Pool as ThreadPool + + +class RestHelpers(object): + """This is the helper class with functions that manipulate REST data""" + + def __init__(self, rdmcObject): + self.rdmc = rdmcObject # relies on the updated reference to the RDMC class object + + def get_resource(self, url): + """ + Perform a GET request for the specified URL + :param url: the URL of the resource to fetch + :type: string + :returns: object containing the REST responsed + :rtype: RestResponse object + """ + accepted_status = [200, 202] + resp = self.rdmc.app.get_handler(url, service=True, silent=True) + if resp and resp.status in accepted_status and resp.dict: + return resp.dict + return None + + def retrieve_memory_resources(self): + """ + Retrieve the expanded Memory Collection resource + """ + return self.get_resource("/redfish/v1/systems/1/memory?$expand=.") + + def retrieve_mem_domain_resources(self, chunks_flag=True): + """ + Retrieve Memory Domain Resources and All Chunks + """ + mem_domain_resources = self.get_resource("/redfish/v1/systems/1/MemoryDomains?$expand=.") + domain_members = [] + all_chunks = [] + if mem_domain_resources: + domain_members = mem_domain_resources.get("Members") + if not chunks_flag: + return domain_members + chunk_id_list = list() + for member in domain_members: + chunk_id_list.append(member.get("MemoryChunks").get("@odata.id") + "?$expand=.") + if chunk_id_list: + chunks = self.concurrent_get(chunk_id_list) + else: + chunks = [] + # combining all chunks + for chunk in chunks: + chunk_members = chunk.get("Members") + if chunk_members: + temp_chunks = [member for member in chunk_members] + else: + temp_chunks = [] + all_chunks.extend(temp_chunks) + return domain_members, all_chunks + + @staticmethod + def filter_task_members(task_members): + """ + Filters new memory chunk task members + :param task_members: list of task members to be filtered + :return: list of filtered task members + """ + members = list() + for member in task_members: + # retrieving only new tasks + if member.get("TaskState") == "New": + target_uri = member.get("Payload").get("TargetUri") + # checking if type Memory Chunk collection + if "MemoryChunks" in target_uri: + json_body = member.get("Payload").get("JsonBody") + if json_body: + member["Payload"]["JsonBody"] = json.loads(json_body) + members.append(member) + return members + + def retrieve_task_members(self): + """ + Retrieve task members + :returns: list of task members + """ + # retrieving task resources + task_resources = self.get_resource("/redfish/v1/TaskService/Tasks?$expand=.") + # getting task members + if task_resources: + task_members = task_resources.get("Members") + if task_members: + return task_members + return [] + + def retrieve_mem_and_mem_domains(self): + """ + Helper Function to retrieve Memory Resources and Memory Domain Resources + concurrently + :returns: memory resources, memory domain members and a list of memory chunks + """ + # 'resource_list' is a list of functions that will be called to retrieve resources. + resource_list = [ + self.retrieve_memory_resources, + self.retrieve_mem_domain_resources, + ] + # 'response_list' contains the list of responses received on calling the functions + # that are a part of 'resource_list', in the same order. + response_list = self.concurrent_retrieve(resource_list) + # Segregating individual responses from 'response_list': + # The 'get()' call is responsible for retrieving the value that a function returns + # from the 'AsyncResult' object returned by 'apply_async' in 'concurrent_retrieve'. + memory = response_list[0].get() + # domain_members and all chunks are returned as a tuple in 'response_list' as per the + # 'retrieve_mem_domain_resources()' function definition. They are unpacked here. + (domain_members, all_chunks) = response_list[1] + # Returns the values of resources obtained above. + return memory, domain_members, all_chunks + + def retrieve_task_members_and_mem_domains(self): + """ + Helper Function to retrieve Task Resources and Memory Domain Resources + concurrently + :returns: task members, memory domain members and a list of memory chunks + """ + # 'resource_list' is a list of functions that will be called to retrieve resources. + resource_list = [self.retrieve_task_members, self.retrieve_mem_domain_resources] + # 'response_list' contains the list of responses received on calling the functions + # that are a part of 'resource_list', in the same order. + response_list = self.concurrent_retrieve(resource_list) + # Segregating individual responses from 'response_list': + # The 'get()' call is responsible for retrieving the value that a function returns + # from the 'AsyncResult' object returned by 'apply_async' in 'concurrent_retrieve'. + task_members = response_list[0].get() + # domain_members and all chunks are returned as a tuple in 'response_list' as per the + # 'retrieve_mem_domain_resources()' function definition. They are unpacked here. + (domain_members, all_chunks) = response_list[1] + # Returns the values of resources obtained above. + return task_members, domain_members, all_chunks + + @staticmethod + def concurrent_retrieve(resource_list): + """ + Concurrently retrieve Resources + :param resource_list: List of resources to retrieve concurrently + :type: List of functions + :returns: List of response objects obtained as a result of GET requests + :rtype: List of objects + """ + response_list = list() + # Spawn a pool of worker threads based on the CPU core count and + # the number of functions to call. + # 'len(resource_list) - 1' ensures that the last function call happens + # on the main thread and not on a worker thread. + pool = ThreadPool(min(len(resource_list) - 1, multiprocessing.cpu_count())) + # Asynchronously call funtions from 'resource_list' on worker threads and + # append responses to 'response_list'. These responses will be 'AsyncResult' + # objects and the actual return value will have to be retrieved by a 'get()' + # call to the returned response. + for resource in resource_list[:-1]: + response_list.append(pool.apply_async(resource)) + # Call the last function in 'resource_list' on the main thread so that + # the main thread is doing some useful work and not sitting idle waiting + # for all other threads to complete. This reduces the thread communication + # overhead as well. The response returned here will not be an 'AsyncResult' + # object and no 'get()' call is required to retrieve the actual return value. + temp_response = resource_list[-1]() + # Wait for all worker threads to complete execution. + pool.close() + pool.join() + # Once all threads have finished execution, append the response recieved + # on the main thread to the 'response_list'. + response_list.append(temp_response) + # Return list of responses obtained from the main and worker threads. + return response_list + + def concurrent_get(self, uri_list): + """ + Spawns a Threadpool and sends concurrent GET Requests + :param uri_list: List of URIs on which the GET Requests + are supposed to be made + :returns: List of responses from the GET Requests + """ + # Spawn a pool of worker threads based on the CPU core count and + # the number of GET requests to be sent + pool = ThreadPool(min(multiprocessing.cpu_count(), len(uri_list))) + response_list = pool.map(self.get_resource, uri_list) + # Wait for worker threads to complete + pool.close() + pool.join() + # Return list of responses obtained via GET requests from worker threads + return response_list + + def delete_resource(self, url): + """ + Perform a delete request for the specified URL + :param url: the URL of the resource to delete + :type: string + :returns: status code + """ + resp = self.rdmc.app.delete_handler(url, service=True, silent=True) + if resp and resp.status in [200, 202]: + return resp.status + return None + + def post_resource(self, path, body): + """ + Perform a post request for the specified path with body + :param path: the URL path + :type: string + :body: the body to be sent + :type: string + :returns: status code + """ + accepted_status = [200, 201, 202, 204] + resp = self.rdmc.app.post_handler(path, body, service=True, silent=True) + if resp and resp.status in accepted_status: + return resp.status + return None + + def in_post(self): + """ + Check whether the server is in POST + :return: True if server is in Post, False otherwise + :rtype: Boolean + """ + resp_body = self.get_resource("/redfish/v1/systems/1") + if resp_body: + if resp_body.get("Oem").get("Hpe").get("PostState") == "FinishedPost": + return False + return True + + def retrieve_security_state(self, path): + """ + Get the security state + """ + return self.get_resource(path) + + def retrieve_pmem_location(self): + """ + Retrieve the Pmem location + """ + return self.get_resource("/redfish/v1/Systems/1/Memory/?$expand=.#") + + def retrieve_model(self, rdmcObj): + """ + Retrieve the Model + """ + self.rdmc = rdmcObj + return self.get_resource("/redfish/v1/Chassis/1") diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/DisplayHelpers.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/DisplayHelpers.cpython-310.pyc new file mode 100644 index 0000000..13584dd Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/DisplayHelpers.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/Mapper.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/Mapper.cpython-310.pyc new file mode 100644 index 0000000..101a7a3 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/Mapper.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/MapperRenderers.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/MapperRenderers.cpython-310.pyc new file mode 100644 index 0000000..a1e1c5e Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/MapperRenderers.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/PmemHelpers.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/PmemHelpers.cpython-310.pyc new file mode 100644 index 0000000..ab4d585 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/PmemHelpers.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/RestHelpers.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/RestHelpers.cpython-310.pyc new file mode 100644 index 0000000..1d37a6f Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/RestHelpers.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..67f0781 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/PERSISTENT_MEMORY_COMMANDS/lib/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawDeleteCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawDeleteCommand.py new file mode 100644 index 0000000..046a34a --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawDeleteCommand.py @@ -0,0 +1,219 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""RawDelete Command for rdmc""" + +import sys + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class RawDeleteCommand: + """Raw form of the delete command""" + + def __init__(self): + self.ident = { + "name": "rawdelete", + "usage": None, + "description": "Run to to delete data from" + ' the passed in path.\n\texample: rawdelete "/redfish/v1/' + 'Sessions/(session ID)"\n', + "summary": "Raw form of the DELETE command.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main raw delete worker function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + headers = {} + + if hasattr(options, "sessionid") and options.sessionid: + self.sessionvalidation(options) + else: + self.deletevalidation(options) + + if options.path.startswith('"') and options.path.endswith('"'): + options.path = options.path[1:-1] + + if options.expand: + options.path = options.path + "?$expand=." + + try: + currentsess = self.rdmc.app.current_client.session_location + except: + currentsess = None + + if options.headers: + extraheaders = options.headers.split(",") + + for item in extraheaders: + header = item.split(":") + + try: + headers[header[0]] = header[1] + except: + InvalidCommandLineError("Invalid format for --headers option.") + + if currentsess and (options.path in currentsess): + self.rdmc.app.logout() + self.rdmc.ui.printer( + "Your session has been terminated (deleted).\nPlease log " "back in if you wish to continue.\n" + ) + else: + returnresponse = False + + if options.response or options.getheaders: + returnresponse = True + + results = self.rdmc.app.delete_handler( + options.path, + headers=headers, + silent=options.silent, + service=options.service, + ) + + if returnresponse and results: + if options.getheaders: + self.rdmc.ui.print_out_json(dict(results.getheaders())) + + if options.response: + sys.stdout.write(results.read) + self.rdmc.ui.printer("\n") + elif results.status == 404: + return ReturnCodes.NO_CONTENTS_FOUND_FOR_OPERATION + elif results.status != 200: + return ReturnCodes.UI_CLI_USAGE_EXCEPTION + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def deletevalidation(self, options): + """Raw delete validation function + + :param options: command line options + :type options: list. + """ + self.rdmc.login_select_validation(self, options, skipbuild=True) + + def sessionvalidation(self, options): + """Raw delete session validation function + + :param options: command line options + :type options: list. + """ + + url = None + if options.user or options.password or options.url: + if options.url: + url = options.url + else: + if self.rdmc.app.config.get_url(): + url = self.rdmc.app.config.get_url() + if url and "https://" not in url: + url = "https://" + url + + return url + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "path", + help="Uri on iLO to be deleted.", + ) + customparser.add_argument( + "--response", + dest="response", + action="store_true", + help="Use this flag to return the iLO response body.", + default=False, + ) + customparser.add_argument( + "--getheaders", + dest="getheaders", + action="store_true", + help="Use this flag to return the iLO response headers.", + default=False, + ) + customparser.add_argument( + "--headers", + dest="headers", + help="Use this flag to add extra headers to the request." + "\t\t\t\t\t Usage: --headers=HEADER:VALUE,HEADER:VALUE", + default=None, + ) + customparser.add_argument( + "--silent", + dest="silent", + action="store_true", + help="""Use this flag to silence responses""", + default=False, + ) + customparser.add_argument( + "--service", + dest="service", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) + customparser.add_argument( + "--expand", + dest="expand", + action="store_true", + help="""Use this flag to expand the path specified using the """ """expand notation '?$expand=.'""", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawGetCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawGetCommand.py new file mode 100644 index 0000000..c704d75 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawGetCommand.py @@ -0,0 +1,322 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""RawGet Command for rdmc""" + +import json + +import redfish +import requests +from urllib.parse import urljoin +import sys + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ResourceNotReadyError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ResourceNotReadyError, + ReturnCodes, + ) + + +class RawGetCommand: + """Raw form of the get command""" + + def __init__(self): + self.ident = { + "name": "rawget", + "usage": None, + "description": "Run to to retrieve data from " + 'the passed in path.\n\tExample: rawget "/redfish/v1/' + 'systems/(system ID)"', + "summary": "Raw form of the GET command.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main raw get worker function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + headers = {} + + if getattr(options, "no_auth"): + pass + else: + if hasattr(options, "sessionid") and options.sessionid: + _ = self.sessionvalidation(options) + else: + self.getvalidation(options) + + if options.path.endswith("?=."): + path = options.path + strip = path[:-3] + options.path = strip + "?$expand=." + + if options.path.startswith('"') and options.path.endswith('"'): + options.path = options.path[1:-1] + + if options.expand: + options.path = options.path + "?$expand=." + + if options.headers: + extraheaders = options.headers.split(",") + for item in extraheaders: + header = item.split(":") + + try: + headers[header[0]] = header[1] + except: + InvalidCommandLineError("Invalid format for --headers " "option.") + + returnresponse = False + if options.response or options.getheaders: + returnresponse = True + + extra_path = None + if "#" in options.path: + path_list = options.path.split("#") + options.path = path_list[0] + extra_path = path_list[1] + if "/" in extra_path: + extra_path_list = extra_path.split("/") + extra_path_list = list(filter(None, extra_path_list)) + if getattr(options, "no_auth"): + if options.url: + url = "https://" + options.url.rstrip("/") + else: + url = "https://16.1.15.1" + path = urljoin(url, options.path.lstrip("/")) + try: + response = requests.get(path, verify=False) + if response.status_code == 200: + data = response.json() + self.rdmc.ui.printer("%s\n" % json.dumps(data, indent=4)) + return ReturnCodes.SUCCESS + except Exception as e: + sys.stdout.write("Error: Failed to complete operation.\n") + return ReturnCodes.INVALID_COMMAND_LINE_ERROR + + results = self.rdmc.app.get_handler( + options.path, + sessionid=options.sessionid, + headers=headers, + silent=options.silent, + service=options.service, + username=options.user, + password=options.password, + base_url=options.url, + noauth=options.no_auth, + stderr_flag=options.stderr_flag, + ) + result = None + if results.dict: + if extra_path: + result = results.dict + for p in extra_path_list: + if p.isdigit(): + p = int(p) + result = result[p] + else: + result = results.dict + + if results and results.status == 200 and options.binfile: + output = results.read + filehndl = open(options.binfile[0], "wb") + filehndl.write(output) + filehndl.close() + elif results and returnresponse: + if options.getheaders: + self.rdmc.ui.printer(json.dumps(dict(results.getheaders())) + "\n") + if options.response: + self.rdmc.ui.printer(results.read + "\n") + elif results and results.status == 200: + if results.dict: + if options.filename: + output = json.dumps( + result, + indent=2, + cls=redfish.ris.JSONEncoder, + sort_keys=True, + ) + + filehndl = open(options.filename[0], "w") + filehndl.write(output) + filehndl.close() + + self.rdmc.ui.printer("Results written out to '%s'.\n" % options.filename[0]) + else: + if not result: + result = results.dict + if options.service: + self.rdmc.ui.printer("%s\n" % result) + else: + self.rdmc.ui.print_out_json(result) + else: + json_payload = json.loads(results._http_response.data) + try: + message_id = json_payload["error"]["@Message.ExtendedInfo"][0]["MessageId"] + self.rdmc.ui.error("%s" % message_id) + if "ResourceNotReadyRetry" in message_id: + raise ResourceNotReadyError("Resources are not ready in iLO, Please wait for some time and retry") + except: + self.rdmc.ui.error("An invalid or incomplete response was received: %s\n" % json_payload) + return ReturnCodes.NO_CONTENTS_FOUND_FOR_OPERATION + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def getvalidation(self, options): + """Raw get validation function + + :param options: command line options + :type options: list. + """ + self.rdmc.login_select_validation(self, options, skipbuild=True) + + def sessionvalidation(self, options): + """Raw patch session validation function + + :param options: command line options + :type options: list. + """ + + url = None + if options.user or options.password or options.url: + if options.url: + url = options.url + else: + if self.rdmc.app.redfishinst and self.rdmc.app.redfishinst.base_url: + url = self.rdmc.app.redfishinst.base_url + if url and "blobstore://" not in url and "https://" not in url: + url = "https://" + url + + return url + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "path", + help="Uri on iLO", + ) + customparser.add_argument( + "--response", + dest="response", + action="store_true", + help="Use this flag to return the iLO response body.", + default=False, + ) + customparser.add_argument( + "--getheaders", + dest="getheaders", + action="store_true", + help="Use this flag to return the iLO response headers.", + default=False, + ) + customparser.add_argument( + "--headers", + dest="headers", + help="Use this flag to add extra headers to the request." " example: --headers=HEADER:VALUE,HEADER:VALUE", + default=None, + ) + customparser.add_argument( + "--silent", + dest="silent", + action="store_true", + help="""Use this flag to silence responses""", + default=False, + ) + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="""Write results to the specified file.""", + action="append", + default=None, + ) + customparser.add_argument( + "-b", + "--writebin", + dest="binfile", + help="""Write the results to the specified file in binary.""", + action="append", + default=None, + ) + customparser.add_argument( + "--service", + dest="service", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) + customparser.add_argument( + "--expand", + dest="expand", + action="store_true", + help="""Use this flag to expand the path specified using the """ """expand notation '?$expand=.'""", + default=False, + ) + customparser.add_argument( + "--no_auth", + dest="no_auth", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) + customparser.add_argument( + "--stderr_out", + "--stderr_flag", + dest="stderr_flag", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawHeadCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawHeadCommand.py new file mode 100644 index 0000000..b534781 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawHeadCommand.py @@ -0,0 +1,148 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""RawHead Command for rdmc""" + +import json + +import redfish + +try: + from rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes +except ImportError: + from ilorest.rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes + + +class RawHeadCommand: + """Raw form of the head command""" + + def __init__(self): + self.ident = { + "name": "rawhead", + "usage": None, + "description": "Run to to retrieve data from the " + 'passed in path\n\texample: rawhead "/redfish/v1/systems/' + '(system ID)"\n', + "summary": "Raw form of the HEAD command.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main raw head worker function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.headvalidation(options) + + if options.path.startswith('"') and options.path.endswith('"'): + options.path = options.path[1:-1] + + results = self.rdmc.app.head_handler(options.path, silent=options.silent, service=options.service) + + content = None + tempdict = dict() + + if results and results.status == 200: + if results._http_response: + content = results.getheaders() + else: + content = results._headers + + tempdict = dict(content) + + if options.filename: + output = json.dumps(tempdict, indent=2, cls=redfish.ris.JSONEncoder, sort_keys=True) + filehndl = open(options.filename[0], "w") + filehndl.write(output) + filehndl.close() + + self.rdmc.ui.printer("Results written out to '%s'.\n" % options.filename[0]) + else: + if options.service: + self.rdmc.ui.printer("%s\n" % tempdict) + else: + self.rdmc.ui.print_out_json(tempdict) + else: + return ReturnCodes.NO_CONTENTS_FOUND_FOR_OPERATION + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def headvalidation(self, options): + """Raw head validation function + + :param options: command line options + :type options: list. + """ + self.rdmc.login_select_validation(self, options, skipbuild=True) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "path", + help="Uri on iLO to query HEAD.", + ) + customparser.add_argument( + "--silent", + dest="silent", + action="store_true", + help="""Use this flag to silence responses""", + default=None, + ) + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="""Use the provided filename to perform operations.""", + action="append", + default=None, + ) + customparser.add_argument( + "--service", + dest="service", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawPatchCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawPatchCommand.py new file mode 100644 index 0000000..1e23ff0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawPatchCommand.py @@ -0,0 +1,264 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""RawPatch Command for rdmc""" + +import json +import re +import sys +from collections import OrderedDict +from urllib.parse import urljoin + +import requests + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + ReturnCodes, + ) + + +class RawPatchCommand: + """Raw form of the patch command""" + + def __init__(self): + self.ident = { + "name": "rawpatch", + "usage": None, + "description": "Run to send a patch from the data" + " in the input file.\n\tMultiple PATCHes can be performed in sequence by " + "\n\tadding more path/body key/value pairs.\n" + "\n\texample: rawpatch rawpatch.txt" + '\n\n\tExample input file:\n\t{\n\t "/redfish/' + 'v1/systems/(system ID)":\n\t {\n\t ' + '"AssetTag": "NewAssetTag"\n\t }\n\t}', + "summary": "Raw form of the PATCH command.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main raw patch worker function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + headers = {} + results = [] + if getattr(options, "no_auth"): + pass + else: + if hasattr(options, "sessionid") and options.sessionid: + _ = self.sessionvalidation(options) + else: + self.patchvalidation(options) + + contentsholder = None + try: + with open(options.path, "r") as _if: + contentsholder = json.loads(_if.read(), object_pairs_hook=OrderedDict) + except IOError: + raise InvalidFileInputError( + "File '%s' doesn't exist. " "Please create file by running 'save' command." % options.path + ) + except ValueError: + raise InvalidFileFormattingError("Input file '%s' was not " "formatted properly." % options.path) + + if options.headers: + extraheaders = options.headers.split(",") + + for item in extraheaders: + header = item.split(":") + + try: + headers[header[0]] = header[1] + except: + raise InvalidCommandLineError("Invalid format for --headers option.") + if getattr(options, "no_auth"): + if options.url: + url = "https://" + options.url.rstrip("/") + else: + url = "https://16.1.15.1" + path = urljoin(url, contentsholder["path"].lstrip("/")) + body = contentsholder["body"] + try: + response = requests.patch(path, json=body, verify=False) + if response.status_code == 200: + sys.stdout.write(response.content.decode("utf-8") + "\n") + return ReturnCodes.SUCCESS + except Exception as e: + sys.stdout.write("Error: Failed to complete operation.\n") + return ReturnCodes.INVALID_COMMAND_LINE_ERROR + + if "path" in contentsholder and "body" in contentsholder: + results.append( + self.rdmc.app.patch_handler( + contentsholder["path"], + contentsholder["body"], + headers=headers, + silent=options.silent, + optionalpassword=options.biospassword, + service=options.service, + noauth=options.no_auth, + ) + ) + + elif all([re.match(r"^/(\S+/?)+$", key) for key in contentsholder]): + for path, body in contentsholder.items(): + results.append( + self.rdmc.app.patch_handler( + path, + body, + headers=headers, + silent=options.silent, + optionalpassword=options.biospassword, + service=options.service, + noauth=options.no_auth, + ) + ) + else: + raise InvalidFileFormattingError("Input file '%s' was not format properly." % options.path) + + returnresponse = False + + if options.response or options.getheaders: + returnresponse = True + + if results and returnresponse: + for result in results: + if options.getheaders: + self.rdmc.ui.print_out_json(dict(result.getheaders())) + + if options.response: + self.rdmc.ui.printer(result.read) + self.rdmc.ui.printer("\n") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def patchvalidation(self, options): + """Raw patch validation function + + :param options: command line options + :type options: list. + """ + self.rdmc.login_select_validation(self, options, skipbuild=True) + + def sessionvalidation(self, options): + """Raw patch session validation function + + :param options: command line options + :type options: list. + """ + + url = None + if options.user or options.password or options.url: + if options.url: + url = options.url + else: + if self.rdmc.app.redfishinst.base_url: + url = self.rdmc.app.redfishinst.base_url + if url and "https://" not in url: + url = "https://" + url + + return url + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "path", + help="Path to the JSON file containing the data to be patched.", + ) + customparser.add_argument( + "--silent", + dest="silent", + action="store_true", + help="""Use this flag to silence responses""", + default=False, + ) + customparser.add_argument( + "--response", + dest="response", + action="store_true", + help="Use this flag to return the iLO response body.", + default=False, + ) + customparser.add_argument( + "--getheaders", + dest="getheaders", + action="store_true", + help="Use this flag to return the iLO response headers.", + default=False, + ) + customparser.add_argument( + "--headers", + dest="headers", + help="Use this flag to add extra headers to the request." + "\t\t\t\t\t Usage: --headers=HEADER:VALUE,HEADER:VALUE", + default=None, + ) + customparser.add_argument( + "--service", + dest="service", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) + customparser.add_argument( + "--no_auth", + dest="no_auth", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawPostCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawPostCommand.py new file mode 100644 index 0000000..730ae63 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawPostCommand.py @@ -0,0 +1,276 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""RawPost Command for rdmc""" + +import json +import re +import sys +from collections import OrderedDict +import requests +from urllib.parse import urljoin + +try: + from rdmc_helper import ( + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + ReturnCodes, + ) + + +class RawPostCommand: + """Raw form of the post command""" + + def __init__(self): + self.ident = { + "name": "rawpost", + "usage": None, + "description": "Run to send a post from " + "the data in the input file.\n\tMultiple POSTs can be performed in sequence by" + " \n\tadding more path/body key/value pairs.\n" + "\n\texample: rawpost rawpost." + 'txt\n\n\tExample input file:\n\t{\n\t "/' + "redfish/v1/systems/(system ID)/Actions/ComputerSystem." + 'Reset":\n\t {\n\t "ResetType": ' + '"ForceRestart"\n\t }\n\t}', + "summary": "Raw form of the POST command.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main raw patch worker function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + headers = {} + results = [] + if getattr(options, "no_auth"): + pass + else: + if hasattr(options, "sessionid") and options.sessionid: + self.sessionvalidation(options) + else: + self.postvalidation(options) + + contentsholder = None + + try: + with open(options.path, "r") as _if: + contentsholder = json.loads(_if.read(), object_pairs_hook=OrderedDict) + except IOError: + raise InvalidFileInputError( + "File '%s' doesn't exist. " "Please create file by running 'save' command." % options.path + ) + except ValueError: + raise InvalidFileFormattingError("Input file '%s' was not " "formatted properly." % options.path) + if options.encode: + if ( + "body" in contentsholder + and "UserName" in contentsholder["body"] + and "Password" in contentsholder["body"] + and len(list(contentsholder["body"].keys())) == 2 + ): + encobj = Encryption() + contentsholder["body"]["UserName"] = encobj.decode_credentials(contentsholder["body"]["UserName"]) + contentsholder["body"]["Password"] = encobj.decode_credentials(contentsholder["body"]["Password"]) + + if options.headers: + extraheaders = options.headers.split(",") + for item in extraheaders: + header = item.split(":") + + try: + headers[header[0]] = header[1] + except: + raise InvalidCommandLineError("Invalid format for --headers option.") + + if getattr(options, "no_auth"): + if options.url: + url = "https://" + options.url.rstrip("/") + else: + url = "https://16.1.15.1" + path = urljoin(url, contentsholder["path"].lstrip("/")) + body = contentsholder["body"] + try: + response = requests.post(path, json=body, verify=False) + if response.status_code == 200: + sys.stdout.write(response.content.decode("utf-8") + "\n") + # sys.stdout.write("Operation completed successfully.\n") + return ReturnCodes.SUCCESS + except Exception as e: + sys.stdout.write("Error: Failed to complete operation.\n") + return ReturnCodes.INVALID_COMMAND_LINE_ERROR + + if "path" in contentsholder and "body" in contentsholder: + results.append( + self.rdmc.app.post_handler( + contentsholder["path"], + contentsholder["body"], + headers=headers, + silent=options.silent, + service=options.service, + noauth=options.no_auth, + ) + ) + elif all([re.match(r"^/(\S+/?)+$", key) for key in contentsholder]): + for path, body in contentsholder.items(): + results.append( + self.rdmc.app.post_handler( + path, + body, + headers=headers, + silent=options.silent, + service=options.service, + noauth=options.no_auth, + ) + ) + else: + raise InvalidFileFormattingError("Input file '%s' was not " "formatted properly." % options.path) + returnresponse = False + + if options.response or options.getheaders: + returnresponse = True + + if results and returnresponse: + for result in results: + if options.getheaders: + self.rdmc.ui.print_out_json(dict(result.getheaders())) + + if options.response: + if isinstance(result.ori, bytes): + self.rdmc.ui.printer(result.ori.decode("utf-8") + "\n") + else: + self.rdmc.ui.printer(result.ori + "\n") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def postvalidation(self, options): + """Raw post validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options, skipbuild=True) + + def sessionvalidation(self, options): + """Raw post session validation function + + :param options: command line options + :type options: list. + """ + + url = None + if options.user or options.password or options.url: + if options.url: + url = options.url + else: + if self.rdmc.app.redfishinst.base_url: + url = self.rdmc.app.redfishinst.base_url + if url and "https://" not in url: + url = "https://" + url + + return url + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "path", + help="Path to the JSON file containing the data to be patched.", + ) + customparser.add_argument( + "--response", + dest="response", + action="store_true", + help="Use this flag to return the iLO response body.", + default=False, + ) + customparser.add_argument( + "--getheaders", + dest="getheaders", + action="store_true", + help="Use this flag to return the iLO response headers.", + default=False, + ) + customparser.add_argument( + "--headers", + dest="headers", + help="Use this flag to add extra headers to the request." + "\t\t\t\t\t Usage: --headers=HEADER:VALUE,HEADER:VALUE", + default=None, + ) + customparser.add_argument( + "--silent", + dest="silent", + action="store_true", + help="""Use this flag to silence responses""", + default=False, + ) + customparser.add_argument( + "--service", + dest="service", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) + customparser.add_argument( + "--no_auth", + dest="no_auth", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawPutCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawPutCommand.py new file mode 100644 index 0000000..9e18f28 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/RawPutCommand.py @@ -0,0 +1,268 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""RawPut Command for rdmc""" + +import json +import re +import sys +from collections import OrderedDict +from urllib.parse import urljoin + +import requests + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + ReturnCodes, + ) + + +class RawPutCommand: + """Raw form of the put command""" + + def __init__(self): + self.ident = { + "name": "rawput", + "usage": None, + "description": "Run to send a post from " + "the data in the input file.\n\tMultiple PUTs can be performed in sequence by " + "\n\tadding more path/body key/value pairs.\n" + "\n\texample: rawput rawput." + 'txt\n\n\tExample input file:\n\t{\n\t "/redfish/' + 'v1/systems/(system ID)/bios/Settings/":\n\t {\n\t' + '\t"Attributes": {\n\t\t' + ' "BaseConfig": "default"\n\t\t}\n\t }\n\t}', + "summary": "Raw form of the PUT command.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main raw put worker function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + headers = {} + results = [] + + if hasattr(options, "sessionid") and options.sessionid: + self.sessionvalidation(options) + else: + self.putvalidation(options) + + contentsholder = None + + try: + with open(options.path, "r") as _if: + contentsholder = json.loads(_if.read(), object_pairs_hook=OrderedDict) + except IOError: + raise InvalidFileInputError( + "File '%s' doesn't exist. " "Please create file by running 'save' command." % options.path + ) + except ValueError: + raise InvalidFileFormattingError("Input file '%s' was not " "formatted properly." % options.path) + + if options.headers: + extraheaders = options.headers.split(",") + + for item in extraheaders: + header = item.split(":") + + try: + headers[header[0]] = header[1] + except: + raise InvalidCommandLineError("Invalid format for --headers option.") + + if getattr(options, "no_auth"): + if options.url: + url = "https://" + options.url.rstrip("/") + else: + url = "https://16.1.15.1" + path = urljoin(url, contentsholder["path"].lstrip("/")) + body = contentsholder["body"] + try: + response = requests.put(path, json=body, verify=False) + if response.status_code == 200: + sys.stdout.write(response.content.decode("utf-8") + "\n") + # sys.stdout.write("Operation completed successfully.\n") + return ReturnCodes.SUCCESS + except Exception as e: + sys.stdout.write("Error: Failed to complete operation.\n") + return ReturnCodes.INVALID_COMMAND_LINE_ERROR + + if "path" in contentsholder and "body" in contentsholder: + results.append( + self.rdmc.app.put_handler( + contentsholder["path"], + contentsholder["body"], + headers=headers, + silent=options.silent, + optionalpassword=options.biospassword, + service=options.service, + noauth=options.no_auth, + ) + ) + elif all([re.match(r"^/(\S+/?)+$", key) for key in contentsholder]): + for path, body in contentsholder.items(): + results.append( + self.rdmc.app.put_handler( + path, + body, + headers=headers, + silent=options.silent, + optionalpassword=options.biospassword, + service=options.service, + noauth=options.no_auth, + ) + ) + else: + raise InvalidFileFormattingError("Input file '%s' was not " "formatted properly." % options.path) + + returnresponse = False + + if options.response or options.getheaders: + returnresponse = True + + if results and returnresponse: + for result in results: + if options.getheaders: + sys.stdout.write(json.dumps(dict(result.getheaders())) + "\n") + + if options.response: + if isinstance(result.read, bytes): + sys.stdout.write(result.read.decode("utf-8")) + else: + sys.stdout.write(result.read) + sys.stdout.write("\n") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def putvalidation(self, options): + """Raw put validation function + + :param options: command line options + :type options: list. + """ + self.rdmc.login_select_validation(self, options, skipbuild=True) + + def sessionvalidation(self, options): + """Raw put session validation function + + :param options: command line options + :type options: list. + """ + + url = None + if options.user or options.password or options.url: + if options.url: + url = options.url + else: + if getattr(self.rdmc.app.redfishinst, "base_url", False): + url = self.rdmc.app.redfishinst.base_url + if url and "https://" not in url: + url = "https://" + url + + return url + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "path", + help="Path to the JSON file containing the data to be patched.", + ) + customparser.add_argument( + "--response", + dest="response", + action="store_true", + help="Use this flag to return the iLO response body.", + default=False, + ) + customparser.add_argument( + "--getheaders", + dest="getheaders", + action="store_true", + help="Use this flag to return the iLO response headers.", + default=False, + ) + customparser.add_argument( + "--headers", + dest="headers", + help="Use this flag to add extra headers to the request." + "\t\t\t\t\t Usage: --headers=HEADER:VALUE,HEADER:VALUE", + default=None, + ) + customparser.add_argument( + "--silent", + dest="silent", + action="store_true", + help="""Use this flag to silence responses""", + default=False, + ) + customparser.add_argument( + "--service", + dest="service", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) + customparser.add_argument( + "--no_auth", + dest="no_auth", + action="store_true", + help="""Use this flag to enable service mode and increase the function speed""", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawDeleteCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawDeleteCommand.cpython-310.pyc new file mode 100644 index 0000000..139c6df Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawDeleteCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawGetCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawGetCommand.cpython-310.pyc new file mode 100644 index 0000000..7018aa8 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawGetCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawHeadCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawHeadCommand.cpython-310.pyc new file mode 100644 index 0000000..a2d5e90 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawHeadCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawPatchCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawPatchCommand.cpython-310.pyc new file mode 100644 index 0000000..01a227c Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawPatchCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawPostCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawPostCommand.cpython-310.pyc new file mode 100644 index 0000000..825dfa6 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawPostCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawPutCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawPutCommand.cpython-310.pyc new file mode 100644 index 0000000..7ad912c Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/RawPutCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..adbb064 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/RAW_COMMANDS/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/ClearControllerConfigCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/ClearControllerConfigCommand.py new file mode 100644 index 0000000..b6284a2 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/ClearControllerConfigCommand.py @@ -0,0 +1,166 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Clear Controller Configuration Command for rdmc""" + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class ClearControllerConfigCommand: + """Drive erase/sanitize command""" + + def __init__(self): + self.ident = { + "name": "clearcontrollerconfig", + "usage": None, + "description": "To clear a controller" + " config.\n\tExample: clearcontrollerconfig --controller=1" + '\n\texample: clearcontrollerconfig --controller="Slot0"', + "summary": "Clears smart array controller configuration.", + "aliases": [], + "auxcommands": ["SelectCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main disk inventory worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.clearcontrollerconfigvalidation(options) + + ilo_ver = self.rdmc.app.getiloversion() + if ilo_ver >= 6.110: + self.rdmc.ui.printer( + "ClearController option is not supported for this device." + "\nUse factoryresetcontroller command to reset the controller," + "\tthe action does erase or sanitize data on the drives.\n" + ) + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + else: + self.auxcommands["select"].selectfunction("SmartStorageConfig.") + content = self.rdmc.app.getprops() + + if not options.controller: + raise InvalidCommandLineError("You must include a controller to select.") + if options.controller: + controllist = [] + contentsholder = { + "LogicalDrives": [], + "Actions": [{"Action": "ClearConfigurationMetadata"}], + "DataGuard": "Disabled", + } + try: + if options.controller.isdigit(): + slotlocation = self.get_location_from_id(options.controller) + if slotlocation: + slotcontrol = slotlocation.lower().strip('"').split("slot")[-1].lstrip() + for control in content: + if slotcontrol.lower() == control["Location"].lower().split("slot")[-1].lstrip(): + controllist.append(control) + elif "slot" in options.controller.lower(): + controllerid = options.controller.strip("Slot ") + slotlocation = self.get_location_from_id(controllerid) + if slotlocation: + slotcontrol = slotlocation.lower().strip('"').split("slot")[-1].lstrip() + for control in content: + if slotcontrol.lower() == control["Location"].lower().split("slot")[-1].lstrip(): + controllist.append(control) + if not controllist: + raise InvalidCommandLineError("") + except InvalidCommandLineError: + raise InvalidCommandLineError("Selected controller not found in the current " "inventory list.") + for controller in controllist: + # self.rdmc.ui.printer( + # "ClearController path and payload: %s, %s\n" + # % (controller["@odata.id"], contentsholder) + # ) + self.rdmc.app.put_handler(controller["@odata.id"], contentsholder) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def get_storage_location_from_id(self, storage_id): + for sel in self.rdmc.app.select("StorageController", path_refresh=True): + if "Collection" not in sel.maj_type: + controller_storage = sel.dict + if controller_storage["Id"] == str(storage_id): + return controller_storage["Location"]["PartLocation"]["ServiceLabel"] + return None + + def get_location_from_id(self, controller_id): + for sel in self.rdmc.app.select("SmartStorageArrayController", path_refresh=True): + if "Collection" not in sel.maj_type: + controller = sel.dict + if controller["Id"] == str(controller_id): + return controller["Location"] + return None + + def clearcontrollerconfigvalidation(self, options): + """clear controller config validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller " "using either the slot number or index.", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/CreateVolumeCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/CreateVolumeCommand.py new file mode 100644 index 0000000..cfb18f3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/CreateVolumeCommand.py @@ -0,0 +1,1167 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Create Volume Command for rdmc""" + +from argparse import RawDescriptionHelpFormatter + +from redfish.ris import IdTokenError +from redfish.ris.rmc_helper import IloResponseError, InstanceNotFoundError + +try: + from rdmc_helper import ( + IloLicenseError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidSmartArrayConfigurationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IloLicenseError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidSmartArrayConfigurationError, + ReturnCodes, + ) + + +class CreateVolumeCommand: + """Create volume command""" + + def __init__(self): + self.ident = { + "name": "createvolume", + "usage": None, + "description": "Creates volumes on compatible HPE SSA RAID controllers\nTo view " + "help on specific sub-commands run: createvolume -h\n\n" + "NOTE: Refer http://www.hpe.com/info/scmo for additional information " + "on creating Volumes on Gen11 servers.\n\t" + "Also, when you select multiple physicaldrives you can select by both\n\t" + "physical drive name and by the location at the same time.\n\t" + "You can also select controllers by slot number as well as index.\n\t" + "For iLO6, storage id need to be specified using --storageid=DE00E000 " + "along with --controller=1", + "summary": "Creates a new volume on the selected controller.", + "aliases": ["createlogicaldrive"], + "auxcommands": ["SelectCommand", "StorageControllerCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main disk inventory worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.createvolumevalidation(options) + ilo_ver = self.rdmc.app.getiloversion() + if ilo_ver < 6.110: + try: + self.auxcommands["select"].selectfunction("StorageController.") + content = self.rdmc.app.getprops() + if options.command == "quickdrive": + options.sparedrives = None + raise InvalidCommandLineError( + "This controller is not compatible with quickdrive option. " + "Please use customdrive or volume option\n" + ) + ilo_ver = 6.110 + options.command = "volume" + except Exception: + pass + + if ilo_ver >= 6.110: + if options.command == "customdrive" or options.command == "quickdrive": + raise InvalidCommandLineError("customdrive or quickdrive subcommand is not supported on iLO6(Gen11).\n") + if not options.storageid: + raise InvalidCommandLineError("--storageid option is mandatory for iLO6 or latest iLO5 onwards.\n") + else: + if options.command == "volume": + raise InvalidCommandLineError("volume subcommand is not supported on iLO5(Gen10).\n") + if options.controller: + if ilo_ver >= 6.110: + if options.storageid: + controllers = self.auxcommands["storagecontroller"].storagecontroller(options, single_use=True) + else: + raise InvalidCommandLineError("--storageid option is mandatory for iLO6 onwards.\n") + else: + try: + controllers = self.auxcommands["storagecontroller"].controllers(options, single_use=True) + if len(controllers) == 0: + if not options.storageid: + raise InvalidCommandLineError( + "--storageid option is mandatory. Please input storageid as well so that " + "controllers/volumes can be identified.\n" + ) + controllers = self.auxcommands["storagecontroller"].storagecontroller(options, single_use=True) + options.command = "volume" + ilo_ver = 6.110 + except InstanceNotFoundError: + if not options.storageid: + raise InvalidCommandLineError( + "--storageid option is mandatory. Please input storageid as well so that " + "controllers/volumes can be identified.\n" + ) + controllers = self.auxcommands["storagecontroller"].storagecontroller(options, single_use=True) + options.command = "volume" + ilo_ver = 6.110 + try: + controller = controllers[next(iter(controllers))] + (create_flag, newdrive) = self.createvolume(options, controller) + if create_flag: + if ilo_ver >= 6.110: + temp_odata = controller["@odata.id"] + volume_path = temp_odata.split("Controllers")[0] + "Volumes" + self.rdmc.ui.printer("CreateVolume path and payload: %s, %s\n" % (volume_path, newdrive)) + result = self.rdmc.app.post_handler(volume_path, newdrive) + if result.status == 400: + self.rdmc.ui.error("iLO has thrown BadRequest for this command attempt \n") + return ReturnCodes.RIS_ILO_RESPONSE_ERROR + + self.rdmc.ui.printer("Volume created successfully \n") + if options.sparedrives: + controller["physical_drives"] = self.auxcommands[ + "storagecontroller" + ].storagephysical_drives(options, controller, options.storageid, single_use=True) + array = options.disks.split(",") + set_spare = False + newdrive1 = {"Links": {"DedicatedSpareDrives": [{}]}} + volume_path1 = volume_path + result.session_location.split("Volumes")[1] + + sparedrives = options.sparedrives.split(",") + s_array = [] + if len(controller["physical_drives"]) > 0: + for p_id in controller["physical_drives"]: + p_loc = self.convertloc( + controller["physical_drives"][str(p_id)]["PhysicalLocation"]["PartLocation"][ + "ServiceLabel" + ] + ) + l_loc = p_loc.split(":")[1:] + p_loc = ":".join(l_loc) + + for sdrive in sparedrives: + if sdrive == p_loc and sdrive not in array: + s_array.append( + { + "@odata.id": controller["physical_drives"][str(p_id)]["@odata.id"], + } + ) + set_spare = True + newdrive1["Links"]["DedicatedSpareDrives"] = s_array + if not set_spare: + raise InvalidCommandLineError( + "Invalid spare drive given, check whether given spare drive is different from " + "disks given or check whether spare drive belongs to same controller" + ) + + self.rdmc.app.patch_handler(volume_path1, newdrive1) + self.rdmc.ui.printer("Successfully added the sparedrives %s\n" % (newdrive1)) + + return ReturnCodes.SUCCESS + + else: + temp_odata = controller["@odata.id"] + payload_dict = dict() + payload_dict["DataGuard"] = "Disabled" + if "settings" not in temp_odata: + temp_odata = temp_odata + "settings/" + settings_controller = self.rdmc.app.get_handler(temp_odata, service=False, silent=True) + # Fix for multiple logical creation at single reboot + if self.rdmc.app.typepath.defs.isgen9: + payload_dict["logical_drives"] = dict() + payload_dict["logical_drives"]["new"] = newdrive + else: + payload_dict["LogicalDrives"] = settings_controller.dict["LogicalDrives"] + payload_dict["LogicalDrives"].append(newdrive) + self.rdmc.ui.printer("CreateVolume path and payload: %s, %s\n" % (temp_odata, payload_dict)) + self.rdmc.app.put_handler( + temp_odata, + payload_dict, + headers={"If-Match": self.getetag(temp_odata)}, + ) + self.rdmc.app.download_path([temp_odata], path_refresh=True, crawl=False) + self.rdmc.ui.printer( + "One or more properties were changed and will not take effect " "until system is reset \n" + ) + except IloLicenseError: + self.rdmc.ui.error("License Error Occured while creating volume\n") + return ReturnCodes.ILO_LICENSE_ERROR + except IdTokenError: + self.rdmc.ui.error("Insufficient Privilege to create volume\n") + return ReturnCodes.RIS_MISSING_ID_TOKEN + except IloResponseError: + self.rdmc.ui.error("iLO threw iLOResponseError\n") + return ReturnCodes.RIS_ILO_RESPONSE_ERROR + except InvalidSmartArrayConfigurationError as excp: + self.rdmc.ui.error(excp.args[0]) + return ReturnCodes.INVALID_SMART_ARRAY_PAYLOAD + except StopIteration: + self.rdmc.ui.error("Drive or Controller not exist, Please check drive or controller\n") + except InvalidCommandLineError as excp: + self.rdmc.ui.error(excp.args[0]) + return ReturnCodes.INVALID_COMMAND_LINE_ERROR + except Exception as excp: + self.rdmc.ui.error(excp.args[0]) + + else: + self.rdmc.ui.error("Provide the controller \n") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def convertloc(self, servicelabel): + loc = servicelabel.split(":") + temp_str = str( + loc[0].split("=")[1] + ":" + loc[1].split("=")[1] + ":" + loc[2].split("=")[1] + ":" + loc[3].split("=")[1] + ) + return temp_str + + def get_allowed_list(self, storage_id, attr): + url = "/redfish/v1/Systems/1/Storage/" + storage_id + "/Volumes/Capabilities" + attr_allowed_str = attr + "@Redfish.AllowableValues" + + results = self.rdmc.app.get_handler(url, service=True, silent=True).dict + return results[attr_allowed_str] + + def createvolume(self, options, controller): + """Create volume""" + global p_loc + ilo_ver = self.rdmc.app.getiloversion() + if options.command == "volume": + ilo_ver = 6.110 + if ilo_ver >= 6.110: + try: + raidlvllist = self.get_allowed_list(options.storageid, "RAIDType") + except: + raidlvllist = [ + "Raid0", + "Raid1", + "Raid1ADM", + "Raid10", + "Raid10ADM", + "Raid5", + "Raid50", + "Raid6", + "Raid60", + ] + + else: + raidlvllist = [ + "Raid0", + "Raid1", + "Raid1ADM", + "Raid10", + "Raid10ADM", + "Raid5", + "Raid50", + "Raid6", + "Raid60", + ] + interfacetypelist = ["SAS", "SATA", "NVMe"] + mediatypelist = ["SSD", "HDD"] + sparetypelist = ["Dedicated", "Roaming"] + acceltypelist = ["ControllerCache", "IOBypass", "None"] + locationtypelist = ["Internal", "External"] + legacylist = ["Primary", "Secondary", "All", "None"] + paritylist = ["Default", "Rapid"] + iOPerfModeEnabledlist = ["true", "false"] + if ilo_ver >= 6.110: + try: + readCachePolicylist = self.get_allowed_list(options.storageid, "ReadCachePolicy") + # writeCachePolicylist = self.get_allowed_list(options.storageid, "WriteCachePolicy") + writeCachePolicylist = [ + "Off", + "WriteThrough", + "ProtectedWriteBack", + "UnprotectedWriteBack", + ] + except: + readCachePolicylist = ["Off", "ReadAhead"] + writeCachePolicylist = [ + "Off", + "WriteThrough", + "ProtectedWriteBack", + "UnprotectedWriteBack", + ] + # InitMethodlist = self.get_allowed_list(options.storageid, "InitializeMethod") + WriteHoleProtectionPolicyList = ["Yes", "No"] + sparedrives = [] + changes = False + + if ilo_ver >= 6.110: + controller["physical_drives"] = self.auxcommands["storagecontroller"].storagephysical_drives( + options, controller, options.storageid, single_use=True + ) + else: + try: + controller["physical_drives"] = self.auxcommands["storagecontroller"].physical_drives( + options, controller, single_use=True + ) + except: + controller["physical_drives"] = self.auxcommands["storagecontroller"].storagephysical_drives( + options, controller, options.storageid, single_use=True + ) + if ilo_ver >= 6.110: + controller["logical_drives"] = self.auxcommands["storagecontroller"].storagelogical_drives( + options, controller, options.storageid, single_use=True + ) + else: + try: + controller["logical_drives"] = self.auxcommands["storagecontroller"].logical_drives( + options, controller, single_use=True + ) + except: + controller["logical_drives"] = self.auxcommands["storagecontroller"].storagelogical_drives( + options, controller, options.storageid, single_use=True + ) + if controller.get("Links"): + if ilo_ver >= 6.110: + newdrive = {"Links": {"Drives": {}}} + else: + newdrive = {"Links": {"DataDrives": {}}} + else: + if ilo_ver >= 6.110: + newdrive = {"Links": {"Drives": {}}} + else: + newdrive = {"links": {"DataDrives": {}}} + + changes = False + itemadded = False + + for item in raidlvllist: + if options.raid.lower() == item.lower(): + if options.command == "customdrive" or options.command == "volume": + drivecount = len(options.disks.replace(", ", ",").split(",")) + else: + try: + drivecount = int(options.disks) + except ValueError: + raise InvalidCommandLineError("Number of drives is not an integer.") + if self.raidvalidation(item.lower(), drivecount, options): + itemadded = True + if ilo_ver >= 6.110: + newdrive["RAIDType"] = options.raid.upper() + else: + newdrive["Raid"] = item + break + + if not itemadded: + raise InvalidCommandLineError("Invalid raid type or configuration.") + else: + itemadded = False + + if options.command == "customdrive": + if options.sparedrives: + sparedrives = options.sparedrives.replace(", ", ",").split(",") + newdrive["SpareDrives"] = [] + newdrive["SpareRebuildMode"] = "Dedicated" + + drives = options.disks.replace(", ", ",").split(",") + newdrive["DataDrives"] = [] + + if len(controller["physical_drives"]) > 0: + for id in controller["physical_drives"]: + for drive in drives: + drv_id = controller["physical_drives"][str(id)] + if "Location" in drv_id: + drv_loc = drv_id["Location"] + else: + location = drv_id["PhysicalLocation"]["PartLocation"]["ServiceLabel"] + loc = location.split(":") + del loc[0] + if len(loc) == 3: + temp_str = str( + loc[0].split("=")[1] + ":" + loc[1].split("=")[1] + ":" + loc[2].split("=")[1] + ) + drv_loc = temp_str + + if drive == drv_loc: + newdrive["DataDrives"].append(drive) + + for sdrive in sparedrives: + drv_id = controller["SpareDrives"][str(id)] + if "Location" in drv_id: + drv_loc = drv_id["Location"] + else: + location = drv_id["PhysicalLocation"]["PartLocation"]["ServiceLabel"] + loc = location.split(":") + del loc[0] + if len(loc) == 3: + temp_str = str( + loc[0].split("=")[1] + ":" + loc[1].split("=")[1] + ":" + loc[2].split("=")[1] + ) + drv_loc = temp_str + + if drive == drv_loc: + newdrive["DataDrives"].append(drive) + if sdrive == drv_id: + newdrive["SpareDrives"].append(sdrive) + else: + raise InvalidCommandLineError("No Physical Drives in this controller") + + if drivecount > len(newdrive["DataDrives"]): + raise InvalidCommandLineError( + "Not all of the selected drives could " "be found in the specified locations." + ) + + if options.sparetype: + itemadded = False + for item in sparetypelist: + if options.sparetype.lower() == item.lower(): + newdrive["SpareRebuildMode"] = item + itemadded = True + break + + if not itemadded: + raise InvalidCommandLineError("Invalid spare drive type.") + + if options.drivename: + newdrive["LogicalDriveName"] = options.drivename + + if options.capacitygib: + try: + capacitygib = int(options.capacitygib) + except ValueError: + raise InvalidCommandLineError("Capacity is not an integer.") + newdrive["CapacityGiB"] = capacitygib + + if options.acceleratortype: + itemadded = False + for item in acceltypelist: + if options.acceleratortype.lower() == item.lower(): + newdrive["Accelerator"] = item + itemadded = True + break + + if not itemadded: + raise InvalidCommandLineError("Invalid accelerator type.") + + if options.legacyboot: + itemadded = False + for item in legacylist: + if options.legacyboot.lower() in item.lower(): + newdrive["LegacyBootPriority"] = item + itemadded = True + break + + if not itemadded: + raise InvalidCommandLineError("Invalid legacy boot priority.") + + if options.capacityblocks: + try: + capacityblocks = int(options.capacityblocks) + except ValueError: + raise InvalidCommandLineError("Capacity is not an integer.") + + newdrive["CapacityBlocks"] = capacityblocks + + if options.paritygroup: + try: + paritygroup = int(options.paritygroup) + except ValueError: + raise InvalidCommandLineError("Parity group is not an integer.") + + newdrive["ParityGroupCount"] = paritygroup + + if options.paritytype: + itemadded = False + for item in paritylist: + if options.paritytype.lower() == item.lower(): + newdrive["ParityInitializationType"] = item + itemadded = True + break + + if not itemadded: + raise InvalidCommandLineError("Invalid parity type") + + if options.blocksize: + try: + blocksize = int(options.blocksize) + except ValueError: + raise InvalidCommandLineError("Block size is not an integer.") + + newdrive["BlockSizeBytes"] = blocksize + + if options.stripsize: + try: + stripsize = int(options.stripsize) + except ValueError: + raise InvalidCommandLineError("Strip size is not an integer.") + + newdrive["StripSizeBytes"] = stripsize + + if options.stripesize: + try: + stripesize = int(options.stripesize) + except ValueError: + raise InvalidCommandLineError("Stripe size is not an integer.") + + newdrive["StripeSizeBytes"] = stripesize + elif options.command == "quickdrive": + try: + numdrives = int(options.disks) + except ValueError: + raise InvalidCommandLineError("Number of drives is not an integer.") + + newdrive["DataDrives"] = { + "DataDriveCount": numdrives, + "DataDriveMinimumSizeGiB": 0, + } + for item in mediatypelist: + if options.drivetype.lower() == item.lower(): + newdrive["DataDrives"]["DataDriveMediaType"] = item + itemadded = True + break + if not itemadded: + raise InvalidCommandLineError("Invalid media type.") + else: + itemadded = False + for item in interfacetypelist: + if options.interfacetype.lower() == item.lower(): + newdrive["DataDrives"]["DataDriveInterfaceType"] = item + itemadded = True + break + if not itemadded: + raise InvalidCommandLineError("Invalid interface type.") + + if options.locationtype: + for item in locationtypelist: + if options.locationtype.lower() == item.lower(): + newdrive["DataDrives"]["DataDriveLocation"] = item + break + if options.minimumsize: + try: + minimumsize = int(options.minimumsize) + except ValueError: + raise InvalidCommandLineError("Minimum size is not an integer.") + newdrive["DataDrives"]["DataDriveMinimumSizeGiB"] = minimumsize + newdrive["CapacityGiB"] = minimumsize + elif options.command == "volume": + idval = [] + if len(controller["physical_drives"]) > 0: + array = options.disks.split(",") + DA = ["DA000000", "DA000001"] + for p_id in controller["physical_drives"]: + if p_id not in DA: + p_loc = self.convertloc( + controller["physical_drives"][str(p_id)]["PhysicalLocation"]["PartLocation"]["ServiceLabel"] + ) + + for ar in array: + if ar in p_loc: + idval.append(controller["physical_drives"][str(p_id)]["@odata.id"]) + array.remove(ar) + newdrive["Links"]["Drives"] = [] + if idval is not None: + for id in idval: + newdrive["Links"]["Drives"].append( + { + "@odata.id": id, + } + ) + else: + raise InvalidCommandLineError( + "Disk location given is invalid , Kindly recheck and provide valid location" + ) + + if "capacitygib" in options and options.capacitygib: + options.capacitybytes = int(options.capacitygib) * 1024 * 1024 * 1024 + if "capacitybytes" in options and options.capacitybytes is not None: + if options.capacitybytes: + try: + if isinstance(options.capacitybytes, list): + capacitybytes = int(options.capacitybytes[0]) + else: + capacitybytes = int(options.capacitybytes) + except ValueError: + raise InvalidCommandLineError("Capacity is not an integer.") + + newdrive["CapacityBytes"] = capacitybytes + + if "iOPerfModeEnabled" in options and options.iOPerfModeEnabled: + for item in iOPerfModeEnabledlist: + if ( + options.iOPerfModeEnabled.lower() == item.lower() + and options.iOPerfModeEnabled.lower() == "false" + ): + newdrive["IOPerfModeEnabled"] = eval(options.iOPerfModeEnabled) + itemadded = True + break + elif ( + options.iOPerfModeEnabled.lower() == item.lower() + and options.iOPerfModeEnabled.lower() == "true" + ): + if "SSD" in controller["SupportedDeviceProtocols"]: + newdrive["IOPerfModeEnabled"] = eval(options.iOPerfModeEnabled) + itemadded = True + break + else: + raise InvalidCommandLineError( + " IOPerfModeEnabled can be true only when supported protocol is SSD" + ) + if not itemadded: + raise InvalidCommandLineError("Invalid IOPerfModeEnabled, Value should be either False or True") + else: + itemadded = False + + if "ReadCachePolicy" in options and options.ReadCachePolicy is not None: + for item in readCachePolicylist: + if options.ReadCachePolicy.lower() == item.lower(): + newdrive["ReadCachePolicy"] = item + itemadded = True + break + if not itemadded: + raise InvalidCommandLineError("Invalid ReadCachePolicy, Value should be 'Off' or 'ReadAhead'") + else: + itemadded = False + + if "WriteCachePolicy" in options and options.WriteCachePolicy is not None: + for item in writeCachePolicylist: + if options.WriteCachePolicy.lower() == item.lower(): + newdrive["WriteCachePolicy"] = item + itemadded = True + break + if not itemadded: + raise InvalidCommandLineError( + "Invalid WriteCachePolicy, Values should be 'Off', 'WriteThrough','ProtectedWriteBack'," + "'UnprotectedWriteBack'" + ) + else: + itemadded = False + + if "VROC" in controller["Model"]: + if options.WriteHoleProtectionPolicy is not None: + for item in WriteHoleProtectionPolicyList: + if ( + options.WriteHoleProtectionPolicy.lower() == item.lower() + and options.WriteHoleProtectionPolicy.lower() == "yes" + ): + newdrive["WriteHoleProtectionPolicy"] = "Journaling" + newdrive["Links"]["JournalingMedia"] = idval + itemadded = True + break + if not itemadded: + raise InvalidCommandLineError( + "Invalid WriteHoleProtectionPolicy, Values can be either Yes or No" + ) + else: + itemadded = False + if "drivename" in options and options.drivename: + options.DisplayName = options.drivename + if "DisplayName" in options and options.DisplayName is not None: + newdrive["DisplayName"] = options.DisplayName + + if newdrive: + if options.command == "quickdrive": + newdrive_count = newdrive["DataDrives"]["DataDriveCount"] + elif options.command == "customdrive": + newdrive_count = len(newdrive["DataDrives"]) + elif options.command == "volume": + newdrive_count = len(newdrive["Links"]["Drives"]) + + if len(controller["physical_drives"]) >= newdrive_count: + drives_avail = len(controller["physical_drives"]) + accepted_drives = 0 + for cnt, drive in enumerate(controller["physical_drives"]): + drivechecks = (False, False, False, False) + if drives_avail < newdrive_count: + raise InvalidSmartArrayConfigurationError( + "Unable to continue, requested number of drives not actually present under the storage ID" + " or the drives requested are in use by another volume\n" + ) + else: + drivechecks = (True, False, False, False) + + if options.command == "quickdrive": + if ( + controller["physical_drives"][drive]["InterfaceType"] + == newdrive["DataDrives"]["DataDriveInterfaceType"] + ): + drivechecks = (True, True, False, False) + else: + drives_avail -= 1 + continue + if ( + controller["physical_drives"][drive]["MediaType"] + == newdrive["DataDrives"]["DataDriveMediaType"] + ): + drivechecks = (True, True, True, False) + else: + drives_avail -= 1 + continue + else: + drivechecks = (True, True, True, False) + in_use = False + if controller["logical_drives"] is not None: + for existing_logical_drives in controller["logical_drives"]: + _logical_drive = controller["logical_drives"][existing_logical_drives] + if _logical_drive.get("LogicalDrives"): + for _data_drive in _logical_drive["LogicalDrives"]["DataDrives"]: + if drive == _logical_drive["LogicalDrives"]["DataDrives"][_data_drive]: + in_use = True + elif _logical_drive.get("Links"): + if not ilo_ver >= 6.110: + for _data_drive in _logical_drive["Links"]["DataDrives"]: + if drive == _logical_drive["Links"]["DataDrives"][_data_drive]: + in_use = True + else: + for _data_drive in _logical_drive["Links"]["Drives"]: + if drive in _data_drive["@odata.id"] and _logical_drive["RAIDType"] != "None": + in_use = True + elif _logical_drive.get("links"): + for _data_drive in _logical_drive["links"]["DataDrives"]: + if drive == _logical_drive["links"]["DataDrives"][_data_drive]: + in_use = True + if in_use: + drives_avail -= 1 + continue + else: + drivechecks = (True, True, True, True) + if drivechecks[0] and drivechecks[1] and drivechecks[2]: + if controller.get("Links"): + pass + else: + if not ilo_ver >= 6.110: + newdrive["links"]["DataDrives"][int(drive)] = controller["physical_drives"][drive] + accepted_drives += 1 + changes = True + if accepted_drives == newdrive_count: + break + else: + drives_avail -= 1 + + if changes: + if self.rdmc.app.typepath.defs.isgen9: + controller["logical_drives"]["new"] = newdrive + else: + if not ilo_ver >= 6.110: + try: + newdrive.pop("Links") + except KeyError: + newdrive.pop("links") + if not ilo_ver >= 6.110: + controller["LogicalDrives"].append(newdrive) + del controller["logical_drives"] + del controller["physical_drives"] + + return (changes, newdrive) + + def getetag(self, path): + """get etag from path""" + etag = None + instance = self.rdmc.app.monolith.path(path) + if instance: + etag = ( + instance.resp.getheader("etag") + if "etag" in instance.resp.getheaders() + else instance.resp.getheader("ETag") + ) + return etag + + def raidvalidation(self, raidtype, numdrives, options): + """vaidation function for raid levels + :param raidtype: raid type + :type options: string. + :param numdrives: number of drives + :type numdrives: int. + :param options: command line options + :type options: list. + """ + + valid = True + if raidtype == "raid1": + if numdrives < 2: # or options.stripsize: + valid = False + elif raidtype == "raid5": + if numdrives < 3: # or options.stripsize: + valid = False + elif raidtype == "raid6": + if numdrives < 4: # or options.stripsize: + valid = False + elif raidtype == "raid50": + if numdrives < 6: + valid = False + elif raidtype == "raid60": + if numdrives < 8: + valid = False + + return valid + + def createvolumevalidation(self, options): + """Create volume validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + @staticmethod + def options_argument_group(parser): + """Define optional arguments group + + :param parser: The parser to add the login option group to + :type parser: ArgumentParser/OptionParser + """ + group = parser.add_argument_group( + "GLOBAL OPTIONS", + "Options are available for all" "arguments within the scope of this command.", + ) + + group.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller " + "using either the slot number or index.\nexample: --controller=Slot 0 OR " + "--controller=1", + default=None, + required=True, + ) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + # self.options_argument_group(customparser) + + subcommand_parser = customparser.add_subparsers(dest="command") + subcommand_parser.required = True + qd_help = ( + "Create a volume with a minimal number of arguments (utilizes default " + "values on the controller). This option is only for iLO5 or Gen10" + ) + # quickdrive sub-parser + qd_parser = subcommand_parser.add_parser( + "quickdrive", + help=qd_help, + description=qd_help + "\n\texample: createvolume quickdrive " + " " + "--locationtype=Internal --minimumsize=0 --controller=1", + formatter_class=RawDescriptionHelpFormatter, + ) + qd_parser.add_argument( + "raid", + help="Specify the RAID level for the volume to be created.", + metavar="Raid_Level", + ) + qd_parser.add_argument( + "disks", + help="For quick drive creation, specify number of disks.", + metavar="Drives", + ) + qd_parser.add_argument( + "drivetype", + help="Specify the drive media type of the physical disk(s) (i.e. HDD or SSD)", + metavar="Drive_Media_Type", + ) + qd_parser.add_argument( + "interfacetype", + help="Specify the interface type of the physical disk(s) (i.e. SATA or SAS or NVMe)", + metavar="Drive_Interface_Type", + ) + qd_parser.add_argument( + "--locationtype", + dest="locationtype", + help="Optionally specify the location of the physical disks(s) (i.e. Internal or External)", + default=None, + ) + qd_parser.add_argument( + "--minimumsize", + dest="minimumsize", + help="""Optionally include to set the minimum size of the drive """ + """in GiB. (usable in quick creation only, use -1 for max size)""", + default=None, + ) + qd_parser.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller " + "using either the slot number or index.\nexample: --controller=Slot 0 OR " + "--controller=1", + default=None, + required=True, + ) + qd_parser.add_argument( + "--storageid", + dest="storageid", + help="Use this flag to select the corresponding storageid " + "using either the slot number or index.\nexample: --storageid=DE123234", + default=None, + required=False, + ) + self.cmdbase.add_login_arguments_group(qd_parser) + # self.options_argument_group(qd_parser) + + cd_help = ( + "Create a customised volume using all available properties (as optional " + "arguments) for creation. This option is only for iLO5 or Gen10" + ) + # customdrive sub-parser + cd_parser = subcommand_parser.add_parser( + "customdrive", + help=cd_help, + description=cd_help + "\n\texample: createvolume customdrive " + " --controller=1 " + "--name=drivename --spare-drives=1I:1:1,1I:1:3 " + "--spare-type=Dedicated --capacitygib=10 " + "--accelerator-type=None\n\n\tOPTIONS:\n\traid-level:\t\t" + "Raid0, Raid1, Raid1ADM, Raid10, Raid10ADM, Raid5, Raid50, " + "Raid6, Raid60\n\tphysicaldrivelocation(s):\tLocation, Drive-name\n\t" + "media-type:\t\tSSD,HDD\n\tinterface-type:" + "\t\tSAS, SATA, NVMe\n\tdrive-location:\t\tInternal, External\n\t" + "--spare-type:\t\tDedicated, Roaming\n\t--accelerator-type:\t" + "ControllerCache, IOBypass, None\n\t--paritytype:\t\tDefault, Rapid" + "\n\t--capacitygib:\t\t-1 (for Max Size)\n\t--capacityblocks:\t" + "-1 (for Max Size)\n\n\t", + formatter_class=RawDescriptionHelpFormatter, + ) + cd_parser.add_argument( + "raid", + help="Specify the RAID level for the volume to be created.", + metavar="Raid_Level", + ) + cd_parser.add_argument( + "disks", + help="For custom drive, specify a comma separated physical disk locations.", + metavar="Drive_Indices", + ) + cd_parser.add_argument( + "-n", + "--name", + dest="drivename", + help="""Optionally include to set the drive name (usable in """ """custom creation only).""", + default=None, + ) + cd_parser.add_argument( + "--spare-drives", + dest="sparedrives", + help="""Optionally include to set the spare drives by the """ + """physical drive's location. (usable in custom creation only)""", + default=None, + ) + cd_parser.add_argument( + "--capacitygib", + dest="capacitygib", + help="""Optionally include to set the capacity of the drive in """ + """GiB. (usable in custom creation only, use -1 for max """ + """size)""", + default=None, + ) + cd_parser.add_argument( + "--accelerator-type", + dest="acceleratortype", + help="""Optionally include to choose the accelerator type.""", + default=None, + ) + cd_parser.add_argument( + "--spare-type", + dest="sparetype", + help="""Optionally include to choose the spare drive type. """ """(usable in custom creation only)""", + default=None, + ) + cd_parser.add_argument( + "--minimumsize", + dest="minimumsize", + help="""Optionally include to set the minimum size of the drive """ + """in GiB. (usable in quick creation only, use -1 for max size)""", + default=None, + ) + cd_parser.add_argument( + "--legacy-boot", + dest="legacyboot", + help="""Optionally include to choose the legacy boot priority. """ """(usable in custom creation only)""", + default=None, + ) + cd_parser.add_argument( + "--storageid", + dest="storageid", + help="Use this flag to select the corresponding storageid " + "using either the slot number or index.\nexample: --storageid=DE123234", + default=None, + required=False, + ) + cd_parser.add_argument( + "--capacityblocks", + dest="capacityblocks", + help="""Optionally include to choose the capacity in blocks. """ + """(use -1 for max size, usable in custom creation only)""", + default=None, + ) + cd_parser.add_argument( + "--paritygroupcount", + dest="paritygroup", + help="""Optionally include to include the number of parity """ + """groups to use. (only valid for certain RAID levels)""", + default=None, + ) + cd_parser.add_argument( + "--paritytype", + dest="paritytype", + help="""Optionally include to choose the parity initialization""" + """ type. (usable in custom creation only)""", + default=None, + ) + cd_parser.add_argument( + "--block-size-bytes", + dest="blocksize", + help="""Optionally include to choose the block size of the disk""" + """ drive. (usable in custom creation only)""", + default=None, + ) + cd_parser.add_argument( + "--strip-size-bytes", + dest="stripsize", + help="""Optionally include to choose the strip size in bytes. """ """(usable in custom creation only)""", + default=None, + ) + cd_parser.add_argument( + "--stripe-size-bytes", + dest="stripesize", + help="""Optionally include to choose the stripe size in bytes. """ """(usable in custom creation only)""", + default=None, + ) + cd_parser.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller " + "using either the slot number or index.\nexample: --controller=Slot 0 OR " + "--controller=1", + default=None, + required=True, + ) + self.cmdbase.add_login_arguments_group(cd_parser) + # self.options_argument_group(cd_parser) + v_help = ( + "Create a volume using all available properties (as optional " + "arguments) for creation on gen 11 or higher " + ) + # volume sub-parser + v_parser = subcommand_parser.add_parser( + "volume", + help=v_help, + description=v_help + "\n\texample: createvolume volume " + " " + " " + " --storageid=DE009000 --controller=0 " + " " + "\n\n\t", + formatter_class=RawDescriptionHelpFormatter, + ) + v_parser.add_argument( + "raid", + help="Specify the RAID level for the volume to be created.", + metavar="Raid_Level", + ) + v_parser.add_argument( + "disks", + help="For custom drive, specify a comma separated physical disk locations.", + metavar="Drive_Indices", + ) + v_parser.add_argument( + "--storageid", + dest="storageid", + help="Use this flag to select the corresponding storageid " + "using either the slot number or index.\nexample: --storageid=DE123234", + default=None, + required=True, + ) + v_parser.add_argument( + "--DisplayName", + dest="DisplayName", + help="""Optionally include to set the drive name """, + default=None, + ) + v_parser.add_argument( + "--iOPerfModeEnabled", + dest="iOPerfModeEnabled", + help="""Optionally include to choose the IOPerfModeEnabled . Allowed values are 'True', 'False'""", + default=None, + ) + v_parser.add_argument( + "--ReadCachePolicy", + dest="ReadCachePolicy", + help="""Optionally include to choose the ReadCachePolicy. """ """Allowed values are 'Off', 'ReadAhead'""", + default=None, + ) + v_parser.add_argument( + "--WriteCachePolicy", + dest="WriteCachePolicy", + help="Optionally include to set the WriteCachePolicy " + "Allowed values are 'Off', 'WriteThrough','ProtectedWriteBack','UnprotectedWriteBack'", + default=None, + ) + v_parser.add_argument( + "--WriteHoleProtectionPolicy", + dest="WriteHoleProtectionPolicy", + help="""Optionally include to choose the WriteHoleProtectionPolicy """ + """this is applicable only for VROC, You can send either Yes or No as values""", + default=None, + ) + v_parser.add_argument( + "--sparedrives", + dest="sparedrives", + help="""Optionally include to set the spare drives by the """ + """physical drive's location. (usable in custom creation only)""", + action="append", + default=None, + ) + v_parser.add_argument( + "--capacitybytes", + dest="capacitybytes", + help="""Optionally include to set the capacity of the drive in """ + """bytes. (usable in custom creation only, use -1 for max """ + """size)""", + action="append", + default=None, + ) + v_parser.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller " + "using either the slot number or index.\nexample: --controller=Slot 0 OR " + "--controller=1", + default=None, + required=True, + ) + + self.cmdbase.add_login_arguments_group(v_parser) + # self.options_argument_group(v_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/DeleteVolumeCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/DeleteVolumeCommand.py new file mode 100644 index 0000000..98c1268 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/DeleteVolumeCommand.py @@ -0,0 +1,441 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Delete Volume Command for rdmc""" + +from six.moves import input + +from redfish.ris import InstanceNotFoundError + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class DeleteVolumeCommand: + """Delete volume command""" + + def __init__(self): + self.ident = { + "name": "deletevolume", + "usage": None, + "description": "To delete a volume " + "to a controller by index.\n\texample: deletevolume " + "1 --controller=1\n\n\tTo delete multiple volumes by " + "index.\n\tExample: deletevolume 1,2 --controller=1" + "\n\n\tTo delete all volumes on a controller.\n\t" + "deletevolume --controller=1 --all\n\n\t" + 'deletevolume --controller="Slot1" --all\n\n\tNOTE: ' + "You can also delete volumes by " + '"VolumeUniqueIdentifier".\n\n\t' + "For iLO6, storage id need to be specified using --storageid=DE00E000 along with --controller=1\n\n\t" + "You can also delete volumes by VolumeName", + "summary": "Deletes volumes from the selected controller.", + "aliases": ["deletelogicaldrive"], + "auxcommands": ["SelectCommand", "StorageControllerCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main disk inventory worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.deletevolumevalidation(options) + ilo_ver = self.rdmc.app.getiloversion() + if ilo_ver < 6.110: + try: + self.auxcommands["select"].selectfunction("StorageController.") + content = self.rdmc.app.getprops() + ilo_ver = 6.110 + except Exception: + pass + + if ilo_ver >= 6.110: + if not options.storageid: + raise InvalidCommandLineError( + "--storageid option is mandatory for iLO6 along with --controller option.\n" + ) + controller_logicaldrives = self.auxcommands["storagecontroller"].storagelogical_drives( + options, options.controller, options.storageid, single_use=True + ) + if len(controller_logicaldrives) == 0: + raise InvalidCommandLineError("No Logical drives found.\n") + get_contrller = [] + self.auxcommands["select"].selectfunction("StorageCollection.") + st_content = self.rdmc.app.getprops() + for st_controller in st_content: + path = st_controller["Members"] + for mem in path: + for val in mem.values(): + if "DE" in val and options.storageid in val: + getval = self.rdmc.app.get_handler(val, silent=True, service=True).dict + controller = getval["Controllers"] + for ctl in controller.values(): + ctl = ctl + "/0" + get_sel = self.rdmc.app.get_handler(ctl, silent=True, service=True) + get_contrller.append(get_sel) + else: + self.auxcommands["select"].selectfunction("SmartStorageConfig.") + content = self.rdmc.app.getprops() + + if not args and not options.all: + raise InvalidCommandLineError("You must include a logical drive to delete.") + elif not options.controller: + raise InvalidCommandLineError("You must include a controller to select.") + else: + if len(args) > 1: + logicaldrives = args + elif len(args) == 1: + logicaldrives = args[0].replace(", ", ",").split(",") + else: + logicaldrives = None + + controllist = [] + + try: + if ilo_ver >= 6.110: + if options.controller.isdigit(): + slotlocation = self.storageget_location_from_id(options.controller, options.storageid) + if slotlocation: + slotcontrol = slotlocation.lower().strip('"').split("slot")[-1].lstrip().strip("=") + for control in get_contrller: + cont_temp = control.dict + temp = cont_temp["Location"]["PartLocation"]["ServiceLabel"] + if "Location" in cont_temp and slotcontrol.lower() == temp.lower().split("slot")[ + -1 + ].lstrip().strip("="): + controllist.append(cont_temp) + elif "slot" in options.controller.lower(): + slotlocation = options.controller + if slotlocation: + slotcontrol = slotlocation.lower().strip('"').split("slot")[-1].lstrip().strip("=") + for control in get_contrller: + cont_temp = control.dict + temp = cont_temp["Location"]["PartLocation"]["ServiceLabel"] + if "Location" in cont_temp and slotcontrol.lower() == temp.lower().split("slot")[ + -1 + ].lstrip().strip("="): + controllist.append(cont_temp) + if not controllist: + raise InvalidCommandLineError("") + else: + try: + controllers = self.auxcommands["storagecontroller"].storagecontroller(options, single_use=True) + options.command = "volume" + ilo_ver = 6.110 + except InstanceNotFoundError: + if not options.storageid: + raise InvalidCommandLineError( + "--storageid option is mandatory. Please input storageid as well so that " + "controllers/volumes can be identified.\n" + ) + controllers = self.auxcommands["storagecontroller"].storagecontroller(options, single_use=True) + options.command = "volume" + ilo_ver = 6.110 + if options.controller.isdigit(): + slotlocation = self.get_location_from_id(options.controller) + if slotlocation: + slotcontrol = slotlocation.lower().strip('"').split("slot")[-1].lstrip() + for control in content: + if slotcontrol.lower() == control["Location"].lower().split("slot")[-1].lstrip(): + controllist.append(control) + if not controllist: + raise InvalidCommandLineError("") + except InvalidCommandLineError: + raise InvalidCommandLineError("Selected controller not found in the current inventory " "list.") + if ilo_ver >= 6.110: + self.storagedeletevolume(controller_logicaldrives, logicaldrives, options.all, options.force) + else: + self.deletevolume(controllist, logicaldrives, options.all, options.force) + self.rdmc.ui.printer( + "One or more properties were changed and will not take effect " "until system is reset \n" + ) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def get_location_from_id(self, controller_id): + # self.rdmc.ui.printer("Controller ID: %s\n" % (controller_id)) + for sel in self.rdmc.app.select("SmartStorageArrayController", path_refresh=True): + if "Collection" not in sel.maj_type: + controller = sel.dict + self.rdmc.ui.printer("Controller ID: %s\n" % (controller["Id"])) + if controller["Id"] == str(controller_id): + self.rdmc.ui.printer("Controller Location: %s\n" % (controller["Location"])) + return controller["Location"] + return None + + def storageget_location_from_id(self, controller_id, storageid): + get_contrller = [] + # self.auxcommands["select"].selectfunction("StorageCollection.") + st_content = self.rdmc.app.getprops() + for st_controller in st_content: + path = st_controller["Members"] + for mem in path: + for val in mem.values(): + if "DE" in val and storageid in val: + getval = self.rdmc.app.get_handler(val, silent=True, service=True).dict + controller = getval["Controllers"] + for ctl in controller.values(): + ctl = ctl + "/0" + get_sel = self.rdmc.app.get_handler(ctl, silent=True, service=True) + get_contrller.append(get_sel) + for sel in get_contrller: + if "Collection" not in sel.path: + controller = sel.dict + if controller["Id"] == str(controller_id): + return controller["Location"]["PartLocation"]["ServiceLabel"] + return None + + def deletevolume(self, controllist, drivelist, allopt, force): + """Gets volumes ready for deletion + + :param controllist: list of controllers + :type controllist: list. + :param drivelist: volumes to delete + :type drivelist: list. + :param allopt: flag for deleting all volumes + :type allopt: bool. + """ + + for controller in controllist: + changes = False + + numdrives = len(controller["LogicalDrives"]) + deletecount = 0 + + if allopt: + controller["LogicalDrives"] = [] + controller["DataGuard"] = "Disabled" + if not force: + if not self.inputaccept(None): + return + self.lastlogicaldrive(controller) + changes = True + else: + for deldrive in drivelist: + found = False + + if deldrive.isdigit(): + deldrive = int(deldrive) + + for idx, ldrive in enumerate(controller["LogicalDrives"]): + if deldrive == ldrive["LogicalDriveNumber"]: + if not force: + if not self.inputaccept(ldrive["LogicalDriveName"]): + return + + controller["LogicalDrives"][idx]["Actions"] = [{"Action": "LogicalDriveDelete"}] + + controller["DataGuard"] = "Permissive" + deletecount += 1 + + changes = True + found = True + + break + + if not found: + raise NoContentsFoundForOperationError("Logical " "drive %s not found." % str(deldrive)) + + if deletecount == numdrives: + self.lastlogicaldrive(controller) + + if changes: + # self.rdmc.ui.printer( + # "DeleteVolume path and payload: %s, %s\n" + # % (controller["@odata.id"], controller) + # ) + put_path = controller["@odata.id"] + if "settings" not in put_path: + put_path = put_path + "settings/" + controller["@odata.id"] = put_path + self.rdmc.app.put_handler( + put_path, + controller, + headers={"If-Match": self.getetag(controller["@odata.id"])}, + ) + self.rdmc.app.download_path([controller["@odata.id"]], path_refresh=True, crawl=False) + + def lastlogicaldrive(self, controller): + """Special case that sets required properties after last drive deletion + + :param controller: controller change settings on + :type controller: dict. + """ + changelist = [ + "PredictiveSpareRebuild", + "SurfaceScanAnalysisPriority", + "FlexibleLatencySchedulerSetting", + "DegradedPerformanceOptimization", + "CurrentParallelSurfaceScanCount", + "SurfaceScanAnalysisDelaySeconds", + "MonitorAndPerformanceAnalysisDelaySeconds", + "InconsistencyRepairPolicy", + "DriveWriteCache", + "ExpandPriority", + "EncryptionEULA", + "NoBatteryWriteCache", + "ReadCachePercent", + "WriteCacheBypassThresholdKiB", + "RebuildPriority", + "QueueDepth", + "ElevatorSort", + ] + + for item in changelist: + if item in list(controller.keys()): + controller[item] = None + + def inputaccept(self, drivename): + while True: + if drivename is None: + ans = input("Are you sure you would" " like to continue deleting all volumes" "? (y/n)") + else: + ans = input("Are you sure you would" " like to continue deleting volume" " %s? (y/n)" % drivename) + + if ans.lower() == "y": + return 1 + elif ans.lower() == "n": + self.rdmc.ui.printer("Stopping command without deleting volume/volumes.\n") + return 0 + + def storagedeletevolume(self, logical_drives, drivelist, allopt, force): + if allopt: + sorted_dict = sorted(logical_drives, reverse=True) + if not force: + if not self.inputaccept(None): + return + for drive in sorted_dict: + self.rdmc.app.delete_handler(logical_drives[drive]["@odata.id"], {}) + else: + found = False + sorted_dict = sorted(logical_drives, reverse=True) + sorted_drivelist = sorted(drivelist, reverse=True) + for deldrive in sorted_drivelist: + found = False + changes = False + for drive in sorted_dict: + if logical_drives[drive]["Id"] == deldrive: + if not force: + if not self.inputaccept(logical_drives[drive]["Name"]): + return + self.rdmc.ui.printer("Setting volume %s " "for deletion\n" % logical_drives[drive]["Name"]) + changes = True + found = True + break + try: + if changes: + # self.rdmc.ui.printer( + # "DeleteVolume path and payload: %s, %s\n" + # % (logical_drives[drive]["@odata.id"], "{}") + # ) + self.rdmc.app.delete_handler(logical_drives[drive]["@odata.id"], {}) + except: + self.rdmc.ui.printer( + "This volume cannot" + "be deleted without deleting " + "the higher volumes in the same Physical drive first.\n" + ) + raise + if not found: + raise NoContentsFoundForOperationError("Logical " "drive not found.") + + def getetag(self, path): + """get etag from path""" + etag = None + instance = self.rdmc.app.monolith.path(path) + if instance: + etag = ( + instance.resp.getheader("etag") + if "etag" in instance.resp.getheaders() + else instance.resp.getheader("ETag") + ) + return etag + + def deletevolumevalidation(self, options): + """delete volume validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller " "using either the slot number or index.", + default=None, + ) + customparser.add_argument( + "--storageid", + dest="storageid", + help="Use this flag to select the corresponding storageid in iLO6 only.", + default=None, + ) + customparser.add_argument( + "--all", + dest="all", + help="""Use this flag to delete all volumes on a controller.""", + action="store_true", + default=False, + ) + customparser.add_argument( + "--force", + dest="force", + help="""Use this flag to override the "are you sure?" text when deleting a volume.""", + action="store_true", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/DriveSanitizeCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/DriveSanitizeCommand.py new file mode 100644 index 0000000..7881a0a --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/DriveSanitizeCommand.py @@ -0,0 +1,537 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Drive Erase/ Sanitize Command for rdmc""" + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class DriveSanitizeCommand: + """Drive erase/sanitize command""" + + def __init__(self): + self.ident = { + "name": "drivesanitize", + "usage": None, + "description": "To sanitize a physical drive " + 'by index.\n\texample: drivesanitize "1I:1:1" --controller=1\n\n\tTo' + " sanitize multiple drives by specifying location.\n\texample: " + "drivesanitize 1I:1:1,1I:1:2 --controller=1 --mediatype HDD/SSD\n\texample: drivesanitize " + "1I:1:1,1I:1:2 --controller=Slot 1 --mediatype=HDD " + "if incorrect mediatype is specified, error will generated " + "For iLO6, storage id need to be specified using --storageid=DE00E000 along with --controller=1\n" + "Once drivesanitize function is performed in iLO6, It may take a while to complete.\n " + "To check the status of Sanitization, perform to following command-\n" + "drivesanitize 1I:1:1 --controller=1 --storageid=DE00900 --status\n" + "Once the process in 100% complete, use the --drivereset tag to reset the drive. Example-\n" + "drivesanitize 1I:1:1 --controller=1 --storageid=DE00900 --drivereset\n", + "summary": "Erase/Sanitize physical drive(s)", + "aliases": ["DriveEraseCommand"], + "auxcommands": ["SelectCommand", "RebootCommand", "StorageControllerCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main disk inventory worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.drivesanitizevalidation(options) + + ilo_ver = self.rdmc.app.getiloversion() + + if ilo_ver < 6.110: + try: + controllers = self.auxcommands["storagecontroller"].controllers(options, single_use=True) + if len(controllers) == 0: + if not options.storageid: + raise InvalidCommandLineError( + "--storageid option is mandatory. Please input storageid as well so that " + "controllers/volumes can be identified.\n" + ) + ilo_ver = 6.110 + except: + if not options.storageid: + raise InvalidCommandLineError( + "--storageid option is mandatory. Please input storageid as well so that " + "controllers/volumes can be identified.\n" + ) + + if ilo_ver >= 6.110: + if not options.storageid: + raise InvalidCommandLineError( + "--storageid option is mandatory for iLO6" " along with --controller option.\n" + ) + self.auxcommands["select"].selectfunction("StorageController") + content = self.rdmc.app.getprops() + controllers = self.auxcommands["storagecontroller"].storagecontroller(options, single_use=True) + if controllers: + for controller in controllers: + if ( + getattr(options, "controller", False) == controller + or controllers[controller]["Location"]["PartLocation"]["ServiceLabel"][-1] + == getattr(options, "controller", False)[-1] + ): + controller_physicaldrives = self.auxcommands["storagecontroller"].storagephysical_drives( + options, + options.controller, + options.storageid, + single_use=True, + ) + else: + self.auxcommands["select"].selectfunction("SmartStorageConfig") + content = self.rdmc.app.getprops() + controllers = self.auxcommands["storagecontroller"].controllers(options, single_use=True) + if controllers: + for controller in controllers: + if int(controller) == int(options.controller): + controller_physicaldrives = self.auxcommands["storagecontroller"].physical_drives( + options, controllers[controller], single_use=True + ) + + if not args and not options.all: + raise InvalidCommandLineError("You must include a physical drive to sanitize.") + elif not options.controller: + raise InvalidCommandLineError("You must include a controller to select.") + else: + if len(args) > 1: + physicaldrives = args + elif len(args) == 1: + physicaldrives = args[0].replace(", ", ",").split(",") + else: + physicaldrives = None + + controllist = [] + + try: + if ilo_ver >= 6.110: + if options.controller.isdigit(): + slotlocation = self.storageget_location_from_id(options.controller, options.storageid) + if slotlocation: + slotcontrol = slotlocation.lower().strip('"').split("slot")[-1].lstrip().strip("=") + for control in controllers.values(): + if "Location" in control and slotcontrol.lower() == control["Location"]["PartLocation"][ + "ServiceLabel" + ].lower().split("slot")[-1].lstrip().strip("="): + controllist.append(control) + elif "slot" in options.controller.lower(): + slotlocation = options.controller + if slotlocation: + slotcontrol = slotlocation.lower().strip('"').split("slot")[-1].lstrip().strip("=") + for control in controllers.values(): + if "Location" in control and slotcontrol.lower() == control["Location"]["PartLocation"][ + "ServiceLabel" + ].lower().split("slot")[-1].lstrip().strip("="): + controllist.append(control) + if not controllist: + raise InvalidCommandLineError("") + else: + if options.controller.isdigit(): + slotlocation = self.get_location_from_id(options.controller) + if slotlocation: + slotcontrol = slotlocation.lower().strip('"').split("slot")[-1].lstrip() + for control in content: + if slotcontrol.lower() == control["Location"].lower().split("slot")[-1].lstrip(): + controllist.append(control) + if not controllist: + raise InvalidCommandLineError("") + except InvalidCommandLineError: + raise InvalidCommandLineError("Selected controller not found in the current inventory " "list.") + + if ilo_ver >= 6.110: + if options.status: + if options.all: + for drive in controller_physicaldrives.values(): + pdrive_inilo = self.convertloc(drive["PhysicalLocation"]["PartLocation"]["ServiceLabel"]) + if len(drive["Operations"]) != 0: + self.rdmc.ui.printer( + "The drive is in %s state, %s percent complete.\n" + % ( + drive["Operations"][0]["OperationName"], + drive["Operations"][0]["PercentageComplete"], + ) + ) + if int(drive["Operations"][0]["PercentageComplete"]) == 100: + self.rdmc.ui.printer("You can reset %s using --drivereset now.\n" % (pdrive_inilo)) + elif len(drive["Operations"]) == 0: + mr_ct = drive["Status"]["State"] + self.rdmc.ui.printer("Sanitization is completed and updated drive status = %s\n" % (mr_ct)) + else: + self.rdmc.ui.printer("Sanitization failed for drive %s.\n" % (pdrive_inilo)) + else: + for drive in controller_physicaldrives.values(): + pdrive_inilo = self.convertloc(drive["PhysicalLocation"]["PartLocation"]["ServiceLabel"]) + for userdrive in physicaldrives: + if userdrive == pdrive_inilo: + if len(drive["Operations"]) != 0: + self.rdmc.ui.printer( + "The drive is in %s state, %s percent complete.\n" + % ( + drive["Operations"][0]["OperationName"], + drive["Operations"][0]["PercentageComplete"], + ) + ) + if int(drive["Operations"][0]["PercentageComplete"]) == 100: + self.rdmc.ui.printer("You can reset %s using --drivereset now.\n" % (userdrive)) + elif len(drive["Operations"]) == 0: + mr_ct = drive["Status"]["State"] + self.rdmc.ui.printer( + "Sanitization is completed and updated drive status = %s\n" % (mr_ct) + ) + + else: + self.rdmc.ui.printer("Sanitization failed for drive %s.\n" % (userdrive)) + elif options.drivereset: + if options.all: + for drive in controller_physicaldrives.values(): + pdrive_inilo = self.convertloc(drive["PhysicalLocation"]["PartLocation"]["ServiceLabel"]) + if "Actions" in drive: + if "#Drive.Reset" in drive["Actions"]: + path = drive["@odata.id"] + "/Actions/Drive.Reset" + contentsholder = {"ResetType": "ForceOn"} + self.rdmc.ui.printer("DriveReset path and payload: %s, %s\n" % (path, contentsholder)) + self.rdmc.app.post_handler(path, contentsholder) + else: + if len(drive["Operations"]) != 0: + self.rdmc.ui.printer( + "Sanitization is in progress for drive %s. Use --status to check it's status.\n" + % (pdrive_inilo) + ) + else: + self.rdmc.ui.printer( + "Drive sanitization is not being performed on %s\n" % (pdrive_inilo) + ) + else: + self.rdmc.ui.printer("Drive sanitization is not being performed on %s\n" % (pdrive_inilo)) + else: + for drive in controller_physicaldrives.values(): + pdrive_inilo = self.convertloc(drive["PhysicalLocation"]["PartLocation"]["ServiceLabel"]) + for userdrive in physicaldrives: + if userdrive == pdrive_inilo: + if "Actions" in drive: + if "#Drive.Reset" in drive["Actions"]: + path = drive["@odata.id"] + "/Actions/Drive.Reset" + contentsholder = {"ResetType": "ForceOn"} + self.rdmc.ui.printer( + "DriveReset path and payload: %s, %s\n" % (path, contentsholder) + ) + self.rdmc.app.post_handler(path, contentsholder) + else: + if len(drive["Operations"]) != 0: + self.rdmc.ui.printer( + "Santization is in progress for drive %s. Use --status to check its " + "status.\n" % (pdrive_inilo) + ) + else: + self.rdmc.ui.printer( + "Drive sanitization is not being performed on %s\n" % (pdrive_inilo) + ) + else: + self.rdmc.ui.printer( + "Drive sanitization is not being performed on %s\n" % (pdrive_inilo) + ) + else: + if self.storagesanitizedrives(physicaldrives, controller_physicaldrives, options.all): + if options.reboot: + self.auxcommands["reboot"].run("ColdBoot") + self.rdmc.ui.printer("Preparing for sanitization...\n") + self.monitorsanitization() + + else: + if options.status is True or options.drivereset is True: + raise InvalidCommandLineError("--status and --drivereset options are not supported on iLO 5\n") + if self.sanitizedrives( + controllist, + physicaldrives, + controller_physicaldrives, + options.mediatype, + options.all, + ): + if options.reboot: + self.auxcommands["reboot"].run("ColdBoot") + self.rdmc.ui.printer("Preparing for sanitization...\n") + self.monitorsanitization() + else: + self.rdmc.ui.printer("Sanitization will occur on the next system reboot.\n") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def get_location_from_id(self, controller_id): + for sel in self.rdmc.app.select("SmartStorageArrayController", path_refresh=True): + if "Collection" not in sel.maj_type: + controller = sel.dict + if controller["Id"] == str(controller_id): + return controller["Location"] + return None + + def storageget_location_from_id(self, controller_id, storage_id): + for sel in self.rdmc.app.select("StorageController", path_refresh=True): + if "Collection" not in sel.maj_type and storage_id in sel.dict["@odata.id"]: + controller = sel.dict + if controller["Id"] == str(controller_id): + return controller["Location"]["PartLocation"]["ServiceLabel"] + return None + + def sanitizedrives(self, controllist, drivelist, controller_drives, mediatype, optall): + """Gets drives ready for sanitization + + :param controllist: list of controllers + :type controllist: list. + :param drivelist: physical drives to sanitize + :type drivelist: list. + :param optall: flag for sanitizing all drives + :type optall: bool. + """ + sanitizedrivelist = [] + logicaldrivelist = [] + changes = False + + for controller in controllist: + pdrivelist = [x["DataDrives"] for x in controller["LogicalDrives"]] + + for plist in pdrivelist: + for drive in plist: + logicaldrivelist.append(drive) + + if optall: + sanitizedrivelist = [x["Location"] for x in controller["PhysicalDrives"]] + else: + for erasedrive in drivelist: + try: + for idx, pdrive in enumerate(controller["PhysicalDrives"]): + if erasedrive == pdrive["Location"]: + if pdrive["Location"] in logicaldrivelist: + raise InvalidCommandLineError( + "Unable to" + " sanitize configured drive. Remove" + " any volume(s) associated " + "with drive %s and try again." % pdrive["Location"] + ) + + # Validate Media Type + if not (self.validate_mediatype(erasedrive, mediatype, controller_drives)): + raise InvalidCommandLineError( + "One or more of the drives given does not match the " + "mediatype %s which is specified" % mediatype + ) + self.rdmc.ui.printer( + "Setting physical drive %s " "for sanitization\n" % pdrive["Location"] + ) + sanitizedrivelist.append(pdrive["Location"]) + break + except KeyError as excp: + raise NoContentsFoundForOperationError( + "The property '%s' is missing " "or invalid." % str(excp) + ) + + if sanitizedrivelist: + changes = True + if mediatype == "SSD": + erase_pattern_string = "SanitizeRestrictedBlockErase" + else: + erase_pattern_string = "SanitizeRestrictedOverwrite" + + contentsholder = { + "Actions": [ + { + "Action": "PhysicalDriveErase", + "ErasePattern": erase_pattern_string, + "PhysicalDriveList": sanitizedrivelist, + } + ], + "DataGuard": "Disabled", + } + + # self.rdmc.ui.printer( + # "DriveSanitize path and payload: %s, %s\n" + # % (controller["@odata.id"], contentsholder) + # ) + patch_path = controller["@odata.id"] + if "settings" not in patch_path: + patch_path = patch_path + "settings/" + controller["@odata.id"] = patch_path + + self.rdmc.app.patch_handler(controller["@odata.id"], contentsholder) + + return changes + + def convertloc(self, servicelabel): + loc = servicelabel.split(":") + temp_str = str(loc[1].split("=")[1] + ":" + loc[2].split("=")[1] + ":" + loc[3].split("=")[1]) + return temp_str + + def storagesanitizedrives(self, drivelist, controller_drives, optall): + sanitizedrivelist = [] + changes = False + plocation = "" + if len(controller_drives) != 0: + for plist in controller_drives.values(): + logicaldrivelist = [] + logicaldrivelist.extend(plist["Links"]["Volumes"]) + plocation = self.convertloc(plist["PhysicalLocation"]["PartLocation"]["ServiceLabel"]) + + if optall: + if logicaldrivelist: + self.rdmc.ui.printer( + "Unable to" + " sanitize drive %s. Remove" + " any volume(s) associated " + "with this drive and try again.\n" % plocation + ) + break + sanitizedrivelist.append(plocation) + else: + for erasedrive in drivelist: + try: + if erasedrive == plocation: + if logicaldrivelist: + self.rdmc.ui.printer( + "Unable to" + " sanitize drive %s. Remove" + " any volume(s) associated " + "with this drive and try again.\n" % plocation + ) + self.rdmc.ui.printer("Setting physical drive %s " "for sanitization\n" % plocation) + sanitizedrivelist.append(plocation) + break + except KeyError as excp: + raise NoContentsFoundForOperationError( + "The property '%s' is missing " "or invalid.\n" % str(excp) + ) + + if sanitizedrivelist: + changes = True + path = plist["@odata.id"] + "/Actions/Drive.SecureErase" + contentsholder = {} + + self.rdmc.ui.printer("DriveSanitize path and payload: %s, %s\n" % (path, contentsholder)) + + self.rdmc.app.post_handler(path, contentsholder) + sanitizedrivelist = [] + self.rdmc.ui.printer("You can check the sanitization status of %s using --status\n" % (plocation)) + + return changes + + def validate_mediatype(self, erasedrive, mediatype, controller_drives): + """validates media type as HDD or SSD""" + for idx in list(controller_drives.keys()): + phy_drive = controller_drives[idx] + if phy_drive["Location"] == erasedrive and phy_drive["MediaType"] == mediatype: + return True + return False + + def monitorsanitization(self): + """monitors sanitization percentage""" + # TODO: Add code to give updates on sanitization + + def drivesanitizevalidation(self, options): + """drive sanitize validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller " "using either the slot number or index.", + default=None, + ) + customparser.add_argument( + "--storageid", + dest="storageid", + help="Use this flag to select the corresponding storageid in iLO6 only.", + default=None, + ) + customparser.add_argument( + "--mediatype", + dest="mediatype", + help="""Use this flag to select the mediatype of the hard disk """, + default=None, + required=True, + ) + customparser.add_argument( + "--reboot", + dest="reboot", + help="Include this flag to perform a coldboot command " + "function after completion of operations and monitor " + "sanitization.", + action="store_true", + default=False, + ) + customparser.add_argument( + "--all", + dest="all", + help="""Use this flag to sanitize all physical drives on a """ """controller.""", + action="store_true", + default=False, + ) + customparser.add_argument( + "--drivereset", + dest="drivereset", + help="""Use this flag to reset physical drives on a """ """controller.""", + action="store_true", + default=False, + ) + customparser.add_argument( + "--status", + dest="status", + help="""Use this flag to check sanitization status of a """ """controller.""", + action="store_true", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/FactoryResetControllerCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/FactoryResetControllerCommand.py new file mode 100644 index 0000000..3d53557 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/FactoryResetControllerCommand.py @@ -0,0 +1,223 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Factory Reset Controller Command for rdmc""" + + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class FactoryResetControllerCommand: + """Factory reset controller command""" + + def __init__(self): + self.ident = { + "name": "factoryresetcontroller", + "usage": None, + "description": "Run without " + "arguments for the current controller options.\n\texample: " + "factoryresetcontroller\n\n\tTo factory reset a controller " + "by index.\n\texample: factoryresetcontroller --controller=2" + '\n\texample: factoryresetcontroller --controller="Slot 1" \n\n' + "\tTo factory reset a controller on Gen11 server\n" + "\texample: factoryresetcontroller --reset_type resetall --storageid DE000100\n" + "\texample: factoryresetcontroller --reset_type preservevolumes --storageid DE000100\n", + "summary": "Factory resets a controller by index or location.", + "aliases": [], + "auxcommands": ["SelectCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main disk inventory worker function + + :param line: command line input + :type line: string. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.frcontrollervalidation(options) + + ilo_ver = self.rdmc.app.getiloversion() + if ilo_ver >= 6.110: + if not options.storageid: + raise InvalidCommandLineError( + "--storageid option is mandatory for iLO6 along with --controller option.\n" + ) + if ( + (options.controller is not None) + and (options.storageid is not None) + and (options.reset_type is not None) + ): + raise InvalidCommandLineError("--controller is not supported in iLO6.") + + elif (options.storageid is not None) and (options.reset_type is not None): + st_content = [] + self.auxcommands["select"].selectfunction("StorageCollection.") + get_content = self.rdmc.app.getprops() + for st_controller in get_content: + path = st_controller["Members"] + for mem in path: + for val in mem.values(): + if "DE" in val and options.storageid in val: + getval = self.rdmc.app.get_handler(val, silent=True, service=True).dict + if options.storageid: + if getval["Id"] == options.storageid: + st_content.append(getval) + else: + raise InvalidCommandLineError( + "Selected storage id not found in the current inventory " "list." + ) + else: + st_content.append(getval) + for st_controller in st_content: + path = st_controller["Actions"]["#Storage.ResetToDefaults"]["target"] + + if options.reset_type.lower() == "resetall": + actionitem = "ResetAll" + body = {"ResetType": actionitem} + elif options.reset_type.lower() == "preservevolumes": + actionitem = "PreserveVolumes" + body = {"ResetType": actionitem} + else: + raise InvalidCommandLineError("Invalid command.") + + # self.rdmc.ui.printer( + # "FactoryReset path and payload: %s, %s\n" + # % (path, body) + # ) + self.rdmc.app.post_handler(path, body) + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + elif (options.storageid is None) or (options.reset_type is None): + raise InvalidCommandLineError("Invalid command\n" "--reset_type and --storageid is required") + + else: + self.auxcommands["select"].selectfunction("SmartStorageConfig.") + content = self.rdmc.app.getprops() + if options.controller and options.reset_type is None and options.storageid is None: + controllist = [] + + try: + if options.controller and options.controller.isdigit(): + slotlocation = self.get_location_from_id(options.controller) + if slotlocation: + slotcontrol = slotlocation.lower().strip('"').split("slot")[-1].lstrip() + for control in content: + if slotcontrol.lower() == control["Location"].lower().split("slot")[-1].lstrip(): + controllist.append(control) + # else: + # self.parser.print_help() + # return ReturnCodes.SUCCESS + if not controllist: + raise InvalidCommandLineError("") + except InvalidCommandLineError: + raise InvalidCommandLineError("Selected controller not found in the current inventory " "list.") + for controller in controllist: + contentsholder = { + "Actions": [{"Action": "FactoryReset"}], + "DataGuard": "Disabled", + } + self.rdmc.ui.printer( + "FactoryReset path and payload: %s, %s\n" % (controller["@odata.id"], contentsholder) + ) + self.rdmc.app.patch_handler(controller["@odata.id"], contentsholder) + + elif options.controller and (options.reset_type is not None) or (options.storageid is not None): + raise InvalidCommandLineError( + "Invalid command\n" "--reset_type and --storageid is not supported in iLO5" + ) + + for idx, val in enumerate(content): + self.rdmc.ui.printer("[%d]: %s\n" % (idx, val["Location"])) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def get_location_from_id(self, controller_id): + for sel in self.rdmc.app.select("SmartStorageArrayController", path_refresh=True): + if "Collection" not in sel.maj_type: + controller = sel.dict + if controller["Id"] == str(controller_id): + return controller["Location"] + return None + + def frcontrollervalidation(self, options): + """Factory reset controller validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller " "using either the slot number or index.", + default=None, + ) + + customparser.add_argument( + "--reset_type", + dest="reset_type", + help="Use this flag to pass the reset type for storage controller in iLO6 ", + default=None, + ) + + customparser.add_argument( + "--storageid", + dest="storageid", + help="Use this flag to select the corresponding storage id in iLO6", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/StorageControllerCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/StorageControllerCommand.py new file mode 100644 index 0000000..961af60 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/StorageControllerCommand.py @@ -0,0 +1,2200 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Storage Controller Command for rdmc""" +import json +import sys +from argparse import RawDescriptionHelpFormatter + +import redfish + +try: + from rdmc_helper import ( + UI, + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + UI, + InvalidFileInputError, + Encryption, + ) + +from redfish.ris.resp_handler import ResponseHandler + +# from redfish.ris.utils import iterateandclear + +__config_file__ = "storagecontroller_config.json" + +# from rdmc_base_classes import HARDCODEDLIST + +__subparsers__ = ["load", "save", "state"] + + +class StorageControllerCommand: + """Storage controller command""" + + def __init__(self): + self.ident = { + "name": "storagecontroller", + "usage": None, + "description": "\tTo Manage PLDM for RDE storage devices. For other storage devices (i.e NVMe)," + "use the `select type/get/set` paradigm or raw commands.\n\n" + "\tRun without arguments for the " + "current list of Storage controllers.\n\texample: " + "storagecontroller\n\n\tTo get more details on a specific controller " + "select it by index Note: On ILO6, --storageid is mandatory along with --controller.\n\t" + "example: storagecontroller --storageid=DE00E000 --controller=2" + "\n\n\tTo get more details on a specific controller select " + 'it by location.\n\texample: storagecontroller --storageid=DE00E000 --controller "Slot 0"' + "\n\n\tIn order to get a list of all physical drives for " + "each controller.\n\texample: storagecontroller --physicaldrives" + "\n\n\tTo obtain details about physical drives for a " + "specific controller.\n\texample: storagecontroller --storageid=DE00E000 --controller=3 " + "--physicaldrives\n\n\tTo obtain details about a specific " + "physical drive for a specific controller.\n\texample: storagecontroller " + "--storageid=DE00E000 --controller=3 --pdrive=1I:1:1\n\n\tIn order to get a list of " + "all volumes for the each controller.\n\texample: " + "storagecontroller --logicaldrives\n\n\tTo obtain details about " + "volumes for a specific controller.\n\texample: " + "storagecontroller --storageid=DE00E000 --controller=3 --logicaldrives\n\n\tTo obtain " + "details about a specific volume for a specific " + "controller.\n\texample: storagecontroller --storageid=DE00E000 --controller=3 --ldrive=1\n\n\tTo obtain " + "details about a specific Storage id for a specific " + "controllers.\n\texample: storagecontroller --storageid=DE00E000\n", + "summary": "Discovers all storage controllers installed in the " "server and managed by the SmartStorage.", + "aliases": ["smartarray"], + "auxcommands": ["SelectCommand"], + } + self.config_file = None + self.fdata = None + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def file_handler(self, filename, operation, options, data=None, sk=None): + """ + Wrapper function to read or write data to a respective file + + :param data: data to be written to output file + :type data: container (list of dictionaries, dictionary, etc.) + :param file: filename to be written + :type file: string (generally this should be self.clone_file or tmp_clone_file + :param operation: file operation to be performed + :type operation: string ('w+', 'a+', 'r+') + :param sk: sort keys flag + :type sk: boolean + :param options: command line options + :type options: attribute + :returns: json file data + """ + writeable_ops = ["w", "w+", "a", "a+"] + fdata = None + + try: + if operation in writeable_ops: + if getattr(options, "encryption", False): + with open(filename, operation + "b") as outfile: + outfile.write( + Encryption().encrypt_file( + json.dumps( + data, + indent=2, + cls=redfish.ris.JSONEncoder, + sort_keys=sk, + ), + getattr(options, "encryption", False), + ) + ) + else: + with open(filename, operation) as outfile: + outfile.write( + json.dumps( + data, + indent=2, + cls=redfish.ris.JSONEncoder, + sort_keys=sk, + ) + ) + else: + if getattr(options, "encryption", False): + with open(filename, operation + "b") as file_handle: + fdata = json.loads( + Encryption().decrypt_file( + file_handle.read(), + getattr(options, "encryption", False), + ) + ) + else: + with open(filename, operation) as file_handle: + fdata = json.loads(file_handle.read()) + return fdata + except Exception as excp: + raise InvalidFileInputError( + "Unable to open file: %s.\nVerify the file location " "and the file has a valid JSON format.\n" % excp + ) + + def controller_id(self, options): + """ + Get iLO types from server and save storageclone URL get Controller + :parm options: command line options + :type options: attribute + :returns: returns list + """ + self.auxcommands["select"].selectfunction("StorageControllerCollection.") + ctr_content = self.rdmc.app.getprops() + ctrl_data = [] + all_ctrl = dict() + for ct_controller in ctr_content: + path = ct_controller["Members"] + for i in path: + res = i["@odata.id"] + ctrl_data.append(res) + ctrl_id_url = res + "?$expand=." + get_ctr = self.rdmc.app.get_handler(ctrl_id_url, silent=True, service=True).dict + _ = get_ctr["@odata.id"].split("/") + get_ctr = self.rdmc.app.removereadonlyprops(get_ctr, False, True) + all_ctrl[get_ctr["Name"]] = get_ctr + return all_ctrl + + def get_volume(self, options): + """ + Get iLO types from server and save storageclone URL get volumes + :parm options: command line options + :type options: attribute + :returns: returns list + """ + self.auxcommands["select"].selectfunction("VolumeCollection.") + vol_content = self.rdmc.app.getprops() + vol_data = [] + all_vol = dict() + for st_volume in vol_content: + path = st_volume["Members"] + for i in path: + res = i["@odata.id"] + if self.rdmc.opts.verbose: + sys.stdout.write("Saving properties of type %s \t\n" % res) + vol_data.append(res) + vol_id_url = res + "?$expand=." + get_vol = self.rdmc.app.get_handler(vol_id_url, silent=True, service=True).dict + # print("Assigned drive", get_vol["Links"]["Drives"]) + get_vol = self.rdmc.app.removereadonlyprops(get_vol, False, True) + all_vol[get_vol["Name"]] = get_vol + return all_vol + + def run(self, line, help_disp=False): + """Main sstorage controller worker function + :param help_disp: command line input + :type help_disp: bool. + :param line: command line input + :type line: string. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + ident_subparser = False + for cmnd in __subparsers__: + if cmnd in line: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + ident_subparser = True + break + if not ident_subparser: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.storagecontrollervalidation(options) + ilo_ver = self.rdmc.app.getiloversion() + + if options.command == "state": + if not options.storageid: + raise InvalidCommandLineError("Please provide --storageid option.\n") + if ilo_ver >= 6: + storage_ctlr = self.storagecontroller(options, print_ctrl=False, single_use=True) + for storage in storage_ctlr: + if storage_ctlr[storage].get("@Redfish.Settings"): + time = storage_ctlr[storage]["@Redfish.Settings"].get("Time", "Not Available") + sys.stdout.write("Last Configuration Attempt: %s\n" % str(time)) + for message in storage_ctlr[storage]["@Redfish.Settings"].get("Messages", []): + ResponseHandler( + self.rdmc.app.validationmanager, + self.rdmc.app.typepath.defs.messageregistrytype, + ).message_handler( + response_data=message, + message_text="", + verbosity=2, + dl_reg=False, + ) + else: + sys.stdout.write( + "Previous storage controller configuration status messages are " + "not available for controller '%s'\n" % (storage) + ) + ctrl_data = dict() + ctrl_data.update({"Location": storage_ctlr[storage].get("Location", "Unknown")}) + ctrl_data.update({"Model": storage_ctlr[storage].get("Model", "Unknown")}) + UI().print_out_json(ctrl_data) + + else: + controllers = self.controllers(options, print_ctrl=False, single_use=True) + for controller in controllers: + if controllers[controller].get("@Redfish.Settings"): + time = controllers[controller]["@Redfish.Settings"].get("Time", "Not Available") + sys.stdout.write("Last Configuration Attempt: %s\n" % str(time)) + for message in controllers[controller]["@Redfish.Settings"].get("Messages", []): + ResponseHandler( + self.rdmc.app.validationmanager, + self.rdmc.app.typepath.defs.messageregistrytype, + ).message_handler( + response_data=message, + message_text="", + verbosity=2, + dl_reg=False, + ) + else: + sys.stdout.write( + "Previous storage controller configuration status messages are " + "not available for controller '%s'\n" % (controller) + ) + ctrl_data = dict() + ctrl_data.update({"Location": controllers[controller].get("Location", "Unknown")}) + ctrl_data.update({"Model": controllers[controller].get("Model", "Unknown")}) + UI().print_out_json(ctrl_data) + + if options.command == "save": + if ilo_ver >= 6.110: + storage = {} + all_stgcntrl = {} + st_content = self.rdmc.app.get_handler("/redfish/v1/Systems/1/Storage/", silent=True, service=True).dict + sel = st_content["Members"] + for i in sel: + res = i["@odata.id"] + if "DE" in res: + storage_id_url = res + "?$expand=." + getval = self.rdmc.app.get_handler(storage_id_url, silent=True, service=True).dict + vol = self.get_volume(options) + ctr = self.controller_id(options) + getval = self.rdmc.app.removereadonlyprops(getval, False, True) + storage.update(getval) + getval["Controllers"]["Members"].append(ctr) + del getval["Controllers"]["Members"][0] + getval["Volumes"]["Members"].append(vol) + del getval["Volumes"]["Members"][0] + all_stgcntrl[getval["Id"]] = getval + self.file_handler(self.config_file, "w", options, all_stgcntrl, sk=True) + sys.stdout.write("Storage Controller configuration saved to '%s'.\n" % self.config_file) + else: + st_content = self.rdmc.app.get_handler("/redfish/v1/Systems/1/Storage/", silent=True, service=True).dict + sel = st_content["Members"] + for i in sel: + res = i["@odata.id"] + if "DE" in res: + break + else: + continue + storage = {} + all_stgcntrl = {} + storage_id_url = res + "?$expand=." + getval = self.rdmc.app.get_handler(storage_id_url, silent=True, service=True).dict + vol = self.get_volume(options) + ctr = self.controller_id(options) + getval = self.rdmc.app.removereadonlyprops(getval, False, True) + storage.update(getval) + getval["Controllers"]["Members"].append(ctr) + del getval["Controllers"]["Members"][0] + getval["Volumes"]["Members"].append(vol) + del getval["Volumes"]["Members"][0] + all_stgcntrl[getval["Id"]] = getval + self.file_handler(self.config_file, "w", options, all_stgcntrl, sk=True) + sys.stdout.write("Storage Controller configuration saved to '%s'.\n" % self.config_file) + controllers = self.controllers(options, print_ctrl=False, single_use=True) + for key, controller in controllers.items(): + physical_drives = self.physical_drives(options, controller, print_ctrl=False, single_use=True) + logical_drives = self.logical_drives(options, controller, print_ctrl=False, single_use=True) + if self.rdmc.app.typepath.defs.isgen10: + for drivec_idx, drivec in enumerate(controller.get("PhysicalDrives", [])): + for drive in physical_drives: + if drivec["Location"] == physical_drives[drive]["Location"]: + controller["PhysicalDrives"][drivec_idx].update(physical_drives[drive]) + break + for drivec_idx, drivec in enumerate(controller.get("LogicalDrives", [])): + for drive in logical_drives: + if drivec["LogicalDriveNumber"] == logical_drives[drive]["Id"]: + controller["LogicalDrives"][drivec_idx].update(logical_drives[drive]) + break + if controller.get("Links"): + controller["Links"]["LogicalDrives"] = logical_drives + controller["Links"]["PhysicalDrives"] = physical_drives + else: + controller["links"]["LogicalDrives"] = logical_drives + controller["links"]["PhysicalDrives"] = physical_drives + + self.file_handler(self.config_file, "w", options, controllers, sk=True) + sys.stdout.write("Storage Controller configuration saved to '%s'.\n" % self.config_file) + + if options.command == "load": + if ilo_ver >= 6.110: + storage_load = self.file_handler(self.config_file, operation="rb", options=options) + for starageid in storage_load: + + controller_id = storage_load[starageid]["Controllers"] + logical_id = storage_load[starageid]["Volumes"] + physical_id = storage_load[starageid]["Drives"] + + if controller_id: + if "Members" in controller_id: + id_controller = "/redfish/v1/Systems/1/Storage/" + starageid + controllers = self.rdmc.app.get_handler(id_controller, silent=True).dict + readonly_removed_controllers = self.storagecontrollerremovereadonly(controllers) + readonly_removed = controller_id + self.load_property_helper(readonly_removed, readonly_removed_controllers, id_controller) + else: + for data in controller_id: + id_controller = controller_id[data].get("@odata.id") + controllers = self.rdmc.app.get_handler(id_controller, silent=True).dict + readonly_removed_controllers = self.storagecontrollerremovereadonly(controllers) + readonly_removed = storage_load["Controllers"] + for controller, val in readonly_removed.items(): + for i in readonly_removed_controllers: + try: + if (i in val) and (controllers[i] != val[i]): + body = {str(i): val[i]} + self.rdmc.app.patch_handler(id_controller, body) + except: + sys.stdout.write("Unable to update the property " "for: '%s'.\n" % i) + continue + if logical_id: + if "Members" in logical_id: + id_logical = "/redfish/v1/Systems/1/Storage/" + starageid + "/Volumes/1" + logicals = self.rdmc.app.get_handler(id_logical, silent=True).dict + readonly_removed_logical = self.storagecontrollerremovereadonly(logicals) + readonly_logical = logical_id + self.load_property_helper(readonly_logical, readonly_removed_logical, id_logical) + else: + for logical_data in logical_id: + id_logical = logical_id[logical_data].get("@odata.id") + logicals = self.rdmc.app.get_handler(id_logical, silent=True).dict + readonly_removed_logical = self.storagecontrollerremovereadonly(logicals) + readonly_logical = storage_load["Volumes"] + for logical, val in readonly_logical.items(): + for lg in readonly_removed_logical: + try: + if (lg in val) and (logicals[lg] != val[lg]): + body = {str(lg): val[lg]} + self.rdmc.app.patch_handler(id_logical, body) + except: + sys.stdout.write("Unable to update the property " "for: '%s'.\n" % lg) + continue + if physical_id: + if "Members" in physical_id: + id_physical = "/redfish/v1/Systems/1/Storage/" + starageid + "/Drives" + physicals = self.rdmc.app.get_handler(id_physical, silent=True).dict + readonly_removed_physical = self.storagecontrollerremovereadonly(physicals) + readonly_physical = physical_id + self.load_property_helper(readonly_physical, readonly_removed_physical, id_physical) + else: + for physical_data in physical_id: + id_physical = physical_id[physical_data].get("@odata.id") + physicals = self.rdmc.app.get_handler(id_physical, silent=True).dict + readonly_removed_physical = self.storagecontrollerremovereadonly(physicals) + readonly_physical = storage_load["Drives"] + for logical, val in readonly_physical.items(): + for lg in readonly_removed_physical: + try: + if (lg in val) and (physicals[lg] != val[lg]): + body = {str(lg): val[lg]} + self.rdmc.app.patch_handler(id_physical, body) + except: + sys.stdout.write("Unable to update the property " "for: '%s'.\n" % lg) + continue + sys.stdout.write( + "Storage Controller configuration loaded. Reboot the server to " "finalize the configuration.\n" + ) + + else: + controllers = self.file_handler(self.config_file, operation="rb", options=options) + + for controller in controllers: + put_path = controllers[controller]["@Redfish.Settings"]["SettingsObject"]["@odata.id"] + if controllers[controller].get("DataGuard"): + controllers[controller]["DataGuard"] = "Disabled" + readonly_removed = self.rdmc.app.removereadonlyprops(controllers[controller]) + readonly_removed["LogicalDrives"] = controllers[controller]["LogicalDrives"] + readonly_removed["PhysicalDrives"] = controllers[controller]["PhysicalDrives"] + # self.rdmc.app.put_handler(controllers[controller]["@odata.id"], readonly_removed) + self.rdmc.app.put_handler(put_path, readonly_removed) + + sys.stdout.write( + "Storage Controller configuration loaded. Reboot the server to " + "finalize the configuration.\n" + ) + elif options.command == "default": + if ilo_ver >= 6: + if options.storageid and not options.controller: + self.storageid(options, print_ctrl=True, single_use=False) + elif options.storageid and options.controller: + self.storagecontroller(options, print_ctrl=True, single_use=False) + elif options.controller and not options.storageid: + raise InvalidCommandLineError( + "with --controller option, --storageid option is mandatory for 'iLO 6' and 'iLO 5'.\n" + ) + elif ( + options.physicaldrives or options.pdrive or options.logicaldrives or options.ldrive + ) and not options.storageid: + raise InvalidCommandLineError("--storageid option is mandatory for 'iLO 6' and 'iLO 5'.\n") + elif ( + not options.storageid + and not options.controller + and not options.physicaldrives + and not options.pdrive + and not options.logicaldrives + and not options.ldrive + ): + self.list_all_storages(options, print_ctrl=True, single_use=False) + else: + self.controllers(options, print_ctrl=True, single_use=False) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def load_property_helper(self, readonly, readonly_removed, id): + for item, val in readonly.items(): + for lg in readonly_removed: + try: + if (lg in val) and (item[lg] != val[lg]): + body = {str(lg): val[lg]} + self.rdmc.app.patch_handler(id, body) + except: + sys.stdout.write("Unable to update the property " "for: '%s'.\n" % lg) + continue + + def list_all_storages(self, options, print_ctrl=False, single_use=False): + # self.auxcommands["select"].selectfunction("StorageCollection.") + st_url = "/redfish/v1/Systems/1/Storage/" + st_content = self.rdmc.app.get_handler(st_url, silent=True, service=True) + if "error" not in st_content.dict: + if st_content.dict["Members@odata.count"] == 0: + raise InvalidCommandLineError("Redfish Enabled Controllers not found in this server.\n") + if st_content.dict["Members"]: + st_content = st_content.dict["Members"] + + if "json" in options and not options.json: + if self.rdmc.opts.verbose: + sys.stdout.write("---------------------------------\n") + sys.stdout.write("List of RDE storage devices\n") + sys.stdout.write("---------------------------------\n") + if options.json: + outjson = dict() + for st_controller in st_content: + for val in st_controller.values(): + if "DE" in val: + st = self.rdmc.app.get_handler(val, silent=True, service=True).dict + health = "" + if "Health" in st["Status"]: + health = st["Status"]["Health"] + elif "HealthRollup" in st["Status"]: + health = st["Status"]["HealthRollup"] + if options.json: + outjson[st["Id"]] = dict() + outjson[st["Id"]]["Name"] = st["Name"] + outjson[st["Id"]]["Health"] = health + outjson[st["Id"]]["State"] = st["Status"]["State"] + else: + sys.stdout.write( + "%s: %s: Health %s: %s\n" % (st["Id"], st["Name"], health, st["Status"]["State"]) + ) + + if options.json: + UI().print_out_json(outjson) + else: + raise InvalidCommandLineError( + "Storage controllers are not ready , Kindly re run the command after sometime\n" + ) + else: + raise InvalidCommandLineError( + "Storage controllers are not ready , " "Kindly re run the command after sometime\n" + ) + + def storagecontrollerremovereadonly(self, controller): + templist = [ + "Ports", + "CacheSummary", + "SupportedRAIDTypes", + "PCIeInterface", + "SupportedControllerProtocols", + "SupportedDeviceProtocols", + "SKU", + "SpeedGbps", + "SerialNumber", + "PartNumber", + "Model", + "@odata.etag", + "Status", + "Manufacturer", + "Location", + "Identifiers", + "FirmwareVersion", + "Name", + "Id", + "@odata.type", + "@odata.id", + "Links", + ] + + remove_data = self.rdmc.app.removereadonlyprops(controller, False, True, templist) + return remove_data + + def odataremovereadonly(self, tmp): + templist = [ + "@odata.etag", + "@odata.type", + "@odata.context", + ] + remove_data = self.rdmc.app.removereadonlyprops(tmp, False, True, templist) + return remove_data + + def controllers(self, options, print_ctrl=False, single_use=False): + """ + Identify/parse volumes (and child properties) of a parent array controller + + :param options: command line options (options.controller is a required attribute) + :type options: object attributes + :param print_ctrl: flag for console print enablement/disablement (default disabled) + :type print_ctrl: bool + :param single_use: singular usage - True or explore mode - False, + returns dictionary of results to calling function (default disabled) - see returns. + :type single_use: bool + :returns: None, dictionary of all controllers identified by 'Id' + """ + no_need_ilo5 = False + if getattr(options, "controller", False): + controller_ident = False + + if ( + not (getattr(options, "json", False)) + and not (getattr(options, "controller", False)) + and print_ctrl + and (options.physicaldrives is None) + and (options.pdrive is None) + and (options.logicaldrives is None) + and (options.ldrive is None) + ): + if not options.storageid and not options.controllers: + sys.stdout.write("---------------------------------\n") + sys.stdout.write("List of RDE storage devices\n") + sys.stdout.write("---------------------------------\n") + + st_flag = False + storage_data = {} + get_contrller = [] + list_storageid = self.rdmc.app.get_handler("/redfish/v1/Systems/1/Storage/", silent=True, service=True) + if "error" not in list_storageid.dict: + if list_storageid.dict["Members@odata.count"] == 0: + raise InvalidCommandLineError("Redfish Enabled Controllers not found in this server.\n") + if len(list_storageid.dict["Members"]) > 0: + list_storageid = list_storageid.dict["Members"] + for val in list_storageid: + storageid_fromurl = val["@odata.id"] + # storageid_fromurl = val.split("/")[-2] + try: + stdout = "DE" in storageid_fromurl and options.storageid in storageid_fromurl + except: + stdout = "DE" in storageid_fromurl + if "DE" in storageid_fromurl and stdout: + st_flag = True + getval = self.rdmc.app.get_handler(storageid_fromurl, silent=True, service=True).dict + try: + controller = getval["Controllers"] + volumes = getval["Volumes"] + list_controllers = self.rdmc.app.get_handler( + controller["@odata.id"], silent=True, service=True + ).dict + list_volumes = self.rdmc.app.get_handler( + volumes["@odata.id"], silent=True, service=True + ).dict + except KeyError: + self.rdmc.ui.printer("Please check controller was Gen11.\n") + return ReturnCodes.NO_CONTENTS_FOUND_FOR_OPERATION + + for ctl in list_controllers["Members"]: + ctl = ctl["@odata.id"] + get_sel = self.rdmc.app.get_handler(ctl, silent=True, service=True) + get_contrller.append(get_sel) + else: + continue + if not st_flag: + raise InvalidCommandLineError( + "Storage ID {} not found or Storage ID is not Redfish enabled " + "and does not have DExxxxxx\n".format(options.storageid) + ) + return + no_need_ilo5 = True + outjson = dict() + for sel in get_contrller: + if "Collection" not in sel.path: + controller = sel.dict + storage_id = controller["@odata.id"].split("/") + storage_id = storage_id[6] + if getattr(options, "controller", False): + if ( + getattr(options, "controller", False) == controller["Id"] + or getattr(options, "controller", False)[-1] + == controller["Location"]["PartLocation"]["ServiceLabel"][-1] + ): + controller_ident = True + else: + continue + if ( + print_ctrl + and not getattr(options, "controller", False) + and not getattr(options, "physicaldrives", False) + and not getattr(options, "pdrive", False) + and not getattr(options, "ldrive", False) + and not getattr(options, "logicaldrives", False) + and options.storageid is None + and not options.json + and not getattr(options, "controllers", False) + ): + health = "" + if "Health" in controller["Status"]: + health = controller["Status"]["Health"] + elif "HealthRollup" in controller["Status"]: + health = controller["Status"]["HealthRollup"] + sys.stdout.write( + "%s: %s: Health %s: %s\n" + % (storage_id, controller["Model"], health, controller["Status"]["State"]) + ) + elif ( + getattr(options, "json", False) + and (options.controller is None) + and (options.physicaldrives is None) + and (options.pdrive is None) + and (options.ldrive is None) + and (options.logicaldrives is None) + and options.storageid is None + and options.json + and not options.controllers + ): + tmp = dict() + tmp["Name"] = controller["Name"] + tmp["State"] = controller["Status"]["Health"] + tmp["Health"] = controller["Status"]["State"] + outjson[storage_id] = tmp + + if ( + not getattr(options, "json", False) + and options.storageid + and not getattr(options, "controllers", False) + and not getattr(options, "physicaldrives", False) + and not getattr(options, "logicaldrives", False) + and not getattr(options, "ldrive", False) + and not getattr(options, "pdrive", False) + and not getattr(options, "controller", False) + and options.command not in "state" + ): + sys.stdout.write("-----------------------------------\n") + sys.stdout.write("Details of Storage %s\n" % options.storageid) + sys.stdout.write("-----------------------------------\n") + sys.stdout.write("\tId: %s\n\t" % getval["Id"]) + sys.stdout.write("Health: %s\n\t" % getval["Status"]["HealthRollup"]) + sys.stdout.write("Name: %s\n\t" % getval["Name"]) + sys.stdout.write("Number of Controllers: %s\n\t" % list_controllers["Members@odata.count"]) + sys.stdout.write("Number of Volumes: %s\n\t" % list_volumes["Members@odata.count"]) + sys.stdout.write("Number of Drives: %s\n\t\n" % getval["Drives@odata.count"]) + if ( + getattr(options, "json", False) + and options.storageid + and options.json + and not options.physicaldrives + and not options.logicaldrives + and not options.ldrive + and not options.pdrive + and not options.controller + and not options.controllers + ): + storage_data["Storage"] = dict() + + storage_data["Storage"].update({"Id": getval["Id"]}) + storage_data["Storage"].update({"Health": getval["Status"]["HealthRollup"]}) + storage_data["Storage"].update({"Name": getval["Name"]}) + storage_data["Storage"].update( + {"Number of Controllers": list_controllers["Members@odata.count"]} + ) + storage_data["Storage"].update({"Number of Volumes": list_volumes["Members@odata.count"]}) + storage_data["Storage"].update({"Number of Drives": getval["Drives@odata.count"]}) + UI().print_out_json(storage_data) + elif ( + # print_ctrl + getattr(options, "json", False) + and (options.controller is not None) + and ( + (options.logicaldrives is not None) + or (options.physicaldrives is not None) + or (options.ldrive is not None) + or (options.pdrive is not None) + ) + ): + pass + + elif ( + getattr(options, "json", False) + and (options.controller is not None) + and ( + (options.logicaldrives is not None) + or (options.physicaldrives is not None) + or (options.ldrive is not None) + or (options.pdrive is not None) + ) + ): + pass + # controllers code with json and without json + elif ( + getattr(options, "controllers", False) + and options.storageid + and options.controllers + and options.json is None + ): + sys.stdout.write("---------------------------------------------------\n") + sys.stdout.write("List of Controllers in this Storage ID %s\n" % options.storageid) + sys.stdout.write("---------------------------------------------------\n") + list_ctr = list_controllers["Members"] + for lst in list_ctr: + l_data = lst["@odata.id"] + get_ctr = self.rdmc.app.get_handler(l_data, silent=True, service=True).dict + sys.stdout.write("\tId: %s\n\t" % get_ctr["Id"]) + sys.stdout.write("Health: %s\n\t" % get_ctr["Status"]["Health"]) + sys.stdout.write("Name: %s\n\t" % get_ctr["Name"]) + sys.stdout.write("FirmwareVersion: %s\n\t" % get_ctr["FirmwareVersion"]) + sys.stdout.write("SerialNumber: %s\n\t\n" % get_ctr["SerialNumber"]) + elif ( + getattr(options, "json", False) + and options.storageid + and print_ctrl + and options.json + and options.controllers + ): + # sys.stdout.write("---------------------------------------------------\n") + # sys.stdout.write("List of Controllers in this Storage ID %s\n" % options.storageid) + # sys.stdout.write("---------------------------------------------------\n") + list_ctr = list_controllers["Members"] + for lst in list_ctr: + l_data = lst["@odata.id"] + get_ctr = self.rdmc.app.get_handler(l_data, silent=True, service=True).dict + ctr_data = dict() + ctr_data["Controllers"] = dict() + ctr_data["Controllers"].update({"Id": get_ctr["Id"]}) + ctr_data["Controllers"].update({"Health": get_ctr["Status"]["Health"]}) + ctr_data["Controllers"].update({"Name": get_ctr["Name"]}) + ctr_data["Controllers"].update({"FirmwareVersion": get_ctr["FirmwareVersion"]}) + ctr_data["Controllers"].update({"SerialNumber": get_ctr["SerialNumber"]}) + UI().print_out_json(ctr_data) + + elif ( + getattr(options, "controller", False) + and print_ctrl + and (options.controller is not None) + and options.json is None + and not options.physicaldrives + and not options.pdrive + and not options.logicaldrives + and not options.ldrive + ): + controller_info = "---------------------------------------------\n" + controller_info += "Controller {} Details on Storage Id {}" "\n".format( + options.controller, storage_id + ) + controller_info += "---------------------------------------------\n" + controller_info += "Id: %s\n" % controller["Id"] + controller_info += "StorageId: %s\n" % storage_id + controller_info += "Name: %s\n" % controller["Name"] + controller_info += "FirmwareVersion: %s\n" % controller["FirmwareVersion"] + controller_info += "Manufacturer: %s\n" % controller["Manufacturer"] + controller_info += "Model: %s\n" % controller["Model"] + controller_info += "PartNumber: %s\n" % controller["PartNumber"] + controller_info += "SerialNumber: %s\n" % controller["SerialNumber"] + try: + controller_info += "SKU: %s\n" % controller["SKU"] + except KeyError: + pass + controller_info += "Status: %s\n" % controller["Status"] + try: + controller_info += ( + "SupportedDeviceProtocols: %s\n" % controller["SupportedDeviceProtocols"] + ) + except KeyError: + pass + try: + controller_info += ( + "SupportedControllerProtocols: %s\n" % controller["SupportedControllerProtocols"] + ) + except KeyError: + pass + self.rdmc.ui.printer(controller_info, verbose_override=True) + + elif getattr(options, "json", False) and (options.controller is not None): + outjson_data = dict() + outjson_data.update({"Id": controller["Id"]}) + outjson_data.update({"StorageId": storage_id}) + outjson_data.update({"Name": controller["Name"]}) + outjson_data.update({"FirmwareVersion": controller["FirmwareVersion"]}) + outjson_data.update({"Manufacturer": controller["Manufacturer"]}) + outjson_data.update({"Model": controller["Model"]}) + outjson_data.update({"PartNumber": controller["PartNumber"]}) + outjson_data.update({"SerialNumber": controller["SerialNumber"]}) + try: + outjson_data.update({"SKU": controller["SKU"]}) + except KeyError: + pass + outjson_data.update({"Status": controller["Status"]}) + try: + outjson_data.update( + {"SupportedDeviceProtocols": controller["SupportedDeviceProtocols"]} + ) + except KeyError: + pass + try: + outjson_data.update( + {"SupportedControllerProtocols": controller["SupportedControllerProtocols"]} + ) + except KeyError: + pass + UI().print_out_json(outjson_data) + + if getattr(options, "logicaldrives", False) or getattr(options, "ldrive", False) or single_use: + self.storagelogical_drives(options, controller, storage_id, print_ctrl) + + if getattr(options, "physicaldrives", False) or getattr(options, "pdrive", False) or single_use: + self.storagephysical_drives(options, controller, storage_id, print_ctrl) + + if single_use: + storage_data[controller["Id"]] = controller + if getattr(options, "controller", False) and not controller_ident and print_ctrl: + raise InvalidCommandLineError( + "Controller in position '%s' was " "not found\n" % (getattr(options, "controller", False)) + ) + if single_use: + return storage_data + + else: + if not no_need_ilo5: + controller_data = {} + for sel in self.rdmc.app.select("SmartStorageArrayController", path_refresh=True): + if "Collection" not in sel.maj_type: + controller = sel.dict + if getattr(options, "controller", False): + if ( + getattr(options, "controller", False) == controller["Id"] + or getattr(options, "controller", False) == controller["Location"] + ): + controller_ident = True + else: + continue + + if self.rdmc.app.typepath.defs.isgen10: + for g10controller in self.rdmc.app.select("SmartStorageConfig.", path_refresh=True): + if g10controller.dict.get("Location") == controller.get("Location"): + id = controller.get("Id") + controller.update(g10controller.dict) + if id: # iLO Bug - overwrite the Id from collection + controller["Id"] = id + break + + if ( + print_ctrl + and (options.controller is None) + and (options.physicaldrives is None) + and (options.pdrive is None) + and (options.ldrive is None) + and (options.logicaldrives is None) + ): + sys.stdout.write( + "[%s]: %s - %s\n" + % ( + controller["Id"], + controller["Location"], + controller["Model"], + ) + ) + elif ( + getattr(options, "json", False) + and (options.controller is None) + and (options.physicaldrives is None) + and (options.pdrive is None) + and (options.ldrive is None) + and (options.logicaldrives is None) + ): + outjson = dict() + outjson["Controllers"] = dict() + outjson["Controllers"][controller["Id"]] = dict() + outjson["Controllers"][controller["Id"]]["Location"] = controller["Location"] + outjson["Controllers"][controller["Id"]]["Model"] = controller["Model"] + UI().print_out_json(outjson) + + elif ( + print_ctrl + and (options.controller is not None) + and ((options.ldrive is not None) or options.pdrive is not None) + ): + pass + + elif ( + getattr(options, "json", False) + and (options.controller is not None) + and ((options.ldrive is not None) or (options.pdrive is not None)) + ): + pass + + elif print_ctrl and (options.controller is not None) and options.json is None: + controller_info = "------------------------------------------------\n" + controller_info += "Controller Info \n" + controller_info += "------------------------------------------------\n" + controller_info += "Id: %s\n" % controller["Id"] + controller_info += "AdapterType: %s\n" % controller["AdapterType"] + controller_info += "DataGuard: %s\n" % controller["DataGuard"] + controller_info += "Description: %s\n" % controller["Description"] + controller_info += "FirmwareVersion: %s\n" % controller["FirmwareVersion"] + controller_info += "Name: %s\n" % controller["Name"] + controller_info += "Model: %s\n" % controller["Model"] + controller_info += "Location: %s\n" % controller["Location"] + # controller_info += "Links: %s\n" % controller["Links"] + controller_info += "SerialNumber: %s\n" % controller["SerialNumber"] + controller_info += "Status: %s\n" % controller["Status"] + self.rdmc.ui.printer(controller_info, verbose_override=True) + + elif getattr(options, "json", False) and (options.controller is not None): + outjson_data = dict() + outjson_data.update({"Id": controller["Id"]}) + outjson_data.update({"AdapterType": controller["AdapterType"]}) + outjson_data.update({"DataGuard": controller["DataGuard"]}) + outjson_data.update({"Description": controller["Description"]}) + outjson_data.update({"FirmwareVersion": controller["FirmwareVersion"]}) + outjson_data.update({"Name": controller["Name"]}) + outjson_data.update({"Model": controller["Model"]}) + outjson_data.update({"Location": controller["Location"]}) + # outjson_data.update({"Links": controller["Links"]}) + outjson_data.update({"SerialNumber": controller["SerialNumber"]}) + outjson_data.update({"Status": controller["Status"]}) + UI().print_out_json(outjson_data) + + if ( + getattr(options, "logicaldrives", False) + or getattr(options, "ldrive", False) + or single_use + ): + self.logical_drives(options, controller, print_ctrl) + + if ( + getattr(options, "physicaldrives", False) + or getattr(options, "pdrive", False) + or single_use + ): + self.physical_drives(options, controller, print_ctrl) + + if single_use: + controller_data[controller["Id"]] = controller + + if getattr(options, "controller", False) and not controller_ident and print_ctrl: + raise InvalidCommandLineError( + "Controller in position '%s' was not found\n" % (getattr(options, "controller", False)) + ) + if getattr(options, "json", False) and not options.storageid: + if self.rdmc.opts.verbose: + sys.stdout.write("---------------------------------\n") + sys.stdout.write("List of RDE storage devices\n") + sys.stdout.write("---------------------------------\n") + UI().print_out_json(outjson) + if single_use: + return controller_data + else: + raise InvalidCommandLineError( + "Storage controller is not ready, Kindly re run the command after sometime\n" + ) + else: + raise InvalidCommandLineError("Storage controller is not ready, Kindly re run the command after sometime\n") + + def storagecontroller(self, options, print_ctrl=False, single_use=False): + if getattr(options, "controller", False): + controller_ident = False + + if ( + not (getattr(options, "json", False)) + and not (getattr(options, "controller", False)) + and print_ctrl + and (options.physicaldrives is None) + and (options.pdrive is None) + and (options.logicaldrives is None) + and (options.ldrive is None) + ): + sys.stdout.write("Controllers:\n") + + storage_data = {} + get_contrller = [] + + list_storageid = self.rdmc.app.get_handler("/redfish/v1/Systems/1/Storage/", silent=True, service=True).dict[ + "Members" + ] + for val in list_storageid: + storageid_fromurl = val["@odata.id"] + if "DE" in storageid_fromurl and options.storageid in storageid_fromurl: + getval = self.rdmc.app.get_handler(storageid_fromurl, silent=True, service=True).dict + try: + controller = getval["Controllers"] + list_controllers = self.rdmc.app.get_handler( + controller["@odata.id"], silent=True, service=True + ).dict + except KeyError: + self.rdmc.ui.printer("Please check controller was Gen11.\n") + return ReturnCodes.NO_CONTENTS_FOUND_FOR_OPERATION + + for ctl in list_controllers["Members"]: + ctl = ctl["@odata.id"] + get_sel = self.rdmc.app.get_handler(ctl, silent=True, service=True) + get_contrller.append(get_sel) + + for sel in get_contrller: + if "Collection" not in sel.path: + controller = sel.dict + storage_id = controller["@odata.id"].split("/") + storage_id = storage_id[6] + if getattr(options, "controller", False): + if ( + getattr(options, "controller", False) == controller["Id"] + or getattr(options, "controller", False)[-1] + == controller["Location"]["PartLocation"]["ServiceLabel"][-1] + ): + controller_ident = True + else: + continue + if ( + print_ctrl + and (options.controller is None) + and (options.physicaldrives is None) + and (options.pdrive is None) + and (options.ldrive is None) + and (options.logicaldrives is None) + ): + sys.stdout.write( + "StorageId:%s - ControllerId:%s - %s - %s\n" + % ( + storage_id, + controller["Id"], + controller["Location"]["PartLocation"]["ServiceLabel"], + controller["Model"], + ) + ) + + elif ( + getattr(options, "json", False) + and (options.controller is None) + and (options.physicaldrives is None) + and (options.pdrive is None) + and (options.ldrive is None) + and (options.logicaldrives is None) + ): + outjson = dict() + outjson["Controllers"] = dict() + outjson["Controllers"][storage_id] = dict() + outjson["Controllers"][storage_id]["controllerid"] = controller["Id"] + outjson["Controllers"][storage_id]["controllerid"]["Location"] = controller["Location"][ + "PartLocation" + ]["ServiceLabel"] + outjson["Controllers"][storage_id]["controllerid"]["Model"] = controller["Model"] + UI().print_out_json(outjson) + + elif ( + print_ctrl + and (options.controller is not None) + and ( + (options.logicaldrives is not None) + or (options.physicaldrives is not None) + or (options.ldrive is not None) + or (options.pdrive is not None) + ) + ): + pass + + elif ( + getattr(options, "json", False) + and (options.controller is not None) + and ( + (options.logicaldrives is not None) + or (options.physicaldrives is not None) + or (options.ldrive is not None) + or (options.pdrive is not None) + ) + ): + pass + + elif print_ctrl and (options.controller is not None) and options.json is None: + controller_info = "---------------------------------------------\n" + controller_info += "Controller {} Details on Storage Id {}\n".format(options.controller, storage_id) + controller_info += "---------------------------------------------\n" + controller_info += "Id: %s\n" % controller["Id"] + controller_info += "StorageId: %s\n" % storage_id + controller_info += "Name: %s\n" % controller["Name"] + controller_info += "FirmwareVersion: %s\n" % controller["FirmwareVersion"] + controller_info += "Manufacturer: %s\n" % controller["Manufacturer"] + controller_info += "Model: %s\n" % controller["Model"] + controller_info += "PartNumber: %s\n" % controller["PartNumber"] + controller_info += "SerialNumber: %s\n" % controller["SerialNumber"] + try: + controller_info += "SKU: %s\n" % controller["SKU"] + except KeyError: + pass + controller_info += "Status: %s\n" % controller["Status"] + try: + controller_info += "SupportedDeviceProtocols: %s\n" % controller["SupportedDeviceProtocols"] + except KeyError: + pass + try: + controller_info += ( + "SupportedControllerProtocols: %s\n" % controller["SupportedControllerProtocols"] + ) + except KeyError: + pass + self.rdmc.ui.printer(controller_info, verbose_override=True) + + elif getattr(options, "json", False) and (options.controller is not None): + outjson_data = dict() + outjson_data.update({"Id": controller["Id"]}) + outjson_data.update({"StorageId": storage_id}) + outjson_data.update({"Name": controller["Name"]}) + outjson_data.update({"FirmwareVersion": controller["FirmwareVersion"]}) + outjson_data.update({"Manufacturer": controller["Manufacturer"]}) + outjson_data.update({"Model": controller["Model"]}) + outjson_data.update({"PartNumber": controller["PartNumber"]}) + outjson_data.update({"SerialNumber": controller["SerialNumber"]}) + try: + outjson_data.update({"SKU": controller["SKU"]}) + except KeyError: + pass + outjson_data.update({"Status": controller["Status"]}) + try: + outjson_data.update({"SupportedDeviceProtocols": controller["SupportedDeviceProtocols"]}) + except KeyError: + pass + try: + outjson_data.update( + {"SupportedControllerProtocols": controller["SupportedControllerProtocols"]} + ) + except KeyError: + pass + UI().print_out_json(outjson_data) + + if getattr(options, "logicaldrives", False) or getattr(options, "ldrive", False) or single_use: + self.storagelogical_drives(options, controller, storage_id, print_ctrl) + + if getattr(options, "physicaldrives", False) or getattr(options, "pdrive", False) or single_use: + self.storagephysical_drives(options, controller, storage_id, print_ctrl) + + if single_use: + storage_data[controller["Id"]] = controller + if getattr(options, "controller", False) and not controller_ident and print_ctrl: + raise InvalidCommandLineError( + "Controller in position '%s' was " "not found\n" % (getattr(options, "controller", False)) + ) + if single_use: + return storage_data + + def storageid(self, options, print_ctrl=False, single_use=False): + st_url = "/redfish/v1/Systems/1/Storage/" + st_content = self.rdmc.app.get_handler(st_url, silent=True, service=True).dict["Members"] + st_flag = False + for st_controller in st_content: + url = st_controller["@odata.id"] + if options.storageid in url and "DE" in url: + st_flag = True + break + else: + continue + if not st_flag: + raise InvalidCommandLineError( + "\nStorage ID {} not found or Storage ID is not Redfish enabled and does not have DExxxxxx\n".format( + options.storageid + ) + ) + return + + res_dict = dict() + res_dict["Storage"] = dict() + + storage_id_url = "/redfish/v1/Systems/1/Storage/" + options.storageid + "?$expand=." + + storage_resp = self.rdmc.app.get_handler(storage_id_url, silent=True, service=True).dict + if not storage_resp: + raise InvalidCommandLineError("\nStorage Id {} not found\n".format(options.storageid)) + return + controllers = storage_resp["Controllers"]["Members"] + volumes = storage_resp["Volumes"]["Members"] + drives = storage_resp["Drives"] + stg_data = [] + actual_no_drives = 0 + for d in storage_resp["Drives"]: + if d["Status"]["State"] == "Enabled": + actual_no_drives += 1 + if print_ctrl and storage_resp and options.json is None: + if ( + options.storageid + and not options.controllers + and not options.physicaldrives + and not options.logicaldrives + and not options.ldrive + and not options.pdrive + ): + sys.stdout.write("-----------------------------------\n") + sys.stdout.write("Details of Storage %s\n" % options.storageid) + sys.stdout.write("-----------------------------------\n") + sys.stdout.write("\tId: %s\n\t" % storage_resp["Id"]) + sys.stdout.write("Health: %s\n\t" % storage_resp["Status"]["HealthRollup"]) + sys.stdout.write("Name: %s\n\t" % storage_resp["Name"]) + sys.stdout.write("Number of Controllers: %s\n\t" % storage_resp["Controllers"]["Members@odata.count"]) + sys.stdout.write("Number of Volumes: %s\n\t" % storage_resp["Volumes"]["Members@odata.count"]) + sys.stdout.write("Number of Drives: %s\n\t\n" % actual_no_drives) + elif ( + options.storageid + and not options.controllers + and options.physicaldrives + and not options.logicaldrives + and not options.ldrive + and not options.pdrive + ): + self.storagephysical_drives(options, options.controller, options.storageid, print_ctrl) + elif ( + options.storageid + and not options.controllers + and not options.physicaldrives + and options.logicaldrives + and not options.ldrive + and not options.pdrive + ): + self.storagelogical_drives(options, options.controller, options.storageid, print_ctrl) + elif options.storageid and options.ldrive: + self.storagelogical_drives(options, options.controller, options.storageid, print_ctrl) + elif options.storageid and options.pdrive: + self.storagephysical_drives(options, options.controller, options.storageid, print_ctrl) + + elif options.json is not None: + if options.logicaldrives or options.ldrive: + self.storagelogical_drives(options, options.controller, options.storageid, print_ctrl) + if options.physicaldrives or options.pdrive: + self.storagephysical_drives(options, options.controller, options.storageid, print_ctrl) + + storage_data = dict() + storage_data.update({"Id": storage_resp["Id"]}) + storage_data.update({"Health": storage_resp["Status"]["HealthRollup"]}) + storage_data.update({"Name": storage_resp["Name"]}) + storage_data.update({"Number of Controllers": storage_resp["Controllers"]["Members@odata.count"]}) + storage_data.update({"Number of Volumes": storage_resp["Volumes"]["Members@odata.count"]}) + storage_data.update({"Number of Drives": actual_no_drives}) + stg_data.append(storage_data) + + _ = self.print_list_controllers(options, controllers, print_ctrl) + _ = self.print_list_volumes(options, volumes, print_ctrl) + _ = self.print_list_drives(options, drives, print_ctrl) + + # Append controller, volume and drive information to final dictionary + if ( + not options.controllers + and res_dict + and options.json + and not options.physicaldrives + and not options.logicaldrives + and not options.ldrive + and not options.pdrive + ): + if ( + options.storageid + and not options.controllers + and not options.physicaldrives + and not options.logicaldrives + and not options.ldrive + and not options.pdrive + ): + res_dict["Storage"].update(storage_data) + UI().print_out_json(res_dict) + + elif options.controllers and options.storageid and options.json is None: + sys.stdout.write("---------------------------------------------------\n") + sys.stdout.write("List of Controllers in this Storage ID %s\n" % options.storageid) + sys.stdout.write("---------------------------------------------------\n") + list_ctr = controllers + for lst in list_ctr: + l_data = lst["@odata.id"] + get_ctr = self.rdmc.app.get_handler(l_data, silent=True, service=True).dict + sys.stdout.write("\tId: %s\n\t" % get_ctr["Id"]) + sys.stdout.write("Health: %s\n\t" % get_ctr["Status"]["Health"]) + sys.stdout.write("Name: %s\n\t" % get_ctr["Name"]) + sys.stdout.write("FirmwareVersion: %s\n\t" % get_ctr["FirmwareVersion"]) + sys.stdout.write("SerialNumber: %s\n" % get_ctr["SerialNumber"]) + + elif options.controllers and print_ctrl and options.json: + list_ctr = controllers + for lst in list_ctr: + l_data = lst["@odata.id"] + get_ctr = self.rdmc.app.get_handler(l_data, silent=True, service=True).dict + ctr_data = dict() + ctr_data["Controllers"] = dict() + ctr_data["Controllers"].update({"Id": get_ctr["Id"]}) + ctr_data["Controllers"].update({"Health": get_ctr["Status"]["Health"]}) + ctr_data["Controllers"].update({"Name": get_ctr["Name"]}) + ctr_data["Controllers"].update({"FirmwareVersion": get_ctr["FirmwareVersion"]}) + ctr_data["Controllers"].update({"SerialNumber": get_ctr["SerialNumber"]}) + UI().print_out_json(ctr_data) + + def print_list_controllers(self, options, controllers, print_ctrl): + c_data = [] + if not options.json: + sys.stdout.write("Controllers Details:\n") + if not controllers: + sys.stdout.write("\tNone:\n\n") + for c in controllers: + ctrlr = c["@odata.id"] + getctrl = self.rdmc.app.get_handler(ctrlr, silent=True, service=True).dict + if print_ctrl and getctrl and options.json is None: + sys.stdout.write("\tId: %s\n\t" % getctrl["Id"]) + sys.stdout.write("Health: %s\n\t" % getctrl["Status"]["Health"]) + sys.stdout.write("Location: %s\n\t" % getctrl["Location"]["PartLocation"]["ServiceLabel"]) + sys.stdout.write("Name: %s\n\t\n" % getctrl["Name"]) + elif options.json is not None: + ctrl_data = dict() + ctrl_data.update({"Id": getctrl["Id"]}) + ctrl_data.update({"Health": getctrl["Status"]["Health"]}) + ctrl_data.update({"Location": getctrl["Location"]["PartLocation"]["ServiceLabel"]}) + ctrl_data.update({"Name": getctrl["Name"]}) + c_data.append(ctrl_data) + return c_data + + def print_list_volumes(self, options, volumes, print_ctrl): + v_data = [] + if "json" in options and options.json: + vol_data = dict() + if not options.json: + sys.stdout.write("\nVolume Details:\n") + if not volumes: + sys.stdout.write("\tNone\n\n") + for vol in volumes: + volume = vol["@odata.id"] + getvolumes = self.rdmc.app.get_handler(volume, silent=True, service=True).dict + if print_ctrl and getvolumes and options.json is None: + sys.stdout.write("\tId: %s\n\t" % getvolumes["Id"]) + sys.stdout.write("Name: %s\n\t" % getvolumes["Name"]) + sys.stdout.write("RAIDType: %s\n\t" % getvolumes["RAIDType"]) + sys.stdout.write("VolumeUniqueId: %s\n\t" % getvolumes["Identifiers"][0]["DurableName"]) + sys.stdout.write("Health: %s\n\n" % getvolumes["Status"]["Health"]) + elif options.json is not None: + vol_data.update({"Id": getvolumes["Id"]}) + vol_data.update({"Name": getvolumes["Name"]}) + vol_data.update({"Health": getvolumes["Status"]["Health"]}) + vol_data.update({"RAIDType": getvolumes["RAIDType"]}) + vol_data.update({"VolumeUniqueId": getvolumes["Identifiers"][0]["DurableName"]}) + v_data.append(vol_data) + return v_data + + def print_list_drives(self, options, drives, print_ctrl): + d_data = [] + if not options.json: + sys.stdout.write("Drive Details:\n\n") + if not drives: + sys.stdout.write("\tNone\n\n") + for drive in drives: + dve = drive["@odata.id"] + getdrives = self.rdmc.app.get_handler(dve, silent=True, service=True).dict + if print_ctrl and getdrives and options.json is None: + sys.stdout.write("\tId: %s\n\t" % getdrives["Id"]) + sys.stdout.write("Location: %s\n\t" % getdrives["PhysicalLocation"]["PartLocation"]["ServiceLabel"]) + sys.stdout.write("Health: %s\n\t" % getdrives["Status"].get("Health", "NA")) + sys.stdout.write("Name: %s\n\t" % getdrives["Name"]) + sys.stdout.write("Model: %s\n\t" % getdrives.get("Model", "NA")) + sys.stdout.write("Revision: %s\n\t" % getdrives.get("Revision", "NA")) + sys.stdout.write("Serial Number: %s\n\t" % getdrives.get("SerialNumber", "NA")) + sys.stdout.write("Protocol: %s\n\t" % getdrives.get("Protocol", "NA")) + sys.stdout.write("MediaType: %s\n\t" % getdrives.get("MediaType", "NA")) + sys.stdout.write("CapacityBytes: %s\n\t\n" % getdrives.get("CapacityBytes", "NA")) + + elif options.json is not None: + drives_data = dict() + drives_data.update({"Id": getdrives["Id"]}) + drives_data.update({"Model": getdrives.get("Model", "NA")}) + drives_data.update({"Revision": getdrives.get("Revision", "NA")}) + drives_data.update({"Protocol": getdrives.get("Protocol", "NA")}) + drives_data.update({"MediaType": getdrives.get("MediaType", "NA")}) + drives_data.update({"CapacityBytes": getdrives.get("CapacityBytes", "NA")}) + drives_data.update({"Name": getdrives["Name"]}) + drives_data.update({"Health": getdrives["Status"].get("Health", "NA")}) + drives_data.update({"Serial Number": getdrives.get("SerialNumber", "NA")}) + drives_data.update({"Location": getdrives["PhysicalLocation"]["PartLocation"]["ServiceLabel"]}) + d_data.append(drives_data) + return d_data + + def storagephysical_drives(self, options, controller, storage_id, print_ctrl=False, single_use=False): + found_entries = False + if not getattr(options, "json", False) and print_ctrl: + sys.stdout.write("--------------------------------------------------\n") + if not getattr(options, "pdrive", False): + if controller: + sys.stdout.write("Drives on Controller %s and Storage %s\n" % (options.controller, storage_id)) + else: + sys.stdout.write("Drives on Storage %s\n" % storage_id) + else: + if controller: + sys.stdout.write( + "Drive %s on Controller %s and Storage %s\n" % (options.pdrive, options.controller, storage_id) + ) + else: + sys.stdout.write("Drive %s on Storage %s\n" % (options.pdrive, storage_id)) + sys.stdout.write("--------------------------------------------------\n") + if getattr(options, "pdrive", False): + pdrive_ident = False + else: + pdrive_ident = True + if single_use: + physicaldrives = {} + if getattr(options, "json", False): + outjson = dict() + outjson["Drives"] = dict() + print_drives = [] + storage_url = self.rdmc.app.typepath.defs.systempath + "Storage/" + storage_id + dd_list = self.rdmc.app.get_handler(storage_url, silent=True).dict.get("Drives", {}) + if dd_list: + found_entries = True + for dd in dd_list: + drive_url = dd["@odata.id"] + drive_data = self.rdmc.app.get_handler(drive_url, silent=True).dict + location = drive_data["PhysicalLocation"]["PartLocation"]["ServiceLabel"] + loc = location.split(":") + del loc[0] + if len(loc) == 3: + temp_str = str(loc[0].split("=")[1] + ":" + loc[1].split("=")[1] + ":" + loc[2].split("=")[1]) + location = temp_str + else: + pass + + if getattr(options, "pdrive", False): + if options.pdrive != location: + continue + else: + pdrive_ident = True + if single_use: + physicaldrives.update({drive_data["Id"]: drive_data}) + if ( + print_ctrl + and pdrive_ident + and options.pdrive is None + and options.json is None + and (location not in print_drives) + ): + sys.stdout.write( + "\t[%s]: %s, Model %s, Location %s, Type %s, Serial %s - %s Bytes\n" + % ( + location, + drive_data.get("Name", "NA"), + drive_data.get("Model", "NA"), + location, + drive_data.get("MediaType", "NA"), + drive_data.get("SerialNumber", "NA"), + drive_data.get("CapacityBytes", "NA"), + ) + ) + elif ( + print_ctrl + and pdrive_ident + and options.pdrive + and options.json is None + and (location not in print_drives) + ): + sys.stdout.write( + "\tId: %s\n\t%s\n\tModel: %s\n\tLocation: %s\n\tType: %s\n\tSerial: %s\n\tCapacity: %s Bytes\n" + % ( + drive_data["Id"], + drive_data["Name"], + drive_data.get("Model", "NA"), + location, + drive_data.get("MediaType", "NA"), + drive_data.get("SerialNumber", "NA"), + drive_data.get("CapacityBytes", "NA"), + ) + ) + break + elif getattr(options, "json", False) and pdrive_ident and (location not in print_drives): + outjson["Drives"][location] = dict() + outjson["Drives"][location]["Id"] = drive_data["Id"] + outjson["Drives"][location]["Name"] = drive_data["Name"] + outjson["Drives"][location]["Model"] = drive_data.get("Model", "NA") + outjson["Drives"][location]["Location"] = location + outjson["Drives"][location]["MediaType"] = drive_data.get("MediaType", "NA") + outjson["Drives"][location]["SerialNumber"] = drive_data.get("SerialNumber", "NA") + outjson["Drives"][location]["CapacityBytes"] = drive_data.get("CapacityBytes", "NA") + if options.pdrive: + break + + print_drives.append(location) + if getattr(options, "json", False) and pdrive_ident: + UI().print_out_json(outjson) + if getattr(options, "pdrive", False) and not pdrive_ident and print_ctrl: + raise InvalidCommandLineError("\tPhysical drive in position '%s' was not found\n" % options.pdrive) + elif not found_entries and print_ctrl: + sys.stdout.write("\tPhysical drives not found.\n") + + if single_use: + return physicaldrives + + def storagelogical_drives(self, options, controller, storage_id, print_ctrl=False, single_use=False): + found_entries = False + printed = False + if not getattr(options, "json", False) and print_ctrl: + sys.stdout.write("--------------------------------------------------\n") + if not getattr(options, "ldrive", False): + sys.stdout.write("Volumes on Controller %s and Storage %s\n" % (options.controller, storage_id)) + else: + if controller: + sys.stdout.write( + "Volume %s on Controller %s and Storage %s\n" % (options.ldrive, options.controller, storage_id) + ) + else: + sys.stdout.write("Volume %s on Storage %s\n" % (options.ldrive, storage_id)) + sys.stdout.write("--------------------------------------------------\n") + if getattr(options, "ldrive", False): + ldrive_ident = False + else: + ldrive_ident = True + if single_use: + logicaldrives = {} + if getattr(options, "json", False): + outjson = dict() + outjson["volumes"] = dict() + list_volumes = self.rdmc.app.get_handler( + "/redfish/v1/Systems/1/Storage/" + storage_id + "/Volumes/", + silent=True, + service=True, + ).dict["Members"] + found = False + for tmp in list_volumes: + if not found: + tmp = tmp["@odata.id"] + tmp = self.rdmc.app.get_handler(tmp, silent=True, service=True).dict + try: + std_v = options.storageid in tmp["@odata.id"] + except: + std_v = tmp["@odata.id"] + if std_v: + if getattr(options, "ldrive", False): + ldrive_ident = False + else: + ldrive_ident = True + if getattr(options, "ldrive", False): + if options.ldrive != tmp["Id"]: + continue + else: + ldrive_ident = True + found = True + + if "Identifiers" in tmp: + durablename = tmp["Identifiers"] + if durablename is not None: + d_name = [name["DurableName"] for name in durablename] + found_entries = True + + drives = [] + d_data = tmp["Links"]["Drives"] + for dd in d_data: + drive = dd["@odata.id"] + drive_data = self.rdmc.app.get_handler(drive, silent=True).dict + location = drive_data["PhysicalLocation"]["PartLocation"]["ServiceLabel"] + loc = location.split(":") + del loc[0] + if len(loc) == 3: + temp_str = str( + loc[0].split("=")[1] + ":" + loc[1].split("=")[1] + ":" + loc[2].split("=")[1] + ) + location = temp_str + drives.append(location) + volumes = ", ".join(str(item) for item in drives) + if print_ctrl and ldrive_ident and options.ldrive is None and not getattr(options, "json", False): + sys.stdout.write( + "\t[%s]: Name %s RAIDType %s VUID %s Capacity %s Bytes - Health %s\n" + % ( + tmp["Id"], + tmp["Name"], + tmp["RAIDType"], + d_name[0], + tmp["CapacityBytes"], + tmp["Status"]["Health"], + ) + ) + elif getattr(options, "json", False) and ldrive_ident: + outjson["volumes"][tmp["Id"]] = dict() + outjson["volumes"][tmp["Id"]]["VolumeName"] = tmp["Name"] + outjson["volumes"][tmp["Id"]]["RAIDType"] = tmp["RAIDType"] + outjson["volumes"][tmp["Id"]]["VolumeUniqueIdentifier"] = d_name[0] + # if options.ldrive: + # outjson["volumes"][tmp["Id"]]["Drive Location"] = volumes + outjson["volumes"][tmp["Id"]]["Capacity"] = tmp["CapacityBytes"] + outjson["volumes"][tmp["Id"]]["Health"] = tmp["Status"]["Health"] + printed = True + elif print_ctrl and ldrive_ident and options.ldrive is not None: + sys.stdout.write( + "\tId: %s\n\tName: %s\n\tRaidType: %s\n\tVolumeUniqueId: %s\n\tCapacity: %s Bytes\n\n" + % ( + tmp["Id"], + tmp["Name"], + tmp["RAIDType"], + d_name[0], + tmp["CapacityBytes"], + ) + ) + printed = True + if single_use: + logicaldrives.update({tmp["Id"]: tmp}) + if getattr(options, "json", False) and ldrive_ident: + UI().print_out_json(outjson) + printed = True + if getattr(options, "ldrive", False) and not ldrive_ident: + sys.stdout.write("\tVolume '%s' was not found.\n" % options.ldrive) + elif not found_entries and print_ctrl: + sys.stdout.write("\tVolumes not found.\n") + if not getattr(options, "logicaldrives", False) and not printed: + if getattr(options, "ldrive", False): + raise InvalidCommandLineError("\tVolume '%s' does not exists.\n" % getattr(options, "ldrive", False)) + + if single_use: + return logicaldrives + + def get_storage_data_drives(self, options, drives, print_ctrl=False, single_use=False): + if not (getattr(options, "json", False)) and print_ctrl: + sys.stdout.write("\tData Drives:\n\n") + if single_use: + subsetdrives = {drives["Id"]: drives} + found_entries = False + if print_ctrl: + self.tranverse_func(drives) + found_entries = True + + elif getattr(options, "json", False): + UI().print_out_json(drives) + + if not found_entries and print_ctrl: + sys.stdout.write("\t\tComponent drives not found.\n") + + if single_use: + return subsetdrives + + def tranverse_func(self, data, indent=0): + for key, value in data.items(): + sys.stdout.write("\t %s :" % str(key)) + if isinstance(value, dict): + self.tranverse_func(value, indent + 1) + else: + sys.stdout.write("%s \n" % str(value)) + + def physical_drives(self, options, content, print_ctrl=False, single_use=False): + """ + Identify/parse physical drives (and child properties) of a parent array controller + + :param options: command line options + :type options: object attributes + :param content: dictionary of physical drive href or @odata.id paths (expect @odata.type + "SmartStorageArrayController", @odata.id /Systems/1/SmartStorage/ArrayControllers/X/) + :type content: dict + :param print_ctrl: flag for console print enablement/disablement (default disabled) + :type print_ctrl: bool + :param single_use: singular usage - True or explore mode - False, + returns dictionary of results to calling function (default disabled) - see returns. + :type single_use: bool + :returns: None, dictionary of all physical drives identified by 'Id' + """ + dd = "" + confd = content.get("PhysicalDrives") + try: + dd = content["links"]["PhysicalDrives"][self.rdmc.app.typepath.defs.hrefstring] + except: + dd = content["Links"]["PhysicalDrives"][self.rdmc.app.typepath.defs.hrefstring] + finally: + if not getattr(options, "json", False) and print_ctrl: + sys.stdout.write("Physical Drives:\n") + if getattr(options, "pdrive", False): + pdrive_ident = False + else: + pdrive_ident = True + if single_use: + physicaldrives = {} + found_entries = False + for data in self.rdmc.app.get_handler(dd, silent=True).dict.get("Members", {}): + try: + tmp = data[self.rdmc.app.typepath.defs.hrefstring] + except: + tmp = data[next(iter(data))] + finally: + tmp = self.rdmc.app.get_handler(tmp, silent=True).dict + found_entries = True + if confd: + for confdd in confd: + if confdd.get("Location") == tmp.get("Location"): + tmp.update(confdd) + break + if getattr(options, "pdrive", False): + if options.pdrive != tmp["Location"]: + continue + else: + pdrive_ident = True + if single_use: + physicaldrives[tmp["Id"]] = tmp + if print_ctrl and pdrive_ident and options.pdrive is None: + sys.stdout.write( + "\t[%s]: Model %s, Location %s, Type %s, Serial %s - %s MiB\n" + % ( + tmp["Location"], + tmp.get("Model", "NA"), + tmp.get("Location", "NA"), + tmp.get("MediaType", "NA"), + tmp.get("SerialNumber", "NA"), + tmp.get("CapacityMiB", "NA"), + ) + ) + elif getattr(options, "json", False) and pdrive_ident and options.pdrive is None: + outjson = dict() + outjson["PhysicalDrives"] = dict() + outjson["PhysicalDrives"][tmp["Location"]] = dict() + outjson["PhysicalDrives"][tmp["Location"]]["Model"] = tmp.get("Model", "NA") + outjson["PhysicalDrives"][tmp["Location"]]["Location"] = tmp.get("Location", "NA") + outjson["PhysicalDrives"][tmp["Location"]]["MediaType"] = tmp.get("MediaType", "NA") + outjson["PhysicalDrives"][tmp["Location"]]["SerialNumber"] = tmp.get("SerialNumber", "NA") + outjson["PhysicalDrives"][tmp["Location"]]["CapacityMiB"] = tmp.get("CapacityMiB", "NA") + UI().print_out_json(outjson) + + elif print_ctrl and pdrive_ident and options.pdrive is not None: + ctl_logical = dict() + contoller_loc = content["Location"] + "-" + content["Model"] + ctl_logical.update({"Controller": contoller_loc}) + remove_odata = self.odataremovereadonly(tmp) + ctl_logical.update(remove_odata) + location = ctl_logical["Location"] + if print_ctrl and ctl_logical and options.ldrive is None and not getattr(options, "json", False): + sys.stdout.write( + "\tId:\t%s\n\tModel:\t%s\n\tLocation:\t%s\n\tType:\t%s\n\tSerial:\t%s\n\tCapacity:\t%s " + "Bytes\n" + % ( + ctl_logical["Id"], + ctl_logical["Model"], + ctl_logical["Location"], + ctl_logical["MediaType"], + ctl_logical["SerialNumber"], + ctl_logical["CapacityLogicalBlocks"], + ) + ) + elif getattr(options, "json", False) and ctl_logical: + outjson = dict() + outjson["Drives"] = dict() + outjson["Drives"][location] = dict() + outjson["Drives"][location]["Id"] = ctl_logical["Id"] + outjson["Drives"][location]["Model"] = ctl_logical["Model"] + outjson["Drives"][location]["Location"] = location + outjson["Drives"][location]["MediaType"] = ctl_logical["MediaType"] + outjson["Drives"][location]["SerialNumber"] = ctl_logical["SerialNumber"] + outjson["Drives"][location]["CapacityLogicalBlocks"] = ctl_logical["CapacityLogicalBlocks"] + UI().print_out_json(outjson) + + elif getattr(options, "json", False) and pdrive_ident and options.pdrive is not None: + ctl_logical = dict() + contoller_loc = content["Location"] + "-" + content["Model"] + ctl_logical.update({"Controller": contoller_loc}) + remove_odata = self.odataremovereadonly(tmp) + ctl_logical.update(remove_odata) + UI().print_out_json(ctl_logical) + + if getattr(options, "pdrive", False) and pdrive_ident: + break + if getattr(options, "pdrive", False) and not pdrive_ident and print_ctrl: + raise InvalidCommandLineError("\tPhysical drive in position '%s' was not found \n" % options.pdrive) + elif not found_entries and print_ctrl: + sys.stdout.write("\tPhysical drives not found.\n") + + if single_use: + return physicaldrives + + def logical_drives(self, options, storage_id, content, print_ctrl=False, single_use=False): + """ + Identify/parse volumes (and child properties) of an associated array controller + + :param options: command line options + :type options: object attributes + :param content: dictionary of volume href or @odata.id paths (expect @odata.type + "SmartStorageArrayController", @odata.id /Systems/1/SmartStorage/ArrayControllers/X/) + :type content: dict + :param print_ctrl: flag for console print enablement/disablement (default disabled) + :type print_ctrl: bool + :param single_use: singular usage - True or explore mode - False, + returns dictionary of results to calling function (default disabled) - see returns. + :type single_use: bool + :returns: None, dictionary of all volumes identified by 'Id' + """ + + confd = content.get("LogicalDrives") + try: + _ = content["links"]["LogicalDrives"][self.rdmc.app.typepath.defs.hrefstring] + except: + _ = content["Links"]["LogicalDrives"][self.rdmc.app.typepath.defs.hrefstring] + finally: + if not getattr(options, "json", False) and print_ctrl: + sys.stdout.write("Volumes:\n") + if getattr(options, "ldrive", False): + ldrive_ident = False + else: + ldrive_ident = True + if single_use: + logicaldrives = {} + found_entries = False + try: + list_volumes = self.rdmc.app.get_handler( + "/redfish/v1/Systems/1/Storage/" + options.storageid + "/Volumes/", + silent=True, + service=True, + ).dict["Members"] + except: + list_volumes = self.rdmc.app.get_handler( + "/redfish/v1/Systems/1/Storage/" + storage_id + "/Volumes/", + silent=True, + service=True, + ).dict["Members"] + for data in list_volumes: + try: + tmp = data[self.rdmc.app.typepath.defs.hrefstring] + except: + tmp = data[next(iter(data))] + finally: + tmp = self.rdmc.app.get_handler(tmp, silent=True).dict + found_entries = True + if confd: + for confdd in confd: + if confdd.get("LogicalDriveNumber") == tmp.get("LogicalDriveNumber"): + tmp.update(confdd) + break + if getattr(options, "ldrive", False): + if options.ldrive != tmp["Id"]: + continue + else: + ldrive_ident = True + if print_ctrl and ldrive_ident and options.ldrive is None: + sys.stdout.write( + "\t[%s]: Name %s Raid %s VUID %s - %s MiB\n" + % ( + tmp["Id"], + tmp["LogicalDriveName"], + tmp["Raid"], + tmp["VolumeUniqueIdentifier"], + tmp["CapacityMiB"], + ) + ) + elif getattr(options, "json", False) and ldrive_ident and options.ldrive is None: + outjson = dict() + outjson["LogicalDrives"] = dict() + outjson["LogicalDrives"][tmp["Id"]] = dict() + outjson["LogicalDrives"][tmp["Id"]]["LogicalDriveName"] = tmp["LogicalDriveName"] + outjson["LogicalDrives"][tmp["Id"]]["Raid"] = tmp["Raid"] + outjson["LogicalDrives"][tmp["Id"]]["VolumeUniqueIdentifier"] = tmp["VolumeUniqueIdentifier"] + outjson["LogicalDrives"][tmp["Id"]]["CapacityMiB"] = tmp["CapacityMiB"] + UI().print_out_json(outjson) + elif print_ctrl and ldrive_ident and options.ldrive is not None: + ctl_logical = dict() + contoller_loc = content["Location"] + "-" + content["Model"] + ctl_logical.update({"Controller": contoller_loc}) + remove_odata = self.odataremovereadonly(tmp) + ctl_logical.update(remove_odata) + id = ctl_logical["Id"] + if print_ctrl and ctl_logical and options.json is None: + sys.stdout.write("\tId: %s\n\t" % ctl_logical["Id"]) + sys.stdout.write("Name: %s\n\t" % ctl_logical["LogicalDriveName"]) + sys.stdout.write("RAIDType: %s\n\t" % ctl_logical["Raid"]) + sys.stdout.write("VolumeUniqueId: %s\n\t" % ctl_logical["VolumeUniqueIdentifier"]) + sys.stdout.write("Capacity: %s\n\t" % ctl_logical["CapacityBlocks"]) + sys.stdout.write("Health: %s\n\n" % ctl_logical["Status"]["Health"]) + # UI().print_out_json(ctl_logical) + elif getattr(options, "json", False) and ctl_logical: + outjson = dict() + outjson["Volumes"] = dict() + outjson["Volumes"][id] = dict() + # outjson["Volumes"][id]["Id"] = ctl_logical["Id"] + outjson["Volumes"][id]["VolumeName"] = ctl_logical["LogicalDriveName"] + outjson["Volumes"][id]["RAIDType"] = ctl_logical["Raid"] + outjson["Volumes"][id]["Health"] = ctl_logical["Status"]["Health"] + outjson["Volumes"][id]["Capacity"] = ctl_logical["CapacityBlocks"] + outjson["Volumes"][id]["VolumeUniqueIdentifier"] = ctl_logical["VolumeUniqueIdentifier"] + UI().print_out_json(outjson) + elif getattr(options, "json", False) and ldrive_ident and options.ldrive is not None: + ctl_logical = dict() + contoller_loc = content["Location"] + "-" + content["Model"] + ctl_logical.update({"Controller": contoller_loc}) + remove_odata = self.odataremovereadonly(tmp) + ctl_logical.update(remove_odata) + UI().print_out_json(ctl_logical) + + elif getattr(options, "json", False) and ldrive_ident: + UI().print_out_json(tmp) + if single_use: + logicaldrives[tmp["Id"]] = tmp + if getattr(options, "ldrive", False) and ldrive_ident: + break + if getattr(options, "ldrive", False) and not ldrive_ident: + sys.stdout.write("\tVolume '%s' was not found \n" % options.pdrive) + elif not found_entries and print_ctrl: + sys.stdout.write("\tVolumes not found.\n") + + if single_use: + return logicaldrives + + def get_data_drives(self, options, drives, print_ctrl=False, single_use=False): + """ + Identify/parse a physical component drive collection of a respective volume. The + physical disk properties as well as some logical RAID parameters within each respective + member. + + :param options: command line options + :type options: object attributes + :param content: collection of data drives href or @odata.id paths (as members) as attributed + to a respective volume. + :type content: dict + :param print_ctrl: flag for console print enablement/disablement (default disabled) + :type print_ctrl: bool + :param single_use: singular usage, returns dictionary of results to calling function + (default disabled) + :type single_use: bool + :returns: None, dictionary of all physical drives composing a parent volume, + each instance identified by 'Id' + """ + + if not (getattr(options, "json", False)) and print_ctrl: + sys.stdout.write("\tData Drives:\n\n") + if single_use: + subsetdrives = {} + found_entries = False + for member in drives.get("Members", {}): + try: + tmp = member[self.rdmc.app.typepath.defs.hrefstring] + except: + tmp = member[next(iter(member))] + finally: + tmp = self.rdmc.app.get_handler(tmp, silent=True).dict + found_entries = True + if single_use: + subsetdrives[tmp["Id"]] = tmp + if print_ctrl: + self.tranverse_func(tmp) + elif getattr(options, "json", False): + UI().print_out_json(tmp) + if not found_entries and print_ctrl: + sys.stdout.write("\t\tComponent drives not found.\n") + + if single_use: + return subsetdrives + + def storagecontrollervalidation(self, options): + """Storage controller validation function + + :param options: command line options + :type options: list. + """ + if "json" in options and options.json: + self.rdmc.json = True + self.cmdbase.login_select_validation(self, options) + + if getattr(options, "sa_conf_filename", False): + if options.command == "save": + open(options.sa_conf_filename, "w+b") + if options.command == "load": + open(options.sa_conf_filename, "r+b") + self.config_file = options.sa_conf_filename + else: + self.config_file = __config_file__ + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + + default_parser = subcommand_parser.add_parser( + "default", + help="Running without any sub-command will return storage controller configuration data\n" + "on the currently logged in server. Additional optional arguments will narrow the\n" + "scope of returned data to individual controllers, physical or volumes", + ) + default_parser.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller using either the slot " + "number or index. \n\tExamples:\n\t1. To get more details on a specific " + "controller, select it by index.\tstoragecontroller --controller=2" + "\n\t2. To get more details on a specific controller select " + "it by location.\tstoragecontroller --controller='Slot 0'", + default=None, + ) + default_parser.add_argument( + "--storage_id", + "--storageid", + dest="storageid", + help="Use this flag to select the corresponding controller using either the storageid " + "id. \n\tExamples:\n\t1. To get more details on a specific " + "controller, select it by storageid.\tstoragecontroller --storageid=DE00E000" + "\n\t2. To get more details on a specific controller select " + "it by location.\tstoragecontroller --storageid='DE00E000'", + default=None, + ) + default_parser.add_argument( + "--controllers", + dest="controllers", + action="store_true", + help="Use this flag to return the controllers for the storageid selected." + "\n\tExamples:\n\t1. storagecontroller --controllers\n", + default=None, + ) + default_parser.add_argument( + "--physicaldrives", + "--drives", + dest="physicaldrives", + action="store_true", + help="Use this flag to return the physical drives for the controller selected." + "\n\tExamples:\n\t1. storagecontroller --drives\n\t2. To obtain details about " + "physical drives for a specific controller.\tstoragecontroller --controller=3 " + "--physicaldrives", + default=None, + ) + default_parser.add_argument( + "--logicaldrives", + "--volumes", + dest="logicaldrives", + action="store_true", + help="Use this flag to return the volumes for the controller selected.\n\t " + "\n\tExamples:\n\t1. storagecontroller --logicaldrives\n\t2. To obtain details about " + "volumes for a specific controller.\tstoragecontroller --controller=3 " + "--logicaldrives", + default=None, + ) + default_parser.add_argument( + "--pdrive", + "--drive", + dest="pdrive", + help="Use this flag to select the corresponding physical disk.\n\tExamples:\n\t " + "1. To obtain details about a specific physical drive for a specific controller." + "\tstoragecontroller --controller=3 --pdrive=1I:1:1", + default=None, + ) + default_parser.add_argument( + "--ldrive", + "--volume", + dest="ldrive", + help="Use this flag to select the corresponding logical disk.\n\tExamples:\n\t " + "1. To obtain details about a specific physical drive for a specific controller." + "\tstoragecontroller --controller=3 --ldrive=1", + default=None, + ) + default_parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="""Use this flag to output data in JSON format.""", + default=None, + ) + self.cmdbase.add_login_arguments_group(default_parser) + + state_help = "Print state/event information from @Redfish.Settings (if available)" + state_parser = subcommand_parser.add_parser( + "state", + help=state_help, + description=state_help + "\n\tExample: storagecontroller state", + formatter_class=RawDescriptionHelpFormatter, + ) + state_parser.add_argument( + "--controller", + dest="controller", + help="Use this flag to select the corresponding controller using either the slot " + "number or index. \n\tExamples:\n\t1. To get more details on a specific " + "controller, select it by index.\tstoragecontroller state --controller=2" + "\n\t2. To get more details on a specific controller select " + "it by location.\tstoragecontroller state --controller='Slot 0'", + default=None, + ) + state_parser.add_argument( + "--storage_id", + "--storageid", + dest="storageid", + help="Use this flag to select the corresponding controller using either the storageid " + "id. \n\tExamples:\n\t1. To get more details on a specific " + "controller, select it by storageid.\tstoragecontroller state --storageid=DE00E000" + "\n\t2. To get more details on a specific controller select " + "it by location.\tstoragecontroller state --storageid='DE00E000'", + default=None, + ) + self.cmdbase.add_login_arguments_group(state_parser) + json_save_help = ( + "Save a JSON file with all current configurations (all controllers, logical and \nphysical drives)." + ) + # json save sub-parser + json_save_parser = subcommand_parser.add_parser( + "save", + help=json_save_help, + description=json_save_help + "\n\tExample: storagecontroller save -f ", + formatter_class=RawDescriptionHelpFormatter, + ) + json_save_parser.add_argument( + "-f", + dest="sa_conf_filename", + help="Specify a filename for saving the storagecontroller configuration. (Default: " + "'storagecontroller_config.json')", + default=None, + ) + self.cmdbase.add_login_arguments_group(json_save_parser) + json_load_help = ( + "Load a JSON file with modified storagecontroller configurations (All read-only " + "properties\nare discarded)." + ) + # json load sub-parser + json_load_parser = subcommand_parser.add_parser( + "load", + help=json_load_help, + description=json_load_help + "\n\tExample: storagecontroller load -f ", + formatter_class=RawDescriptionHelpFormatter, + ) + json_load_parser.add_argument( + "-f", + dest="sa_conf_filename", + help="Specify a filename for loading a storagecontroller configuration. (Default: " + "'storagecontroller_config.json')", + default=None, + ) + self.cmdbase.add_login_arguments_group(json_load_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/ClearControllerConfigCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/ClearControllerConfigCommand.cpython-310.pyc new file mode 100644 index 0000000..13ce644 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/ClearControllerConfigCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/CreateVolumeCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/CreateVolumeCommand.cpython-310.pyc new file mode 100644 index 0000000..9ee9df1 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/CreateVolumeCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/DeleteVolumeCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/DeleteVolumeCommand.cpython-310.pyc new file mode 100644 index 0000000..9030531 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/DeleteVolumeCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/DriveSanitizeCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/DriveSanitizeCommand.cpython-310.pyc new file mode 100644 index 0000000..cb04d95 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/DriveSanitizeCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/FactoryResetControllerCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/FactoryResetControllerCommand.cpython-310.pyc new file mode 100644 index 0000000..ab05be6 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/FactoryResetControllerCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/StorageControllerCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/StorageControllerCommand.cpython-310.pyc new file mode 100644 index 0000000..ceea9b5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/StorageControllerCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..519d96b Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_ARRAY_COMMANDS/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/SmartNicCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/SmartNicCommand.py new file mode 100644 index 0000000..9907da5 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/SmartNicCommand.py @@ -0,0 +1,720 @@ +### +# Copyright 2016-2022 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Smart Nic Command for rdmc""" +import os +import time + +try: + from rdmc_helper import ( + UI, + FirmwareUpdateError, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + UploadError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + UI, + FirmwareUpdateError, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + UploadError, + ) + + +class SmartNicCommand: + """Smart nic command""" + + def __init__(self): + self.ident = { + "name": "smartnic", + "usage": None, + "description": "\tRun without arguments for the " + "current list of smartnic including json format.\n\texample: " + "smartnic/smartnic -j\n\n" + "Show all available options\n\n\t" + "example: smartnic --id \n\t" + "smartnic --id -j\n\t" + "smartnic --id --logs\n\t" + "smartnic --id --bootprogress\n\t" + "smartnic --id --clearlog\n\t" + "smartnic --id --update_fw \n\t" + "smartnic --id --reset < GracefulShutdown / ForceRestart / Nmi / GracefulRestart >\n", + "summary": "Discovers all pensando nic installed in the " "server", + "aliases": [], + "auxcommands": ["SelectCommand"], + } + self.config_file = None + self.fdata = None + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Smartnic command""" + + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + if args: + raise InvalidCommandLineError("servernic command takes no " "arguments.") + + ilo_ver = self.rdmc.app.getiloversion() + if ilo_ver < 5.268: + raise IncompatibleiLOVersionError( + "Please upgrade to iLO 5 2.68 or " "greater to ensure correct flash of this firmware." + ) + + self.smartnicvalidation(options) + + path = self.rdmc.app.typepath.defs.systempath + + info = self.gatherinfo(path) + + if not info: + raise InvalidCommandLineError("No SmartNic available.") + + fw_version = self.get_fw_version() + options_system = True + + if options.json and options.id is not None: + string_id = options.id + to_list = list(string_id.split(",")) + for id in to_list: + if options.bootprogress: + if id == info["Id"]: + json_content = self.print_get_bootprogress(info) + UI().print_out_json(json_content) + elif id != info["Id"]: + self.rdmc.ui.printer("No bootprogress present for given smartnic id %s \n" % id) + + elif options.logs: + if id == info["Id"]: + json_content = self.print_get_logs(info) + UI().print_out_json(json_content) + elif id != info["Id"]: + self.rdmc.ui.printer("No logs present for given smartnic id %s \n" % id) + + elif options_system: + if id == info["Id"] and info["SystemType"] == "DPU": + json_content = self.build_json_out(info, fw_version) + UI().print_out_json(json_content) + elif id != info["Id"] or info["SystemType"] != "DPU": + self.rdmc.ui.printer("System %s:\n" % id) + self.rdmc.ui.printer("\tNo SmartNic present in given id %s \n" % id) + + elif options.json and options.id is None and options_system and not options.logs and not options.bootprogress: + if info["SystemType"] == "DPU": + json_content = self.build_json_out(info, fw_version) + UI().print_out_json(json_content) + if info["SystemType"] != "DPU": + self.rdmc.ui.printer("System %s:\n" % id) + self.rdmc.ui.printer("\tNo SmartNic present in given id %s \n" % id) + + elif (options.json and options.logs and options.id is None) or ( + options.logs and not options.json and options.id is None + ): + raise InvalidCommandLineError( + "No command --logs for smartnic\n" + "usage: smartnic --id --logs \n" + "\t smartnic --id --logs -j" + ) + + elif (options.bootprogress and options.json and options.id is None) or ( + options.bootprogress and not options.json and options.id is None + ): + raise InvalidCommandLineError( + "No command --bootprogress for smartnic\n" + "usage: smartnic --id --bootprogress\n" + "\t smartnic --id --bootprogress -j" + ) + + else: + if options.id is not None: + string_id = options.id + str_lst = list(string_id.split(",")) + if options.update_fw is None and options.reset is None: + for id in str_lst: + if options.bootprogress: + if id == info["Id"]: + self.get_bootprogress(info) + elif id != info["Id"]: + self.rdmc.ui.printer("No bootprogress present for given smartnic id %s\n" % id) + + elif options.logs: + if id == info["Id"]: + self.get_logs(info) + elif id != info["Id"]: + self.rdmc.ui.printer("No logs present for given smartnic id %s \n" % id) + + elif options_system and not options.clearlog and not options.logs: + if id == info["Id"] and info["SystemType"] == "DPU": + self.prettyprintinfo(info, fw_version) + elif id != info["Id"] or info["SystemType"] != "DPU": + self.rdmc.ui.printer("System %s:\n" % id) + self.rdmc.ui.printer("\tNo SmartNic present in given id %s \n" % id) + + elif options.clearlog and options_system: + try: + for id in str_lst: + if id == info["Id"]: + self.clearlog(options, info, id) + elif id != info["Id"]: + self.rdmc.ui.printer("Given smartnic id is not present to clear the logs.") + except Exception as excp: + raise excp + + elif options.update_fw is not None: + try: + self.upload_firmware(options, info) + except Exception as excp: + # if SessionExpired: + # time.sleep(320) # wait till reboot the server + # self.smartnicvalidation(options) + # self.cmdbase.login_select_validation(self, options) + # else: + raise excp + + elif options.reset is not None: + try: + for id in str_lst: + if id == info["Id"]: + self.get_resettype(options, info, id) + elif id != info["Id"]: + self.rdmc.ui.printer("No reset type present for given smartnic id %s \n" % id) + + except Exception as excp: + raise excp + + else: + if options.clearlog and options_system: + raise InvalidCommandLineError( + "No command --clearlog for smartnic\n" "usage: smartnic --id --clearlog" + ) + + if options_system and not options.clearlog: + if info["SystemType"] == "DPU": + self.prettyprintinfo(info, fw_version) + elif info["SystemType"] != "DPU": + self.rdmc.ui.printer("No SmartNic available.") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def get_fw_version(self): + # to get pensando firmware version + name = [] + f_version = "" + fw_path = "/redfish/v1/UpdateService/FirmwareInventory/?$expand=." + fw_result = self.rdmc.app.get_handler(fw_path, service=True, silent=True) + fw_result = fw_result.dict + members = fw_result["Members"] + for n in members: + name_version = n["Name"], n["Version"] + name.append(name_version) + + for fw_name in name: + if "Pensando" in fw_name[0]: + f_version += fw_name[1] + return f_version + + def clearlog(self, options, info, id): + try: + log_service = info["LogServices"] + for log_id in log_service.values(): + data = self.rdmc.app.get_handler(log_id, service=True, silent=True).dict + members = data["Members"] + for val in members: + for dpu in val.values(): + dpu_data = self.rdmc.app.get_handler(dpu, service=True, silent=True).dict + path = dpu_data["Actions"]["#LogService.ClearLog"]["target"] + action = path.split("/")[-2] + action = {"Action": action} + self.rdmc.app.post_handler(path, action) + + except Exception as excp: + raise excp + + def get_resettype(self, options, info, id): + self.printreboothelp(options.reset) + time.sleep(3) + + if id == info["Id"]: + put_path = info["@odata.id"] + + for item in info["Actions"]: + if "Reset" in item: + if self.rdmc.app.typepath.defs.isgen10: + # action = item.split("#")[-1] + put_path = info["Actions"][item]["target"] + break + + if options.reset.lower() == "gracefulshutdown": + body = {"ResetType": "GracefulShutdown"} + elif options.reset.lower() == "forcerestart": + body = {"ResetType": "ForceRestart"} + elif options.reset.lower() == "nmi": + body = {"ResetType": "Nmi"} + elif options.reset.lower() == "gracefulrestart": + body = {"ResetType": "GracefulRestart"} + + self.rdmc.app.post_handler(put_path, body) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def printreboothelp(self, flag): + if flag.upper() == "FORCERESTART": + self.rdmc.ui.warn( + "\nForcing a server restart. Note, the current session will be " + "terminated.\nPlease wait for the server to boot completely before logging in " + "again.\nRebooting the server in 3 seconds...\n" + ) + elif flag.upper() == "NMI": + self.rdmc.ui.warn( + "\nA non-maskable interrupt will be issued to this server. Note, the " + "current session will be terminated.\nIssuing interrupt in 3 seconds...\n" + ) + + elif flag.upper() == "GRACEFULSHUTDOWN": + self.rdmc.ui.warn( + "\nThe server will be graceful shutdown. Note, the current session will be " + "terminated.\nPlease wait for the server to boot completely before logging in " + "again.\nRebooting the server in 3 seconds...\n" + ) + + elif flag.upper() == "GRACEFULRESTART": + self.rdmc.ui.warn( + "\nGraceful server restart. Note, the current session will be " + "terminated.\nPlease wait for the server to boot completely before logging in " + "again.\nRebooting the server in 3 seconds...\n" + ) + + def upload_firmware(self, options, info): + try: + string_id = options.id + to_list = list(string_id.split(",")) + for id in to_list: + for info_id in info["Id"]: + if id == info_id: + target = info["@odata.id"] + fw_path = options.update_fw + results = self.rdmc.app.select(selector="UpdateService.", path_refresh=True)[0].dict + results = results["Actions"]["#UpdateService.SimpleUpdate"]["target"] + string_split = fw_path.split("/") + for tar in string_split: + if tar.endswith("tar"): + self.rdmc.ui.printer("Uploading firmware: %s\n" % os.path.basename(tar)) + + res = self.rdmc.app.post_handler(results, {"ImageURI": fw_path, "Targets": [target]}) + + # Taskmonitor to find the firmware status + task_id = res.dict["TaskMonitor"] + + if res.status == 202: + status = self.wait_for_state_change(task_id, options) + if status: + self.rdmc.ui.printer( + "Component " + tar + " uploaded successfully.\n" + "A reboot may be required for firmware changes to take effect.\n" + ) + + if not status: + # Failed to upload the component. + raise UploadError("Error while processing the component.") + + elif (res.status == 404) or (res.status == 402): + return ReturnCodes.FAILED_TO_UPLOAD_COMPONENT + + except (FirmwareUpdateError, UploadError) as excp: + raise excp + + def wait_for_state_change(self, taskid, options, wait_time=4800): + """Wait for the iLO UpdateService to a move to terminal state. + :param options: command line options + :type options: list. + :param wait_time: time to wait on upload + :type wait_time: int. + """ + total_time = 0 + spinner = ["|", "/", "-", "\\"] + state = "" + self.rdmc.ui.printer("Waiting for iLO UpdateService to finish flashing the firmware \n") + + while total_time < wait_time: + state, _ = self.get_update_service_state(taskid) + if state == "ERROR": + return False + elif ( + (state == "Running") + or (state == "New") + or (state == "Starting") + or (state == "Interrupted") + or (state == "Suspended") + ): + # Lets try again after 8 seconds + count = 0 + # fancy spinner + while count <= 32: + self.rdmc.ui.printer("Updating: %s\r" % spinner[count % 4]) + time.sleep(0.25) + count += 1 + total_time += 8 + elif state == "Completed": + break + + if total_time >= wait_time: + raise FirmwareUpdateError("UpdateService in " + state + " state for " + str(wait_time) + "s") + + return True + + def get_update_service_state(self, taskid): + results = self.rdmc.app.get_handler(taskid, service=True, silent=True) + if results.status == 202: + return results.dict["TaskState"], results.dict + + if results and results.status == 200 and results.dict: + output = results.dict + err = output["error"]["@Message.ExtendedInfo"] + for e in err: + msgsrg = e["MessageArgs"] + return msgsrg[0], results.dict + else: + return "UNKNOWN", {} + + def print_get_bootprogress(self, info): + try: + if info is not None: + for id in info["Id"]: + content = {"Id": id} + content.update({"LastState": info["BootProgress"]["LastState"]}) + content.update({"OemLastState": info["BootProgress"]["OemLastState"]}) + content_json = {"Boot Progress": content} + return content_json + except Exception as excp: + raise excp + + def get_bootprogress(self, info): + boot_output = "" + boot_output = "------------------------------------------------\n" + boot_output += "Boot Progress \n" + boot_output += "------------------------------------------------\n" + if info is not None: + for id in info["Id"]: + boot_output += "Id: %s\n" % id + boot_output += "LastState: %s\n" % info["BootProgress"]["LastState"] + boot_output += "OemLastState: %s\n" % info["BootProgress"]["OemLastState"] + self.rdmc.ui.printer(boot_output, verbose_override=True) + + def print_get_logs(self, info): + log_service = info["LogServices"] + for log_id in log_service.values(): + data = self.rdmc.app.get_handler(log_id, service=True, silent=True).dict + members = data["Members"] + for val in members: + for dpu in val.values(): + dpu_data = self.rdmc.app.get_handler(dpu, service=True, silent=True).dict + entry = dpu_data["Entries"] + for dpu_entry in entry.values(): + result = self.rdmc.app.get_handler(dpu_entry, service=True, silent=True).dict + result = result["Members"] + if not result: + content = {} + else: + for member in result: + get_res = member["@odata.id"] + get_result = self.rdmc.app.get_handler(get_res, service=True, silent=True).dict + content = {"Id": get_result["Id"]} + content.update({"Name": get_result["Name"]}) + content.update({"Created": get_result["Created"]}) + content.update({"EntryType": get_result["EntryType"]}) + content.update({"Severity": get_result["Severity"]}) + message = get_result["Message"] + message = message.split(":") + content.update({message[0]: message[1]}) + content_json = {"Logs View": content} + return content_json + + def get_logs(self, info): + log_output = "" + log_service = info["LogServices"] + for log_id in log_service.values(): + data = self.rdmc.app.get_handler(log_id, service=True, silent=True).dict + members = data["Members"] + log_output = "------------------------------------------------\n" + log_output += "Logs View \n" + log_output += "------------------------------------------------\n" + for val in members: + for dpu in val.values(): + dpu_data = self.rdmc.app.get_handler(dpu, service=True, silent=True).dict + entry = dpu_data["Entries"] + for dpu_entry in entry.values(): + result = self.rdmc.app.get_handler(dpu_entry, service=True, silent=True).dict + result = result["Members"] + for member in result: + get_res = member["@odata.id"] + get_result = self.rdmc.app.get_handler(get_res, service=True, silent=True).dict + log_output += "Id: %s\n" % get_result["Id"] + log_output += "Name: %s\n" % get_result["Name"] + log_output += "Created: %s\n" % get_result["Created"] + log_output += "EntryType: %s\n" % get_result["EntryType"] + log_output += "Severity: %s\n" % get_result["Severity"] + log_output += "%s\n" % get_result["Message"] + self.rdmc.ui.printer(log_output, verbose_override=True) + + def prettyprintinfo(self, info, fw_version): + output = "" + output = "------------------------------------------------\n" + output += "SmartNic Information\n" + output += "------------------------------------------------\n" + + if info is not None: + for id in info["Id"]: + output += "System %s:\n" % id + output += "\tModel: %s\n" % info["Model"] + output += "\tManufacturer: %s\n" % info["Manufacturer"] + output += "\tName: %s\n" % info["Name"] + output += "\tFirmware Version: %s\n" % fw_version + output += "\tPowerState: %s\n" % info["PowerState"] + output += "\tSerialNumber: %s\n" % info["SerialNumber"] + output += "\tSystemType: %s\n" % info["SystemType"] + try: + output += "\tHealth: %s\n" % info["Status"]["Health"] + except KeyError: + pass + try: + output += "\tState: %s\n" % info["Status"]["State"] + except KeyError: + pass + try: + output += ( + "\tOperating System: %s \n" + % info["Oem"][self.rdmc.app.typepath.defs.oemhp]["OperatingSystem"]["Kernel"]["Name"] + ) + except KeyError: + pass + try: + output += ( + "\tOS Version: %s \n" + % info["Oem"][self.rdmc.app.typepath.defs.oemhp]["OperatingSystem"]["Kernel"]["Version"] + ) + except KeyError: + pass + try: + avail_sys = info["Oem"][self.rdmc.app.typepath.defs.oemhp]["AvailableSystemCapabilities"] + avail_sys = str(avail_sys).strip("[]''") + output += "\tAvailable SystemCapabilities: %s \n" % avail_sys + except KeyError: + pass + try: + enable_sys = info["Oem"][self.rdmc.app.typepath.defs.oemhp]["EnabledSystemCapabilities"] + enable_sys = str(enable_sys).strip("[]''") + output += "\tEnable SystemCapabilities: %s \n" % enable_sys + except KeyError: + pass + try: + integration_con = info["Oem"][self.rdmc.app.typepath.defs.oemhp]["IntegrationConfig"] + integration_con = str(integration_con).strip("'{} ") + integration_con = integration_con.replace("'", "") + output += "\tIntegration Config: %s \n" % integration_con + except KeyError: + pass + + output += "\tUUID: %s\n" % info["UUID"] + + self.rdmc.ui.printer(output, verbose_override=True) + + def build_json_out(self, info, fw_version): + if info is not None: + for id in info["Id"]: + content = {"model": info["Model"]} + content.update({"Manufacturer": info["Manufacturer"]}) + content.update({"Name": info["Name"]}) + content.update({"Firmware Version": fw_version}) + content.update({"PowerState": info["PowerState"]}) + content.update({"SerialNumber": info["SerialNumber"]}) + content.update({"SystemType": info["SystemType"]}) + try: + content.update({"Health": info["Status"]["Health"]}) + except KeyError: + pass + try: + content.update({"State": info["Status"]["State"]}) + except KeyError: + pass + try: + content.update( + { + "Operating System": info["Oem"][self.rdmc.app.typepath.defs.oemhp]["OperatingSystem"][ + "Kernel" + ]["Name"] + } + ) + except KeyError: + pass + try: + content.update( + { + "OS Version": info["Oem"][self.rdmc.app.typepath.defs.oemhp]["OperatingSystem"]["Kernel"][ + "Version" + ] + } + ) + except KeyError: + pass + try: + content.update( + { + "Available SystemCapabilities": info["Oem"][self.rdmc.app.typepath.defs.oemhp][ + "AvailableSystemCapabilities" + ] + } + ) + except KeyError: + pass + try: + content.update( + { + "Enable SystemCapabilities": info["Oem"][self.rdmc.app.typepath.defs.oemhp][ + "EnabledSystemCapabilities" + ] + } + ) + except KeyError: + pass + try: + content.update( + {"Integration Config": info["Oem"][self.rdmc.app.typepath.defs.oemhp]["IntegrationConfig"]} + ) + except KeyError: + pass + content.update({"UUID": info["UUID"]}) + system = {"System %s" % id: content} + content_json = {"SmartNic Information": system} + return content_json + + def gatherinfo(self, path): + try: + # info = {} + results = "" + csysresults = self.rdmc.app.select(selector="ComputerSystemCollection.") + csysresults = csysresults[0].dict + members = csysresults["Members"] + for id in members: + for mem_id in id.values(): + if path != mem_id: + results = self.rdmc.app.get_handler(mem_id, service=True, silent=True).dict + return results + except: + pass + + def smartnicvalidation(self, options): + """smartnic validation function. + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + + customparser.add_argument( + "--id", + dest="id", + help="Optionally include this flag to retrive the selected smartnic .", + default=None, + ) + + customparser.add_argument( + "--logs", + dest="logs", + action="store_true", + help="Optionally include this flag to list all the logs .", + default=False, + ) + + customparser.add_argument( + "--bootprogress", + dest="bootprogress", + action="store_true", + help="Optionally include this flag to list the bootprogress information", + default=False, + ) + + customparser.add_argument( + "--update_fw", + dest="update_fw", + help="Include this flag to update firmware to pensando card", + default=None, + ) + # customparser.add_argument( + # "--update_os", + # dest="update_os", + # help="Include this flag to update OS to pensando card", + # default=None, + # ) + + customparser.add_argument( + "--reset", + dest="reset", + help="Add this flag to reset the server", + nargs="?", + type=str, + const="GracefulRestart", + ) + + customparser.add_argument( + "--clearlog", + dest="clearlog", + action="store_true", + help="Optionally include this flag to clear log", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/__pycache__/SmartNicCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/__pycache__/SmartNicCommand.cpython-310.pyc new file mode 100644 index 0000000..996cfa0 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/__pycache__/SmartNicCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..1b0f034 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/SMART_NIC_COMMANDS/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/__init__.py new file mode 100644 index 0000000..60ddfb7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/__init__.py @@ -0,0 +1,32 @@ +"""find and add dynamic extensions""" + +import os + +tl = [] +classNames = [] +Commands = {} + +extensionDir = os.path.dirname(__file__) + +if os.name != "nt": + replacement = "/" +else: + replacement = "\\" + +for cwd, dirs, filenames in os.walk(extensionDir): + dirs[:] = [d for d in dirs if not d[0] == "."] + tl.append((cwd, [files for files in filenames if not files[0] == "."])) + +for cwd, names in tl: + cn = cwd.split("extensions")[-1] + cn = cn.replace(replacement, ".") + comms = [] + for name in names: + if name.endswith(".pyc") and "__" not in name: + name = name.replace(".pyc", "") + classNames.append(cn + "." + name + "." + name) + elif name.endswith(".py") and "__" not in name: + name = name.replace(".py", "") + if name + ".pyc" in names: + continue + classNames.append(cn + "." + name + "." + name) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..3454b84 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/AHSdiagCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/AHSdiagCommand.py new file mode 100644 index 0000000..4136b90 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/AHSdiagCommand.py @@ -0,0 +1,519 @@ +### +# Copyright 2017 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""AHS diag Command for rdmc""" + +import os +from ctypes import POINTER, c_char_p, c_int, c_ubyte, create_string_buffer + +import redfish.hpilo.risblobstore2 as risblobstore2 + +try: + from rdmc_helper import ( + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + LOGGER, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +from redfish.hpilo.rishpilo import HpIlo + +if os.name != "nt": + from _ctypes import dlclose + + +class AHSdiagCommand: + """Add Sign/Marker posts into the AHS logs of the server""" + + def __init__(self): + self.ident = { + "name": "ahsdiag", + "usage": None, + "description": "Adding sign or Marker Post into AHS logs." + "\n\tahsdiag --WriteSignPost \n\tahsdiag --WriteMarkerPost" + ' --instance 1 --markervalue 3 --markertext "DIMM Test Start"', + "summary": "Adding sign or Marker Post into AHS logs.", + "aliases": ["ahsops"], + "auxcommands": [ + "LoginCommand", + "SelectCommand", + "LogoutCommand", + "ServerlogsCommand", + ], + } + + self.dynamicclass = -1 + self.lib = None + self.channel = None + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main ahsdiag function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.ahsdiagvalidation(options) + self.ahsdiagworkerfunction(options) + self.cmdbase.logout_routine(self, options) + return ReturnCodes.SUCCESS + + def ahsdiagworkerfunction(self, options): + """Main ahsdig worker function + + :param options: command line options + :type options: list. + """ + if not options: + raise InvalidCommandLineErrorOPTS("") + + if self.lib is None and self.channel is None: + self.resetilo() + + if options.signpost and options.markerpost: + raise InvalidCommandLineErrorOPTS("") + + if options.signpost: + if options.inst or options.mval or options.mtext: + raise InvalidCommandLineError("") + self.addsignpost(options) + elif options.markerpost: + if options.inst and options.mval and options.mtext: + self.addmarkerpost(options) + else: + raise InvalidCommandLineErrorOPTS("") + else: + self.rdmc.ui.printer("Choose an operation!\n") + raise InvalidCommandLineErrorOPTS("") + + def addsignpost(self, options): + """Function to add signpost + + :param options: command line options + :type options: list. + """ + excp = None + for _ in range(0, 3): # channel loop for iLO + self.rdmc.ui.printer("Start writing sign post ... \n") + try: + if not self.spregister(): + raise + self.rdmc.ui.printer("Successfully registered.\n") + fail = True + + for _ in range(3): + if self.signpostregister(): + self.rdmc.ui.printer("Signpost was successful!\n") + fail = False + break + else: + self.rdmc.ui.printer("Signpost attempt failed!\n") + + if options.postIML and fail: + self.addimlpost(options) + + self.unregister() + return + except Exception: + if self.rdmc.opts.verbose: + self.rdmc.ui.printer("Retrying with new channel...\n") + self.resetilo() + raise excp + + def spregister(self): + """Function that registers the signpost""" + self.bb_class_allocate() + + if self.dynamicclass == -1: + return False + + self.lib.bb_register_wrapper(self.dynamicclass) + size = self.lib.sizeofbbregister() + rcode = self.dowriteread(size) + + if rcode == 0: + self.lib.bb_descriptor_code_wrapper(self.dynamicclass) + size = self.lib.sizeofbbdescriptorcode() + rcode = self.dowriteread(size) + + # if rcode > 0: + # return True + # else: + # return False + return True + + def bb_class_allocate(self): + """Function to obtain a handle for further operations""" + self.lib.bb_class_allocate_wrapper() + self.lib.getreq.restype = POINTER(c_ubyte) + ptrreq = self.lib.getreq() + + # sizeofbbchifresp = self.lib.sizeofBBCHIFRESP() + size = self.lib.sizeofbbclassalloc() + data = bytearray(ptrreq[:size][:]) + + if self.rdmc.opts.debug: + LOGGER.debug("Sending data to chif:{0}\n".format(bytes(data))) + + resp = self.channel.chif_packet_exchange(data) + + resp = create_string_buffer(bytes(resp)) + self.lib.setresp.argtypes = [c_char_p] + self.lib.setresp(resp) + + if len(resp) != 0: + self.dynamicclass = self.lib.respmsgrcode() + self.rdmc.ui.printer("Do Read Return error code:{0}\n".format(self.dynamicclass)) + else: + self.dynamicclass = 0 + + def unregister(self): + """Function to unregister the handle""" + self.lib.bb_unregister_wrapper() + size = self.lib.sizeinunregister() + rcode = self.dowriteread(size) + + if rcode == 0: + self.rdmc.ui.printer("Unregistration successful!\n") + else: + self.rdmc.ui.printer("Failed to unregister with code:{0}.\n".format(rcode)) + + def dowriteread(self, size): + """Function responsible for communication with the iLO + + :param size: size of the buffer to be read or written + :type size: int + """ + ptrreq = self.lib.getreq() + data = bytearray(ptrreq[:size][:]) + if self.rdmc.opts.debug: + LOGGER.debug("Sending data to chif:{0}\n".format(data)) + try: + resp = self.channel.chif_packet_exchange(data) + except Exception: + self.resetilo() + return -1 + + self.lib.setresp.argtypes = [c_char_p] + resp = create_string_buffer(bytes(resp)) + self.lib.setresp(resp) + rcode = self.lib.respmsgrcode() + + if rcode == 0: + self.lib.updatehandle() + + return rcode + + def signpostregister(self): + """Worker function of signpost register""" + self.lib.bb_code_set_wrapper() + size = self.lib.sizeinbbcodeset() + returnval = self.dowriteread(size) + + if returnval != 0: + self.printsignpostregister(returnval) + return False + + self.lib.bb_log_wrapper() + size = self.lib.sizeinbblog() + returnval = self.dowriteread(size) + + if returnval != 0: + self.printsignpostregister(returnval) + return False + + self.lib.bb_log_static_wrapper() + size = self.lib.sizeinbblog() + returnval = self.dowriteread(size) + + if returnval != 0: + self.printsignpostregister(returnval) + return False + + return True + + def printsignpostregister(self, returnval): + """Commonly used output statement. + + :param returnval: return code for signpost + :type returnval: int + """ + self.rdmc.ui.printer("return signpost register failed={0}\n".format(returnval)) + + def addmarkerpost(self, options): + """Function to add marker post + + :param options: command line options + :type options: list. + """ + excp = None + for _ in range(0, 3): # channel loop for iLO + self.rdmc.ui.printer("Start writing marker post ... \n") + + try: + for i in range(3): + if self.mpregister(): + break + + self.rdmc.ui.printer("attempting to register Marker Post..." "failed {0} times\n".format(i + 1)) + + self.resetilo() + + self.rdmc.ui.printer("Successfully registered.\n") + fail = True + + for _ in range(3): + if self.markerpostregister(options): + self.rdmc.ui.printer("Marker Post was successful!\n") + fail = False + break + else: + self.rdmc.ui.printer("Marker Post attempt failed!\n") + + if options.postIML and fail: + self.addimlpost(options) + + self.unregister() + return + except Exception: + if self.rdmc.opts.verbose: + self.rdmc.ui.printer("Retrying with new channel...\n") + self.resetilo() + raise excp + + def mpregister(self): + """Function that registers the marker post""" + self.bb_class_allocate() + self.rdmc.ui.printer("return code(dynamic class)={0}.\n".format(self.dynamicclass)) + + if self.dynamicclass == -1: + return False + + self.lib.bb_register_mwrapper(self.dynamicclass) + size = self.lib.sizeofbbregister() + rcode = self.dowriteread(size) + + if rcode == 0: + self.lib.bb_descriptor_code_mwrapper(self.dynamicclass) + size = self.lib.sizeofbbdescriptorcode() + rcode = self.dowriteread(size) + + if rcode == -1: + return False + self.lib.bb_descriptor_field_mwrapper(self.dynamicclass) + size = self.lib.sizeofbbdescriptorcode() + rcode = self.dowriteread(size) + if rcode == -1: + return False + + return True + + def markerpostregister(self, options): + """Worker function of marker post register + + :param options: command line options + :type options: list. + """ + self.lib.bb_code_set_mwrapper() + size = self.lib.sizeinbbcodeset() + returnval = self.dowriteread(size) + + if returnval != 0: + return False + + mval = int(options.mval) + minst = int(options.inst) + mtext = create_string_buffer(bytes(options.mtext, "utf-8")) + + self.lib.markerpost_wrapper.argtypes = [c_int, c_int, c_char_p] + self.lib.markerpost_wrapper(mval, minst, mtext) + + size = self.lib.sizeinbblogm() + returnval = self.dowriteread(size) + + if returnval != 0: + return False + + return True + + def resetilo(self): + """Function to reset iLO""" + if self.channel: + self.channel.close() + self.loadlib() + self.channel = HpIlo(dll=self.lib) + self.lib.ChifDisableSecurity() + + def loadlib(self): + """Function to load the so library""" + self.closedll() + try: + if os.name == "nt": + self.rdmc.ui.printer("Operation can be performed only on Unix based systems!\n") + raise InvalidCommandLineErrorOPTS("") + else: + self.lib = risblobstore2.BlobStore2.gethprestchifhandle() + except Exception as excp: + raise InvalidCommandLineErrorOPTS(excp) + + def closedll(self): + """Deallocate dll handle.""" + try: + dlclose(self.lib) + except Exception: + pass + + def addimlpost(self, options): + """Adding maintenance post from serverlogs. + + :param options: command line options + :type options: list. + """ + if options.signpost: + imltext = "Signpost Writing Failed" + elif options.markerpost: + imltext = "Markerpost Writing Failed" + + options.service = "IML" + options.clearlog = None + options.mainmes = imltext + + path = self.serverlogsobj.returnimlpath() + self.serverlogsobj.addmaintenancelogentry(options, path=path) + + def ahsdiagvalidation(self, options): + """ahsdiag method validation function""" + client = None + + try: + self.cmdbase.login_select_validation(self, options) + client = self.rdmc.app.current_client + except Exception: + if client: + if not client.base_url == "blobstore://.": + raise InvalidCommandLineError("ahsdiag command " "available in local mode only.\n") + + if self.rdmc.config.url: + raise InvalidCommandLineError("ahsdiag command " "available in local mode only.\n") + + if not client: + self.lobobj.run("") + + def definearguments(self, customparser): + """Defines the required arguments for ahsdiag command. + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--selectlog", + dest="service", + help="""Read log from the given log service. Options: IML, """ """IEL or AHS.""", + default=None, + ) + customparser.add_argument( + "--clearlog", + "-c", + dest="clearlog", + action="store_true", + help="""Clears the logs for a the selected option.""", + default=None, + ) + customparser.add_argument( + "--maintenancemessage", + "-m", + dest="mainmes", + help="""Maintenance message to be inserted into the log. """ """(IML LOGS ONLY FEATURE)""", + default=None, + ) + customparser.add_argument( + "--customiseAHS", + dest="customiseAHS", + help="""Allows customised AHS log data to be downloaded.""", + default=None, + ) + customparser.add_argument( + "-s", + "--WriteSignPost", + dest="signpost", + action="store_true", + help="""Writes a sign post into the AHS log.""", + default=False, + ) + customparser.add_argument( + "-r", + "--WriteMarkerPost", + dest="markerpost", + action="store_true", + help="""Writes a marker post into the AHS log.""", + default=False, + ) + customparser.add_argument( + "-i", + "--instance", + dest="inst", + help="""Argument required by marker post.""", + default=None, + ) + customparser.add_argument( + "-l", + "--markervalue", + dest="mval", + help="""Argument required by marker post.""", + default=None, + ) + customparser.add_argument( + "-t", + "--markertext", + dest="mtext", + help="""Argument required by marker post.""", + default=None, + ) + customparser.add_argument( + "-w", + "--WriteIMLPost", + dest="postIML", + action="store_true", + help="""Writes an IML entry if failure occurs.""", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/AutomaticTestingCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/AutomaticTestingCommand.py new file mode 100644 index 0000000..53fc9f3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/AutomaticTestingCommand.py @@ -0,0 +1,2782 @@ +### +# Copyright 2016 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + + +# -*- coding: utf-8 -*- +"""Automatic Testing for rdmc""" + +import datetime +import json +import os +import platform +import random +import re +import string +import sys +import time +import traceback +from logging import FileHandler +from zipfile import ZipFile + +from six.moves.urllib.request import urlopen, urlretrieve + +try: + from rdmc_helper import ( + HARDCODEDLIST, + CommandNotEnabledError, + Encryption, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidOrNothingChangedSettingsError, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + PathUnavailableError, + ReturnCodes, + ) +except: + from ilorest.rdmc_helper import ( + HARDCODEDLIST, + CommandNotEnabledError, + Encryption, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidOrNothingChangedSettingsError, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + PathUnavailableError, + ReturnCodes, + ) + +from redfish.ris.rmc_helper import IloResponseError + +__error_logfile__ = "automatictesting_error_logfile.log" +__pass_fail__ = "passfail" +__last_line__ = "lastline" + + +def log_decor(func): + """decorator to gather and log traceback while continuing testing""" + + def func_wrap(*args, **kwargs): + """decorator wrapper function""" + try: + func(*args, **kwargs) + except Exception as exp: + if isinstance(exp, dict) or isinstance(exp, list): + # DOES NOT WORK YET. For errors which are captured and gathered before a raise, there + # is no point in showing the DEBUG log. + logging(command=func.__name__, error=exp, logfile_ignore=True) + elif args: + logging(command=func.__name__, error=exp, args=args[0]) + else: + logging(command=func.__name__, error=exp) + else: + logging(command=func.__name__, args=args[0]) + + return func_wrap + + +def logging(command, error=None, args=None, logfile_ignore=False): + """ + handler for logging errors as well as the pass/fail log. + + :param command: string representing the command/function which was tested + :param error: error string (if provided in the event of an exception) + :param args: arguments provided to the command/function which was tested. May need this + for various purposes (file handles, references to objects, references to object attributes) + + """ + debug_ret_err = False + debug_log_errs = [] + data2 = [] + data_passed_failed = {} + lockout = False + pass_flag = True + + error_strs = ["Code:400", "Code:401", "Exception", "EXCEPTION", "Warning", "WARNING"] + if not error: + try: + if args: + if args.rdmc.opts.debug: + for handle in args.rdmc.app.logger.parent.handlers: + if isinstance(handle, FileHandler): + # obtain last 25 lines of the debug log (if '-d' is used) + with open(handle.baseFilename, "rb") as dlfh: + debug_log_errs = tail_debug_log(dlfh, 25) + for line in debug_log_errs: + for err in error_strs: + if err in line: + with open(__last_line__, "r") as ll_file: + tmp_err = ll_file.readline() + if err not in tmp_err: + with open(__last_line__, "w+") as ll_file: + ll_file.write(line) + error = line + pass_flag = False + break + if error: + break + except: + debug_ret_err = True + + error_strs = ["Code:400", "Code:401", "Exception", "EXCEPTION", "Warning", "WARNING"] + if not error: + try: + if args: + if args.rdmc.opts.debug: + for handle in args.rdmc.app.logger.parent.handlers: + if isinstance(handle, FileHandler): + # obtain last 25 lines of the debug log (if '-d' is used) + with open(handle.baseFilename, "rb") as dlfh: + debug_log_errs = tail_debug_log(dlfh, 25) + for line in debug_log_errs: + for err in error_strs: + if err in line: + with open(__last_line__, "r") as ll_file: + tmp_err = ll_file.readline() + if err not in tmp_err: + with open(__last_line__, "w+") as ll_file: + ll_file.write(line) + error = line + pass_flag = False + break + if error: + break + except: + debug_ret_err = True + + try: + fhandle = open(__pass_fail__, "r") + data = fhandle.readlines() + fhandle.close() + for itr in data: + if "Total: " in itr: + data2.append("Total: " + str(int(itr.split(":")[-1].split("\n")[0]) + 1)) + continue + elif "Fail:" in itr and error: + data2.append("Fail: " + str(int(itr.split(":")[-1].split("\n")[0]) + 1)) + data_passed_failed[command] = "'{}' Command - Failed".format(command) + elif "Pass:" in itr and pass_flag: + data2.append("Pass: " + str(int(itr.split(":")[-1].split("\n")[0]) + 1)) + if command not in data_passed_failed: + data_passed_failed[command] = "'{}' Command - Passed".format(command) + elif "Command - " in itr: + resultdata = itr.split("'") + data_passed_failed[(resultdata[1])] = itr + elif not lockout: + lockout = True + data2.append(itr.split("\n")[0]) + except Exception: + sys.stderr.write("An error occurred with Pass/Fail log counter.\n") + data2 = None + pass + finally: + fhandle = open(__pass_fail__, "w+") + for element in data2: + fhandle.writelines(element + "\n") + fhandle.writelines("\n") + for k, v in list(data_passed_failed.items()): + if "\n" in v: + fhandle.writelines(v) + else: + fhandle.writelines(v + "\n") + fhandle.close() + + if error: + sys.stderr.write("An error occurred in: %s - %s\n" % (command, error)) + with open(__error_logfile__, "a+") as fhandle: + fhandle.write("\n" + command + " - Test#: ") + if data[0]: + fhandle.write(str(int(data[0].split(":")[-1].split("\n")[0]) + 1)) + else: + fhandle.write(" 1") + + fhandle.write("\nSimplified Error: " + str(error) + "\n") + # if args: + fhandle.write("\nTraceback:\n------------------------\n") + fhandle.write(str(traceback.format_exc()) + "\n") + if not debug_ret_err and not logfile_ignore: + fhandle.write("\nTail of Debug Logfile:") + fhandle.write("\n------------------------\n") + for item in debug_log_errs: + fhandle.write(item) + fhandle.write("\n------END OF DEBUG------\n") + + +def tail_debug_log(f, lines=1, _buffer=4098): + """Returns last n lines from the filename. + + :param f: filename for 'tail' retrieval + :type f: file handle + :param lines: number of lines to retrieve + :type lines: int + :param _buffer: buffer space for blocks + :type _buffer: bytes + """ + # place holder for the lines found + lines_found = [] + + # block counter will be multiplied by buffer + # to get the block size from the end + block_counter = -1 + + # loop until we find X lines + while len(lines_found) < lines: + try: + f.seek(block_counter * _buffer, os.SEEK_END) + except IOError: # either file is too small, or too many lines requested + f.seek(0) + lines_found = f.readlines() + break + + lines_found = f.readlines() + block_counter -= 1 + + return lines_found[-lines:] + + +class AutomaticTestingCommand: + """Automatic testing class command""" + + def __init__(self): + self.ident = { + "name": "automatictesting", + "usage": None, + "description": "Automatic testing command, not customer facing.", + "summary": "Automatic testing command, not customer facing.", + "aliases": [], + "auxcommands": [ + "LoginCommand", + "LogoutCommand", + "TypesCommand", + "SelectCommand", + "GetCommand", + "ListCommand", + "InfoCommand", + "SetCommand", + "StatusCommand", + "CommitCommand", + "SaveCommand", + "LoadCommand", + "RawDeleteCommand", + "RawHeadCommand", + "RawGetCommand", + "RawPatchCommand", + "RawPutCommand", + "RawPostCommand", + "IscsiConfigCommand", + "FirmwareUpdateCommand", + "IloResetCommand", + "ISToolCommand", + "ResultsCommand", + "BootOrderCommand", + "ServerStateCommand", + "VirtualMediaCommand", + "IloAccountsCommand", + "IloFederationCommand", + "ClearRestApiStateCommand", + "CertificateCommand", + "SigRecomputeCommand", + "ESKMCommand", + "SendTestCommand", + "SingleSignOnCommand", + "ServerlogsCommand", + "AHSdiagCommand", + "SMBiosCommand", + "IloLicenseCommand", + "DeleteComponentCommand", + "DownloadComponentCommand", + "InstallSetCommand", + "ListComponentCommand", + "UpdateTaskQueueCommand", + "UploadComponentCommand", + "HpGooeyCommand", + "PendingChangesCommand", + "ServerInfoCommand", + "MaintenanceWindowCommand", + "FwpkgCommand", + "RebootCommand", + "IPProfilesCommand", + "FirmwareIntegrityCheckCommand", + "SetPasswordCommand", + "BiosDefaultsCommand", + "FwpkgCommand", + "ServerCloneCommand", + "FactoryDefaultsCommand", + "DirectoryCommand", + "SmartArrayCommand", + ], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self.ret_code = ReturnCodes.SUCCESS + + def run(self, line, help_disp=False): + """Main automatic testing worker function + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + if sys.platform == "darwin": + raise CommandNotEnabledError("'%s' command is not supported on MacOS" % str(self.name)) + elif "VMkernel" in platform.uname(): + raise CommandNotEnabledError("'%s' command is not supported on VMWare" % str(self.name)) + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.getautomatictestingvalidation(options) + with open(__last_line__, "w+") as ll_file: + ll_file.write("") + with open(__pass_fail__, "w+") as pf_file: + pf_file.write("Total: 0\nPass: 0\nFail: 0\n") + with open(__error_logfile__, "w+") as log_file: + log_file.write("") + + self.rdmc.ui.printer("***************************************************" "******************************\n") + self.rdmc.ui.printer("****************************AUTOMATIC TESTING " "STARTING***************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.options = options + if options.sumtest: + self.testsumcode() + return ReturnCodes.SUCCESS + if options.ahsdiagtest: + self.ahsdiagtesting(options.local) + + # erase iLORest Debug Log (if debugging enabled) + if self.rdmc.opts.debug: + for handle in self.rdmc.app.logger.parent.handlers: + if isinstance(handle, FileHandler): + with open(handle.baseFilename, "w+") as dlfh: + dlfh.write("") + if self.rdmc.config: + if self.rdmc.config._ac__savefile: + self.logfile = self.rdmc.config._ac__savefile + + self.helpmenusautomatictesting() + self.loginautomatictesting() + self.typesandselectautomatictesting(options.local, options.latestschema) + self.getandsetautomatictesting(options.latestschema) + self.saveandloadautomatictesting(options.local, options.latestschema) + self.iscsiautomatictesting() + self.istoolautomatictesting() + self.resultsautomatictesting() + self.bootorderautomatictesting() + self.serverstateautomatictesting() + self.virtualmediaautomatictesting() + self.accountautomatictesting() + self.federationautomatictesting() + self.certificateautomatictesting() + self.eskmautomatictesting() + self.sendtestautomatictesting() + self.ssoautomatictesting() + self.sigrecomputeautomatictesting() + self.serverlogstesting() + self.ilolicensetesting() + self.setbiospasswordautomatictesting(options.local) + self.smbiostesting() + self.cloningautomatictesting() + self.pendingautomatictesting() + self.serverinfoautomatictesting() + self.directoryautomatictesting() + if options.complete: + self.serverclonecommandautomatictesting(options.local) + self.fwpkgcommandautomatictesting() + self.ipprofilesautomatictesting() + self.fwintegritycheckautomatictesting() + self.uploadcomptesting() + self.listcomptesting() + self.updatetaskqueuetesting() + self.installsettesting() + self.maintenancewindowautomatictesting() + self.listanddownloadcomptesting() + self.deletecomptesting() + if options.complete: + self.setbiosdefaultsautomatictesting() # Sets bios default + self.rawautomatictesting(options.local) + if options.complete: + self.smartarrayautomatictesting() + self.firmwareautomatictesting() + + self.rdmc.ui.printer("***************************************************" "******************************\n") + self.rdmc.ui.printer("******************************AUTOMATIC TESTING " "DONE*****************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + # Show PASS/FAIL + with open(__pass_fail__, "r") as fhandle: + data = fhandle.readlines() + self.rdmc.ui.printer("Results are:\n") + for item in data: + if item == "\n": + break + else: + self.rdmc.ui.printer(item) + + self.rdmc.ui.printer("Review logfiles: '%s', '%s',\n" % (__pass_fail__, __error_logfile__)) + + if options.archive: + self.archive_handler(options.archive[0]) + + self.cmdbase.logout_routine(self, options) + # Return code + self.rdmc.ui.printer("\n") + return self.ret_code + + def getautomatictestingvalidation(self, options): + """get inventory validation function + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + @log_decor + def helpmenusautomatictesting(self): + """handler for help menu auto test""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("**********************STARTING HELP MENU AUTOMATIC " "TESTING***********************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + # Argparse will attempt a force exit when using '-h'? Maybe we need to re-add the help + # command again... + # try: + # self.rdmc.run(['--h']) + # except Exception as exp: + # pass + + for command in self.auxcommands: + self.auxcommands[command].run("-h") + self.rdmc.ui.printer( + "\n*********************************************" "************************************\n" + ) + self.rdmc.ui.printer( + "***********************************************" "**********************************\n" + ) + self.rdmc.ui.printer( + "***********************************************" "**********************************\n\n" + ) + + @log_decor + def loginautomatictesting(self): + """handler for login auto test""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("************************STARTING LOGIN AUTOMATIC " "TESTING*************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + loopcount = 10 + + for xint in range(loopcount): + self.rdmc.ui.printer("Login operation counter: %d\n" % xint) + self.auxcommands["login"].loginfunction("") + + self.auxcommands["logout"].logoutfunction("") + self.rdmc.ui.printer("\n") + + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + @log_decor + def typesandselectautomatictesting(self, local, latestschema): + """handler for types and select auto test + + :param local: flag to enable local login + :type local: boolean. + :param latestschema: flag to determine if we should use smart schema. + :type latestschema: boolean. + """ + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*******************STARTING TYPES AND SELECT " "AUTOMATIC TESTING*******************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + if local: + self.auxcommands["login"].loginfunction("") + + typeslist = self.auxcommands["types"].typesfunction("", returntypes=True) + + for item in typeslist: + if item and not item.startswith("Session."): + self.rdmc.ui.printer("Selected option: '%s'\n" % item) + self.auxcommands["select"].selectfunction(item) + self.auxcommands["select"].selectfunction("") + + self.getandinfoautomatictesting(latestschema=latestschema) + + self.rdmc.ui.printer( + "\n*****************************************" "***************************************\n" + ) + self.rdmc.ui.printer( + "*******************************************" "*************************************\n" + ) + self.rdmc.ui.printer( + "*******************************************" "*************************************\n\n" + ) + + self.auxcommands["logout"].logoutfunction("") + + @log_decor + def getandsetautomatictesting(self, latestschema): + """handler for get and set auto test + + :param latestschema: flag to determine if we should use smart schema. + :type latestschema: boolean. + """ + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************STARTING GET AND SET AUTOMATIC" " TESTING**********************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].loginfunction("") + self.rdmc.ui.printer("\n") + + if self.rdmc.app.typepath.flagiften: + biosinputlist = [ + "Attributes/BootMode", + "Attributes/MissingItem", + "Attributes/AdminName", + "Attributes/AdminEmail", + "Attributes/AdminPhone", + "Attributes/CustomPostMessage", + "Attributes/ServerAssetTag", + "Attributes/ServerName", + "Attributes/ServerOtherInfo", + "Attributes/ServerPrimaryOs", + "Attributes/ServiceEmail", + "Attributes/ServiceName", + "Attributes/ServiceOtherInfo", + "Attributes/ServicePhone", + ] + else: + biosinputlist = [ + "BootMode", + "MissingItem", + "AdminName", + "AdminEmail", + "AdminPhone", + "CustomPostMessage", + "ServerAssetTag", + "ServerName", + "ServerOtherInfo", + "ServerPrimaryOs", + "ServiceEmail", + "ServiceName", + "ServiceOtherInfo", + "ServicePhone", + ] + + self.setautomatichelper(biosinputlist, "HpBios.", latestschema) + + self.rdmc.ui.printer("\n*************************************************" "*******************************\n") + self.rdmc.ui.printer("***************************************************" "*****************************\n") + self.rdmc.ui.printer("***************************************************" "*****************************\n\n") + + biosinputlist = ["Boot/BootSourceOverrideTarget", "AssetTag"] + self.setautomatichelper(biosinputlist, "ComputerSystem.", latestschema) + + self.rdmc.ui.printer("\n*************************************************" "*******************************\n") + self.rdmc.ui.printer("***************************************************" "*****************************\n") + self.rdmc.ui.printer("***************************************************" "*****************************\n\n") + self.auxcommands["status"].run("") + + self.rdmc.ui.printer("\n*************************************************" "*******************************\n") + self.rdmc.ui.printer("***************************************************" "*****************************\n") + self.rdmc.ui.printer("***************************************************" "*****************************\n\n") + self.auxcommands["commit"].run("") + + @log_decor + def setautomatichelper(self, inputlist, selector, latestschema): + """handler for set auto test + + :param inputlist: list of items to be set. + :type inputlist: list. + :param selector: type to be select for items that will be set. + :type selector: str. + :param latestschema: flag to determine if we should use smart schema. + :type latestschema: boolean. + """ + self.rdmc.ui.printer("STARTING TEST FOR SETTINGS ON '%s'.\n" % selector) + self.auxcommands["select"].selectfunction([selector]) + + for item in inputlist: + listresults = self.auxcommands["get"].getworkerfunction(item, self.options, results=True) + for getresults in listresults: + if getresults and isinstance(getresults[next(iter(getresults))], dict): + for key in getresults: + getresults = getresults[key] + + for key, value in list(getresults.items()): + if key == "BootSourceOverrideTarget": + if value == "None": + randstring = "Cd" + else: + randstring = "None" + else: + randstring = "".join( + random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10) + ) + + if value == randstring: + value = value[::-1] + + self.rdmc.ui.printer("Setting: '%s'\t\tBefore:'%s'\tAfter:'%s" "'\n" % (key, value, randstring)) + + inputline = item + "=" + randstring + + if latestschema: + inputline += " --latestschema" + + try: + self.auxcommands["set"].run(inputline, skipprint=True) + except: + pass + elif getresults: + for key, value in list(getresults.items()): + randstring = "".join( + random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10) + ) + + if value == randstring: + value = value[::-1] + + self.rdmc.ui.printer("Setting: '%s'\t\tBefore:'%s'\tAfter:'%s" "'\n" % (key, value, randstring)) + + inputline = key + "=" + randstring + + if latestschema: + inputline += " --latestschema" + + try: + self.auxcommands["set"].run(inputline, skipprint=True) + except: + pass + else: + InvalidCommandLineError("Variable '%s' was not found in the " "current selection.\n" % item) + + @log_decor + def getandinfoautomatictesting(self, item="", latestschema=False, getval=None, already_checked=[]): + """handler for get and info auto test + + :param item: list of items to be set. + :type item: list. + :param latestschema: flag to determine if we should use smart schema. + :type latestschema: boolean. + :param selectitems: list of selected items. + :type selectitems: list. + :param origin: location for current item. + :type origin: str. + :param already_checked: List of keys already checked. + :type already_checked: list. + """ + listdata = self.auxcommands["get"].getworkerfunction(item, self.options, results=True) + if not listdata: + self.rdmc.ui.warn("(GET) No list contents found for section: '%s'\n" % item) + + getdata = self.auxcommands["get"].getworkerfunction(item, self.options, uselist=True, results=True) + if getdata and item and self.rdmc.opts.verbose: + self.rdmc.ui.printer("Contents found for section: '%s'\n" % item) + elif not getdata: + self.rdmc.ui.warn("(LIST) No contents found for section: '%s'\n" % item) + + cline = item + " --latestschema" if latestschema else item + try: + if not self.auxcommands["info"].run(cline, autotest=True): + logging("info", cline, add_traceback=False) + raise KeyError + except: + self.rdmc.ui.warn("(LIST) No info contents found for section: '%s'\n" % cline) + + if not getdata: + return + + getdata = getval if getval is not None or item else getdata + if isinstance(getdata, dict): + for key, val in list(getdata.items()): + if key.lower() in HARDCODEDLIST or key.lower() in already_checked: + continue + key = (item + "/" + key) if item else key + already_checked.append(key) + self.getandinfoautomatictesting( + item=key, + latestschema=latestschema, + getval=val, + already_checked=already_checked, + ) + elif isinstance(getdata, (list, tuple)): + for kii in getdata: + if not isinstance(kii, dict): + continue + for key, val in list(kii.items()): + if key.lower() in HARDCODEDLIST: + continue + key = (item + "/" + key) if item else key + if key in already_checked: + continue + else: + already_checked.append(key) + self.getandinfoautomatictesting( + item=key, + latestschema=latestschema, + getval=val, + already_checked=already_checked, + ) + + @log_decor + def saveandloadautomatictesting(self, local, latestschema): + """handler for save and load auto test + + :param local: flag to enable local login + :type local: boolean. + :param latestschema: flag to determine if we should use smart schema. + :type latestschema: boolean. + """ + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("********************STARTING SAVE AND LOAD " "AUTOMATIC TESTING*********************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + biosinputlist = [ + "AdminName", + "AdminEmail", + "AdminPhone", + "CustomPostMessage", + "ServerAssetTag", + "ServerName", + "ServerOtherInfo", + "ServerPrimaryOs", + "ServiceEmail", + "ServiceName", + "ServiceOtherInfo", + "ServicePhone", + ] + # TODO: Verify the data in file matches server after load + # TODO: Add multi select save/load checking + if local: + self.auxcommands["login"].loginfunction("") + + self.auxcommands["save"].run("--selector=HpBios. -f ilorest.json") + + fileh = open("ilorest.json", "r") + tempholder = fileh.read() + + for item in biosinputlist: + restring = '"' + item + '": ".*?"' + randstring = "".join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(10)) + + replacestring = '"' + item + '": "' + randstring + '"' + tempholder = re.sub(restring, replacestring, tempholder, flags=re.DOTALL) + + outfile = open("ilorest.json", "w") + outfile.write(tempholder) + outfile.close() + + self.rdmc.ui.printer("\n") + + if latestschema: + self.auxcommands["load"].run("--latestschema") + else: + self.auxcommands["load"].run("-f ilorest.json") + + self.rdmc.ui.printer("\n") + self.auxcommands["logout"].run("") + + @log_decor + def iscsiautomatictesting(self): + """iscsi automatic testing""" + self.rdmc.ui.printer("***************************************************" "******************************\n") + self.rdmc.ui.printer("*****************STARTING ISCSI CONFIGURATION " "AUTOMATIC TESTING******************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + found = 0 + testfile = None + self.auxcommands["iscsiconfig"].run("") + + try: + self.auxcommands["iscsiconfig"].run("--delete 1") + except: + pass + + self.auxcommands["iscsiconfig"].run("--add [1]") + self.auxcommands["iscsiconfig"].run("--list") + self.auxcommands["iscsiconfig"].run("--list -f TESTFILE") + + try: + testfile = open("TESTFILE") + for line in testfile: + if '"Attempt 1"' in line: + found = 1 + self.rdmc.ui.printer("Output Validated.\n") + break + + if found == 0: + raise NoContentsFoundForOperationError("Change not found.") + except Exception as excp: + raise excp + + try: + testfile.seek(0) + found = 1 + self.auxcommands["iscsiconfig"].run("--delete 1") + self.auxcommands["iscsiconfig"].run("--list -f TESTFILE") + + for line in testfile: + if '"Attempt 1"' in line: + raise NoChangesFoundOrMadeError("Not deleted") + + if found == 0: + self.rdmc.ui.printer("Attempt successfully deleted.\n") + except Exception as excp: + raise excp + + self.auxcommands["logout"].run("") + + @log_decor + def rawautomatictesting(self, local): + """raw commands automatic testing + + :param local: flag to enable local login + :type local: boolean. + """ + if local: + self.auxcommands["login"].loginfunction("") + + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************STARTING RAW_COMMANDS " "AUTOMATIC TESTING*********************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["rawhead"].run('"/rest/v1/systems/1" -f TESTFILE') + + try: + testfile = open("TESTFILE") + json.loads(testfile.read()) + except Exception as excp: + raise excp + finally: + testfile.close() + + self.rdmc.ui.printer("json return validated.\n") + self.auxcommands["rawget"].run('"/rest/v1/systems/1/bios/Settings" -f TESTFILE') + + try: + testfile = open("TESTFILE") + json.loads(testfile.read()) + except Exception as excp: + raise excp + finally: + testfile.close() + + self.rdmc.ui.printer("json return validated.\n") + + try: + writefile = open("WRITEFILE", "w") + writefile.write( + '{\n\t"path": "/rest/v1/systems/1/bios/Settings",' '\n\t"body": {\n\t\t"AdminName": "RexySmith"\n\t}\n}' + ) + writefile.close() + + self.auxcommands["rawpatch"].run("WRITEFILE") + self.auxcommands["rawget"].run('"/rest/v1/systems/1/bios/Settings" -f TESTFILE') + testfile = open("TESTFILE") + found = 0 + + for line in testfile: + if '"AdminName"' in line: + if '"RexySmith"' in line: + found = 1 + self.rdmc.ui.printer("json return validated.\n") + break + + if found == 0: + raise NoContentsFoundForOperationError("Change not found.") + + except Exception as excp: + raise excp + + finally: + testfile.close() + + # ******************RAW PUT************************* + try: + writefile = open("WRITEFILE", "w") + if self.rdmc.app.typepath.flagiften: + writefile.write( + '{\n\t"path":"/rest/v1/systems/1/bios/Settings",\n' + '\t"body":{\n\t\t"Attributes":{\n\t\t\t"BaseConfig":' + ' "default"\n\t\t}\n\t}\n}' + ) + else: + writefile.write( + '{\n\t"path": "/rest/v1/systems/1/bios/Settings",\n' + '\t"body":{\n\t\t"BaseConfig": "default"' + "\n\t}\n}" + ) + writefile.close() + + self.auxcommands["rawput"].run("WRITEFILE") + self.auxcommands["rawget"].run('"/rest/v1/systems/1/bios/Settings" -f TESTFILE') + testfile = open("TESTFILE") + found = 0 + + for line in testfile: + if '"BaseConfig"' in line: + if '"default"' in line: + found = 1 + self.rdmc.ui.printer("json return validated\n") + break + + if found == 0: + raise NoContentsFoundForOperationError("Change not found.") + except Exception as excp: + raise excp + finally: + testfile.close() + + # ******************RAW POST************************ + if not local: + try: + writefile = open("WRITEFILE", "w") + + if self.rdmc.app.typepath.flagiften: + writefile.write( + '{\n\t"path": "/redfish/v1/Systems/1/' + 'Actions/ComputerSystem.Reset/",\n\t"body": ' + '{\n\t\t"ResetType": "ForceRestart"\n\t}\n}' + ) + else: + writefile.write( + '{\n\t"path": "/rest/v1/Systems/1",\n' + '\t"body": {\n\t\t"Action": "Reset",' + '\n\t\t"ResetType": "ForceRestart"\n\t}\n}' + ) + + writefile.close() + + self.auxcommands["rawpost"].run("WRITEFILE") + self.rdmc.ui.printer("Waiting for BIOS to reset. See you in 7 mins.\n") + time.sleep(60) + self.rdmc.ui.printer("6 minutes remaining...\n") + time.sleep(60) + self.rdmc.ui.printer("5 minutes remaining...\n") + time.sleep(60) + self.rdmc.ui.printer("4 minutes remaining...\n") + time.sleep(60) + self.rdmc.ui.printer("3 minutes remaining...\n") + time.sleep(60) + self.rdmc.ui.printer("2 minutes remaining...\n") + time.sleep(60) + self.rdmc.ui.printer("1 minute remaining...\n") + time.sleep(60) + + self.auxcommands["rawget"].run('"/rest/v1/systems/1/bios/Settings" -f TESTFILE') + testfile = open("TESTFILE") + found = 0 + + # may error if bios don't update upon restart + for line in testfile: + if '"AdminEmail"' in line: + if '""' in line: + found = 1 + self.rdmc.ui.printer("reset validated\n") + break + + if found == 0: + raise NoContentsFoundForOperationError("Change not found.") + except Exception as excp: + raise excp + finally: + testfile.close() + + # ******************RAW DELETE********************** + self.auxcommands["rawget"].run('"/rest/v1/Sessions" -f TESTFILE') + + try: + testfile = open("TESTFILE") + next_item = False + + for line in testfile: + if next_item: + session = line + break + + if '"MySession":' in line and '"MySession": false' not in line and '"MySession": true' not in line: + next_item = True + + session = session.split('"') + + for item in session: + if "/Sessions/" in item: + session = '"%s"' % item + break + except Exception as excp: + raise excp + finally: + testfile.close() + + try: + self.auxcommands["rawdelete"].run(session) + self.rdmc.ui.printer("Session Deleted\n") + except Exception as excp: + raise excp + + ''' Rework to configure firmware update test. + @log_decor + def firmwareautomatictesting(self): + """ firmware update automatic testing """ + + self.rdmc.ui.printer("\n*************************************************" \ + "********************************\n") + self.rdmc.ui.printer("*********************SKIPING FIRMWARE " \ + "AUTOMATIC TESTING***************************\n") + self.rdmc.ui.printer("***************************************************" \ + "******************************\n\n") + + fw_url = "http://infinitymaster.us.rdlabs.hpecorp.net:1051/automatic_testing/components"\ + "/FIRMWARE/blah" + tmp = urlopen(fw_url) + rom_url = fw_url + "/ROM/" + ilo_url = fw_url + "/iLO/" + +# rom_fw_dict = [] +# ilo_fw_dict = [] + + rom_fw_arr = self.automatictesting_helper_fb(rom_url) +# ilo_fw_arr = self.automatictesting_helper_fb(ilo_url) + + try: + for i in rom_fw_arr: + #tmparr = path.split('/') + #urlretrieve(fw_url + path, tmparr[-1]) + (fmw, _) = urlretrieve(rom_url + i, i) + self.auxcommands['flashfwpkg'].run(fmw + ' --forceupload --ignorechecks') + + except Exception as excp: + self.rdmc.ui.printer("A General Error Occured: %s" %excp) + block = True + + #self.auxcommands['login'].run("") + +# if local: +# return + +# self.auxcommands['login'].run("") +# tests = {'rom': [], 'firmware': firmware} +# (romfamily, _) = self.rdmc.app.getbiosfamilyandversion() + +# for version in rom: +# if romfamily.lower() in version.lower(): +# tests['rom'].append(version) + +# for test in tests: +# for link in tests[test]: +# counter = 0 +# keep_going = True + +# self.auxcommands['logout'].run("") +# self.auxcommands['login'].run("") +# self.auxcommands['firmwareupdate'].run(link) + +# if test == 'rom': +# self.auxcommands['login'].run("") +# self.auxcommands['iloreset'].run("") + +# self.rdmc.ui.printer("Waiting for iLO...\n") +# time.sleep(60) +# self.rdmc.ui.printer("1 minute remaining...\n") +# time.sleep(60) + +# while keep_going: +# try: +# if counter > 50: +# raise + +# counter += 1 +# keep_going = False +# self.auxcommands['login'].run("") +# except Exception as excp: +# if counter > 50: +# raise excp +# else: +# keep_going = True + + ''' + + @log_decor + def istoolautomatictesting(self): + """istool automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************STARTING ISTOOL " "AUTOMATIC TESTING***************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + self.auxcommands["istool"].run("") + self.auxcommands["logout"].run("") + + @log_decor + def resultsautomatictesting(self): + """results automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************STARTING RESULTS " "AUTOMATIC TESTING**************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + self.auxcommands["results"].run("") + self.auxcommands["logout"].run("") + + @log_decor + def bootorderautomatictesting(self): + """boot order automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************STARTING BOOT ORDER " "AUTOMATIC TESTING***********************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + try: + self.auxcommands["bootorder"].run("--disablebootflag --commit") + except: + pass + + self.auxcommands["bootorder"].run("[5,4,3,2,1]") + self.auxcommands["logout"].run("") + + self.auxcommands["bootorder"].run("--onetimeboot=Cd") + self.auxcommands["commit"].run("") + btype, target = self.bootorderhelper() + + if not btype == "Once" or not target == "Cd": + raise InvalidOrNothingChangedSettingsError + + self.auxcommands["bootorder"].run("--onetimeboot=Hdd") + self.auxcommands["bootorder"].run("--continuousboot=Hdd --commit") + btype, target = self.bootorderhelper() + + if not btype == "Continuous" or not target == "Hdd": + raise InvalidOrNothingChangedSettingsError + + # self.auxcommands['bootorder'].run("--onetimeboot=Cd") + # self.auxcommands['bootorder'].run("--continuousboot=Hdd") + # self.auxcommands['bootorder'].run("--disablebootflag --commit") + # btype, target = self.bootorderhelper() + # + # if not btype == 'Disabled' or not target == 'None': + # raise InvalidOrNothingChangedSettingsError + + self.auxcommands["bootorder"].run("--disablebootflag") + # try: + # self.auxcommands['bootorder'].run("--disablebootflag") + # except: + # self.rdmc.ui.printer("Entry is the current boot setting.\n") + + self.auxcommands["logout"].run("") + + self.auxcommands["login"].run("") + self.auxcommands["bootorder"].run("--securebootkeys=deletepk") + self.auxcommands["logout"].run("") + + self.auxcommands["login"].run("") + self.auxcommands["bootorder"].run("--securebootkeys=defaultkeys") + self.auxcommands["logout"].run("") + + self.auxcommands["login"].run("") + self.auxcommands["bootorder"].run("--securebootkeys=deletekeys") + self.auxcommands["logout"].run("") + + @log_decor + def bootorderhelper(self): + """boot order helper for automatic testing""" + self.auxcommands["login"].run("") + results = self.rdmc.app.get_handler(self.rdmc.app.typepath.defs.systempath, service=True, silent=True) + + btype = results.dict["Boot"]["BootSourceOverrideEnabled"] + target = results.dict["Boot"]["BootSourceOverrideTarget"] + + return btype, target + + @log_decor + def serverstateautomatictesting(self): + """server state automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************SERVER STATE " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + self.auxcommands["serverstate"].run("") + self.auxcommands["logout"].run("") + + @log_decor + def virtualmediaautomatictesting(self): + """virtual media automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************VIRTUAL MEDIA " "AUTOMATIC TESTING*****************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + return + + self.auxcommands["login"].run("") + self.auxcommands["virtualmedia"].run("") + + try: + self.auxcommands["virtualmedia"].run("2 --remove") + except: + pass + + # Needs to be updated + self.auxcommands["virtualmedia"].run("2 http://10.0.0.1/vm.iso --bootnextreset") + results = self.rdmc.app.get_handler("/rest/v1/Managers/1/VirtualMedia/2", service=True, silent=True).dict + + if not results["Inserted"] or not results["Oem"][self.rdmc.app.typepath.defs.oemhp]["BootOnNextServerReset"]: + raise InvalidOrNothingChangedSettingsError("VM not found.") + + self.auxcommands["virtualmedia"].run("2 --remove") + + results = self.rdmc.app.get_handler("/rest/v1/Managers/1/VirtualMedia/2", service=True, silent=True).dict + + if results["Inserted"] or results["Oem"][self.rdmc.app.typepath.defs.oemhp]["BootOnNextServerReset"]: + raise InvalidOrNothingChangedSettingsError("VM not removed.") + + self.auxcommands["logout"].run("") + + @log_decor + def accountautomatictesting(self): + """iLO manager account automatic testing""" + """ - add, modify privs, changepass, delete""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("**************************ACCOUNT " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + self.auxcommands["iloaccounts"].run("") + errors = [] + priv_dict = { + "LoginPriv": 1, + "RemoteConsolePriv": 2, + "UserConfigPriv": 3, + "iLOConfigPriv": 4, + "VirtualMediaPriv": 4, + "VirtualPowerAndResetPriv": 5, + "HostNICConfigPriv": 6, + "HostBIOSConfigPriv": 7, + "HostStorageConfigPriv": 9, + "SystemRecoveryConfigPriv": 7, + } + for i in range(5): + randname = "".join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(6)) + accid = None + rand_privs = random.sample(priv_dict, 3) + rand_priv_str = "--addprivs " + for rp in enumerate(rand_privs): + if rp[1] == "SystemRecoveryConfigPriv": + continue + elif rp[0] == (len(rand_privs) - 1): + rand_priv_str += str(priv_dict[rp[1]]) + else: + rand_priv_str += str(priv_dict[rp[1]]) + "," + acct_str = "add " + randname + " " + randname + " testpassword " + rand_priv_str + try: + self.auxcommands["iloaccounts"].run(acct_str) + except Exception as excp: + if "rand_priv" in locals(): + errors.append("Unable to create account '{}', " "'{}': {}".format(randname, acct_str, str(excp))) + + results = self.rdmc.app.get_handler( + self.rdmc.app.typepath.defs.accountspath, service=True, silent=True + ).dict[self.rdmc.app.typepath.defs.collectionstring] + + if "Id" not in list(results[0].keys()): + newresults = [] + + for acct in results: + acct = self.rdmc.app.get_handler( + acct[self.rdmc.app.typepath.defs.hrefstring], + service=True, + silent=True, + ).dict + newresults.append(acct) + + results = newresults + + for acct in results: + if acct["Oem"][self.rdmc.app.typepath.defs.oemhp]["LoginName"] != randname: + continue + else: + self.rdmc.ui.printer("Created account found.\n") + accid = acct["Id"] + privs = acct["Oem"][self.rdmc.app.typepath.defs.oemhp]["Privileges"] + try: + rand_priv = next(iter(random.sample(privs, 1))) + privstr = "" + if privs[rand_priv]: + privstr = "--removeprivs=" + str(priv_dict[rand_priv]) + else: + privstr = "--addprivs=" + str(priv_dict[rand_priv]) + self.auxcommands["iloaccounts"].run("modify " + randname + " " + privstr) + except Exception as excp: + if "rand_priv" in locals(): + errors.append( + "Unable to modify privilege '{}' for account " + "'{}': {}".format(rand_priv, randname, str(excp)) + ) + else: + errors.append( + "Unable to retrieve privileges for account '{}' " ": {}".format(randname, excp) + ) + break + self.auxcommands["iloaccounts"].run("changepass " + randname + " newpassword") + self.auxcommands["iloaccounts"].run("delete " + accid) + + if not accid: + raise NoChangesFoundOrMadeError("Created account not found.") + + self.auxcommands["logout"].run("") + if errors: + raise Exception("The following errors occurred while testing accounts: %s" % errors) + + @log_decor + def federationautomatictesting(self): + """federation automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************ADDFEDERATION " "AUTOMATIC TESTING*****************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + self.auxcommands["ilofederation"].run("") + + redfish = self.rdmc.app.monolith.is_redfish + randname = "".join(random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(6)) + foundfed = False + + self.auxcommands["ilofederation"].run("add " + randname + " password") + self.auxcommands["ilofederation"].run("") + + path = self.rdmc.app.typepath.defs.federationpath + results = self.rdmc.app.get_handler(path, service=True, silent=True).dict + + if redfish: + results = results["Members"] + else: + results = results["links"]["Member"] + + newresults = [] + for fed in results: + fed = self.rdmc.app.get_handler(fed[self.rdmc.app.typepath.defs.hrefstring], service=True, silent=True).dict + + newresults.append(fed) + results = newresults + + for fed in results: + if fed["Id"] == randname: + self.rdmc.ui.printer("Created federation found.\n") + foundfed = True + break + + if not foundfed: + raise NoChangesFoundOrMadeError("Created federation not found.") + + self.auxcommands["ilofederation"].run("changekey " + randname + " newpassword") + self.auxcommands["ilofederation"].run("delete " + randname) + self.auxcommands["logout"].run("") + + @log_decor + def serverlogstesting(self): + """server logs automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************STARTING SERVERLOGS " "AUTOMATIC TESTING***********************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].loginfunction("") + self.auxcommands["serverlogs"].run("--selectlog=IEL -f IELlog.txt") + self.auxcommands["serverlogs"].run("--selectlog=IML -f IMLlog.txt") + self.auxcommands["serverlogs"].run("--selectlog=AHS --downloadallahs") + self.auxcommands["serverlogs"].run("--selectlog=AHS --clearlog") + self.auxcommands["serverlogs"].run("--selectlog=IEL --clearlog") + self.auxcommands["serverlogs"].run("--selectlog=IML --clearlog") + + self.auxcommands["logout"].run("") + + @log_decor + def ahsdiagtesting(self, local): + """ahs diags automatic testing""" + if local and os.name != "nt": + self.rdmc.ui.printer( + "\n*********************************************" "************************************\n" + ) + self.rdmc.ui.printer( + "*********************STARTING AHSDIAG " "AUTOMATIC TESTING**************************\n" + ) + self.rdmc.ui.printer( + "***********************************************" "**********************************\n\n" + ) + pass + + self.auxcommands["login"].loginfunction("") + self.auxcommands["ahsdiag"].run("--WriteSignPost") + self.auxcommands["ahsdiag"].run( + "--WriteMarkerPost --instance 1 --markervalue" ' 3 --markertext "Automatictesting"' + ) + + self.auxcommands["logout"].run("") + + @log_decor + def crapistesting(self): + """clear rest api automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************CLEARRESTAPI " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["clearrestapistate"].run("") + + if self.rdmc.app.monolith.is_redfish: + path = "/redfish/v1/registries/" + else: + path = "/rest/v1/registries" + + results = self.rdmc.app.get_handler(path, service=True, silent=True).dict + + for item in results[self.rdmc.app.typepath.dict.collectionstring]: + if "attributereg" in item[self.rdmc.app.typepath.defs.hrefstring]: + raise NoChangesFoundOrMadeError("No changes found.") + + self.auxcommands["logout"].run("") + + @log_decor + def certificateautomatictesting(self): + """generate csr automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*******************SSO AND TLS CERTIFICATE " "AUTOMATIC TESTING*********************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + error_dict = dict() + delete_items = list() + + url_path = "http://infinitymaster.us.rdlabs.hpecorp.net:1051/automatic_testing/x509_certs/" + + orgname = " HPE" + orgunit = " _iLOrest_Team" + commonname = " JustSomeGuys" + country = " USA" + state = " Tejas" + city = " Houston" + + self.auxcommands["login"].run("") + self.auxcommands["certificate"].run("csr" + orgname + orgunit + commonname + country + state + city) + + for item in self.automatictesting_helper_fb(url_path): + fld = urlopen(url_path + item) + data = fld.read() + with open(item, "w+b") as certfile: + certfile.write(data) + delete_items.append(item) + try: + self.auxcommands["certificate"].run("tls " + item) + except Exception: + error_dict[file] = "tls " + item + self.rdmc.ui.printer( + "iLO flagged an error while uploading the" " TLS certificate file to iLO: %s\n" % item + ) + + try: + self.auxcommands["singlesignon"].run("importcert " + item) + except Exception: + error_dict[file] = "tls " + item + self.rdmc.ui.printer( + "iLO flagged an error while uploading the" " SSO certificate file to iLO: %s\n" % item + ) + + self.automatictesting_helper_fd(delete_items) + self.auxcommands["logout"].run("") + + if error_dict: + try: + raise ValueError + except ValueError as err: + if not err.args: + err.args = ("iLO flagged an error with components: ",) + for item in error_dict: + err.args = err.args + ("Component: %s, Full String: %s" % (item, error_dict[item]),) + raise + + @log_decor + def eskmautomatictesting(self): + """eskm automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*************************ESKM " "AUTOMATIC TESTING**********************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + pass + + self.auxcommands["login"].run("") + self.auxcommands["eskm"].run("testconnections") + self.auxcommands["eskm"].run("clearlog") + self.auxcommands["logout"].run("") + + @log_decor + def sigrecomputeautomatictesting(self): + """sigrecompute automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************SIGRECOMPUTE " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + + try: + self.auxcommands["sigrecompute"].run("") + except IncompatibleiLOVersionError: + self.rdmc.ui.printer("Server is redfish. Skipping sigrecompute.\n") + + self.auxcommands["logout"].run("") + + @log_decor + def sendtestautomatictesting(self): + """results automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*************************SENDTEST " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + self.auxcommands["login"].run("") + try: + self.auxcommands["select"].run(self.rdmc.app.typepath.defs.snmpservice) + self.auxcommands["select"].run(self.rdmc.app.typepath.defs.managernetworkservicetype) + except: + self.rdmc.ui.printer("Skipping sendtest testing, resource not available.\n") + self.auxcommands["logout"].run("") + return + + try: + # setup for snmpalert + # set SNMPAlertProtocol=SNMPv1Trap + self.auxcommands["select"].run(self.rdmc.app.typepath.defs.snmpservice) + self.auxcommands["set"].run("AlertsEnabled=true") + self.auxcommands["set"].run("AlertDestinations=[testdns.newdnststr] --commit") + except: + pass + + try: + # setup for alertmail + # {"Oem": { "Hpe":{"AlertMailEnabled": true, "AlertMailEmail": "bob@bob.bob", + # "AlertMailSenderDomain": "domain", "AlertMailSMTPServer": "1.35.35.35"}}} + self.auxcommands["select"].run(self.rdmc.app.typepath.defs.managernetworkservicetype) + + if self.rdmc.app.typepath.flagiften: + oem = "Oem/Hpe" + else: + oem = "Oem/Hp" + + self.auxcommands["set"].run( + oem + + "/AlertMailEmail=test@test.test " + + oem + + "/AlertMailEnabled=True " + + oem + + "/AlertMailSMTPServer=testserver.test.test " + + oem + + "/AlertMailSenderDomain=testdomain.test.test --commit" + ) + except: + pass + + try: + # setup for syslog + self.auxcommands["select"].run(self.rdmc.app.typepath.defs.managernetworkservicetype) + self.auxcommands["set"].run( + oem + "/RemoteSyslogServer=testserver.test.svrtest " + oem + "/RemoteSyslogEnabled=True --commit" + ) + except: + pass + + self.auxcommands["login"].run("") + + self.auxcommands["sendtest"].run("syslog") + self.auxcommands["sendtest"].run("alertmail") + self.auxcommands["sendtest"].run("snmpalert") + + self.auxcommands["logout"].run("") + + @log_decor + def ssoautomatictesting(self): + """sso automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*********************SINGLESIGNON " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + self.auxcommands["singlesignon"].run("importdns newdnsname.dnstest") + self.auxcommands["singlesignon"].run("deleterecord 1") + self.auxcommands["singlesignon"].run("importdns newdnsname.dnstest") + self.auxcommands["singlesignon"].run("deleterecord all") + + self.auxcommands["logout"].run("") + + @log_decor + def ilolicensetesting(self): + """iLO license automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("***********************ILOLICENSE " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + + try: + self.auxcommands["ilolicense"].run("xx-xx-xx-xx") + except IloResponseError: + pass + + self.auxcommands["logout"].run("") + + @log_decor + def smbiostesting(self): + """smbios automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("***************************SMBIOS " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + if self.rdmc.app.typepath.flagiften: + self.auxcommands["login"].run("") + self.auxcommands["smbios"].run("smbios") + self.auxcommands["logout"].run("") + else: + self.rdmc.ui.printer("Skipping smbios testing, server not gen10.\n") + + @log_decor + def directoryautomatictesting(self): + """Directory command automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*************************DIRECTORY " "AUTOMATIC TESTING*****************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.rdmc.ui.printer("Testing settings output.\n") + self.auxcommands["directory"].run("kerberos") + self.auxcommands["directory"].run("ldap") + self.auxcommands["directory"].run("kerberos -j") + self.auxcommands["directory"].run("ldap -j") + + self.rdmc.ui.printer("Testing setting properties.\n") + self.auxcommands["directory"].run("kerberos --serviceaddress test.account --port 1337 --realm " "testrealm") + self.auxcommands["directory"].run("ldap testusername testpassword --enable") + self.auxcommands["directory"].run( + "ldap --serviceaddress test2.account --addsearch autotestsearch," "autotestsearch2" + ) + + self.rdmc.ui.printer("Testing adding roles.\n") + self.auxcommands["directory"].run( + 'kerberos --addrole "Administrator:a test,' 'ReadOnly:another test" --disable' + ) + + self.rdmc.ui.printer("Validating changes...\n") + + results = self.rdmc.app.select(selector="AccountService.", path_refresh=True)[0].dict + + if results["LDAP"]["Authentication"]["Username"] == "testusername": + self.rdmc.ui.printer("Validated Username.\n") + else: + sys.stderr.write("Username not changed.\n") + if ( + results["ActiveDirectory"]["ServiceAddresses"][0] == "test.account:1337" + and results["LDAP"]["ServiceAddresses"][0] == "test2.account" + ): + self.rdmc.ui.printer("Validated Service addresses.\n") + else: + sys.stderr.write("Service addresses not changed.\n") + raise IloResponseError("") + if ( + results["Oem"]["Hpe"]["DirectorySettings"]["LdapServerPort"] == 55 + and results["Oem"]["Hpe"]["KerberosSettings"]["KDCServerPort"] == 1337 + ): + self.rdmc.ui.printer("Validated Ports.\n") + else: + sys.stderr.write("Ports not changed.\n") + raise IloResponseError("") + if results["Oem"]["Hpe"]["KerberosSettings"]["KerberosRealm"] == "testrealm": + self.rdmc.ui.printer("Validated Realm.\n") + else: + sys.stderr.write("Realm not changed.\n") + raise IloResponseError("") + if ( + "autotestsearch" in results["LDAP"]["LDAPService"]["SearchSettings"]["BaseDistinguishedNames"] + and "autotestsearch2" in results["LDAP"]["LDAPService"]["SearchSettings"]["BaseDistinguishedNames"] + ): + self.rdmc.ui.printer("Validated SearchSettings.\n") + else: + sys.stderr.write("SearchSettings not changed.\n") + if results["LDAP"]["ServiceEnabled"] and not results["ActiveDirectory"]["ServiceEnabled"]: + self.rdmc.ui.printer("Validated ServiceEnabled.\n") + else: + sys.stderr.write("Service not enabled/disabled.\n") + raise IloResponseError("") + rolecount = 0 + for role in results["LDAP"]["RemoteRoleMapping"]: + if role["RemoteGroup"] == "a test" or role["RemoteGroup"] == "another test": + rolecount += 1 + if rolecount == 2: + self.rdmc.ui.printer("Validated Role mappings.\n") + else: + sys.stderr.write("Remote roles not changed.\n") + raise IloResponseError("") + + self.rdmc.ui.printer("Removing changes...\n") + self.auxcommands["directory"].run('kerberos --serviceaddress "" --realm ""') + self.auxcommands["directory"].run( + 'ldap --serviceaddress "" --disable --removesearch ' "autotestsearch,autotestsearch2" + ) + self.auxcommands["directory"].run('ldap --removerole "dirgroupa test,dirgroupanother test"') + + self.rdmc.ui.printer("Validating removal.\n") + + results = self.rdmc.app.select(selector="AccountService.", path_refresh=True)[0].dict + + if not results["LDAP"]["ServiceEnabled"]: + self.rdmc.ui.printer("Validated Service disabled.\n") + else: + sys.stderr.write("Service not disabled.\n") + raise IloResponseError("") + if not results["ActiveDirectory"]["ServiceAddresses"][0] and not results["LDAP"]["ServiceAddresses"][0]: + self.rdmc.ui.printer("Validated Service addresses.\n") + else: + sys.stderr.write("Service addresses not removed.\n") + raise IloResponseError("") + if not results["Oem"]["Hpe"]["KerberosSettings"]["KerberosRealm"]: + self.rdmc.ui.printer("Validated Realm.\n") + else: + sys.stderr.write("Realm not removed.\n") + raise IloResponseError("") + if ( + not "autotestsearch" in results["LDAP"]["LDAPService"]["SearchSettings"]["BaseDistinguishedNames"] + and not "autotestsearch2" in results["LDAP"]["LDAPService"]["SearchSettings"]["BaseDistinguishedNames"] + ): + self.rdmc.ui.printer("Validated SearchSettings.\n") + else: + sys.stderr.write("SearchSettings not removed.\n") + raise IloResponseError("") + rolecount = 0 + for role in results["LDAP"]["RemoteRoleMapping"]: + if role["RemoteGroup"] == "a test" or role["RemoteGroup"] == "another test": + rolecount += 1 + if rolecount == 0: + self.rdmc.ui.printer("Validated Role mappings.\n") + else: + sys.stderr.write("Remote roles not removed.\n") + raise IloResponseError("") + + @log_decor + def serverclonecommandautomatictesting(self, local): + """copy command automatic testing""" + + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("**************************SERVER CLONE " "AUTOMATIC TESTING*************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + ue_clone_file_name = "ilorest_clone_ue.json" + enc_clone_file_name = "ilorest_clone_enc.json" + delete_list = [ue_clone_file_name, enc_clone_file_name] + error = {} + + self.auxcommands["login"].loginfunction("") + try: + self.rdmc.ui.printer("Testing 'save' operation (unencrypted)...\n") + line_str = "save --nobios --silent -f " + ue_clone_file_name + self.auxcommands["serverclone"].run(line_str) + + self.rdmc.ui.printer("Testing 'load' operation (unencrypted)...\n") + line_str = "load --silent -f " + tmp_file = self.serverclone_helper(ue_clone_file_name, line_str) + delete_list.append(tmp_file) + self.auxcommands["serverclone"].run(line_str + tmp_file) + self.rdmc.ui.printer("Unencrypted Clone Test Complete\n") + + except Exception as excp: + self.rdmc.ui.printer("Unencrypted Clone Test Failed\n") + with open("clone_error_logfile.log", "r") as err_logfile: + error_log = err_logfile.read() + with open("changelog.log", "r") as chng_logfile: + try: + chng_log = json.loads(chng_logfile.read()) + except ValueError: + chng_log = chng_logfile.read() + error["unencrypted_clone"] = { + "base_error": "An error occurred working with serverclone unencrypted save/load: {}".format(excp), + "command": line_str + tmp_file, + "traceback": traceback.format_exc(), + "clone_error_logfile": error_log, + "change_log": chng_log, + } + + try: + self.rdmc.ui.printer("Testing 'save' operation (encrypted)...\n") + line_str = "save --encryption HPESecretAESKey1 --silent --nobios -f " + enc_clone_file_name + self.auxcommands["serverclone"].run(line_str) + + self.rdmc.ui.printer("Testing 'load' operation (encrypted)...\n") + line_str = "load --silent --encryption HPESecretAESKey1 -f " + tmp_file = self.serverclone_helper(enc_clone_file_name, line_str) + delete_list.append(tmp_file) + self.auxcommands["serverclone"].run(line_str) + self.rdmc.ui.printer("Encrypted Clone Test Complete\n") + + except Exception as excp: + self.rdmc.ui.printer("Encrypted Clone Test Failed\n") + with open("clone_error_logfile.log", "r") as err_logfile: + error_log = err_logfile.read() + with open("changelog.log", "r") as chng_logfile: + try: + chng_log = json.loads(chng_logfile.read()) + except ValueError: + chng_log = chng_logfile.read() + error["encrypted_clone"] = { + "base_error": "An error occurred working with serverclone unencrypted save/load: {}".format(excp), + "command": line_str + tmp_file, + "traceback": traceback.format_exc(), + "clone_error_logfile": error_log, + "change_log": chng_log, + } + + """ #DO NOT OPEN UNTIL CHRISTMAS (Ok maybe earlier) + self.rdmc.ui.printer("Attempting load of clone files from NFS server...\n") + for item in self.automatictesting_helper_fb(url_path): + f_name = "" + clone_file = urlopen(url_path + item) + data = clone_file.read() + with open(item, 'w+b') as target: + target.write(data) + if isinstance(item, six.string_types): + f_name += item + delete_list.append(f_name) + self.auxcommands['serverclone'].run("load --silent -f " + f_name) + + self.rdmc.ui.printer("Test Complete...cleaning up.\n") + """ + + try: + err_list, err_str = self.automatictesting_helper_fd(delete_list) + if err_list: + raise Exception(err_str) + except Exception as excp: + error["delete_cfs"] = "An error occurred deleting clone files: '%s'" % str(excp) + + if error: + raise Exception("The following exceptions occured in ServerClone:\n {}".format(error)) + + @log_decor + def cloningautomatictesting(self): + """cloning automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("**************************CLONING " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping iloclone command, server is not gen10.\n") + return ReturnCodes.SUCCESS + + self.auxcommands["login"].run("") + self.cloneobj.run("save -f CLONETEST.json", testing=True) + self.auxcommands["logout"].run("") + self.auxcommands["login"].run("") + self.cloneobj.run("load -f CLONETEST.json", testing=True) + self.auxcommands["logout"].run("") + + @log_decor + def pendingautomatictesting(self): + """pending automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("**************************PENDING " "AUTOMATIC TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + self.auxcommands["pending"].run("") + self.auxcommands["logout"].run("") + + @log_decor + def deletecomtesting(self): + """Delete component command testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*************************DELETE " "COMPONENT TESTING******************************\n") + self.rdmc.ui.printer( + "***************************************************" "********************************\n\n" + ) + + self.auxcommands["login"].run("") + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping delete component command, server is not gen10.\n") + return ReturnCodes.SUCCESS + + self.auxcommands["deletecomp"].run("-a") + self.auxcommands["logout"].run("") + + @log_decor + def uploadcomptesting(self): + """Upload component command testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("***************************UPLOAD " "COMPONENT TESTING******************************\n") + self.rdmc.ui.printer("*************************************************" "********************************\n\n") + + error_dict = dict() + self.auxcommands["login"].run("") + url_path = "http://infinitymaster.us.rdlabs.hpecorp.net:1051/automatic_testing/components" "/COMPONENTS/" + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping upload component command, server is not gen10.\n") + return ReturnCodes.SUCCESS + + # skip = False + # compname = "cp029917.exe" + # compsigname_1 = "cp029917_part1.compsig" + # compsigname_2 = "cp029917_part2.compsig" + # compnamepath = os.path.join(os.getcwd(), compname) + # compsigname_1path = os.path.join(os.getcwd(), compsigname_1) + # compsigname_2path = os.path.join(os.getcwd(), compsigname_2) + + components_dict = self.automatictesting_helper_fb(url_path, True) + delete_list = [] + for _, val in list(components_dict.items()): + compy = None + try: + uploadstr = "" + for file in val: + if len(file.split(".")) <= 2: + tmparr = file.split(".") + fld = urlopen(url_path + file) + data = fld.read() + with open(file, "w+b") as target: + target.write(data) + delete_list.append(file) + if "compsig" in tmparr[-1]: + uploadstr += " --compsig=" + else: + uploadstr += " --component=" + compy = file + uploadstr += file + + self.auxcommands["uploadcomp"].run(uploadstr + " --forceupload") + + except Exception: + error_dict[compy] = uploadstr + self.rdmc.ui.printer("iLO flagged an error while uploading the" " previous files: %s\n" % uploadstr) + self.rdmc.ui.printer("Check for correct file type and compsig.\n") + continue + + # cleanup routine + self.automatictesting_helper_fd(delete_list) + + if error_dict: + try: + raise ValueError + except ValueError as err: + if not err.args: + err.args = ("iLO flagged an error with components: ",) + for item in error_dict: + err.args = err.args + ("Component: %s, Full String: %s" % (item, error_dict[item]),) + raise + + # try: + # if not os.path.isfile(compnamepath): + # (compname, _) = urlretrieve("http://infinitymaster:81"\ + # "/automatic_testing/" \ + # + compname, compname) + # + # if not os.path.isfile(compsigname_1path): + # (compsigname_1, _) = urlretrieve(\ + # "http://16.83.62.70/jack/" + compsigname_1, compsigname_1) + # + # if not os.path.isfile(compsigname_2path): + # (compsigname_2, _) = urlretrieve(\ + # "http://16.83.62.70/jack/" + compsigname_2, compsigname_2) + # except: + # skip = True + # + # if not skip: + # self.auxcommands['uploadcomp'].run("--component={0} --compsig={1} " \ + # "--forceupload".format(compname, compsigname_1)) + # #self.auxcommands['uploadcomp'].run("--component=firmware-nic-qlogic-nx2-2." \ + # # "19.6-1.1.x86_64.rpm --compsig=firmware-" \ + # # "nic-qlogic-nx2-2.19.6-1.1.x86_64.compsig") + # + # self.auxcommands['logout'].run("") + # else: + # self.rdmc.ui.printer("Could not complete test due to missing test " \ + # "files.\n") + + @log_decor + def listanddownloadcomptesting(self): + """List and Download component command testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*************************DOWNLOAD " "COMPONENT TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + delete_list = list() + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping download component command, server is not gen10.\n") + return ReturnCodes.SUCCESS + + self.auxcommands["login"].run("") + + self.rdmc.ui.printer("Components found in iLO Repository:\n\n") + self.auxcommands["listcompt"].run("") + comps = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/ComponentRepository/") + + for item in comps: + uri_str = "" + uri_str += "/fwrepo/" + item["Filename"] + self.rdmc.ui.printer("Downloading component: '%s'.\n" % item["Filename"]) + self.auxcommands["downloadcomp"].run(uri_str) + self.rdmc.ui.printer("Successfully downloaded '%s' from iLO " "Repository.\n" % item["Filename"]) + delete_list.append(item["Filename"]) + + self.rdmc.ui.printer("Test Complete...cleaning up.\n") + self.automatictesting_helper_fd(delete_list) + self.auxcommands["logout"].run("") + return ReturnCodes.SUCCESS + + @log_decor + def deletecomptesting(self): + """Delete from component repository testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("***************************DELETE " "COMPONENT TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + return + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping download component command, server is not gen10.\n") + return ReturnCodes.SUCCESS + + self.auxcommands["login"].run("") + self.rdmc.ui.printer("Components found in iLO Repository:\n\n") + self.auxcommands["listcompt"].run("") + + comps = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/ComponentRepository/") + + for item in comps: + # str = "" + # str += "/fwrepo/" + item['Filename'] + self.rdmc.ui.printer("Deleting component: '%s'.\n" % item["Filename"]) + self.auxcommands["deletecomp"].run(item["Filename"]) + self.rdmc.ui.printer("Successfully downloaded '%s' from iLO " "Repository.\n" % item["Filename"]) + + self.rdmc.ui.printer("Test Complete...\n") + self.auxcommands["logout"].run("") + return ReturnCodes.SUCCESS + + @log_decor + def installsettesting(self): + """Install set command testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*************************INSTALL " "SET COMMAND TESTING*****************************\n") + self.rdmc.ui.printer("**************************************************" "*******************************\n\n") + + self.auxcommands["login"].run("") + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping install set command, server is not gen10.\n") + return ReturnCodes.SUCCESS + + installsetlist = [] + url = "http://infinitymaster.us.rdlabs.hpecorp.net:1051/automatic_testing/install_sets/" + + # skip MakeInstallSet + installsetlist = self.automatictesting_helper_fb(url) + i = 0 + + self.auxcommands["login"].loginfunction("") + + for installset in installsetlist: + try: + (installsetfile, _) = urlretrieve(url + installset, installset) + i = i + 1 + self.rdmc.ui.printer("Uploading Installset: %s\n" % installsetfile) + self.auxcommands["installset"].run("add " + installset) + self.rdmc.ui.printer("Invoking Installset: %s\n" % installsetfile) + self.auxcommands["installset"].run("invoke --name=TestSet" + str(i) + " --cleartaskqueue") + self.rdmc.ui.printer("Removing Installset: %s\n" % installsetfile) + self.auxcommands["installset"].run("delete --name=TestSet" + str(i)) + + except Exception as excp: + self.rdmc.ui.printer( + "A general error occured while attempting to " + "use the file: %s. The following error was " + "logged: %s\n" % (installsetfile, excp) + ) + self.rdmc.ui.printer("Check for missing test files\n") + continue + + self.rdmc.ui.printer("Removing any remaining installsets\n") + self.auxcommands["installset"].run("--removeall") + self.auxcommands["logout"].logoutfunction("") + + for installset in installsetlist: + try: + if os.path.exists(installsetfile): + self.rdmc.ui.printer("Removing local file: %s\n" % installsetfile) + os.remove(installsetfile) + except Exception as excp: + self.rdmc.ui.printer( + "An error occured attempting to remove the " "file: %s, logged: %s\n" % (installsetfile, excp) + ) + continue + + return ReturnCodes.SUCCESS + + @log_decor + def updatetaskqueuetesting(self): + """Update task queue command testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("**************************UPDATE " "TASK QUEUE TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping taskqueue command, server is not gen10.\n") + return ReturnCodes.SUCCESS + + self.auxcommands["taskqueue"].run("") + self.auxcommands["taskqueue"].run("create 30") + self.auxcommands["taskqueue"].run("") + + self.auxcommands["taskqueue"].run("create reboot") + self.auxcommands["taskqueue"].run("") + self.auxcommands["taskqueue"].run("-r") + self.auxcommands["taskqueue"].run("") + + self.auxcommands["logout"].run("") + + @log_decor + def listcomptesting(self): + """List component command testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*****************************LIST " "COMPONENT TESTING******************************\n") + self.rdmc.ui.printer("***************************************************" "******************************\n\n") + + self.auxcommands["login"].run("") + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping list component command, server is not gen10.\n") + return ReturnCodes.SUCCESS + + self.auxcommands["listcompt"].run("") + self.auxcommands["logout"].run("") + + @log_decor + def testsumcode(self): + """SUM commands batch testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*************************SUM " "COMMANDS TESTING******************************\n") + self.rdmc.ui.printer( + "***************************************************" "********************************\n\n" + ) + + self.auxcommands["login"].run("") + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping list component command, server is not gen10.") + return ReturnCodes.SUCCESS + + # TODO:verification of cache to be done manually for now + if not self.rdmc.app.typepath.flagiften: + self.auxcommands["rawget"].run("/rest/v1/Chassis/1/Power") + self.auxcommands["rawget"].run("/redfish/v1/Chassis/1/Power/") + self.auxcommands["rawget"].run("/rest/v1/Systems/1") + self.auxcommands["rawget"].run("/redfish/v1/Systems/1/") + + self.auxcommands["hpgooey"].run(" --list --namespace perm") + # self.auxcommands['hpgooey'].run(" --read --key ipmanager --namespace perm -f " \ + # "c:\ipman.json") + self.auxcommands["logout"].run("") + self.auxcommands["login"].run("") + + # Update the password in session_payload.json file. + # self.auxcommands['rawpost'].run("session_payload.json --getheaders --service") + + session_key = "" + for client in self.rdmc.app._rmc_clients: + session_key = client.get_authorization_key() + session_key = " --sessionid={0}".format(session_key) + break + self.auxcommands["rawpost"].run( + "sut_provider_registration_payload.json " + "--getheaders --response --service --providerid" + "=SUT-PROVIDER" + session_key + ) + self.auxcommands["rawput"].run( + "sut_registry_payload.json --getheaders --response" " --service --providerid=SUT-PROVIDER" + session_key + ) + self.auxcommands["rawput"].run( + "sut_schema.json --getheaders --response --service " "--providerid=SUT-PROVIDER" + session_key + ) + self.auxcommands["rawput"].run( + "sut_settings_schema.json --getheaders --response " "--service --providerid=SUT-PROVIDER" + session_key + ) + self.auxcommands["rawput"].run( + "sut_tasksettings_schema.json --getheaders " + "--response --service --providerid=SUT-PROVIDER " + session_key + ) + self.auxcommands["rawput"].run( + "sut_systeminventory_schema.json --getheaders " + "--response --service --providerid=SUT-PROVIDER " + session_key + ) + self.auxcommands["rawput"].run("HPSUT_rest.json --providerid=HPSUT-PROVIDER") + self.auxcommands["rawput"].run("HPSUTSettings_rest.json --providerid=HPSUT-PROVIDER") + self.auxcommands["rawpatch"].run("HPSUTSettings_rest.json --providerid=HPSUT-PROVIDER") + self.auxcommands["logout"].run("") + + if self.rdmc.app.typepath.flagiften: + self.auxcommands["rawget"].run("/redfish/v1/Managers/1/") + self.auxcommands["rawget"].run("/redfish/v1/Chassis/1/Power/") + self.auxcommands["rawget"].run("/redfish/v1/Systems/1/") + self.auxcommands["rawget"].run("/redfish/v1/Managers/1/UpdateService") + self.auxcommands["rawget"].run("/redfish/v1/Chassis/") + + self.auxcommands["hpgooey"].run(" --list --namespace perm") + # self.auxcommands['hpgooey'].run(" --read --key ipmanager --namespace perm -f " \ + # "c:\ipman.json") + + self.auxcommands["rawget"].run("/redfish/v1/Managers/1/SecurityService/") + self.auxcommands["rawget"].run("/redfish/v1/UpdateService/FirmwareInventory/ --expand") + self.auxcommands["rawget"].run("/redfish/v1/UpdateService/SoftwareInventory/ --expand") + self.auxcommands["rawget"].run("/redfish/v1/updateService/installsets/ --expand") + + # Check if the uploadcomp commands are necessary for the continuation of the code + # compname = "xxx" + # compsigname = "yyy" + compname = "cp029917.exe" + compsigname_1 = "cp029917_part1.compsig" + self.auxcommands["uploadcomp"].run( + "--component={0} --compsig={1} " "--forceupload".format(compname, compsigname_1) + ) + + self.auxcommands["rawget"].run("/redfish/v1/updateService/ComponentRepository/ --expand") + self.auxcommands["rawget"].run("/redfish/v1/Managers/1/EthernetInterfaces/ --expand") + self.auxcommands["logout"].run("") + + self.auxcommands["login"].run("") + # TODO:update the password in session_payload_gen10.json file. + self.auxcommands["rawpost"].run("session_payload_gen10.json --getheaders --service") + session_key = "" + for client in self.rdmc.app._rmc_clients: + session_key = client.get_authorization_key() + session_key = " --sessionid={0}".format(session_key) + break + self.auxcommands["rawdelete"].run( + " " + "--getheaders --response --service --providerid=" + "SUT-PROVIDER" + session_key + ) + self.auxcommands["rawpost"].run( + "sut_provider_registration_payload_gen10.json " + "--getheaders --response --service --providerid=" + "SUT-PROVIDER" + session_key + ) + self.auxcommands["rawput"].run( + "sut_registry_payload_gen10.json --getheaders " + "--response --service --providerid=SUT-PROVIDER " + session_key + ) + self.auxcommands["rawput"].run( + "sut_schema_gen10.json --getheaders --response " "--service --providerid=SUT-PROVIDER" + session_key + ) + self.auxcommands["rawput"].run( + "sut_settings_schema_gen10.json --getheaders " + "--response --service --providerid=SUT-PROVIDER " + session_key + ) + self.auxcommands["rawput"].run( + "sut_tasksettings_schema_gen10.json --getheaders " + "--response --service --providerid=SUT-PROVIDER " + session_key + ) + self.auxcommands["rawput"].run( + "sut_systeminventory_schema_gen10.json --getheaders " + "--response --service --providerid=SUT-PROVIDER " + session_key + ) + + self.auxcommands["rawput"].run("HPSUT_redfish.json --providerid=HPSUT-PROVIDER") + self.auxcommands["rawput"].run("HPSUTSettings_redfish.json --providerid=HPSUT-PROVIDER") + self.auxcommands["rawpatch"].run("HPSUTSettings_redfish.json --providerid=HPSUT-PROVIDER") + self.auxcommands["logout"].run("") + + self.auxcommands["login"].run("") + self.auxcommands["rawget"].run("/redfish/v1/Chassis/1/Power/") + self.auxcommands["rawget"].run("/redfish/v1/Systems/1/") + self.auxcommands["rawget"].run("/redfish/v1/updateService/installsets/") + self.auxcommands["rawget"].run("/redfish/v1/updateService/updatetaskqueue/") + self.auxcommands["rawget"].run("/redfish/v1/UpdateService/FirmwareInventory/") + self.auxcommands["rawget"].run("/redfish/v1/UpdateService/SoftwareInventory/") + self.auxcommands["rawget"].run("/redfish/v1/Managers/1/SecurityService/CertificateAuthentication/") + self.auxcommands["rawget"].run("/redfish/v1/Managers/1/DateTime/") + self.auxcommands["rawget"].run("/redfish/v1/UpdateService/ComponentRepository/") + self.auxcommands["rawget"].run("/redfish/v1/UpdateService/FirmwareInventory/") + self.auxcommands["rawget"].run("/redfish/v1/UpdateService/UpdateTaskQueue/ --expand") + + # compname = "xxx" + # compsigname = "yyy" + compname = "cp029917.exe" + compsigname_1 = "cp029917_part1.compsig" + self.auxcommands["uploadcomp"].run( + "--component={0} --compsig={1} " "--forceupload".format(compname, compsigname_1) + ) + self.auxcommands["rawget"].run("/redfish/v1/updateService/ComponentRepository/") + # Component uri returned by the rawget above + # self.auxcommands['downloadcomp'].run("Component uri returned by the rawget above") + self.auxcommands["logout"].run("") + + @log_decor + def maintenancewindowautomatictesting(self): + """Maintenance window command automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("******************MAINTENANCE WINDOW " "AUTOMATIC TESTING***************************\n") + self.rdmc.ui.printer("***********************************************" "**********************************\n\n") + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping... Not available on Gen 9.") + return + self.auxcommands["login"].loginfunction("") + + for entry_ in range(0, 10): + startoffset = random.randint(1, 10) + endoffset = random.randint(1, 10) + startoffset + + startdatetime = datetime.datetime.now() + datetime.timedelta(days=startoffset) + enddatetime = datetime.datetime.now() + datetime.timedelta(days=endoffset) + + maintenancewindow_start_time = startdatetime.strftime("%Y-%m-%dT%H:%M:%S") + maintenancewindow_descr = '"This is a test for planned maintenance"' + maintenancewindow_end_time = enddatetime.strftime("%Y-%m-%dT%H:%M:%S") + maintenancewindow_str = ( + "add " + + maintenancewindow_start_time + + " --expire=" + + maintenancewindow_end_time + + " --name=TestMaintenanceEntry" + + str(entry_) + + " --description " + + maintenancewindow_descr + ) + + self.rdmc.ui.printer("Adding Maintenance Window: " + maintenancewindow_str + "\n") + self.auxcommands["maintenancewindow"].run(maintenancewindow_str) + + self.auxcommands["maintenancewindow"].run("") + + maintenancewindow_str = "delete TestMaintenanceEntry" + str(entry_) + + self.rdmc.ui.printer("Removing Maintenance Window: " + maintenancewindow_str + "\n") + self.auxcommands["maintenancewindow"].run(maintenancewindow_str) + + self.auxcommands["logout"].logoutfunction("") + + @log_decor + def serverinfoautomatictesting(self): + """Serverinfo command automatic testing""" + info_list = { + "--fans", + "--processor", + "--memory", + "--thermals", + "--power", + "--system", + } + info_string = "--fans --processor --memory --thermals --power --system --showabsent" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*************************SERVER INFO AUTOMATIC " "TESTING***************************\n") + self.rdmc.ui.printer("***********************************************" "**********************************\n\n") + + self.auxcommands["login"].loginfunction("") + + for item in info_list: + self.auxcommands["serverinfo"].run(item) + + self.auxcommands["serverinfo"].run(info_string) + + self.auxcommands["logout"].logoutfunction("") + + @log_decor + def fwpkgcommandautomatictesting(self): + """Fwpkg command automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("************************FIRMWARE PACKAGE AUTOMATIC " "TESTING***********************\n") + self.rdmc.ui.printer("*************************************************" "********************************\n\n") + + self.rdmc.ui.printer("Skipping FW Package testing\n") + # self.fwpackage.run("") + + @log_decor + def ipprofilesautomatictesting(self): + """ipprofiles command automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("*****************************IP PROFILES AUTOMATIC " "TESTING***********************\n") + self.rdmc.ui.printer("*************************************************" "********************************\n\n") + + self.auxcommands["login"].loginfunction("") + try: + self.auxcommands["ipprofiles"].run("") # expect a non-empty string or buffer + # self.rdmc.ui.printer("Update with JSON:\n") + # self.rdmc.ui.printer(repr(self.auxcommands['ipprofiles']obj.run("ipprofiles" + \ + # ipprofile.json))) + # self.rdmc.ui.printer(repr(self.auxcommands['ipprofiles']obj.run("ipprofiles"))) + except PathUnavailableError: + self.rdmc.ui.printer("Skipping IP Profiles test, IP provider is unavailable.\n") + + self.auxcommands["logout"].logoutfunction("") + + @log_decor + def fwintegritycheckautomatictesting(self): + """fwintegrity command automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("******************FIRMWARE INTEGRITY CHECK AUTOMATIC " "TESTING*********************\n") + self.rdmc.ui.printer("*************************************************" "********************************\n\n") + + if not self.rdmc.app.typepath.flagiften: + self.rdmc.ui.printer("Skipping firmware integrity check, only available on Gen 10.") + else: + self.auxcommands["login"].loginfunction("") + self.auxcommands["fwintegritycheck"].run("--results") + self.auxcommands["fwintegritycheck"].run("") + self.auxcommands["logout"].logoutfunction("") + + @log_decor + def setbiospasswordautomatictesting(self, local): + """biospassword command automatic testing""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("**********************SET BIOS PASSWORD AUTOMATIC " "TESTING************************\n") + self.rdmc.ui.printer("***********************************************" "**********************************\n\n") + + if not local: + self.auxcommands["login"].loginfunction("") + self.auxcommands["set"].run("testpassword " + '""') + self.auxcommands["set"].run('""' + " testpassword") + self.auxcommands["logout"].logoutfunction("") + else: + self.rdmc.ui.printer("Skipping setting bios password testing in local.\n") + + @log_decor + def setbiosdefaultsautomatictesting(self): + """biosdefault command automatictesting""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("************************SET BIOS DEFAULTS AUTOMATIC " "TESTING**********************\n") + self.rdmc.ui.printer("***********************************************" "**********************************\n\n") + + self.auxcommands["login"].loginfunction("") + try: + self.rdmc.ui.printer("Setting BIOS Defaults:\n") + self.rdmc.ui.printer(repr(self.auxcommands["biosdefaults"].run("--manufacturingdefaults"))) + except: + self.rdmc.ui.printer("Unable to set BIOS Defaults\n") + self.auxcommands["logout"].logoutfunction("") + + @log_decor + def smartarrayautomatictesting(self): + """smart array command automatictesting""" + self.rdmc.ui.printer("\n*************************************************" "********************************\n") + self.rdmc.ui.printer("************************SMART ARRAY AUTOMATIC " "TESTING*******************\n") + self.rdmc.ui.printer("***********************************************" "********************************\n\n") + + self.auxcommands["select"].selectfunction("SmartStorageConfig.") + content = self.rdmc.app.getprops() + + if content: + self.auxcommands["smartarray"].selection_output(dict(controller=None), content) + else: + raise Exception("This system does not have a valid Smart Storage Controller") + + # helper functions + def automatictesting_helper_fd(self, data): + """ + Helper function to delete files pulled from remote to local machine. + + :param data: dictionary or list of files to be removed + :type data: dictonary or list + """ + errors = [] + + if isinstance(data, dict): + for _, d_file in list(data.items()): + try: + if os.path.exists(d_file): + os.remove(d_file) + except Exception as excp: + errors.append( + "An error occured attempting to remove the file: %s, logged: %s\n" + % (filename.split(".")[0], excp) + ) + + elif isinstance(data, list): + for d_file in data: + try: + if os.path.exists(d_file): + os.remove(d_file) + except Exception as excp: + errors.append( + "An error occured attempting to remove the file: %s, logged: %s\n" + % (filename.split(".")[0], excp) + ) + + err_str = "Errors occurred deleting: " + for blah in errors: + err_str += "{},".format(blah) + return errors + + def serverclone_helper(self, filename=None, line=None): + """ + Helper function to modify and remove properties from a serverclone file + + :param filename: filename of the clone file + :type filename: str + """ + errors = [] + delete_dict = [] + data = {} + try: + with open(filename, "r") as cf: + if "--encryption" in line: + entries_list = [(pos.start(), pos.end()) for pos in list(re.finditer(ending, path))] + encryption_key = None + data = json.loads(Encryption().decrypt_file(cf.read(), encryption_key)) + else: + data = json.loads(cf.read()) + except Exception as excp: + errors.append("An error occurred opening the clone file '%s': %s" % (filename, excp)) + + try: + for type in data: + if "AccountService" in type: + for path in list(data[type].keys()): + if "MinPasswordLength" in data[type][path]: + data[type][path]["MinPasswordLength"] = random.randint(8, 16) + if "AuthFailureDelayTimeSeconds" in data[type][path]: + data[type][path]["AuthFailureDelayTimeSeconds"] = random.randint(12, 40) + if "ComputerSystem." in type: + for path in list(data[type].keys()): + if "IndicatorLED" in data[type][path]: + data[type][path]["IndicatorLED"] = "Blinking" + if "AssetTag" in data[type][path]: + data[type][path]["AssetTag"] = "AutomaticTestAsset".join( + random.SystemRandom().choice(string.ascii_letters + string.digits) for _ in range(6) + ) + if "EthernetInterface." in type: + for path in list(data[type].keys()): + if "SpeedMbps" in data[type][path]: + if data[type][path]["FullDuplex"]: + data[type][path]["FullDuplex"] = False + else: + data[type][path]["FullDuplex"] = True + if "AutoNeg" in data[type][path]: + if data[type][path]["AutoNeg"]: + data[type][path]["AutoNeg"] = False + else: + data[type][path]["AutoNeg"] = True + if "SpeedMbps" in data[type][path]: + if data[type][path]["SpeedMbps"] == 1000: + data[type][path]["SpeedMbps"] = 100 + else: + data[type][path]["SpeedMbps"] = 1000 + if "ManagerAccount." in type: + for path in list(data[type].keys()): + if "Password" in data[type][path]: + data[type][path]["Password"] = "password" + if "iLOLicense" in type: + delete_dict.append(type) + if "SecureBoot" in type: + for path in list(data[type].keys()): + if "SecureBootEnable" in data[type][path]: + data[type][path]["SecureBootEnable"] = False + except (KeyError, ValueError): + pass + + for item in delete_dict: + try: + del data[item] + except KeyError: + pass + + try: + filename2 = filename.split(".")[0] + "_edited" + ".json" + with open(filename2, "wb") as cf: + if "--encryption" in line: + [(pos.start(), pos.end()) for pos in list(re.finditer(ending, path))] + encryption_key = None + cf.write(Encryption().decrypt_file(json.dumps(data, indent=2), encryption_key)) + else: + cf.write(json.dumps(data, indent=2)) + except Exception as excp: + errors.append("An error occurred writing the clone file '%s': %s" % (filename2, excp)) + + if errors: + raise + else: + return filename2 + + def automatictesting_helper_fb(self, url, components=False): + """ + Helper function for browsing and creating file dictionary from a + file server index. Designed around the output display of Node.js + v10.10.0 which places files into an html table. + + :param url: path to the file server. + :type url: string + :param components: flag if file components will be needed + (multiple associated files) + :type components: boolean + """ + + # list of ignored files and paths + ignore_list = ["..", ".", "README"] + + table_row_list = [] + prev_tr = 7 + block = False + try: + source_url = urlopen(url) + read_url = source_url.read() + read_url = read_url[read_url.find("") : read_url.find("
") + read_url.find("
")] + while read_url.find("") > 0: + current_str = read_url[prev_tr : prev_tr + read_url.find("")] + current_str = current_str[ + current_str.find("") + ] + current_str = current_str[ + current_str.find('="') + 2 : current_str.find('="') + 2 + current_str.find('">') + ] + current_str = current_str[: current_str.find(">") - 1] + if current_str.split("/")[-1] in ignore_list or current_str.split("/")[-2] in ignore_list: + self.rdmc.ui.printer("Ignoring url: %s\n" % current_str) + else: + # data[current_str.split('/')[-1]] = [table_row_list.append(current_str) + table_row_list.append(current_str.split("/")[-1]) + + read_url = read_url[read_url.find("") + 1 :] + prev_tr = 0 + + source_url.close() + + if components: + components_dict = {} + + while len(table_row_list) > 0: + for url in table_row_list: + split_url = re.split("/", url) + last_element = split_url[-1] + if last_element.count(".") > 2: + sys.stderr.write("Invalid Filetype...skipping\n") + table_row_list.remove(url) + break + else: + (compname, ext) = last_element.split(".") + if "compsig" in last_element: + if components_dict.get(compname) is not None and compname in last_element: + components_dict[compname].append(url) + table_row_list.remove(url) + continue + if "part" in last_element and "_" in last_element: + (split_compname, _) = compname.split("_") + if split_compname in components_dict and url not in components_dict[split_compname]: + components_dict[split_compname].append(url) + table_row_list.remove(url) + else: + components_dict.setdefault(compname.split("_")[0], []) + continue + else: + components_dict.setdefault(compname, []).append(url) + table_row_list.remove(url) + + return components_dict + + else: + return table_row_list + + except ValueError or TypeError: + self.rdmc.ui.printer("The web server may not be available") + block = True + except Exception as excp: + self.rdmc.ui.printer("A General Error Occured: %s" % excp) + block = True + + def archive_handler(self, archive_file): + """ + Handles archiving of data for bug tracking and reporting + """ + + packlist = [__error_logfile__, __pass_fail__] + + if self.rdmc.opts.debug: + for handle in self.rdmc.app.logger.parent.handlers: + if isinstance(handle, FileHandler): + ilorestlog = handle.baseFilename + + if ilorestlog: + packlist.append(ilorestlog) + + with ZipFile(archive_file, "w") as zip_arch: + for _file in packlist: + try: + zip_arch.write(_file) + os.remove(_file) + except: + pass + + self.rdmc.ui.printer("Logifles archived in '%s'.\n" % archive_file) + zip_arch.printdir() + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--latestschema", + dest="latestschema", + action="store_true", + help="Optionally use the latest schema instead of the one " + "requested by the file. Note: May cause errors in some data " + "retrieval due to difference in schema versions.", + default=None, + ) + customparser.add_argument( + "--complete", + dest="complete", + action="store_true", + help="Run all available tests.", + default=False, + ) + customparser.add_argument( + "--sumtest", + dest="sumtest", + action="store_true", + help="Optionally use run only the sum command tests.", + default=False, + ) + customparser.add_argument( + "--local", + dest="local", + action="store_true", + help="""Use to perform a local login for every operation.""", + default=None, + ) + customparser.add_argument( + "--ahsdiagtest", + dest="ahsdiagtest", + action="store_true", + help="Optionally use run only the sum command tests.", + default=False, + ) + customparser.add_argument( + "--archive", + dest="archive", + help="Optionally archive the logfiles", + action="append", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/ExportAllConfigCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/ExportAllConfigCommand.py new file mode 100644 index 0000000..8f6bb17 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/ExportAllConfigCommand.py @@ -0,0 +1,106 @@ +### +# Copyright 2016 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""ExportAllConfig Command for rdmc""" + +import json + +try: + from rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes +except ImportError: + from ilorest.rdmc_helper import InvalidCommandLineErrorOPTS, ReturnCodes +from redfish.ris import UndefinedClientError + + +class ExportAllConfigCommand: + """exportallconfig class command""" + + def __init__(self): + self.ident = { + "name": "exportallconfig", + "usage": None, + "description": "Saves all the config on to json file", + "summary": "Saves all the config on to json file. Usage: exportallconfig -f filename.json", + "aliases": ["allconfig"], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main exportallconfig worker function + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if hasattr(self.rdmc.app, "config") and self.rdmc.app.config._ac__format.lower() == "json": + options.json = True + if not options.excludeurl: + options.excludeurl = "nothing to exclude" + + if self.rdmc.app.current_client: + results = self.rdmc.app.monolith.captureallconfig(exclueurl=options.excludeurl[0]) + else: + raise UndefinedClientError() + + filepath = options.filename[0] if options.filename else "AllConfig.json" + + with open(filepath, "w") as allconfig: + json.dump(results, allconfig, indent=2) + self.rdmc.ui.printer("File is loaded successfully.\n") + + # Return code + return ReturnCodes.SUCCESS + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag if you wish to save the content to a file, " + "if no file given it will get saved to AllConfig.json", + action="append", + default=None, + ) + customparser.add_argument( + "-e", + "--excludeurl", + dest="excludeurl", + action="append", + help="Use this flag is given with a url that url will be skipped", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/GetInventoryCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/GetInventoryCommand.py new file mode 100644 index 0000000..3f57efe --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/GetInventoryCommand.py @@ -0,0 +1,315 @@ +# ## +# Copyright 2016 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# BMN +# ## + +# -*- coding: utf-8 -*- +"""Get Inventory Command for rdmc""" + + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + +from redfish.ris.resp_handler import ResponseHandler + + +class GetInventoryCommand: + """GetInventory command class""" + + def __init__(self): + self.ident = { + "name": "getinventory", + "usage": None, + "description": "Get complete inventory" + "data from the iLO, including iLO Repo and install set." + "\n\texample: getinventory all", + "summary": "Get complete inventory data from the iLO.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self.response_handler = None + + def run(self, line, help_disp=False): + """Main GetInventory worker function + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.getinventoryvalidation(options) + self.response_handler = ResponseHandler( + self.rdmc.app.validationmanager, + self.rdmc.app.typepath.defs.messageregistrytype, + ) + if self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("GetInventory command is " "available only on iLO 5 node.") + + alldata = {} + results = {} + + # If both are not set, then we will get both inventory and repo data + if not options.inventory and not options.repo_data and not options.sut: + options.inventory = True + options.repo_data = True + options.sut = True + + if options.inventory: + try: + collectiondata = self.rdmc.app.get_handler( + "/redfish/v1/updateservice/firmwareinventory", service=False, silent=True + ).dict + members = self.rdmc.app.getcollectionmembers(collectiondata.get("@odata.id")) + collectiondata.update({"Members": members}) + alldata.update({"firmwareInventory": collectiondata}) + except: + alldata.update({"firmwareInventory": {}}) + try: + collectiondata = self.rdmc.app.get_handler( + "/redfish/v1/updateservice/softwareinventory", service=False, silent=True + ).dict + members = self.rdmc.app.getcollectionmembers(collectiondata.get("@odata.id")) + collectiondata.update({"Members": members}) + alldata.update({"softwareInventory": collectiondata}) + except: + alldata.update({"softwareInventory": {}}) + + try: + members = self.rdmc.app.get_handler("/redfish/v1/systems/", service=False, silent=True).dict["Members"] + systemdata = {} + rootsystem = {} + for id in members: + for mem_id in id.values(): + results = self.rdmc.app.get_handler(mem_id, service=True, silent=True).dict + id = results["Id"] + if id != "1": + rootsystem.update({"systems%s" % id: results}) + systemdata.update({"systems": rootsystem}) + alldata.update(systemdata) + if id == "1": + alldata.update({"systems1": results}) + except: + alldata.update({"systems": {}}) + + try: + sutlocation = alldata.get("systems1").get("Oem").get("Hpe").get("Links") + except: + if results: + sutlocation = next(iter(results)).dict.get("Links") + for key, value in list(sutlocation.items()): + if key == "SUT": + result2 = self.rdmc.app.get_handler(value.get("@odata.id"), service=False, silent=True) + + if result2.status == 200: + alldata["SUT"] = result2.dict + elif result2.status == 204: + pass + else: + return self.printlastfailedresult(result2) + break + try: + collectiondata = self.rdmc.app.get_handler( + "/redfish/v1/Managers/1/EthernetInterfaces/", service=False, silent=True + ).dict + members = self.rdmc.app.getcollectionmembers(collectiondata.get("@odata.id")) + collectiondata.update({"Members": members}) + alldata.update({"EthernetInterfaces": collectiondata}) + except: + alldata.update({"EthernetInterfaces": {}}) + + try: + results = self.rdmc.app.select(selector="HpeiLODateTime.", path_refresh=True) + alldata["DateTime"] = next(iter(results)).resp.dict + except: + alldata.update({"DateTime": {}}) + + try: + collectiondata = self.rdmc.app.get_handler( + "/redfish/v1/UpdateService/", service=False, silent=True + ).dict + alldata.update({"DowngradePolicy": collectiondata}) + except: + alldata.update({"DowngradePolicy": {}}) + + if options.sut: + if "system1" not in alldata: + try: + results = self.rdmc.app.get_handler("/redfish/v1/systems/", service=False, silent=True).dict + members = results["Members"] + for id in members: + for mem_id in id.values(): + results = self.rdmc.app.get_handler(mem_id, service=True, silent=True).dict + id = results["Id"] + if id == "1": + alldata.update({"systems1": results}) + except: + alldata.update({"systems1": {}}) + else: + _ = alldata["systems1"] + + if alldata.get("systems1"): + try: + sutlocation = alldata.get("systems1").get("Oem").get("Hpe").get("Links") + except: + if results: + sutlocation = next(iter(results)).dict.get("Links") + for key, value in list(sutlocation.items()): + if key == "SUT": + result2 = self.rdmc.app.get_handler(value.get("@odata.id"), service=False, silent=True) + + if result2.status == 200: + alldata["SUT"] = result2.dict + elif result2.status == 204: + pass + else: + return self.printlastfailedresult(result2) + break + + if options.repo_data: + try: + collectiondata = self.rdmc.app.get_handler( + "/redfish/v1/UpdateService/installsets", service=False, silent=True + ).dict + members = self.rdmc.app.getcollectionmembers(collectiondata.get("@odata.id")) + if len(members) == 0: + members = [] + collectiondata.update({"Members": members}) + alldata.update({"installsets": collectiondata}) + except: + alldata.update({"installsets": []}) + + try: + collectiondata = self.rdmc.app.get_handler( + "/redfish/v1/UpdateService/updatetaskqueue", service=False, silent=True + ).dict + members = self.rdmc.app.getcollectionmembers(collectiondata.get("@odata.id")) + if len(members) == 0: + members = [] + collectiondata.update({"Members": members}) + alldata.update({"updatetaskqueue": collectiondata}) + except: + alldata.update({"updatetaskqueue": []}) + + if self.rdmc.app.getiloversion(skipschemas=True) >= 5.130: + try: + collectiondata = self.rdmc.app.get_handler( + "/redfish/v1/UpdateService/maintenancewindows", service=False, silent=True + ).dict + members = self.rdmc.app.getcollectionmembers(collectiondata.get("@odata.id")) + if len(members) == 0: + members = [] + collectiondata.update({"Members": members}) + alldata.update({"maintenancewindows": collectiondata}) + except: + alldata.update({"maintenancewindows": []}) + + comp_repo_url = "/redfish/v1/UpdateService/ComponentRepository/" + "?$expand=." + try: + members = self.rdmc.app.get_handler(comp_repo_url, silent=True, service=True).dict + alldata.update({"ComponentRepository": members}) + except: + alldata.update({"ComponentRepository": {}}) + + self.rdmc.ui.print_out_json(alldata) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def combineinstances(self, data, alldata, name, coll=None): + """combine the data into one json dictionary""" + if coll: + coll = coll.resp.dict + finaldata = [] + if "Members" in coll: + for member in data: + finaldata.append(member.resp.dict) + coll["Members"] = finaldata + alldata[name] = coll + elif len(data) > 1: + alldata.update({name: {"Members": []}}) + for item in data: + alldata[name]["Members"].append(item.resp.dict) + else: + alldata.update({name: {"Members": next(iter(data)).resp.dict}}) + + def printlastfailedresult(self, results): + """print last failed result function""" + self.response_handler.output_resp(results) + + return ReturnCodes.NO_CONTENTS_FOUND_FOR_OPERATION + + def getinventoryvalidation(self, options): + """get inventory validation function + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-i", + "--inventory", + dest="inventory", + action="store_true", + help="""Use this option to get only the inventory data.""", + default=False, + ) + customparser.add_argument( + "-s", + "--sut", + dest="sut", + action="store_true", + help="""Use this option to get only SUT data from iLO.""", + default=False, + ) + customparser.add_argument( + "-r", + "--repo_data", + dest="repo_data", + action="store_true", + help="""Use this option to get only iLO repository, install set """ """and Task queue details.""", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/HpGooeyCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/HpGooeyCommand.py new file mode 100644 index 0000000..0b214dc --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/HpGooeyCommand.py @@ -0,0 +1,1071 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Hp Gooey Command for rdmc""" + +import ctypes +import gzip +import itertools +import os +import platform +import string +import struct +import subprocess +import sys +import tempfile +import time +import logging +import xml.etree.ElementTree as et + +from six import BytesIO, StringIO + +import redfish.hpilo.risblobstore2 as risblobstore2 + +try: + from rdmc_helper import ( + BirthcertParseError, + CommandNotEnabledError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + PartitionMoutingError, + ReturnCodes, + StandardBlobErrorHandler, + ) +except ImportError: + from ilorest.rdmc_helper import ( + BirthcertParseError, + CommandNotEnabledError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + PartitionMoutingError, + ReturnCodes, + StandardBlobErrorHandler, + ) + +if os.name == "nt": + import win32api +elif sys.platform != "darwin" and "VMkernel" not in platform.uname(): + import pyudev + +LOGGER = logging.getLogger(__name__) + + +class HpGooeyCommand: + """Hp Gooey class command""" + + def __init__(self): + self.ident = { + "name": "hpgooey", + "usage": None, + "description": "Directly writes/reads from blobstore" + "\n\tBlobstore read example:\n\thpgooey --read " + "--key keyexample --namespace perm -f " + "\n\n\tBlobstore write example:\n\thpgooey --write" + " --key keyexample --namespace perm -f \n\n\tBlobstore delete example:\n\thpgooey " + "--delete --key keyexample --namespace perm\n\n\t" + "Blobstore list example:\n\thpgooey --list " + "--namespace perm\n\n\tNAMESPACES:\n\tperm, " + "tmp, dropbox, sfw, ris, volatile", + "summary": "directly writes/reads from blobstore", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + try: + self.lib = risblobstore2.BlobStore2.gethprestchifhandle() + except: + self.lib = None + + def run(self, line, help_disp=False): + """Access blobstore directly and perform desired function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + if sys.platform == "darwin": + raise CommandNotEnabledError("'%s' command is not supported on MacOS" % str(self.name)) + elif "VMkernel" in platform.uname(): + raise CommandNotEnabledError("'%s' command is not supported on VMWare" % str(self.name)) + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.hpgooeyvalidation(options) + + if options.key == "hpmbirthcert": + self.remote_run(options) + else: + if self.rdmc.app.current_client.base_url.startswith("blobstore"): + self.local_run(options) + else: + self.remote_run(options) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def remote_run(self, options): + path = "/blob" + if options.namespace: + path += "/%s" % options.namespace + if options.key: + path += "/%s" % options.key + + if options.write: + if not (options.key and options.namespace): + raise InvalidCommandLineError("Key and namespace are required for hpblob operations.") + if not options.filename or not os.path.isfile(options.filename[0]): + raise InvalidFileInputError("Please provide a file with input data or given file does not exist") + + blobfiledata = None + if options.binfile: + _read_mode = "rb" + else: + _read_mode = "r" + + with open(options.filename[0], _read_mode) as bfh: + blobfiledata = bfh.read() + + if options.key == "birthcert" or options.key == "hpmbirthcert": + try: + filedatastr = bytes(self.remote_read(path)) + except (StandardBlobErrorHandler, risblobstore2.BlobNotFoundError) as e: + filedatastr = "" + if not isinstance(filedatastr, bytes): + blobdata = filedatastr.encode("utf-8", "ignore") + else: + blobdata = filedatastr + + blobfiledata = self.writebirthcert(blobfiledata=blobfiledata, blobdata=blobdata, key=options.key) + self.remote_write(path, blobfiledata) + + elif "SMARTSTART" in options.key: + # compressed_data = self.writesmartstart(blobfiledata) + # self.remote_write(path, compressed_data) + self.remote_write(path, blobfiledata) + + elif options.read: + if not (options.key and options.namespace): + raise InvalidCommandLineError("Key and namespace are required for hpblob operations.") + + filedata = BytesIO() + + filedatastr = bytes(self.remote_read(path)) + + if options.key == "birthcert" or options.key == "hpmbirthcert": + filedatastr = self.readbirthcert(filedatastr, options.key) + # elif "SMARTSTART" in options.key: + # uncompressed_data = self.readsmartstart(filedatastr) + # filedatastr = uncompressed_data + + if not isinstance(filedatastr, bytes): + filedatastr = filedatastr.encode("utf-8", "ignore") + filedata.write(filedatastr) + if options.filename: + self.rdmc.ui.printer("Writing data to %s..." % options.filename[0]) + + with open(options.filename[0], "wb") as outfile: + outfile.write(filedata.getvalue()) + + self.rdmc.ui.printer("Done\n") + else: + filedata_value = filedata.getvalue() + if isinstance(filedata_value, bytes): + filedata_value = filedata_value.decode("utf-8") + if options.key == "hpmbirthcert" and filedata_value == "": + self.rdmc.ui.printer("HPM Birth Certificate does not exist!!\n") + elif "SMARTSTART" in options.key and filedata_value == "": + self.rdmc.ui.printer("SMARTSTART data does not exist!!\n") + else: + self.rdmc.ui.printer("%s\n" % filedata_value) + + elif options.delete: + if not (options.key and options.namespace): + raise InvalidCommandLineError("Key and namespace are required" " for hpblob operations.") + self.remote_delete(path) + + elif options.list: + if not options.namespace: + raise InvalidCommandLineError("Namespace is required for hpblob operations.") + bs2 = risblobstore2.BlobStore2() + recvpacket = bs2.list(options.namespace) + errorcode = struct.unpack(" 3: + found, path = self.manualmount(label) + if found: + return True, path + + count = count + 1 + time.sleep(1) + + raise PartitionMoutingError("Partition with label %s not found on the NAND, so not able to mount" % label) + + def check_flat_path(self): + """Check flat path directory.""" + context = pyudev.Context() + count = 0 + + while count < 20: + for dev in context.list_devices(subsystem="block"): + if str(dev.get("ID_SERIAL")).startswith("HP_iLO_LUN"): + path = dev.get("DEVNAME") + return True, path + + count = count + 1 + time.sleep(1) + + raise PartitionMoutingError("iLO not responding to request for mounting partition") + + def manualmount(self, label): + """Manually mount after fixed time.""" + context = pyudev.Context() + + for device in context.list_devices(subsystem="block"): + if device.get("ID_FS_LABEL") == label: + dirpath = os.path.join(tempfile.gettempdir(), label) + + if not os.path.exists(dirpath): + try: + os.makedirs(dirpath) + except Exception as excp: + raise excp + + pmount = subprocess.Popen( + ["mount", device.device_node, dirpath], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + _, _ = pmount.communicate() + return True, dirpath + + return False, None + + def get_available_drives(self): + """Obtain all drives""" + if "Windows" not in platform.system(): + return [] + + drive_bitmask = ctypes.cdll.kernel32.GetLogicalDrives() + return list( + itertools.compress( + string.ascii_uppercase, + [ord(drive) - ord("0") for drive in bin(drive_bitmask)[:1:-1]], + ) + ) + + def detecttype(self, readdata): + """Function to detect a packets encryption + + :param readdata: data read from the call + :type readdata: str. + """ + magic_dict = { + "\x1f\x8b\x08": "gz", + "\x42\x5a\x68": "bz2", + "\x50\x4b\x03\x04": "zip", + } + max_len = max(len(x) for x in magic_dict) + file_start = readdata[:max_len] + + for magic, filetype in list(magic_dict.items()): + if file_start.startswith(magic): + return filetype + + return "no match" + + def osunmount(self, labels=None): + """Function to unmount media using labels + + :param labels: list of labels to unmount + :type labels: list. + """ + if labels: + for label in labels: + try: + (_, path) = self.check_mount_path(label) + except PartitionMoutingError: + if self.rdmc.opts.verbose: + self.rdmc.ui.printer("Unable to find {0} partition.".format(label)) + continue + pumount = subprocess.Popen(["umount", path], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + _, _ = pumount.communicate() + + def readbirthcert(self, blobdata, key): + """Function to read the birth certificate + + :param blobdata: data read from birth certificate call + :type blobdata: str. + """ + if key != "hpmbirthcert": + if "blobstore" in self.rdmc.app.redfishinst.base_url: + blobio = BytesIO(blobdata) + filehand = gzip.GzipFile(mode="rb", fileobj=blobio) + + data = filehand.read() + filehand.close() + else: + data = blobdata + else: + blobio = BytesIO(blobdata) + filehand = gzip.GzipFile(mode="rb", fileobj=blobio) + + data = filehand.read() + filehand.close() + return data + + def readsmartstart(self, blobdata): + blobio = BytesIO(blobdata) + filehand = gzip.GzipFile(mode="rb", fileobj=blobio) + + data = filehand.read() + filehand.close() + return data + + def writebirthcert(self, blobdata, blobfiledata, key): + """Function to read the birth certificate + + :param blobdata: data to be written to birth certificate call + :type blobdata: str. + :param blobfiledata: data read from birth certificate call + :type blobfiledata: str. + """ + filetype = self.detecttype(blobfiledata) + if filetype != "no match": + raise StandardBlobErrorHandler + + blobdataunpacked = self.readbirthcert(blobdata, key) + + totdata = self.parsebirthcert(blobdataunpacked, blobfiledata) + + databuf = BytesIO() + + filehand = gzip.GzipFile(mode="wb", fileobj=databuf) + filehand.write(totdata) + filehand.close() + + compresseddata = databuf.getvalue() + return compresseddata + + def writesmartstart(self, blobfiledata): + """Compresses the given data using gzip and returns compressed data.""" + try: + if not isinstance(blobfiledata, bytes): + blobfiledata = blobfiledata.encode("utf-8") # Convert to bytes if not already + + with BytesIO() as databuf: + with gzip.GzipFile(mode="wb", fileobj=databuf) as filehand: + filehand.write(blobfiledata) + + compressed_data = databuf.getvalue() + + LOGGER.info( + f"Successfully compressed data. Original size: {len(blobfiledata)}, " + f"Compressed size: {len(compressed_data)}" + ) + return compressed_data + + except Exception as e: + LOGGER.error(f"Failed to compress data: {str(e)}", exc_info=True) + raise # Re-raise the exception + + def parsebirthcert(self, blobdataunpacked=None, blobfiledata=None): + """Parse birth certificate function.""" + filedata = StringIO(blobfiledata) + if blobdataunpacked: + if isinstance(blobdataunpacked, bytes) or isinstance(blobdataunpacked, bytearray): + blobdataunpacked = blobdataunpacked.decode("utf-8") + readdata = StringIO(blobdataunpacked) + + try: + readtree = et.parse(readdata) + readroot = readtree.getroot() + readstr = b"" + + if readroot.tag == "BC": + for child in readroot: + readstr += et.tostring(child) + + if isinstance(readstr, bytes): + readstr = readstr.decode("utf-8") + totstr = readstr + blobfiledata + totstrdata = StringIO(totstr) + iterdata = itertools.chain("", totstrdata, "") + readroot = et.fromstringlist(iterdata) + totdata = et.tostring(readroot) + else: + raise + except Exception as excp: + self.rdmc.ui.error("Error while parsing birthcert.\n", excp) + raise BirthcertParseError(excp) + else: + iterdata = itertools.chain("", filedata, "") + newroot = et.fromstringlist(iterdata) + totdata = et.tostring(newroot) + + return totdata + + def birthcertdelete(self, options=None, compdata=None): + """Delete birth certificate function.""" + totdata = "" + databuf = StringIO() + filehand = gzip.GzipFile(mode="wb", fileobj=databuf) + + filehand.write(totdata) + filehand.close() + compresseddata = databuf.getvalue() + + if compdata: + compresseddata = compdata + + bs2 = risblobstore2.BlobStore2() + risblobstore2.BlobStore2.initializecreds(options.user, options.password) + + errorcode = bs2.write(options.key, options.namespace, compresseddata) + + if not ( + errorcode == risblobstore2.BlobReturnCodes.SUCCESS or errorcode == risblobstore2.BlobReturnCodes.NOTMODIFIED + ): + raise StandardBlobErrorHandler(errorcode) + + return errorcode + + def hpgooeyvalidation(self, options): + """Download command method validation function + + :param options: command options + :type options: options. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="""Use the provided filename to perform operations.""", + action="append", + default=None, + ) + customparser.add_argument( + "-r", + "--read", + dest="read", + action="store_true", + help="""read data into the provided filename""", + default=None, + ) + customparser.add_argument( + "-w", + "--write", + dest="write", + action="store_true", + help="""use the provided filename to output data""", + default=None, + ) + customparser.add_argument( + "-d", + "--delete", + dest="delete", + action="store_true", + help="""delete the file from the provided namespace""", + default=None, + ) + customparser.add_argument( + "-l", + "--list", + dest="list", + action="store_true", + help="""list the files from the provided namespace""", + default=None, + ) + customparser.add_argument( + "-k", + "--key", + dest="key", + help="""blobstore key name to use for opetations with no """ """spaces and 32 character limit""", + default=None, + ) + customparser.add_argument( + "-n", + "--namespace", + dest="namespace", + help="""namespace where operation is to be performed""", + default=None, + ) + customparser.add_argument( + "--mountabsr", + dest="mountabsr", + action="store_true", + help="""use this flag to mount absaroka repo""", + default=None, + ) + customparser.add_argument( + "--mountgaius", + dest="mountgaius", + action="store_true", + help="""use this flag to mount gaius""", + default=None, + ) + customparser.add_argument( + "--mountvid", + dest="mountvid", + action="store_true", + help="""use this flag to mount vid""", + default=None, + ) + customparser.add_argument( + "--mountflat", + dest="mountflat", + action="store_true", + help="""use this flag to mount flat mode""", + default=None, + ) + customparser.add_argument( + "--unmountabsr", + dest="unmountabsr", + action="store_true", + help="""use this flag to unmount absaroka media""", + default=None, + ) + customparser.add_argument( + "--unmountvid", + dest="unmountvid", + action="store_true", + help="""use this flag to vid media""", + default=None, + ) + customparser.add_argument( + "--unmountgaius", + dest="unmountgaius", + action="store_true", + help="""use this flag to unmount gaius media""", + default=None, + ) + customparser.add_argument( + "--unmountmedia", + dest="unmountmedia", + action="store_true", + help="""use this flag to unmount all NAND partitions""", + default=None, + ) + customparser.add_argument( + "--binfile", + dest="binfile", + action="store_true", + help="""use this flag to write and read binary files""", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/ISToolCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/ISToolCommand.py new file mode 100644 index 0000000..1d75c73 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/ISToolCommand.py @@ -0,0 +1,186 @@ +### +# Copyright 2016 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""ISTool Command for rmdc""" + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class ISToolCommand: + """ISTool class command""" + + def __init__(self): + self.ident = { + "name": "istool", + "usage": None, + "description": "Displays ilo data useful in debugging", + "summary": "displays ilo data useful in debugging", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Debug helper for iLO + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + ilodata = "" + reglist = [] + foundreg = False + + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("Istool command does not take any arguments.") + + self.istoolvalidation(options) + + if self.rdmc.app.monolith.is_redfish: + regstr = "/redfish/v1/Registries/" + ilostr = "/redfish/v1/Managers/1/" + else: + regstr = "/rest/v1/Registries" + ilostr = "/rest/v1/Managers/1" + + try: + biosresults = self.rdmc.app.get_handler(self.rdmc.app.typepath.defs.biospath, service=True, silent=True) + regresults = self.rdmc.app.get_handler(regstr, service=True, silent=True) + romresults = self.rdmc.app.get_handler(self.rdmc.app.typepath.defs.systempath, service=True, silent=True) + iloresults = self.rdmc.app.get_handler(ilostr, silent=True, service=True) + except: + raise + + if biosresults.dict: + try: + biosreg = biosresults.dict["AttributeRegistry"] + self.rdmc.ui.printer("System has attribute registry: %s\n" % biosreg) + except Exception as excp: + biosreg = None + self.rdmc.ui.error("Attribute registry not found in BIOS.\n", excp) + + if regresults.dict: + items = regresults.dict[self.rdmc.app.typepath.defs.collectionstring] + + for item in items: + try: + schema = item["Schema"] + except: + item = self.rdmc.app.get_handler( + item[self.rdmc.app.typepath.defs.hrefstring], + service=True, + silent=True, + ) + + item = item.dict + if "Schema" in item: + schema = item["Schema"] + else: + schema = item["Registry"] + + if schema: + available = True + + locationdict = ( + self.rdmc.app.validationmanager.geturidict(item["Location"][0]) + if self.rdmc.app.validationmanager + else None + ) + extref = ( + self.rdmc.app.get_handler(locationdict, service=True, silent=True) if locationdict else None + ) + + if not extref: + available = False + + if available: + val = schema + " : Available" + reglist.append(val) + else: + val = schema + " : Not Available" + reglist.append(val) + + if schema == biosreg: + foundreg = True + + self.rdmc.ui.printer( + "The following attribute registries are in the registry \ + repository:\n" + ) + for item in reglist: + self.rdmc.ui.printer("%s\n" % item) + + if foundreg: + self.rdmc.ui.printer("The system attribute registry was found in the " "registry repository.\n") + else: + self.rdmc.ui.error("The system attribute registry was not found in " "the registry repository.\n") + + if iloresults.dict: + try: + ilodata = iloresults.dict["FirmwareVersion"] + self.rdmc.ui.printer("iLO Version: %s\n" % ilodata) + except Exception as excp: + self.rdmc.ui.error("Unable to find iLO firmware data.\n", excp) + + if romresults.dict: + try: + biosversion = romresults.dict["BiosVersion"] + self.rdmc.ui.printer("BIOS Version: %s\n" % biosversion) + except Exception as excp: + self.rdmc.ui.error("Unable to find ROM data.\n", excp) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def istoolvalidation(self, options): + """ISTool validation function""" + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/MonolithCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/MonolithCommand.py new file mode 100644 index 0000000..916b730 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/MonolithCommand.py @@ -0,0 +1,120 @@ +### +# Copyright 2016 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Monolith Command for rdmc""" + +import json + +try: + from rdmc_helper import UI, InvalidCommandLineErrorOPTS, ReturnCodes +except ImportError: + from ilorest.rdmc_helper import UI, InvalidCommandLineErrorOPTS, ReturnCodes +from redfish.rest.containers import JSONEncoder +from redfish.ris import UndefinedClientError + + +class MonolithCommand: + """Monolith class command""" + + def __init__(self): + self.ident = { + "name": "monolith", + "usage": None, + "description": "Displays entire cached data structure available", + "summary": "displays entire cached data structure available", + "aliases": ["mono"], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main monolith worker function + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if hasattr(self.rdmc.app, "config") and self.rdmc.app.config._ac__format.lower() == "json": + options.json = True + + if self.rdmc.app.current_client: + results = self.rdmc.app.monolith.capture(redmono=options.redmono) + else: + raise UndefinedClientError() + + if options.filename: + with open(options.filename[0], "w") as monolith: + if options.json: + json.dump(results, monolith, indent=2, cls=JSONEncoder) + else: + monolith.write(str(results)) + + elif options.json: + UI().print_out_json(results) # .reduce()) + else: + UI().print_out_human_readable(results) # .reduce()) + + # Return code + return ReturnCodes.SUCCESS + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag if you wish to save the monolith" " into a file with the given filename.", + action="append", + default=None, + ) + customparser.add_argument( + "-r", + "--reduced", + dest="redmono", + action="store_true", + help="Use this flag if you wish to save the reduced monolith" " into a file with the given filename.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/SMBiosCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/SMBiosCommand.py new file mode 100644 index 0000000..87c9b2d --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/SMBiosCommand.py @@ -0,0 +1,221 @@ +### +# Copyright 2016 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + + +# -*- coding: utf-8 -*- +"""New Command for RDMC""" + +import json +import struct + +import redfish.ris + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + +__filename__ = "smbios.json" + + +class SMBiosCommand: + """Main smbios command class""" + + def __init__(self): + self.ident = { + "name": "smbios", + "usage": None, + "description": "Run to get the smbios for the system." "\n\texample write smbios data to file: smbios", + "summary": "Gets the smbios for the currently logged in server and " + "write results to a file in json format.", + "aliases": [], + "auxcommands": [], + } + self.rdmc = None + self.filename = None + + def smbiosfunction(self, options): + """Main smbios command worker function + + :param options: command options + :type options: options. + """ + + sysresp = self.rdmc.app.get_handler(self.rdmc.app.typepath.defs.systempath, silent=True, service=True) + + try: + path = sysresp.dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["SMBIOS"]["extref"] + except: + raise NoContentsFoundForOperationError("Unable to find ComputerSystems") + + resp = self.rdmc.app.get_handler(path, silent=True, service=True) + + if resp and resp.status == 200: + data = self.unpackdata(resp.ori) + data = {"smbios data": data} + + outfile = open(self.filename, "w") + outfile.write(json.dumps(data, indent=2, cls=redfish.ris.JSONEncoder, sort_keys=True)) + outfile.close() + + self.rdmc.ui.printer("Smbios saved to: %s\n" % self.filename) + else: + raise NoContentsFoundForOperationError("Unable to find smbios.") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def unpackdata(self, data): + """Unpacks and returns json formatted data + + :param data: data to unpack + :type data: string. + """ + + dataptr = 0 + fulldata = [] + if not isinstance(data, (bytes, bytearray)): + data = data.encode() + + try: + while True: + record_header = struct.unpack_from("BBH", data[dataptr : dataptr + 4]) + + record_type = record_header[0] + record_length = record_header[1] + record_handle = record_header[2] + + # skip to the strings + dataptr += record_length + + # read strings from end of record + record_strings = [] + + while True: + tempstr = "" + + while data[dataptr] != 0: + tempstr += chr(data[dataptr]) + dataptr += 1 + + dataptr += 1 + + if len(tempstr) == 0: + break + + record_strings.append(str(tempstr)) + + while data[dataptr] == 0: + dataptr += 1 + + fulldata.append( + { + "Type": str(record_type), + "Length": str(record_length), + "Handle": str(record_handle), + } + ) + + if len(record_strings): + fulldata[-1]["Strings"] = str(record_strings) + except IndexError: + pass + return fulldata + + def run(self, line, help_disp=False): + """Wrapper function for smbios command main function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if self.rdmc.app.typepath.url: + if "http" not in self.rdmc.app.typepath.url: + options.logout = True + self.cmdbase.logout_routine(self, options) + else: + options.logout = True + self.cmdbase.logout_routine(self, options) + self.smbiosvalidation(options) + + if not self.rdmc.app.typepath.flagiften: + raise IncompatibleiLOVersionError("smbios command is RedFish only.") + + self.smbiosfunction(options) + # Return code + return ReturnCodes.SUCCESS + + def smbiosvalidation(self, options): + """smbios command method validation function + + :param options: command options + :type options: options. + """ + self.cmdbase.login_select_validation(self, options) + + # filename validations and checks + self.filename = None + + if options.filename: + self.filename = options.filename[0] + + if not self.filename: + self.filename = __filename__ + + def definearguments(self, customparser): + """Wrapper function for smbios command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag if you wish to use a different" + " filename than the default one. The default filename is" + " %s." % __filename__, + action="append", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/SecurityStatusCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/SecurityStatusCommand.py new file mode 100644 index 0000000..96a32a4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/SecurityStatusCommand.py @@ -0,0 +1,179 @@ +### +# Copyright 2016 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### +# -*- coding: utf-8 -*- +"""Security Status Command for rdmc""" + +import struct +from argparse import SUPPRESS +from ctypes import POINTER, c_char_p, c_ubyte, create_string_buffer + +try: + from rdmc_base_classes import RdmcCommandBase + from rdmc_helper import Encryption, InvalidCommandLineErrorOPTS, ReturnCodes +except ImportError: + from ilorest.rdmc_base_classes import RdmcCommandBase + from ilorest.rdmc_helper import Encryption, InvalidCommandLineErrorOPTS, ReturnCodes +from redfish.hpilo.risblobstore2 import BlobStore2 +from redfish.hpilo.rishpilo import BlobReturnCodes, HpIloChifAccessDeniedError + + +class SecurityStatusCommand(RdmcCommandBase): + """Security Status class command""" + + def __init__(self): + self.ident = { + "name": "securitystatus", + "usage": None, + "description": "Security status example:\n\tsecuritystatus", + "summary": "command to retrieve the current system security status and validate " "credentials via chif.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Access blobstore directly and perform desired function + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if bool(options.user) ^ bool(options.password): + self.rdmc.ui.error("Credentials: Missing\n") + + elif options.user and options.password: + if options.encode: + options.user = Encryption.decode_credentials(options.user) + if isinstance(options.user, bytes): + options.user = options.user.decode("utf-8") + options.password = Encryption.decode_credentials(options.password) + if isinstance(options.password, bytes): + options.password = options.password.decode("utf-8") + + result = self.validate_creds(options.user, options.password) + # self.rdmc.ui.printer("Validate Creds is {}...\n".format(result)) + if result: + secstate = BlobStore2().get_security_state() + if isinstance(secstate, bytes): + if secstate == b"\x00": + state = struct.unpack("B", secstate) + state = str(state).strip(",( )") + self.rdmc.ui.printer("Security State is {}\n".format(state)) + else: + secstate = secstate.decode("utf-8") + self.rdmc.ui.printer("Security State is {}\n".format(secstate)) + else: + self.rdmc.ui.printer("Security State is {}\n".format(secstate)) + self.rdmc.ui.printer("Credentials: Valid\n") + else: + self.rdmc.ui.error("Credentials: Invalid\n") + + else: + secstate = BlobStore2().get_security_state() + if isinstance(secstate, bytes): + if secstate == b"\x00": + state = struct.unpack("B", secstate) + state = str(state).strip(",( )") + self.rdmc.ui.printer("Security State is {}\n".format(state)) + else: + secstate = secstate.decode("utf-8") + self.rdmc.ui.printer("Security State is {}\n".format(secstate)) + else: + self.rdmc.ui.printer("Security State is {}\n".format(secstate)) + + return ReturnCodes.SUCCESS + + def validate_creds(self, user, passwrd): + """Validates credentials via CHIF + + :param user: username to validate + :type user: str. + :param passwrd: password to validate + :type passwrd: str. + """ + + valid = False + dll = BlobStore2.gethprestchifhandle() + dll.ChifInitialize(None) + sec_support = dll.ChifGetSecuritySupport() + if sec_support <= 1: + dll.ChifEnableSecurity() + dll.initiate_credentials.argtypes = [c_char_p, c_char_p] + dll.initiate_credentials.restype = POINTER(c_ubyte) + + usernew = create_string_buffer(user.encode("utf-8")) + passnew = create_string_buffer(passwrd.encode("utf-8")) + + dll.initiate_credentials(usernew, passnew) + credreturn = dll.ChifVerifyCredentials() + if credreturn == 0: + valid = True + else: + valid = False + if not credreturn == BlobReturnCodes.CHIFERR_AccessDenied: + raise HpIloChifAccessDeniedError( + "Error %s - Chif Access Denied occurred while trying " "to open a channel to iLO." % credreturn + ) + + BlobStore2.unloadchifhandle(dll) + return valid + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + customparser.add_argument( + "-u", + "--user", + dest="user", + help="If you are not logged in yet, including this flag along" + " with the password and URL flags can be used to login to a" + " server in the same command." + "", + default=None, + ) + customparser.add_argument( + "-p", + "--password", + dest="password", + help="""Use the provided iLO password to log in.""", + default=None, + ) + customparser.add_argument( + "-e", + "--enc", + dest="encode", + action="store_true", + help=SUPPRESS, + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/AHSdiagCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/AHSdiagCommand.cpython-310.pyc new file mode 100644 index 0000000..58a27e5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/AHSdiagCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/AutomaticTestingCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/AutomaticTestingCommand.cpython-310.pyc new file mode 100644 index 0000000..2d17907 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/AutomaticTestingCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/ExportAllConfigCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/ExportAllConfigCommand.cpython-310.pyc new file mode 100644 index 0000000..38b222f Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/ExportAllConfigCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/GetInventoryCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/GetInventoryCommand.cpython-310.pyc new file mode 100644 index 0000000..26d672f Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/GetInventoryCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/HpGooeyCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/HpGooeyCommand.cpython-310.pyc new file mode 100644 index 0000000..3ff40cd Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/HpGooeyCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/ISToolCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/ISToolCommand.cpython-310.pyc new file mode 100644 index 0000000..acd992e Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/ISToolCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/MonolithCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/MonolithCommand.cpython-310.pyc new file mode 100644 index 0000000..608c3e4 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/MonolithCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/SMBiosCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/SMBiosCommand.cpython-310.pyc new file mode 100644 index 0000000..1f3026f Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/SMBiosCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/SecurityStatusCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/SecurityStatusCommand.cpython-310.pyc new file mode 100644 index 0000000..b1636df Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/SecurityStatusCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..108217a Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/_hidden_commands/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/AppAccountCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/AppAccountCommand.py new file mode 100644 index 0000000..4f84adb --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/AppAccountCommand.py @@ -0,0 +1,693 @@ +### +# Copyright 2016-2024 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +""" AppAccount command for rdmc """ +import ctypes +import os +from argparse import RawDescriptionHelpFormatter +from redfish.hpilo.vnichpilo import AppAccount +from redfish.rest.connections import ChifDriverMissingOrNotFound, VnicNotEnabledError +import redfish + +from redfish.ris.rmc_helper import UserNotAdminError + +try: + from rdmc_helper import ( + LOGGER, + GenerateAndSaveAccountError, + AppAccountExistsError, + ReactivateAppAccountTokenError, + ReturnCodes, + InvalidCommandLineErrorOPTS, + IncompatibleiLOVersionError, + InvalidCommandLineError, + UsernamePasswordRequiredError, + NoAppAccountError, + VnicExistsError, + SavinginTPMError, + SavinginiLOError, + GenBeforeLoginError, + AppIdListError, + UI, + ) +except ImportError: + from ilorest.rdmc_helper import ( + LOGGER, + GenerateAndSaveAccountError, + AppAccountExistsError, + ReactivateAppAccountTokenError, + ReturnCodes, + InvalidCommandLineErrorOPTS, + IncompatibleiLOVersionError, + InvalidCommandLineError, + UsernamePasswordRequiredError, + NoAppAccountError, + VnicExistsError, + SavinginTPMError, + SavinginiLOError, + GenBeforeLoginError, + AppIdListError, + UI, + ) + + +class AppAccountCommand: + """Main command template""" + + def __init__(self): + self.ident = { + "name": "appaccount", + "usage": "appaccount\n\n", + "description": "Manages application accounts in iLO and TPM, allowing creation," + "deletion, and verification with appaccount create, appaccount delete, " + "and appaccount exists." + "Retrieves details of all application accounts using appaccount details.\n" + "Supported only on VNIC-enabled iLO7 servers.\n" + "For help on specific subcommands, run: appaccount -h.\n\n", + "summary": "Creates/Deletes application account, Checks the existence of an" + " application account, Provides details on all app accounts present in the server.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + # Check for admin privileges + if "blobstore" in self.rdmc.app.current_client.base_url or "16.1.15." in self.rdmc.app.current_client.base_url: + if os.name == "nt": + if not ctypes.windll.shell32.IsUserAnAdmin() != 0: + self.rdmc.app.typepath.adminpriv = False + elif not os.getuid() == 0: + self.rdmc.app.typepath.adminpriv = False + + if self.rdmc.app.typepath.adminpriv is False: + raise UserNotAdminError("") + + client = self.appaccountvalidation(options) + if client: + if "16.1.15.1" not in client.base_url: + raise VnicExistsError( + "Appaccount command can only be executed " "from the host OS of a VNIC-enabled iLO7 based server.\n" + ) + + # To populate the correct host app information + if "self_register" in options: + if options.self_register: + if ( + ("hostappname" in options and options.hostappname) + or ("hostappid" in options and options.hostappid) + or ("salt" in options and options.salt) + ): + raise InvalidCommandLineError( + "The parameters provided in the command are invalid." + " You may include either the --self tag " + "or the combination of --hostappid, --hostappname, and --salt tags," + " but not both.\n" + ) + else: + if options.command: + if options.command.lower() == "create": + if not (options.hostappname and options.hostappid and options.salt): + raise InvalidCommandLineError( + "Please provide all the required host application" + " information.\nTo proceed without entering host " + "application details, include " + "--self in the command.\n" + ) + elif options.command.lower() == "delete": + # Validate hostappid is provided + if not options.hostappid: + raise InvalidCommandLineError( + "--hostappid is a required parameter for the appaccount delete command.\n" + ) + + # Check if hostappid is 00b5 (self-registered) - treat as --self + is_self_registered_id = options.hostappid and "00b5" in options.hostappid.lower() + + # For self-registered accounts (--self or 00b5), no credentials required + if not options.self_register and not is_self_registered_id: + # For delete, require either credentials OR host app info (but not both are mandatory) + has_credentials = all([options.user, options.password]) + has_app_info = all([options.hostappname, options.salt]) + + # If hostappid is provided, user must provide EITHER credentials OR app info + if not has_credentials and not has_app_info: + raise InvalidCommandLineError( + "For deleting application accounts, please provide either:\n" + " 1. Username and password (using -u and -p flags), OR\n" + " 2. Host application information (using --hostappname and --salt flags)\n" + ) + + # If partial credentials or partial app info, validate completeness + if (options.user or options.password) and not has_credentials: + raise InvalidCommandLineError("Please provide both username and password\n") + if (options.hostappname or options.salt) and not has_app_info: + raise InvalidCommandLineError( + "Please provide all the required host application" + " information.\nTo proceed without entering host " + "application details, include " + "--self in the command.\n" + ) + elif options.command.lower() == "exists": + if not options.hostappid: + raise InvalidCommandLineError( + "Please provide hostappid." + " To proceed without entering the ID," + " include --self in the command.\n" + ) + elif options.command.lower() == "details": + if not options.hostappid and not options.self_register: + raise InvalidCommandLineError( + "Please provide hostappid using --hostappid or --hostappid all." + " To view self-registered account, use --self.\n" + ) + elif options.command.lower() == "reactivate": + if not (options.user and options.password): + LOGGER.error("Please provide both username and password.\n") + raise InvalidCommandLineError("Please provide both username and password.\n") + + if not (options.hostappname and options.hostappid and options.salt): + LOGGER.error( + "Please provide all the required host application" + " information.\nTo proceed without entering host " + "application details, include " + "--self in the command.\n" + ) + raise InvalidCommandLineError( + "Please provide all the required host application" + " information.\nTo proceed without entering host " + "application details, include " + "--self in the command.\n" + ) + else: + raise InvalidCommandLineError("The command you have entered is invalid.\n") + + try: + # For details command, no credentials needed (read-only operation) + # Only delete command for non-00b5 accounts needs credentials + if options.command and options.command.lower() == "details": + # Handle hostappid="all" - don't pass to AppAccount as it's not a real app ID + hostappid_value = getattr(options, "hostappid", None) if "hostappid" in options else None + if hostappid_value and hostappid_value.lower() == "all": + hostappid_value = None + + app_obj = AppAccount( + appname=getattr(options, "hostappname", None) if "hostappname" in options else "self_register", + appid="self_register", + salt=getattr(options, "salt", None) if "salt" in options else "self_register", + username=None, + password=None, + log_dir=self.rdmc.log_dir, + ) + else: + # Handle hostappid="all" for non-details commands too (though it shouldn't be used) + hostappid_value = options.hostappid if "hostappid" in options else None + if hostappid_value and hostappid_value.lower() == "all": + hostappid_value = None + + app_obj = AppAccount( + appname=options.hostappname if "hostappname" in options else "self_register", + appid=options.hostappid if "hostappid" in options else "self_register", + salt=options.salt if "salt" in options else "self_register", + username=options.user, + password=options.password, + log_dir=self.rdmc.log_dir, + ) + except Exception as excp: + raise NoAppAccountError( + "Error occured while locating application" " account. Please recheck the entered inputs.\n" + ) + + # Function to find out the iLO Generation + self.get_ilover_beforelogin(app_obj) + + # Check if app account exists - skip for details command + already_exists = False + command_lower = options.command.lower() if options.command else "" + hostappid_option = getattr(options, "hostappid", None) + skip_token_check = ( + command_lower == "details" + ) + + if not skip_token_check: + try: + # For short app IDs (4 chars), check using ListAppIds + if ( + command_lower in ["delete", "exists"] + and getattr(options, "hostappid", None) + and len(options.hostappid) == 4 + ): + try: + list_of_appids = self.rdmc.app.ListAppIds(app_obj) + for app_id in list_of_appids: + if app_id["ApplicationID"][-4:].lower() == options.hostappid.lower(): + already_exists = True + break + except Exception: + already_exists = self.rdmc.app.token_exists(app_obj) + else: + already_exists = self.rdmc.app.token_exists(app_obj) + except Exception as excp: + if command_lower != "details": + raise AppAccountExistsError("Error occurred while checking if application account exists.\n") + + if options.command: + if options.command.lower() == "create": + if not options.user or not options.password: # Check if this is the correct variable + raise UsernamePasswordRequiredError("Please enter Username and Password.\n") + + try: + errorcode = self.rdmc.app.generate_save_token(app_obj) + if errorcode == 0: + self.rdmc.ui.printer("Application account has been generated and saved successfully.\n") + return ReturnCodes.SUCCESS + except redfish.hpilo.vnichpilo.AppAccountExistsError: + self.rdmc.ui.printer("Application account already exists for the specified host application.\n") + return ReturnCodes.SUCCESS + except redfish.hpilo.vnichpilo.SavinginTPMError: # Check for specific error messages + raise SavinginTPMError( + "Failed to save the app account in TPM. " + "Please execute the appaccount delete command" + " with the same host application information and " + "attempt to create the app account again.\n" + "Alternatively, you can use the --no_app_account " + "option in the Login Command to log in using your iLO user account credentials.\n" + ) + except redfish.hpilo.vnichpilo.SavinginiLOError: + raise SavinginiLOError( + "Failed to save app account in iLO. " + "Please execute the appaccount delete command" + " with the same host application information and " + "attempt to create the app account again.\n" + "Alternatively, you can use the --no_app_account " + "option in the Login Command to log in using your iLO user account credentials.\n" + ) + except redfish.rest.v1.InvalidCredentialsError: + raise redfish.rest.v1.InvalidCredentialsError(0) + except redfish.hpilo.vnichpilo.GenerateAndSaveAccountError: + raise GenerateAndSaveAccountError( + "Error occurred while generating and saving app account. " + "Please retry after sometime.\n" + "Alternatively, you can use the --no_app_account " + "option in the Login Command to log in using your iLO user account credentials.\n" + ) + + elif options.command.lower() == "delete": + is_self = getattr(options, "self_register", False) + has_credentials = options.user and options.password + has_app_info = options.hostappname and options.salt + + # Try to delete from both TPM and iLO + deleted_from_tpm = False + deleted_from_ilo = False + app_id_to_delete = None + + # Step 1: Try to delete from TPM (may not exist after TPM reset) + try: + errorcode = self.rdmc.app.delete_token(app_obj) + if errorcode == 0: + deleted_from_tpm = True + LOGGER.debug("Successfully deleted app account from TPM") + except Exception as tpm_error: + # TPM deletion failed (token may not exist in TPM), continue to try iLO deletion + LOGGER.debug("TPM deletion failed: %s", str(tpm_error)) + pass + + # Step 2: Try to delete from iLO - requires credentials or authenticated session + if has_credentials or is_self or has_app_info: + try: + # Get the app ID to delete + list_of_appids = self.rdmc.app.ListAppIds(app_obj) + + if is_self: + for app_account in list_of_appids: + if "00b5" in app_account.get("ApplicationID", "").lower(): + app_id_to_delete = app_account["ApplicationID"] + break + elif options.hostappid: + for app_account in list_of_appids: + full_id = app_account["ApplicationID"] + # Match full ID or last 4 chars (short ID) + if full_id == options.hostappid or ( + len(options.hostappid) == 4 and full_id[-4:].lower() == options.hostappid.lower() + ): + app_id_to_delete = full_id + break + + # Delete from iLO via REST API + if app_id_to_delete: + delete_url = ( + f"/redfish/v1/AccountService/Accounts/{app_id_to_delete}" + ) + try: + self.rdmc.app.delete_handler(delete_url) + deleted_from_ilo = True + LOGGER.debug("Successfully deleted app account from iLO") + except Exception as ilo_error: + LOGGER.debug("iLO deletion failed: %s", str(ilo_error)) + except Exception as e: + LOGGER.debug("Error during iLO deletion: %s", str(e)) + + # Report success if deleted from either TPM or iLO + if deleted_from_tpm or deleted_from_ilo: + self.rdmc.ui.printer("Application account has been deleted successfully.\n") + return ReturnCodes.SUCCESS + else: + raise NoAppAccountError( + "The application account you are trying to delete does not exist.\n" + ) + + # Command to check if apptoken exists + elif options.command.lower() == "exists": + if already_exists: + self.rdmc.ui.printer("Application account exists for this host application.\n") + return ReturnCodes.SUCCESS + else: + self.rdmc.ui.printer("Application account does not exist for this hostapp.\n") + return ReturnCodes.ACCOUNT_DOES_NOT_EXIST_ERROR + + # Command to list appids and if they are present in iLO and TPM + elif options.command.lower() == "details": + try: + list_of_appids = self.rdmc.app.ListAppIds(app_obj) + except Exception: + raise AppIdListError("Error occured while retrieving list of App Ids.\n") + + selfdict = list() + if "self_register" in options and options.self_register: + for app_id in list_of_appids: + app_id_value = app_id["ApplicationID"] + # Check only for 00b5 identifier for true self-registered iLORest accounts + # (00b5 is the reserved ID prefix for iLORest self-registration) + if "00b5" in app_id_value.lower(): + selfdict = [app_id] + break + + # If no self-registered app account found, inform user + if not selfdict: + self.rdmc.ui.printer("No self-registered iLORest app account found.\n") + self.rdmc.ui.printer("Use 'appaccount details --hostappid all' to see all app accounts.\n") + return ReturnCodes.SUCCESS + + elif options.hostappid: + if options.hostappid.lower() == "all": + selfdict = list_of_appids + if ( + "onlytoken" in options + and options.onlytoken + or "onlyaccount" in options + and options.onlyaccount + ): + for i in range(len(list_of_appids)): + if "onlytoken" in options and options.onlytoken: + del selfdict[i]["ExistsIniLO"] + elif "onlyaccount" in options and options.onlyaccount: + del selfdict[i]["ExistsInTPM"] + else: + # Expand short app ID (4 chars) to full ID like master code does + target_appid = options.hostappid + if len(options.hostappid) == 4: + try: + target_appid = self.rdmc.app.ExpandAppId(app_obj, options.hostappid) + except Exception: + # If expansion fails, try matching with last 4 chars + pass + + # Handle both full and short (4 char) app IDs + for i in range(len(list_of_appids)): + full_id = list_of_appids[i]["ApplicationID"] + # Match full ID or last 4 chars for short ID + if full_id == target_appid or ( + len(options.hostappid) == 4 + and full_id[-4:].lower() == options.hostappid.lower() + ): + selfdict = [list_of_appids[i]] + if "onlytoken" in options and options.onlytoken: + del selfdict[0]["ExistsIniLO"] + elif "onlyaccount" in options and options.onlyaccount: + del selfdict[0]["ExistsInTPM"] + break + if not selfdict: + raise AppAccountExistsError( + "There is no application account exists for the given hostappid. " + "Please recheck the entered value.\n" + ) + else: + # No --hostappid provided - list all app accounts (master behavior) + selfdict = list_of_appids + if ( + "onlytoken" in options and options.onlytoken + or "onlyaccount" in options and options.onlyaccount + ): + for i in range(len(list_of_appids)): + if "onlytoken" in options and options.onlytoken: + del selfdict[i]["ExistsIniLO"] + elif "onlyaccount" in options and options.onlyaccount: + del selfdict[i]["ExistsInTPM"] + + if options.json: + tempdict = self.print_json_app_details(selfdict) + UI().print_out_json(tempdict) + else: + self.print_app_details(selfdict) + + return ReturnCodes.SUCCESS + elif options.command.lower() == "reactivate": + if not already_exists: + LOGGER.error("The application account you are trying to reactivate does not exist.") + raise NoAppAccountError("The application account you are trying to reactivate does not exist.\n") + try: + return_code = self.rdmc.app.reactivate_token(app_obj) + if return_code == 0: + self.rdmc.ui.printer("Application account has been reactivated successfully.\n") + return ReturnCodes.SUCCESS + except redfish.rest.v1.InvalidCredentialsError: + LOGGER.error("Please enter valid credentials.") + raise redfish.rest.v1.InvalidCredentialsError(0) + except Exception as excp: + LOGGER.error("Error occurred while reactivating application account.") + raise ReactivateAppAccountTokenError("Error occurred while reactivating application account.\n") + else: + raise InvalidCommandLineError("The command you have entered is invalid.\n") + + else: + raise InvalidCommandLineError("The command you have entered is invalid.\n") + + def appaccountvalidation(self, options): + """appaccount validation function + + :param options: command line options + :type options: list. + """ + return self.rdmc.login_select_validation(self, options) + + def print_json_app_details(self, selfdict): + for i in range(len(selfdict)): + selfdict[i]["ApplicationID"] = "**" + selfdict[i]["ApplicationID"][-4:] + return selfdict + + def print_app_details(self, printdict): + final_output = "" + for i in range(len(printdict)): + final_output += "Application Name: " + final_output += printdict[i]["ApplicationName"] + final_output += "\n" + final_output += "Application Id: **" + final_output += printdict[i]["ApplicationID"][-4:] + final_output += "\n" + if "ExistsInTPM" in printdict[i]: + final_output += "App account exists in TPM: " + if printdict[i]["ExistsInTPM"]: + final_output += "yes\n" + else: + final_output += "no\n" + if "ExistsIniLO" in printdict[i]: + final_output += "App account exists in iLO: " + if printdict[i]["ExistsIniLO"]: + final_output += "yes\n" + else: + final_output += "no\n" + final_output += "\n" + + self.rdmc.ui.printer(final_output) + + def get_ilover_beforelogin(self, app_obj): + try: + ilo_ver, sec_state = self.rdmc.app.getilover_beforelogin(app_obj) + if ilo_ver < 7: + raise ChifDriverMissingOrNotFound() + except ChifDriverMissingOrNotFound: + raise IncompatibleiLOVersionError("This feature is only available for iLO 7 or higher.\n") + except VnicNotEnabledError: + raise VnicExistsError( + "Unable to access iLO using virtual NIC. " + "Please ensure virtual NIC is enabled in iLO. " + "Ensure that virtual NIC in the host OS is " + "configured properly. Refer to documentation for more information.\n" + ) + except redfish.hpilo.vnichpilo.InvalidCommandLineError: + raise InvalidCommandLineError( + "There is no app account present for the given hostappid." " Please recheck the entered value.\n" + ) + except Exception: + raise GenBeforeLoginError( + "An error occurred while retrieving the iLO generation. " + "Please ensure that the virtual NIC is enabled for iLO7 based " + "servers, or that the CHIF driver is installed for iLO5 and iLO6 " + "based servers.\n " + "Note: appaccount command can only be executed from the host OS of a VNIC-enabled iLO7 server.\n" + ) + + def definearguments(self, customparser): + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + + # Create apptoken command arguments + help_text = "To generate and save Application account" + create_parser = subcommand_parser.add_parser( + "create", + help=help_text, + description="appaccount create --username temp_user --password " + "pasxx --hostappname xxx --hostappid xxx --salt xxx", + formatter_class=RawDescriptionHelpFormatter, + ) + + create_parser.add_argument("--hostappid", dest="hostappid", help="Parameter to specify hostappid", default=None) + create_parser.add_argument( + "--hostappname", dest="hostappname", help="Parameter to specify hostappname", default=None + ) + create_parser.add_argument( + "--salt", dest="salt", help="Parameter to specify application owned salt", default=None + ) + help_text = "Self tag for customers with no access to host information." + create_parser.add_argument("--self", dest="self_register", help=help_text, action="store_true", default=False) + self.cmdbase.add_login_arguments_group(create_parser) + + # Delete apptoken command arguments + help_text = "To delete Application account" + delete_parser = subcommand_parser.add_parser( + "delete", + help=help_text, + description="appaccount delete --hostappname xxx -u user123 -p passxx", + formatter_class=RawDescriptionHelpFormatter, + ) + delete_parser.add_argument("--hostappid", dest="hostappid", help="Parameter to specify hostappid", default=None) + delete_parser.add_argument( + "--hostappname", dest="hostappname", help="Parameter to specify hostappname", default=None + ) + delete_parser.add_argument( + "--salt", dest="salt", help="Parameter to specify application owned salt", default=None + ) + help_text = "Self tag for customers with no access to host information." + delete_parser.add_argument("--self", dest="self_register", help=help_text, action="store_true", default=False) + self.cmdbase.add_login_arguments_group(delete_parser) + + # token exists command arguments + help_text = "To check if Application account exists" + exists_parser = subcommand_parser.add_parser( + "exists", + help=help_text, + description="appaccount exists --hostappid xxx", + formatter_class=RawDescriptionHelpFormatter, + ) + exists_parser.add_argument("--hostappid", dest="hostappid", help="Parameter to specify hostappid", default=None) + + help_text = "Self tag for customers with no access to host information." + exists_parser.add_argument("--self", dest="self_register", help=help_text, action="store_true", default=False) + self.cmdbase.add_login_arguments_group(exists_parser) + + # Details command arguments + help_text = "To list details of app accounts present in TPM and iLO." + details_parser = subcommand_parser.add_parser( + "details", + help=help_text, + description="appaccount details --hostappid xxx", + formatter_class=RawDescriptionHelpFormatter, + ) + details_parser.add_argument( + "--hostappid", dest="hostappid", help="Parameter to specify hostappid", default=None + ) + details_parser.add_argument( + "--only_token", + dest="onlytoken", + help="Parameter provides details of app account in TPM", + action="store_true", + default=False, + ) + details_parser.add_argument( + "--only_account", + dest="onlyaccount", + help="Parameter provides details of app account in iLO.", + action="store_true", + default=False, + ) + help_text = "Self tag for customers with no access to host information." + details_parser.add_argument("--self", dest="self_register", help=help_text, action="store_true", default=False) + details_parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + self.cmdbase.add_login_arguments_group(details_parser) + + # Reactivate apptoken command arguments + help_text = "To reactivate Application account" + reactivate_parser = subcommand_parser.add_parser( + "reactivate", + help=help_text, + description="appaccount reactivate --username temp_user --password pasxx", + formatter_class=RawDescriptionHelpFormatter, + ) + + reactivate_parser.add_argument( + "--hostappid", dest="hostappid", help="Parameter to specify hostappid", default=None + ) + reactivate_parser.add_argument( + "--hostappname", dest="hostappname", help="Parameter to specify hostappname", default=None + ) + reactivate_parser.add_argument( + "--salt", dest="salt", help="Parameter to specify application owned salt", default=None + ) + help_text = "Self tag for customers with no access to host information." + reactivate_parser.add_argument( + "--self", dest="self_register", help=help_text, action="store_true", default=False + ) + self.cmdbase.add_login_arguments_group(reactivate_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/CertificateCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/CertificateCommand.py new file mode 100644 index 0000000..969268d --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/CertificateCommand.py @@ -0,0 +1,1481 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Certificates Command for rdmc""" + +import time +from argparse import RawDescriptionHelpFormatter + +try: + from rdmc_helper import ( + IloLicenseError, + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + NoContentsFoundForOperationError, + ReturnCodes, + ScepenabledError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IloLicenseError, + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + NoContentsFoundForOperationError, + ReturnCodes, + ScepenabledError, + ) + +__filename__ = "certificate.txt" + +from redfish.ris import IdTokenError + + +class CertificateCommand: + """Commands Certificates actions to the server""" + + def __init__(self): + self.ident = { + "name": "certificate", + "usage": None, + "description": "Generate a certificate signing request (CSR) or import an X509 formatted" + " TLS or CA certificate.\nImport Scep Certificate.\nInvoke Auto Enroll of certificate generation\n\n" + "NOTE: Use quotes to include parameters which contain whitespace when " + 'generating a CSR.\nexample: certificate gen_csr "Hewlett Packard Enterprise"' + '"iLORest Group" "CName"\n"United States" "Texas" "Houston" "False or True"\n\n' + "NOTE: unifiedcertificate command which was augmenting certificate command is merged" + " into single command certificate", + "summary": "Command for importing both iLO and login authorization " + "certificates as well as generating iLO certificate signing requests (CSR)\n", + "aliases": ["unifiedcertificate"], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self.view = None + self.importcert = None + self.exportcert = None + self.delete = None + self.gencsr = None + self.autoenroll = None + + def run(self, line, help_disp=False): + """Main Certificates Command function + + :param options: list of options + :type options: list. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.certificatesvalidation(options) + + returnCode = None + + if options.command == "csr": + self.rdmc.ui.printer( + "This command has been deprecated. Please use the following command: " + 'certificate gen_csr "Hewlet Packard Enterprice" "ILORestGroup" ...\n' + "It performs the same function.\n" + "Check --help for more information.\n" + ) + self.cmdbase.logout_routine(self, options) + # Return code + return returnCode + elif options.command == "ca": + self.rdmc.ui.printer( + "This command has been deprecated. Please use the following " + "command: certificate import --ca_cert \n" + "It performs the same function.\n" + "Check --help for more information.\n" + ) + self.cmdbase.logout_routine(self, options) + # Return code + return returnCode + elif options.command == "getcsr": + returnCode = self.get_csr_helper(options) + elif options.command == "crl": + self.rdmc.ui.printer( + "This command has been deprecated. Please use the following command: " + "certificate import --crl_cert \n" + "It performs the same function.\n" + "Check --help for more information.\n" + ) + self.cmdbase.logout_routine(self, options) + # Return code + return returnCode + elif options.command == "tls": + self.rdmc.ui.printer( + "This command has been deprecated. Please use the following command: " + "certificate import --tls_cert \n" + "It performs the same function.\n" + "Check --help for more information.\n" + ) + self.cmdbase.logout_routine(self, options) + # Return code + return returnCode + if "view" in options.command.lower(): + self.view = True + self.importcert = False + self.delete = False + self.gencsr = False + self.autoenroll = False + self.exportcert = False + elif "import" in options.command.lower(): + self.view = False + self.importcert = True + self.delete = False + self.gencsr = False + self.autoenroll = False + self.exportcert = False + elif "export" in options.command.lower(): + self.view = False + self.importcert = False + self.delete = False + self.gencsr = False + self.autoenroll = False + self.exportcert = True + elif "delete" in options.command.lower(): + self.view = False + self.importcert = False + self.delete = True + self.gencsr = False + self.autoenroll = False + self.exportcert = False + elif "gen_csr" in options.command.lower(): + self.view = False + self.importcert = False + self.delete = False + self.gencsr = True + self.autoenroll = False + self.exportcert = False + elif "auto_enroll" in options.command.lower(): + self.view = False + self.importcert = False + self.delete = False + self.gencsr = False + self.autoenroll = True + self.exportcert = False + + if self.view: + returnCode = self.viewfunction(options) + elif self.importcert: + returnCode = self.importfunction(options) + elif self.exportcert: + returnCode = self.exportfunction(options) + elif self.delete: + returnCode = self.deletefunction(options) + elif self.gencsr: + returnCode = self.gencsrfunction(options) + elif self.autoenroll: + if self.rdmc.app.typepath.defs.isgen10: + returnCode = self.autoenrollfunction(options) + else: + self.rdmc.ui.printer("Gen 9 doesnt support this feature\n") + returnCode = ReturnCodes.SUCCESS + + self.cmdbase.logout_routine(self, options) + # Return code + return returnCode + + def autoenrollfunction(self, options): + """Automatic Scep cert enrollement process + + :param options: list of options + :type options: list. + """ + select = self.rdmc.app.typepath.defs.securityservice + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + bodydict = results.resp.dict + + try: + for item in bodydict["Links"]: + if "AutomaticCertificateEnrollment" in item: + path = bodydict["Links"]["AutomaticCertificateEnrollment"]["@odata.id"] + break + except: + path = path + "AutomaticCertificateEnrollment" + + body = { + "AutomaticCertificateEnrollmentSettings": { + "ServiceEnabled": eval(options.autoenroll_ScepService.strip('"')), + "ServerUrl": options.autoenroll_scep_enrollAddress.strip('"'), + "ChallengePassword": options.autoenroll_challengepassword.strip('"'), + }, + "HttpsCertCSRSubjectValue": { + "OrgName": options.autoenroll_orgname.strip('"'), + "OrgUnit": options.autoenroll_orgunit.strip('"'), + "CommonName": options.autoenroll_commonname.strip('"'), + "Country": options.autoenroll_country.strip('"'), + "State": options.autoenroll_state.strip('"'), + "City": options.autoenroll_city.strip('"'), + "IncludeIP": eval(options.autoenroll_includeIP.strip('"')), + }, + } + + try: + results = self.rdmc.app.patch_handler(path, body, silent=True) + + i = 0 + results1 = self.rdmc.app.get_handler(path, silent=True) + while i < 9 and ( + results1.dict["AutomaticCertificateEnrollmentSettings"]["CertificateEnrollmentStatus"] == "InProgress" + ): + results1 = self.rdmc.app.get_handler(path, silent=True) + time.sleep(1) + i = i + 1 + + if results.status == 200 and ( + not (results1.dict["AutomaticCertificateEnrollmentSettings"]["CertificateEnrollmentStatus"] == "Failed") + ): + self.rdmc.ui.printer("Auto enrolment Enable/Disable operation successful \n") + return ReturnCodes.SUCCESS + elif results.status == 400: + self.rdmc.ui.error( + "There was a problem with auto enroll, Please check whether Scep CA cert is imported \t\n" + ) + + return ReturnCodes.SCEP_ENABLED_ERROR + else: + self.rdmc.ui.error( + "There was a problem with auto enroll, Plese Check the Url/Password whether it is correct\n" + ) + return ReturnCodes.SCEP_ENABLED_ERROR + except IloLicenseError: + self.rdmc.ui.error("License Error Occured while auto enroll\n") + return ReturnCodes.ILO_LICENSE_ERROR + except IdTokenError: + self.rdmc.ui.printer("Insufficient Privilege to auto enroll scep certificate process\n") + return ReturnCodes.RIS_MISSING_ID_TOKEN + + def gencsrfunction(self, options): + """Main Certificates Command function + + :param options: list of options + :type options: list. + """ + try: + select = self.rdmc.app.typepath.defs.hphttpscerttype + results = self.rdmc.app.select(selector=select) + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + bodydict = results.resp.dict + + try: + for item in bodydict["Actions"]: + if "GenerateCSR" in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = "GenerateCSR" + + path = bodydict["Actions"][item]["target"] + break + except: + action = "GenerateCSR" + except: + path = "redfish/v1/Managers/1/SecurityService/HttpsCert" + action = "GenerateCSR" + + body = { + "Action": action, + "OrgName": options.gencsr_orgname.strip('"'), + "OrgUnit": options.gencsr_orgunit.strip('"'), + "CommonName": options.gencsr_commonname.strip('"'), + "Country": options.gencsr_country.strip('"'), + "State": options.gencsr_state.strip('"'), + "City": options.gencsr_city.strip('"'), + "IncludeIP": eval(options.gencsr_parser_includeIP.strip('"')), + } + + try: + results = self.rdmc.app.post_handler(path, body) + if results.status == 200: + return ReturnCodes.SUCCESS + except ScepenabledError: + self.rdmc.ui.printer("SCEP is enabled , CSR cant be generated \n") + return ReturnCodes.SCEP_ENABLED_ERROR + except IloLicenseError: + self.rdmc.ui.error("License Error Occured while generating CSR") + return ReturnCodes.ILO_LICENSE_ERROR + except IdTokenError: + self.rdmc.ui.printer("Insufficient Privilege to generate CSR\n") + return ReturnCodes.RIS_MISSING_ID_TOKEN + + def deletefunction(self, options): + """Certificate Delete Function + + :param options: list of options + :type options: list. + """ + + select = self.rdmc.app.typepath.defs.hphttpscerttype + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + try: + results = self.rdmc.app.delete_handler(path, silent=True) + if results.status == 200: + self.rdmc.ui.printer("Deleted the https certifcate successfully\n") + return ReturnCodes.SUCCESS + if results.status == 403: + self.rdmc.ui.error("Insufficient Privilege to delete certificate\n") + return ReturnCodes.RIS_MISSING_ID_TOKEN + if results.status == 400: + self.rdmc.ui.error("SCEP is enabled , Cant delete the certificate\n") + return ReturnCodes.SCEP_ENABLED_ERROR + except IloLicenseError: + self.rdmc.ui.error("License Error Occured while delete") + return ReturnCodes.ILO_LICENSE_ERROR + except IncompatibleiLOVersionError: + self.rdmc.ui.error("iLO FW version on this server doesnt support this operation") + return ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + + def viewfunction(self, options): + """View scep certifcates or https certificates + + :param options: list of options + :type options: list. + """ + + if options.scep_cert: + if self.rdmc.app.typepath.defs.isgen10: + self.view_scepcertificate() # View scep certificates + else: + self.rdmc.ui.printer("Feature not supported on Gen 9\n") + return ReturnCodes.SUCCESS + if options.https_cert: + self.view_httpscertificate() # View https certificates + + def view_scepcertificate(self): + """ + View Scep certificate + """ + select = self.rdmc.app.typepath.defs.securityservice + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + bodydict = results.resp.dict + + try: + for item in bodydict["Links"]: + if "AutomaticCertificateEnrollment" in item: + path = bodydict["Links"]["AutomaticCertificateEnrollment"]["@odata.id"] + break + except: + path = path + "AutomaticCertificateEnrollment" + + try: + results = self.rdmc.app.get_handler(path, silent=True) + if results.status == 200: + self.rdmc.ui.printer("Scep Certificate details ...\n") + results = results.dict + self.print_cert_info(results) + return ReturnCodes.SUCCESS + + except IloLicenseError: + self.rdmc.ui.error("Error Occured while Uninstall") + return ReturnCodes.ILO_LICENSE_ERROR + + def print_cert_info(self, results): + """ + Prints the cert info + """ + for key, value in results.items(): + if "@odata" not in key: + if type(value) is dict: + self.print_cert_info(value) + else: + self.rdmc.ui.printer(key + ":" + str(value) + "\n") + + def view_httpscertificate(self): + """ + View Https certificate + """ + try: + select = self.rdmc.app.typepath.defs.hphttpscerttype + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + except: + path = "redfish/v1/Managers/1/SecurityService/HttpsCert" + + try: + results = self.rdmc.app.get_handler(path, silent=True) + if results.status == 200: + self.rdmc.ui.printer("Https Certificate details ...\n") + results = results.dict + self.print_cert_info(results) + return ReturnCodes.SUCESS + except IloLicenseError: + self.rdmc.ui.error("Error Occured while Uninstall") + return ReturnCodes.ILO_LICENSE_ERROR + + def exportfunction(self, options): + result = self.exportplatformhelper(options) + + if result: + if options.filename: + self.file_handler(next(iter(options.filename)), result, options, "wb") + self.rdmc.ui.printer("The certificate was saved to: %s\n" % next(iter(options.filename))) + return ReturnCodes.SUCCESS + else: + self.rdmc.ui.printer("The certificate retrieved is as follows:\n%s\n" % result) + return ReturnCodes.SUCCESS + else: + raise NoContentsFoundForOperationError("An error occurred retrieving the requested certificate.\n") + + def exportplatformhelper(self, options): + """Helper function for exporting a platform certificate + :param options: list of options + :type options: list. + """ + + str_type = "PlatformCert" + ss_instance = next(iter(self.rdmc.app.select("SecurityService." + ".", path_refresh=True))) + if options.ldevid_cert: + str_type = "iLOLDevID" + elif options.idevid_cert: + str_type = "iLOIDevID" + elif options.systemiak_cert: + str_type = "SystemIAK" + elif options.systemidevid_cert: + str_type = "SystemIDevID" + instance_path_uri = ( + (ss_instance.dict[str_type]["Certificates"][self.rdmc.app.typepath.defs.hrefstring]) + if ss_instance.dict.get("SystemIAK") + else None + ) + instance_data = self.rdmc.app.get_handler(instance_path_uri, silent=True) + cert = None + if instance_data.dict.get("Members"): + cert = self.rdmc.app.get_handler( + instance_data.dict["Members"][getattr(options, "id", 0) - 1].get( + self.rdmc.app.typepath.defs.hrefstring + ), + silent=True, + ).dict + return cert.get("CertificateString") + else: + raise NoContentsFoundForOperationError( + "Unable to find specified certificate at " "position %s." % getattr(options, "id", 0) + ) + + def file_handler(self, filename, data, options, operation="rb"): + """ + Wrapper function to read or write data to a respective file + :param data: data to be written to output file + :type data: container (list of dictionaries, dictionary, etc.) + :param file: filename to be written + :type file: string (generally this should be self.clone_file or tmp_clone_file + :param operation: file operation to be performed + :type operation: string ('w+', 'a+', 'r+') + :param options: command line options + :type options: attribute + :returns: json file data + """ + writeable_ops = ["wb", "w", "w+", "a", "a+"] + + if operation in writeable_ops: + with open(filename, operation) as fh: + try: + data = data.encode("UTF-8") + except IOError: + raise InvalidFileInputError("Unable to write to file '%s'" % filename) + except UnicodeEncodeError: + pass + finally: + fh.write(data) + else: + with open(filename, operation) as fh: + fdata = fh.read() + try: + return fdata.decode("UTF-8") + except UnicodeDecodeError: + return fdata + except IOError: + raise InvalidFileInputError("Unable to read from file '%s'" % filename) + + def importfunction(self, options): + if self.rdmc.app.typepath.defs.isgen10: + return self.importfunctionhelper(options) + else: + self.rdmc.ui.printer("Gen 9 doesnt support this feature\n") + return ReturnCodes.SUCCESS + + def importscephelper(self, options): + """ + Import Scep certificate + """ + select = self.rdmc.app.typepath.defs.securityservice + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + bodydict = results.resp.dict + + try: + for item in bodydict["Links"]: + if "AutomaticCertificateEnrollment" in item: + path = bodydict["Links"]["AutomaticCertificateEnrollment"]["@odata.id"] + break + except: + path = path + "AutomaticCertificateEnrollment" + + path = path + "Actions/HpeAutomaticCertEnrollment.ImportCACertificate" + + action = "HpeAutomaticCertEnrollment.ImportCACertificate" + + certdata = None + scep_CACert = options.certfile + + try: + with open(scep_CACert) as certfile: + certdata = certfile.read() + certfile.close() + except: + pass + + body = {"Action": action, "Certificate": certdata} + + try: + result = self.rdmc.app.post_handler(path, body) + if result.status == 200: + self.rdmc.ui.printer("Imported the scep certificate successfully\n") + return ReturnCodes.SUCCESS + except IdTokenError: + self.rdmc.ui.printer("Insufficient Privilege to import scep CA certificate\n") + return ReturnCodes.RIS_MISSING_ID_TOKEN + except IloLicenseError: + self.rdmc.ui.error("Error Occured while importing scep certificate") + return ReturnCodes.ILO_LICENSE_ERROR + except IncompatibleiLOVersionError: + self.rdmc.ui.error("iLO FW version on this server doesnt support this operation") + return ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + + def importfunctionhelper(self, options): + result = None + if getattr(options, "scep_cert"): + result = self.importscephelper(options) + elif getattr(options, "ca_cert"): + result = self.importcahelper(options) + elif getattr(options, "crl_cert"): + result = self.importcrlhelper(options) + elif getattr(options, "tls_cert"): + result = self.importtlshelper(options) + elif ( + getattr(options, "ldevid_cert") + or getattr(options, "idevid_cert") + or getattr(options, "systemiak_cert") + or getattr(options, "systemidevid_cert") + or getattr(options, "platform_cert") + ): + result = self.importplatformhelper(options) + + return result + + def generatecerthelper(self, options): + """Main Certificates Command function + + :param options: list of options + :type options: list. + """ + self.rdmc.ui.printer( + "Warning:Command CSR has been replaced with Gen_csr comamnd and " + "CSR command will be deprecated in next release of our tool. Please plan to use gen_csr command instead \n" + ) + select = self.rdmc.app.typepath.defs.hphttpscerttype + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + bodydict = results.resp.dict + + try: + for item in bodydict["Actions"]: + if "GenerateCSR" in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = "GenerateCSR" + + path = bodydict["Actions"][item]["target"] + break + except: + action = "GenerateCSR" + + body = { + "Action": action, + "OrgName": options.csr_orgname.strip('"'), + "OrgUnit": options.csr_orgunit.strip('"'), + "CommonName": options.csr_commonname.strip('"'), + "Country": options.csr_country.strip('"'), + "State": options.csr_state.strip('"'), + "City": options.csr_city.strip('"'), + } + + self.rdmc.ui.printer( + "iLO is creating a new certificate signing request. " "This process can take up to 10 minutes.\n" + ) + + try: + self.rdmc.app.post_handler(path, body) + return ReturnCodes.SUCCESS + except ScepenabledError: + self.rdmc.ui.printer("SCEP is enabled , operation not allowed \n") + return ReturnCodes.SCEP_ENABLED_ERROR + + def getcerthelper(self, options): + """Helper function for importing CRL certificate + + :param options: list of options + :type options: list. + """ + + select = self.rdmc.app.typepath.defs.hphttpscerttype + results = self.rdmc.app.select(selector=select, path_refresh=True) + + try: + results = results[0] + except: + pass + + if results: + try: + csr = results.resp.dict["CertificateSigningRequest"] + if not csr: + raise ValueError + except (KeyError, ValueError): + raise NoContentsFoundForOperationError( + "Unable to find a valid certificate. If " + "you just generated a new certificate " + "signing request the process may take " + "up to 10 minutes." + ) + + if not options.filename: + filename = __filename__ + else: + filename = options.filename[0] + + outfile = open(filename, "w") + outfile.write(csr) + outfile.close() + + self.rdmc.ui.printer("Certificate saved to: %s\n" % filename) + return ReturnCodes.SUCCESS + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + def get_csr_helper(self, options): + result = None + if options.TLSCERT: + instance = next(iter(self.rdmc.app.select("HttpsCert.", path_refresh=True))) + result = instance.dict.get("CertificateSigningRequest") + elif options.PLATFORM: + tmp = self.gen_csr_helper(options) + if tmp: + result = tmp.dict.get("CSRString") + + if result: + if not options.filename: + filename = __filename__ + else: + filename = options.filename[0] + + outfile = open(filename, "w") + outfile.write(result) + outfile.close() + self.rdmc.ui.printer("Certificate saved to: %s\n" % filename) + return ReturnCodes.SUCCESS + else: + self.rdmc.ui.error( + "An error occurred retrieving a CSR. Check whether CSR is requested , " + "if not kindly run gen_csr command and generate CSR" + ) + + def gen_csr_helper(self, options): + """ + :param options: list of options + :type options: attributes. + """ + body = None + path = None + action = "GenerateCSR" + if options.TLSCERT: + instance = next(iter(self.rdmc.app.select("HttpsCert.", path_refresh=True))) + body = { + "Action": action, + "OrgName": options.csr_orgname.strip('"'), + "OrgUnit": options.csr_orgunit.strip('"'), + "CommonName": options.csr_commonname.strip('"'), + "Country": options.csr_country.strip('"'), + "State": options.csr_state.strip('"'), + "City": options.csr_city.strip('"'), + } + + elif options.PLATFORM: + # There is seemingly no way to update the Certificate subject data which + # appears problematic. + _ = next(iter(self.rdmc.app.select("CertificateService.", path_refresh=True))) + ss_instance = next(iter(self.rdmc.app.select("SecurityService.", path_refresh=True))) + cert_obtain_path = ss_instance.dict.get("iLOLDevID")[next(iter(ss_instance.dict.get("iLOLDevID")))].get( + self.rdmc.app.typepath.defs.hrefstring + ) + if not cert_obtain_path: + raise NoContentsFoundForOperationError("Unable to find specified certificate path" " for CSR request") + instance = next(iter(self.rdmc.app.select("CertificateService.", path_refresh=True))) + body = {"CertificateCollection": {self.rdmc.app.typepath.defs.hrefstring: cert_obtain_path}} + + try: + for act in instance.dict.get("Actions"): + if "GenerateCSR" in act: + if self.rdmc.app.typepath.defs.isgen10: + action = act.split("#")[-1] + else: + action = "GenerateCSR" + path = instance.dict["Actions"][act]["target"] + break + except: + raise NoContentsFoundForOperationError("Unable to find specified certificate action" "path for CSR request") + + self.rdmc.ui.printer( + "iLO is creating a new certificate signing request. " "This request may take up to 10 minutes.\n" + ) + return self.rdmc.app.post_handler(path, body) + + def importtlshelper(self, options): + """Helper function for importing TLS certificate + + :param options: list of options + :type options: list. + """ + tlsfile = options.certfile + + try: + with open(tlsfile) as certfile: + certdata = certfile.read() + certfile.close() + except: + raise InvalidFileInputError("Error loading the specified file.") + + select = self.rdmc.app.typepath.defs.hphttpscerttype + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + bodydict = results.resp.dict + try: + for item in bodydict["Actions"]: + if "ImportCertificate" in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = "ImportCertificate" + path = bodydict["Actions"][item]["target"] + break + except: + action = "ImportCertificate" + + body = {"Action": action, "Certificate": certdata} + + try: + result = self.rdmc.app.post_handler(path, body) + if result.status == 200: + self.rdmc.ui.printer("Imported the TLS certificate successfully\n") + return ReturnCodes.SUCCESS + except IdTokenError: + self.rdmc.ui.printer("Insufficient Privilege to import TLS certificate\n") + return ReturnCodes.RIS_MISSING_ID_TOKEN + except ScepenabledError: + self.rdmc.ui.printer("SCEP is enabled , operation not allowed \n") + return ReturnCodes.SCEP_ENABLED_ERROR + except IloLicenseError: + self.rdmc.ui.error("Error occurred while importing TLS certificate") + return ReturnCodes.ILO_LICENSE_ERROR + except IncompatibleiLOVersionError: + self.rdmc.ui.error("iLO FW version on this server doesnt support this operation") + return ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + + def importcrlhelper(self, options): + """Helper function for importing CRL certificate + + :param options: list of options + :type options: list. + """ + if not self.rdmc.app.typepath.flagiften: + raise IncompatibleiLOVersionError("This certificate is not available on this system.") + + select = "HpeCertAuth." + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + bodydict = results.resp.dict + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + for item in bodydict["Actions"]: + if "ImportCRL" in item: + action = item.split("#")[-1] + path = bodydict["Actions"][item]["target"] + break + + body = {"Action": action, "ImportUri": options.certfile} + # self.rdmc.app.post_handler(path, body) + try: + result = self.rdmc.app.post_handler(path, body) + if result.status == 200: + self.rdmc.ui.printer("Imported the CRL certificate successfully\n") + return ReturnCodes.SUCCESS + except IdTokenError: + self.rdmc.ui.printer("Insufficient Privilege to import CRL certificate\n") + return ReturnCodes.RIS_MISSING_ID_TOKEN + except IloLicenseError: + self.rdmc.ui.error("Error Occured while importing CRL certificate") + return ReturnCodes.ILO_LICENSE_ERROR + except IncompatibleiLOVersionError: + self.rdmc.ui.error("iLO FW version on this server doesnt support this operation") + return ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + + def importcahelper(self, options): + """Helper function for importing CA certificate + + :param options: list of options + :type options: list. + """ + if not self.rdmc.app.typepath.flagiften: + raise IncompatibleiLOVersionError("This certificate is not available on this system.") + + tlsfile = options.certfile + + try: + with open(tlsfile) as certfile: + certdata = certfile.read() + certfile.close() + except: + raise InvalidFileInputError("Error loading the specified file.") + + select = "HpeCertAuth." + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + bodydict = results.resp.dict + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + for item in bodydict["Actions"]: + if "ImportCACertificate" in item: + action = item.split("#")[-1] + path = bodydict["Actions"][item]["target"] + break + + body = {"Action": action, "Certificate": certdata} + + try: + result = self.rdmc.app.post_handler(path, body) + if result.status == 200: + self.rdmc.ui.printer("Imported the CA certificate successfully\n") + return ReturnCodes.SUCCESS + except IdTokenError: + self.rdmc.ui.printer("Insufficient Privilege to import CA certificate\n") + return ReturnCodes.RIS_MISSING_ID_TOKEN + except IloLicenseError: + self.rdmc.ui.error("Error occurred while importing CA certificate") + return ReturnCodes.ILO_LICENSE_ERROR + except IncompatibleiLOVersionError: + self.rdmc.ui.error("iLO FW version on this server doesnt support this operation") + return ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + + def importplatformhelper(self, options): + """Helper function for importing a platform certificate + :param options: options attributes + :type options: attributes + """ + + # ss_instance = next( + # iter(self.rdmc.app.select("SecurityService.", path_refresh=True)) + # ) + if getattr(options, "ldevid_cert"): + instance_path_uri = "/redfish/v1/Managers/1/Diagnostics/Actions/HpeiLODiagnostics.ImportiLOLDevID" + else: + ss_instance = self.rdmc.app.get_handler("/redfish/v1/Managers/1/Diagnostics/", service=True, silent=True) + instance_path_uri = None + + if getattr(options, "ldevid_cert"): + type = "iLOLDevID" + elif getattr(options, "idevid_cert"): + type = "iLOIDevID" + elif getattr(options, "systemiak_cert"): + type = "SystemIAK" + elif getattr(options, "systemidevid_cert"): + type = "SystemIDevID" + elif getattr(options, "platform_cert"): + type = "PlatformCert" + else: + raise InvalidCommandLineErrorOPTS( + "An invalid set of options were selected...verify " " options were selected correctly and re-try." + ) + type_totarget = "#HpeiLODiagnostics.Import" + type + + if not instance_path_uri: + instance_path_uri = ss_instance.dict["Actions"][type_totarget]["target"] + + # instance_path_uri = ( + # ( + # ss_instance.dict[type]["Certificates"][ + # self.rdmc.app.typepath.defs.hrefstring + # ] + # ) + # if ss_instance.dict.get("SystemIAK") + # else None + # ) + if instance_path_uri: + certdata = None + try: + with open(options.certfile) as cf: + certdata = cf.read() + except: + raise InvalidFileInputError("Error loading the specified file.") + + payload = {"Certificate": certdata} + + if certdata: + try: + result = self.rdmc.app.post_handler(instance_path_uri, payload) + if result.status == 200: + self.rdmc.ui.printer("Imported the %s certificate successfully\n" % type) + return ReturnCodes.SUCCESS + except IdTokenError: + self.rdmc.ui.printer("Insufficient Privilege to import %s certificate\n" % type) + return ReturnCodes.RIS_MISSING_ID_TOKEN + except IloLicenseError: + self.rdmc.ui.error("Error Occured while importing %s certificate\n" % type) + return ReturnCodes.ILO_LICENSE_ERROR + except IncompatibleiLOVersionError: + self.rdmc.ui.error("iLO FW version on this server doesnt support this operation") + return ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + + def certificatesvalidation(self, options): + """certificates validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for certificates command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + subcommand_parser = customparser.add_subparsers(dest="command") + + # gen csr sub-parser + gen_csr_help = "Please use the below example command to execute the same function." + gen_csr_parser = subcommand_parser.add_parser( + "csr", + help=gen_csr_help, + description=gen_csr_help + "\nexample: certificate gen_csr [ORG_NAME] [ORG_UNIT]" + " [COMMON_NAME] [COUNTRY] [STATE] [CITY]\n\nNOTE: please make " + "certain the order of arguments is correct.", + formatter_class=RawDescriptionHelpFormatter, + ) + gen_csr_parser.add_argument( + "csr_orgname", + help="Organization name. i.e. Hewlett Packard Enterprise.", + metavar="ORGNAME", + ) + gen_csr_parser.add_argument( + "csr_orgunit", + help="Organization unit. i.e. Intelligent Provisioning.", + metavar="ORGUNIT", + ) + gen_csr_parser.add_argument( + "csr_commonname", + help="Organization common name. i.e. Common Organization Name.", + metavar="ORGCNAME", + ) + gen_csr_parser.add_argument( + "csr_country", + help="Organization country. i.e. United States.", + metavar="ORGCOUNTRY", + ) + gen_csr_parser.add_argument("csr_state", help="Organization state. i.e. Texas.", metavar="ORGSTATE") + gen_csr_parser.add_argument("csr_city", help="Organization city. i.e. Houston.", metavar="ORGCITY") + self.cmdbase.add_login_arguments_group(gen_csr_parser) + + # get csr + get_csr_help = ( + "Retrieve the generated certificate signing request (CSR) printed to the " "console or to a json file." + ) + get_csr_parser = subcommand_parser.add_parser( + "getcsr", + help=get_csr_help, + description=get_csr_help + + "\nexample: certificate getcsr\nexample: certificate getcsr --TLS_CERT/--PLATFORM_CERT" + "-f mycsrfile.json", + formatter_class=RawDescriptionHelpFormatter, + ) + get_csr_parser.add_argument( + "--TLS_CERT", + dest="TLSCERT", + help="specify to retrieve a TLS/SSL certificate signing request.", + action="store_true", + default=None, + ) + get_csr_parser.add_argument( + "--PLATFORM_CERT", + dest="PLATFORM", + help="specify to retrieve a platform certificate signing request.", + action="store_true", + default=None, + ) + get_csr_parser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag if you wish to use a different" + " filename for the certificate signing request. The default" + " filename is %s." % __filename__, + action="append", + default=None, + ) + self.cmdbase.add_login_arguments_group(get_csr_parser) + + # ca certificate + ca_help = "This command has been deprecated. Please use the below example command to execute the same function." + ca_parser = subcommand_parser.add_parser( + "ca", + help=ca_help, + description=ca_help + "\nexample: certificate import --ca_cert mycertfile.txt\nNote: The " + "certificate must be in X.509 format", + formatter_class=RawDescriptionHelpFormatter, + ) + ca_parser.add_argument("certfile", help="X.509 formatted CA certificate", metavar="CACERTFILE") + self.cmdbase.add_login_arguments_group(ca_parser) + + # crl certificate + crl_help = ( + "This command has been deprecated. Please use the below example command to execute the same function." + ) + crl_parser = subcommand_parser.add_parser( + "crl", + help=crl_help, + description=crl_help + "\nexample: certificate import --crl_cert https://mycertfileurl/mycertfile.txt" + "\nNote: The certificate must be in X.509 format", + formatter_class=RawDescriptionHelpFormatter, + ) + crl_parser.add_argument( + "certfile_url", + help="URL pointing to the location of the X.509 CA certificate", + metavar="CERTFILEURL", + ) + self.cmdbase.add_login_arguments_group(crl_parser) + + # tls certificate + tls_help = ( + "This command has been deprecated. Please use the below example command to execute the same function." + ) + tls_parser = subcommand_parser.add_parser( + "tls", + help=tls_help, + description=tls_help + "\nexample: certificate import --tls_cert mycertfile.txt\nNote: The " + "certificate must be in TLS X.509 format", + formatter_class=RawDescriptionHelpFormatter, + ) + tls_parser.add_argument("certfile", help="X.509 formatted TLS certificate", metavar="TLSCERTFILE") + self.cmdbase.add_login_arguments_group(tls_parser) + + # view certificate + view_help = "View Certificates (https or scep)" + view_parser = subcommand_parser.add_parser( + "view", + help=view_help, + description=view_help + + "\nexample: certificate view --https_cert \n or \n certificate view --scep_cert \n" + + "Webserver certificate whether self-signed or manually imported or issued by SCEP server can be viewed", + formatter_class=RawDescriptionHelpFormatter, + ) + view_parser.add_argument( + "--scep_cert", + dest="scep_cert", + help="Gets the information of SCEP settings for iLO such as SCEP enable status, URL of the SCEP server," + + " ChallengePassword, SCEP CA certificate name, webserver CSR subject contents, SCEP enrollment status", + action="store_true", + ) + view_parser.add_argument( + "--https_cert", + dest="https_cert", + help="Gets the https certificate whether self-signed or manually imported or issued by SCEP server", + action="store_true", + ) + self.cmdbase.add_login_arguments_group(view_parser) + + # import certificate + import_help = "Imports the Certificates." + import_parser = subcommand_parser.add_parser( + "import", + help=import_help, + description=import_help + + "\nexample: certificate import --scep_cert certificate.txt \n" + + " make sure you are providing a .txt file input.\n", + formatter_class=RawDescriptionHelpFormatter, + ) + import_parser.add_argument( + "--scep_cert", + dest="scep_cert", + help="Gets the https certificate whether self-signed or manually imported or issued by SCEP server", + action="store_true", + ) + import_parser.add_argument( + "--ca_cert", + dest="ca_cert", + help="Upload a X.509 formatted CA certificate to iLO.", + action="store_true", + ) + import_parser.add_argument( + "--crl_cert", + dest="crl_cert", + help="Provide iLO with a URL to retrieve the X.509 formatted CA certificate.", + action="store_true", + ) + import_parser.add_argument( + "--tls_cert", + dest="tls_cert", + help="Upload a X.509 TLS certificate to iLO.", + action="store_true", + ) + import_parser.add_argument( + "--idevid_cert", + dest="idevid_cert", + help="Upload an IDEVID certificate.", + action="store_true", + ) + import_parser.add_argument( + "--ldevid_cert", + dest="ldevid_cert", + help="Upload an LDEVID certificate.", + action="store_true", + ) + import_parser.add_argument( + "--systemiak_cert", + dest="systemiak_cert", + help="Upload an System IAK certificate.", + action="store_true", + ) + import_parser.add_argument( + "--systemidevid_cert", + dest="systemidevid_cert", + help="Upload an system IDEVID certificate.", + action="store_true", + ) + import_parser.add_argument( + "--platform_cert", + dest="platform_cert", + help="Upload a platform certificate.", + action="store_true", + ) + import_parser.add_argument( + "--from_url", + dest="from_url", + help="Use this flag to specify a URL for certificate import", + action="append", + default=None, + ) + import_parser.add_argument( + "certfile", + help="Certificate can be imported via POST action", + metavar="certfile", + ) + self.cmdbase.add_login_arguments_group(import_parser) + + # export cert + export_help = "Pull an X.509 formatted platform certificate from iLO." + export_parser = subcommand_parser.add_parser( + "export", + help=export_help, + description=export_help + "\nExample: certificate export --IDEVID -f " "myidevidfile\n", + formatter_class=RawDescriptionHelpFormatter, + ) + + export_parser.add_argument( + "--idevid_cert", + dest="idevid_cert", + help="Specify for an IDEVID certificate. ", + action="store_true", + default=None, + ) + export_parser.add_argument( + "--ldevid_cert", + dest="ldevid_cert", + help="Specify for an LDEVID certificate. ", + action="store_true", + default=None, + ) + export_parser.add_argument( + "--systemiak_cert", + dest="systemiak_cert", + help="Specify for a system IAK certificate.", + action="store_true", + default=None, + ) + export_parser.add_argument( + "--systemidevid_cert", + dest="systemidevid_cert", + help="Specify for a system IDEVID certificate.", + action="store_true", + default=None, + ) + export_parser.add_argument( + "--platform_cert", + dest="platform_cert", + help="Specify for a platform certificate.", + action="store_true", + default=None, + ) + export_parser.add_argument( + "--id", + dest="id", + help="Optionally specify the certificate instance, if multiples are available. If" + "the instance specified is not available, then the next is retrieved. Default is" + "the first instance", + default=1, + ) + export_parser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag to import a certificate from or export a certificate to a file.", + action="append", + default=None, + ) + + self.cmdbase.add_login_arguments_group(export_parser) + + # delete certificate + delete_help = "Deletes the https Certificate" + delete_parser = subcommand_parser.add_parser( + "delete", + help=delete_help, + description=delete_help + "\nexample: certificate delete \n delete the https_cert certificate ", + formatter_class=RawDescriptionHelpFormatter, + ) + + self.cmdbase.add_login_arguments_group(delete_parser) + + # gen csr sub-parser + gencsr_help = ( + "Generate a certificate signing request (CSR) for iLO SSL certificate " + "authentication.\nNote: iLO will create a Base64 encoded CSR in PKCS " + "#10 Format." + ) + gencsr_parser = subcommand_parser.add_parser( + "gen_csr", + help=gencsr_help, + description=gen_csr_help + "\nexample: certificate gen_csr [ORG_NAME] [ORG_UNIT]" + " [COMMON_NAME] [COUNTRY] [STATE] [CITY] [INCLUDEIP] \n\nNOTE: please make " + "certain the order of arguments is correct.", + formatter_class=RawDescriptionHelpFormatter, + ) + gencsr_parser.add_argument( + "gencsr_orgname", + help="Organization name. i.e. Hewlett Packard Enterprise.", + metavar="ORGNAME", + ) + gencsr_parser.add_argument( + "gencsr_orgunit", + help="Organization unit. i.e. Intelligent Provisioning.", + metavar="ORGUNIT", + ) + gencsr_parser.add_argument( + "gencsr_commonname", + help="Organization common name. i.e. Common Organization Name.", + metavar="ORGCNAME", + ) + gencsr_parser.add_argument( + "gencsr_country", + help="Organization country. i.e. United States.", + metavar="ORGCOUNTRY", + ) + gencsr_parser.add_argument("gencsr_state", help="Organization state. i.e. Texas.", metavar="ORGSTATE") + gencsr_parser.add_argument("gencsr_city", help="Organization city. i.e. Houston.", metavar="ORGCITY") + gencsr_parser.add_argument( + "gencsr_parser_includeIP", + help="Include IP. i.e. True or False.", + metavar="INCLUDEIP", + ) + self.cmdbase.add_login_arguments_group(gencsr_parser) + + # automatic enrollment sub-parser + autoenroll_help = ( + "Use this command for invoking the auto enroll the certificate enrollment process" + "\nMake sure you have imported the scep CA certificate " + "before head using certificate import --scep certifcate.txt" + ) + autoenroll_parser = subcommand_parser.add_parser( + "auto_enroll", + help=autoenroll_help, + description=autoenroll_help + "\nexample: certificate auto_enroll [ORG_NAME] [ORG_UNIT]" + " [COMMON_NAME] [COUNTRY] [STATE] [CITY] [SCEP_ADDRESS] [CHALLENGEPASSWORD] " + "[SERVICEENABLED] [INCLUDEIP]]\n\nNOTE: please make " + "certain the order of arguments is correct.", + formatter_class=RawDescriptionHelpFormatter, + ) + autoenroll_parser.add_argument( + "autoenroll_orgname", + help="Organization name. i.e. Hewlett Packard Enterprise.", + metavar="ORGNAME", + ) + autoenroll_parser.add_argument( + "autoenroll_orgunit", + help="Organization unit. i.e. Intelligent Provisioning.", + metavar="ORGUNIT", + ) + autoenroll_parser.add_argument( + "autoenroll_commonname", + help="Organization common name. i.e. Common Organization Name.", + metavar="ORGCNAME", + ) + autoenroll_parser.add_argument( + "autoenroll_country", + help="Organization country. i.e. United States.", + metavar="ORGCOUNTRY", + ) + autoenroll_parser.add_argument("autoenroll_state", help="Organization state. i.e. Texas.", metavar="ORGSTATE") + autoenroll_parser.add_argument("autoenroll_city", help="Organization city. i.e. Houston.", metavar="ORGCITY") + autoenroll_parser.add_argument( + "autoenroll_scep_enrollAddress", + help="Scep-enroll dll address", + metavar="AEADDRESS", + ) + autoenroll_parser.add_argument( + "autoenroll_challengepassword", + help="challenge password", + metavar="AECHALLPASS", + ) + autoenroll_parser.add_argument( + "autoenroll_ScepService", + help="Scep service enable or disable", + metavar="AESCEPSERVICE", + ) + autoenroll_parser.add_argument( + "autoenroll_includeIP", + help="Include IP. i.e. True or False.", + metavar="INCLUDEIP", + ) + self.cmdbase.add_login_arguments_group(autoenroll_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ChannelInterfaceCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ChannelInterfaceCommand.py new file mode 100644 index 0000000..7014f53 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ChannelInterfaceCommand.py @@ -0,0 +1,130 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""iLO CHIF Reset Command for rdmc""" +import sys +from argparse import RawDescriptionHelpFormatter + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class ChannelInterfaceCommand: + """Reset iLO on the server that is currently logged in""" + + def __init__(self): + self.ident = { + "name": "channelinterface", + "usage": None, + "description": "Reset CHIF on the current logged in" " server.\n\tExample: channelinterface reset", + "summary": "Reset CHIF on the current logged in server.", + "aliases": [], + "auxcommands": [], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main iLO reset worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.iloresetvalidation(options) + if "blobstore" not in self.rdmc.app.current_client.base_url: + raise InvalidCommandLineError("CHIF reset is not supported in remote mode.\n") + + if line[0].lower() == "reset": + + ret = self.chif_reset() + if ret: + sys.stdout.write("Successfully reset the CHIF interface.\n") + return ReturnCodes.SUCCESS + else: + sys.stdout.write("Failed to reset the CHIF interface.\n") + return ReturnCodes.RIS_ILO_CHIF_PACKET_EXCHANGE_ERROR + else: + raise InvalidCommandLineError("Invalid parameter, please provide channelinterface reset command.\n") + + def chif_reset(self): + """Function for handling chif packet exchange + + :param data: data to be sent for packet exchange + :type data: str + + """ + try: + libhandle = self.rdmc.app.current_client.connection._conn.channel.dll + libhandle.ChifTerminate() + libhandle.ChifInitialize(None) + return True + except: + return False + + def iloresetvalidation(self, options): + """reboot method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + + reset_help = "To disconnect from ComputeOpsManagement\n" + reset_parser = subcommand_parser.add_parser( + "reset", + help=reset_help, + description=reset_help + "\n\tExample:\n\tchannelinterface reset", + formatter_class=RawDescriptionHelpFormatter, + ) + self.cmdbase.add_login_arguments_group(reset_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ClearRestApiStateCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ClearRestApiStateCommand.py new file mode 100644 index 0000000..33cd997 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ClearRestApiStateCommand.py @@ -0,0 +1,130 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Clear Rest API State Command for rdmc""" + + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class ClearRestApiStateCommand: + """Clear the rest api state of the server""" + + def __init__(self): + self.ident = { + "name": "clearrestapistate", + "usage": None, + "description": "Clears the persistent rest api state.\n\texample: " + "clearrestapistate\n\n\tNote: Some types such as Bios, " + "Iscsi, and SmartStorageConfig will be unavailable until " + "a system reboot occurs after running this command.", + "summary": "Clears the persistent state of the REST API. Some " + "portions of the API may not be available until after the server reboots.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main clearrestapistate function. + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("clearrestapistate command takes no arguments.") + + self.clearrestapistatevalidation(options) + + select = "Manager." + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Manager. not found.") + + bodydict = results.resp.dict["Oem"][self.rdmc.app.typepath.defs.oemhp] + try: + for item in bodydict["Actions"]: + if "ClearRestApiState" in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = "ClearRestApiState" + path = bodydict["Actions"][item]["target"] + body = {"Action": action} + break + except: + body = {"Action": "ClearRestApiState", "Target": "/Oem/Hp"} + self.rdmc.app.post_handler(path, body) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def clearrestapistatevalidation(self, options): + """clearrestapistate method validation function. + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ComputeOpsManagementCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ComputeOpsManagementCommand.py new file mode 100644 index 0000000..c9d53ad --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ComputeOpsManagementCommand.py @@ -0,0 +1,3092 @@ +### +# Copyright 2021-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### +# -*- coding: utf-8 -*- +"""computeopsmanagement Command for rdmc""" + + +import json +import time +import os +from argparse import RawDescriptionHelpFormatter +from datetime import datetime +from ipaddress import ip_address +from typing import Dict, Any + +import requests +import urllib3 +from requests.exceptions import ConnectionError, Timeout, RequestException +from urllib3.util.retry import Retry + + +try: + from rdmc_helper import ( + CloudConnectFailedError, + CloudConnectTimeoutError, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoCurrentSessionEstablished, + ProxyConfigFailedError, + ReturnCodes, + LOGGER, + ) +except ImportError: + from ilorest.rdmc_helper import ( + CloudConnectFailedError, + CloudConnectTimeoutError, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoCurrentSessionEstablished, + ProxyConfigFailedError, + ReturnCodes, + LOGGER, + ) + +from redfish.ris.ris import SessionExpired +from .data.InputTemplate import template + +ErrorMapping = { + "ProxySettingsInvalid_RDA": "Could not connect to HPE using the provided web proxy. " + "Ensure that the proxy details are correct. Additionally," + " you can set proxy using the --proxy parameter in the computeopsmanagement command.\n", + "ProxyOrFirewallError_RDA": "The proxy or firewall in not configured properly." + " Please check the respective values. Additionally, " + "you can set proxy using the --proxy parameter in the computeopsmanagement command.\n", + "iLOTimeError_RDA": "Could not obtain an identity for this server " + "to connect to Compute Ops Management due to a " + "incorrect iLO system time. Please update time on RBSU.\n", + "iLOTimeError_COM": "Could not verify the identity of Compute Ops Management. " + "This could be due to an incorrect iLO system time. " + "Contact HPE support if the problem persists.\n", + "ProxyOrFirewallError_COM": "Could not connect to Compute Ops Management due to proxy issue.\n", + "ProxySettingsInvalid_COM": "Could not connect to HPE using the " + "provided web proxy. Ensure that the proxy details are correct.\n", + "ActivationKeyRequired": "An activation key is required in order " + "to connect to HPE Compute Ops Management. Please enter an activation key.\n", + "InvalidActivationKey": "The activation key entered is invalid. " + "Please check the value entered. If the problem persists, contact HPE Support.\n", + "ExpiredActivationKey": "The activation key entered is expired. " + "Please enter a valid activation key. " + "If the problem persists, contact HPE Support.\n ", + "WrongiLOVersion": ( + "Unsupported iLO version. Upgrade to the latest iLO version " "to use HPE Compute Ops Management.\n" + ), + "DeviceAssignFailed": "Device assignment has failed. Visit HPE GreenLake, select Device tab and ensure" + " this device is not already added to another Compute Ops Management instance.\n", + "DeviceClaimUnauthorized": "Device claim is unauthorized. Contact your HPE GreenLake administrator to " + "verify that you have the right permissions to add " + "a device to HPE GreenLake device inventory.\n", + "DeviceNotFound": "Device not found. Contact HPE Support to resolve this issue.\n", + "InternalError_RDA": "Unknown error. Reset iLO and re-try connecting." + " Contact HPE support if the problem persists.\n", + "InternalError_COM": "Unknown error. Reset iLO and re-try connecting. " + "Contact HPE support if the problem persists.\n", + "ExternalError_RDA": "External error. Retry after some time. Contact HPE support if the problem persists.\n", + "ExternalError_COM": "External error. Retry after some time. Contact HPE support if the problem persists.\n", + "DisabledByCOM": "Disabled by Compute Ops Management.\n", +} + +Cloudconnectstatus = { + # Network Status Config + "Initializing": "Cloud connection setup is in progress. Please wait a moment.", + "IPAddressNotConfigured": "iLO doesn't have an IP address configured. Please check network settings.", + "DNSResolutionError": "Unable to resolve DNS. Please verify your DNS configuration.", + "Configured": "Configured.", + "NotTested": "Not tested yet.", + "InternalError": "iLO ran into an internal issue while trying " + "to connect to the cloud. Please try again or contact support.", + # Web Connectivity + "iLOTimeError": "The iLO time settings are incorrect. Please synchronize time to proceed.", + "ProxyOrFirewallError": "iLO couldn't reach the endpoint. Check your proxy or firewall settings.", + "Connected": "Connected.", + "ProxySettingsInvalid": "Proxy settings appear to be invalid. Please review and update them.", + "ExternalError": "There was an issue with the external cloud service. Please try again later.", + # iLO Configuration for Cloud Connect + "ActivationKeyRequired": "An activation key is needed to enable cloud connectivity. Please provide a valid key.", + "WrongiLOVersion": "This iLO version doesn't support the requested operation. Please update iLO firmware.", + "InvalidActivationKey": "The activation key provided is invalid. Please check and enter a valid one.", + "DeviceAssignFailed": "iLO was unable to register the device with the cloud. Please try again later.", + "DeviceClaimUnauthorized": "Device claim failed due to insufficient authorization. Please check credentials.", + "DeviceNotFound": "The device couldn't be found in the cloud service. Ensure the activation key is correct.", + "DisabledByCOM": "Cloud connectivity has been disabled by COM settings.", +} + +DEFAULT_TEMPLATE_FILE_NAME = "multiconnect_input_template.json" +DNS_SERVER_EXCLUSION_LIST = ["0.0.0.0", "::"] + + +class ComputeOpsManagementCommand: + """Main new command template class""" + + def __init__(self): + self.ident = { + "name": "computeopsmanagement", + "usage": "computeopsmanagement\n\n", + "description": ( + "Run to enable your servers to be discovered, monitored and managed through ComputeOpsManagement\n\t" + "Example:\n\t" + "computeopsmanagement connect\n\t" + "computeopsmanagement connect --activationkey \n\t" + "computeopsmanagement connect --activationkey --proxy http://proxy.abc.com:8080\n\t" + "computeopsmanagement disconnect\n\t" + "computeopsmanagement multiconnect --input_file_json_template\n\t" + "computeopsmanagement multiconnect --input_file servers.json\n\t" + "computeopsmanagement multiconnect --input_file servers.json --allow_ilo_reset\n\t" + "computeopsmanagement multiconnect --input_file servers.json --output report.json\n\t" + "computeopsmanagement multiconnect --input_file servers.json --output report.json --allow_ilo_reset\n\t" + "computeopsmanagement status\n\t" + "computeopsmanagement status -j\n\n" + ), + "summary": ( + "Enables the server to be discovered, monitored and managed " + "through ComputeOpsManagement, Also gives bulk onboarding of iLOs to COM." + ), + "aliases": [], + "auxcommands": ["EthernetCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def resolve_ilo_config(self, config_json_path: str) -> Dict[str, Dict[str, Any]]: + """Load config from a JSON file and resolve iLO configuration as a dict keyed by IP.""" + with open(config_json_path, "r") as f: + config_json = json.load(f) + + common = config_json.get("commonSettings", {}) + common_dns = common.get("network", {}).get("dns", []) + common_ntp = common.get("network", {}).get("ntp", []) + common_proxy = common.get("proxy", {}) + common_auth = { + "username": common.get("iloAuthentication", {}).get("iloUser"), + "password": common.get("iloAuthentication", {}).get("iloPassword"), + } + + resolved = {} + + def build_config( + ip_str: str, + entry: Dict[str, Any], + default_dns, + default_ntp, + skip_dns=False, + skip_ntp=False, + skip_proxy=False, + ): + network = entry.get("network", {}) + dns = [] if skip_dns else network.get("dns", default_dns) + ntp = [] if skip_ntp else network.get("ntp", default_ntp) + return { + "ip": ip_str, + "dns": dns, + "ntp": ntp, + "skipDns": skip_dns, + "skipNtp": skip_ntp, + "skipProxy": skip_proxy, # Consistent naming without underscore + "proxy": common_proxy, + "auth": common_auth, + } + + individual_entries = config_json.get("targets", {}).get("ilos", {}).get("individual", []) + for entry in individual_entries: + ip = entry["ip"] + skip_dns = entry.get("skipDns", False) + skip_ntp = entry.get("skipNtp", False) + skip_proxy = entry.get("skipProxy", False) # Read from input as "skipProxy" (user-facing) + config = build_config(ip, entry, common_dns, common_ntp, skip_dns, skip_ntp, skip_proxy) + resolved[ip] = config + + range_entries = config_json.get("targets", {}).get("ilos", {}).get("ranges", []) + for range_entry in range_entries: + start_ip = ip_address(range_entry["start"]) + end_ip = ip_address(range_entry["end"]) + skip_dns = range_entry.get("skipDns", False) + skip_ntp = range_entry.get("skipNtp", False) + skip_proxy = range_entry.get("skipProxy", False) # Read from input as "skipProxy" (user-facing) + network = range_entry.get("network", {}) + range_dns = [] if skip_dns else network.get("dns", common_dns) + range_ntp = [] if skip_ntp else network.get("ntp", common_ntp) + + # Optimize: Convert to int once, iterate directly, convert back to string once per IP + start_int = int(start_ip) + end_int = int(end_ip) + for ip_int in range(start_int, end_int + 1): + ip_str = str(ip_address(ip_int)) + config = build_config(ip_str, {}, range_dns, range_ntp, skip_dns, skip_ntp, skip_proxy) + resolved[ip_str] = config + + return resolved + + def _get_progress_bar_implementation(self): + """Get tqdm progress bar implementation or fallback to simple progress bar + + :returns: tqdm function or SimplePbar fallback + :rtype: function + """ + # Try to import tqdm, fall back to simple progress if not available + try: + from tqdm import tqdm + return tqdm + except ImportError: + # Create a simple progress bar fallback that displays to stdout + class SimplePbar: + def __init__(self, total, desc="", unit=""): + self.total = total + self.desc = desc + self.unit = unit + self.current = 0 + # Display initial progress bar to stdout + initial_bar = self._get_progress_bar() + print(f"{desc}: 0/{total} {initial_bar}", end="", flush=True) + + def _get_progress_bar(self, width=40): + """Generate visual progress bar with # symbols""" + progress_pct = (self.current / self.total * 100) if self.total > 0 else 0 + filled_length = int(width * self.current // self.total) if self.total > 0 else 0 + bar = "#" * filled_length + "-" * (width - filled_length) + return f"[{bar}] {progress_pct:.1f}%" + + def set_description(self, desc): + self.desc = desc + + def set_postfix(self, **kwargs): + postfix_str = ", ".join([f"{k}={v}" for k, v in kwargs.items()]) + # Update stdout display with visual progress bar + progress_bar = self._get_progress_bar() + status_info = f" [{postfix_str}]" if postfix_str else "" + print( + f"\r{self.desc}: {self.current}/{self.total} " f"{progress_bar}{status_info}", + end="", + flush=True, + ) + + def update(self, n): + self.current += n + # Display visual progress bar + progress_bar = self._get_progress_bar() + print(f"\r{self.desc}: {self.current}/{self.total} {progress_bar}", end="", flush=True) + + def __enter__(self): + return self + + def __exit__(self, *args): + print() # New line after progress bar completion + + def tqdm(total, desc="", unit="", file=None): + return SimplePbar(total, desc, unit) + + return tqdm + + def proxy_config(self, proxy_server): + """Main cloudconnect worker function + + :param proxy_server: proxy + :type proxy_server: str. + """ + if proxy_server != "None": + try: + body = dict() + body["Oem"] = {} + body["Oem"]["Hpe"] = {} + body["Oem"]["Hpe"]["WebProxyConfiguration"] = {} + proxy_body = body["Oem"]["Hpe"]["WebProxyConfiguration"] + proxy_body["ProxyServer"] = None + proxy_body["ProxyUserName"] = None + proxy_body["ProxyPassword"] = None + if "https" in proxy_server: + proxy_body["ProxyPort"] = 443 + else: + proxy_body["ProxyPort"] = 80 + if "@" in proxy_server: + proxy = proxy_server.split("@") + proxy_usr_pass = proxy[0] + proxy_srv_port = proxy[1] + if "//" in proxy_usr_pass: + proxy_usr_pass = proxy_usr_pass.split("//")[1] + if ":" in proxy_srv_port: + proxy = proxy_srv_port.split(":") + proxy_body["ProxyServer"] = proxy[0] + proxy_body["ProxyPort"] = int(proxy[1]) + else: + proxy_body["ProxyServer"] = proxy_srv_port + if ":" in proxy_usr_pass: + proxy = proxy_usr_pass.split(":") + proxy_body["ProxyPassword"] = proxy[1] + proxy_body["ProxyUserName"] = proxy[0] + else: + proxy_body["ProxyUserName"] = proxy_usr_pass + else: + proxy_srv_port = proxy_server + if "//" in proxy_srv_port: + proxy_srv_port = proxy_srv_port.split("//")[1] + if ":" in proxy_srv_port: + proxy = proxy_srv_port.split(":") + proxy_body["ProxyServer"] = proxy[0] + proxy_body["ProxyPort"] = int(proxy[1]) + else: + proxy_body["ProxyServer"] = proxy_srv_port + + path = self.rdmc.app.getidbytype("NetworkProtocol.") + + if path and body: + self.rdmc.ui.printer("Setting Proxy configuration...\n", verbose_override=True) + self.rdmc.app.patch_handler(path[0], body, service=False, silent=True) + except Exception as e: + raise ProxyConfigFailedError(f"Setting Proxy Server Configuration Failed: {str(e)}\n") + else: + try: + body = dict() + body["Oem"] = {} + body["Oem"]["Hpe"] = {} + body["Oem"]["Hpe"]["WebProxyConfiguration"] = {} + proxy_body = body["Oem"]["Hpe"]["WebProxyConfiguration"] + proxy_body["ProxyServer"] = "" + proxy_body["ProxyPort"] = None + proxy_body["ProxyUserName"] = "" + proxy_body["ProxyPassword"] = None + path = self.rdmc.app.getidbytype("NetworkProtocol.") + + if path and body: + self.rdmc.ui.printer("Clearing Proxy configuration...\n", verbose_override=True) + self.rdmc.app.patch_handler(path[0], body, service=False, silent=True) + except Exception as e: + raise ProxyConfigFailedError(f"Clearing Proxy Server Configuration Failed: {str(e)}\n") + + def get_cloud_status(self, need_reason=None): + path = self.rdmc.app.typepath.defs.managerpath + resp = self.rdmc.app.get_handler(path, service=False, silent=True) + if resp.status != 200: + raise SessionExpired("Invalid session. Please logout and log back in or include credentials.") + status = resp.dict["Oem"]["Hpe"]["CloudConnect"]["CloudConnectStatus"] + if need_reason: + reason = resp.dict["Oem"]["Hpe"]["CloudConnect"].get("FailReason", "") + return status, reason + return status + + def connect_cloud(self, activationkey=None): + """cloud connect function + + :param activationkey: activation key + :type activationkey: str. + """ + status = self.get_cloud_status() + if status == "Connected": + self.rdmc.ui.printer("Warning: ComputeOpsManagement is already connected.\n") + return ReturnCodes.SUCCESS + body = dict() + + if activationkey: + body["ActivationKey"] = activationkey + else: + body = {} + path = self.rdmc.app.typepath.defs.managerpath + "Actions" + self.rdmc.app.typepath.defs.oempath + path = path + "/HpeiLO.EnableCloudConnect" + try: + if path: + self.rdmc.ui.printer("Connecting to ComputeOpsManagement...", verbose_override=True) + self.rdmc.app.post_handler(path, body, service=False, silent=True) + except Exception as e: + raise CloudConnectFailedError(f"ComputeOpsManagement connection Failed: {str(e)}\n") + start_time = time.time() + allowed_seconds = 120 + time_increment = 5 + i = 1 + while True: + # time.sleep(time_increment * i) + time.sleep(time_increment) + current_time = time.time() + elapsed_time = current_time - start_time + if elapsed_time > allowed_seconds: + self.rdmc.ui.printer("\n") + status, reason = self.get_cloud_status(need_reason=True) + if status == "NotEnabled": + raise CloudConnectFailedError(ErrorMapping[reason]) + raise CloudConnectTimeoutError( + "ComputeOpsManagement connection timed out, Please check the " + "activation key and network or proxy settings and try again.\n" + ) + else: + status, reason = self.get_cloud_status(need_reason=True) + # self.rdmc.ui.printer("ComputeOpsManagement connection status is %s.\n" % status) + if status == "Connected": + # Check again after 10 seconds before breaking the loop + time.sleep(10) + status, reason = self.get_cloud_status(need_reason=True) + if status == "Connected": + self.rdmc.ui.printer("\n") + self.rdmc.ui.printer("ComputeOpsManagement connection is successful.\n") + break + # If connection has failed while checking waiting for success message + if status == "ConnectionFailed" or status == "NotConnected": + self.rdmc.ui.printer("\n") + if reason: + raise CloudConnectFailedError(ErrorMapping[reason]) + raise CloudConnectFailedError( + "ComputeOpsManagement connection Failed. Please check the " + "activation key and network or proxy settings and try again.\n" + ) + else: + self.rdmc.ui.printer("..") + i = i + 1 + + def disconnect_cloud(self): + """cloud disconnect function""" + cloud_status = self.get_cloud_status() + if cloud_status == "Connected" or cloud_status == "ConnectionFailed": + path = self.rdmc.app.typepath.defs.managerpath + "Actions" + self.rdmc.app.typepath.defs.oempath + path = path + "/HpeiLO.DisableCloudConnect" + body = dict() + try: + if path: + self.rdmc.ui.printer("Disconnecting ComputeOpsManagement...\n", verbose_override=True) + self.rdmc.app.post_handler(path, body) + time.sleep(10) + cloud_status = self.get_cloud_status() + if cloud_status == "NotEnabled": + self.rdmc.ui.printer("The operation completed successfully.\n") + except Exception as e: + raise CloudConnectFailedError(f"ComputeOpsManagement is not disconnected: {str(e)}\n") + else: + self.rdmc.ui.printer( + "Warning: ComputeOpsManagement is not at all connected.\n", + verbose_override=True, + ) + + def cloud_status(self, json=False): + """cloud connect function + + :param json: json + :type json: bool + """ + path = self.rdmc.app.typepath.defs.managerpath + resp = self.rdmc.app.get_handler(path, service=False, silent=True) + if resp.status != 200: + raise SessionExpired("Invalid session. Please logout and log back in or include credentials.") + cloud_info = resp.dict["Oem"]["Hpe"]["CloudConnect"] + output = "------------------------------------------------\n" + output += "ComputeOpsManagement connection status\n" + output += "------------------------------------------------\n" + output += "ComputeOpsManagement Status : %s\n" % (cloud_info["CloudConnectStatus"]) + if cloud_info["CloudConnectStatus"] != "NotEnabled": + if "CloudActivateURL" in cloud_info: + output += "CloudActivateURL : %s\n" % (cloud_info["CloudActivateURL"]) + if "ActivationKey" in cloud_info: + output += "ActivationKey : %s\n" % (cloud_info["ActivationKey"]) + if "ExtendedStatusInfo" in cloud_info: + output += "\n" + output += "Extended Cloud connect status is as follows:\n\n" + output += "Network Configuration : %s\n" % ( + Cloudconnectstatus[cloud_info["ExtendedStatusInfo"]["NetworkConfig"]] + ) + output += "Web Connectivity : %s\n" % ( + Cloudconnectstatus[cloud_info["ExtendedStatusInfo"]["WebConnectivity"]] + ) + output += "iLO Configuration for Cloud Connect : %s\n" % ( + Cloudconnectstatus[cloud_info["ExtendedStatusInfo"]["iLOConfigForCloudConnect"]] + ) + if not json: + self.rdmc.ui.printer(output, verbose_override=True) + else: + self.rdmc.ui.print_out_json(cloud_info) + + def generate_template_file(self, template_input_file=DEFAULT_TEMPLATE_FILE_NAME): + """Generate template JSON input file for multiconnect""" + + template_str = json.dumps(template, indent=4) + + try: + with open(template_input_file, "w") as outfile: + outfile.write(template_str) + self.rdmc.ui.printer(f"Template written to: {template_input_file}\n", verbose_override=True) + except Exception as e: + LOGGER.error(f"Exception writing template file: {e}") + raise InvalidCommandLineError(f"Failed to write template JSON file: {e}\n") + + def bulk_connect_precheck(self, config_file, output_file=None): + """Perform precheck validation for bulk connect operations + + :param config_file: path to JSON configuration file + :type config_file: str + :param output_file: optional output file for report + :type output_file: str or None + :param allow_ilo_reset: allow iLO reset during operations + :type allow_ilo_reset: bool + """ + # Suppress urllib3 warnings (connection retry warnings, InsecureRequestWarning, etc.) + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + urllib3.disable_warnings(urllib3.exceptions.ConnectionError) + urllib3.disable_warnings() + + current_time = datetime.now() + LOGGER.info(f"Starting bulk_connect precheck: config={config_file}, output={output_file}") + + # Load and validate configuration (do once, not in loop) + try: + config = self._validate_input_file(config_file) + except (Exception) as e: + raise e + + # Generate default output filename if not provided + if not output_file: + output_file = f"onboard_precheck_{current_time.strftime('%m%d%Y_%H%M%S')}.json" + + LOGGER.info(f"Report will be saved to: {output_file}") + + # Initialize report with new format + report = { + "iloPrecheck": [], + "resultCount": {"ilos": "0", "preCheckPassed": "0", "preCheckFailed": "0"} + } + + # Counters for result tracking + counters = {"total": 0, "preCheckPassed": 0, "preCheckFailed": 0} + + # Progressive update for pbar implementation + # This gets updated in every iteration of the for loop below + pbar_postfix = {} + + # Extract common settings + common_settings = config.get("commonSettings", {}) + ilo_auth = ( + common_settings.get("iloAuthentication", {}) + if isinstance(common_settings.get("iloAuthentication"), dict) + else {} + ) + com_settings = ( + common_settings.get("computeOpsManagement", {}) + if isinstance(common_settings.get("computeOpsManagement"), dict) + else {} + ) + network_settings = ( + common_settings.get("network", {}) if isinstance(common_settings.get("network"), dict) else {} + ) + proxy_settings = common_settings.get("proxy", {}) if isinstance(common_settings.get("proxy"), dict) else {} + + # Get target IPs + try: + target_ips = self._get_all_target_ips(config.get("targets", {})) + counters["total_targets"] = len(target_ips) + LOGGER.info(f"Precheck for {len(target_ips)} target iLO(s)") + except Exception as e: + error_msg = f"Error processing target IPs: {str(e)}" + LOGGER.error(error_msg) + raise InvalidCommandLineError(error_msg) + + # Get progress bar implementation + tqdm = self._get_progress_bar_implementation() + + # Process each target IP with progress bar for precheck validation + with tqdm(total=len(target_ips), desc="Validating iLOs", unit="iLO") as pbar: + for ip_info in target_ips: + ip = ip_info["ip"] + individual_settings = ip_info.get("settings", {}) if isinstance(ip_info.get("settings"), dict) else {} + + pbar.set_description(f"Validating {ip}") + LOGGER.info(f"Starting precheck validation for iLO: {ip}") + + # Initialize precheck result + precheck_result = { + "ip": ip, + "managementProcessorModel": "", + "iloVersion": "", + "preCheckResult": "FAILED", + "details": "", + "recommendation": "", + } + + # Initialize status tracking variables + postfix_type = "FAILED" + + # Validate single iLO + validation_result = self._validate_single_ilo_for_precheck( + ip, ilo_auth, com_settings, network_settings, individual_settings + ) + + status = validation_result["status"] + error = validation_result["error"] + recommendation = validation_result["recommendation"] + precheck_result["managementProcessorModel"] = validation_result["managementProcessorModel"] + precheck_result["iloVersion"] = validation_result["iloVersion"] + + # Set postfix type for progress bar + if error == "Authentication or connectivity failed with iLO": + postfix_type = "ERROR" + + # Update precheck result with final values + precheck_result["preCheckResult"] = status + precheck_result["details"] = error + precheck_result["recommendation"] = recommendation + + # Update counters and progress bar + if status == "PASSED": + counters["preCheckPassed"] += 1 + pbar_postfix["status"] = "PASSED" + pbar_postfix["preCheckPassed"] = counters["preCheckPassed"] + pbar.set_postfix(**pbar_postfix) + else: + counters["preCheckFailed"] += 1 + pbar_postfix["status"] = postfix_type + pbar_postfix["preCheckFailed"] = counters["preCheckFailed"] + pbar.set_postfix(**pbar_postfix) + + # Add to report + report["iloPrecheck"].append(precheck_result) + counters["total"] += 1 + pbar.update(1) + + # Finalize report + report["resultCount"] = { + "ilos": str(counters["total"]), + "preCheckPassed": str(counters["preCheckPassed"]), + "preCheckFailed": str(counters["preCheckFailed"]), + } + + # Write report to JSON file + try: + self._save_json_report(report, output_file) + LOGGER.info(f"Precheck report saved to: {output_file}") + self.rdmc.ui.printer(f"Precheck completed. Report saved to: {output_file}\n", verbose_override=True) + self.rdmc.ui.printer(f"Precheck passed for {counters['preCheckPassed']} iLO(s).\n", verbose_override=True) + self.rdmc.ui.printer(f"Precheck failed for {counters['preCheckFailed']} iLO(s).\n", verbose_override=True) + except Exception as e: + LOGGER.error(f"Failed to save precheck report: {str(e)}") + raise InvalidCommandLineError(f"Failed to save precheck report: {str(e)}") + + return ReturnCodes.SUCCESS + + def _get_ilo_credentials(self, ilo_auth, individual_settings): + """Extract iLO credentials, prioritizing individual settings over common settings + + :param ilo_auth: Common iLO authentication settings + :type ilo_auth: dict + :param individual_settings: Individual iLO settings that may contain authentication + :type individual_settings: dict + :returns: tuple of (username, password) + :rtype: tuple + """ + # Check if individual settings contain iLO authentication + individual_auth = individual_settings.get("iloAuthentication", {}) + if isinstance(individual_auth, dict) and individual_auth.get("iloUser") and individual_auth.get("iloPassword"): + username = individual_auth.get("iloUser") + password = individual_auth.get("iloPassword") + return username, password + + # Fall back to common authentication settings + username = ilo_auth.get("iloUser", "administrator") + password = ilo_auth.get("iloPassword", "") + return username, password + + def _validate_single_ilo_for_precheck(self, ip, ilo_auth, com_settings, network_settings, individual_settings): + """Validate a single iLO for precheck operations + + :param ip: iLO IP address + :type ip: str + :param ilo_auth: iLO authentication settings + :type ilo_auth: dict + :param com_settings: COM settings + :type com_settings: dict + :param network_settings: network settings + :type network_settings: dict + :param proxy_settings: proxy settings + :type proxy_settings: dict + :param individual_settings: individual iLO settings + :type individual_settings: dict + :returns: dict with validation results + :rtype: dict + """ + # Initialize results + validation_data = { + "status": "FAILED", + "error": "", + "recommendation": "", + "managementProcessorModel": "", + "iloVersion": "", + } + + try: + # Perform basic connectivity and version checks + base_url = f"https://{ip}" + username, password = self._get_ilo_credentials(ilo_auth, individual_settings) + + if not password: + validation_data["error"] = "No password provided" + validation_data["recommendation"] = "Provide valid iLO credentials" + return validation_data + # Basic connectivity test + from requests.auth import HTTPBasicAuth + + response = requests.get( + f"{base_url}/redfish/v1/", + auth=HTTPBasicAuth(username, password), + verify=False, + timeout=30 + ) + + if response.status_code == 200: + # Get iLO version information + model, ilo_version = self._get_ilo_version_from_response(response.json(), base_url, username, password) + + # Set model and version info + validation_data["managementProcessorModel"] = f"iLO{model}" if model else "" + validation_data["iloVersion"] = str(ilo_version) if ilo_version else "" + + # Check version compatibility + is_compatible, err_msg, rec_msg = self._check_ilo_version_compatibility(model, ilo_version, ip) + if is_compatible: + # Check if iLO is already connected to COM + # Create HTTP session + session = requests.Session() + session.auth = HTTPBasicAuth(username, password) + session.verify = False + + is_connected = self._check_com_connection_status(session, base_url) + # if iLO is connected, set validation_data results as PASSED + if is_connected: + validation_data["status"] = "PASSED" + else: + # Check activationKey/workspace_id based on iLO version capabilities + is_pin_supported = self._is_pin_onboarding_supported(ilo_version, model) + + if not is_pin_supported: + workspace_id = com_settings.get("workspace_id", "") + if not workspace_id: + validation_data["error"] = "The version of iLO requires a valid workspace_id" + validation_data["recommendation"] = ( + "Provide a valid workspace_id in the configuration for onboarding" + ) + return validation_data + else: + activation_key = com_settings.get("activationKey", "") + if not activation_key: + validation_data["error"] = "The version of iLO requires a valid activation Key" + validation_data["recommendation"] = ( + "Provide a valid activation Key in the configuration for onboarding" + ) + return validation_data + + merged_network = network_settings.copy() if isinstance(network_settings, dict) else {} + individual_network = individual_settings.get("network", {}) + if individual_network and isinstance(individual_network, dict): + merged_network.update(individual_network) + + session = self._create_session_with_retries(username, password) + + dns_precheck_result, validation_data = self._precheck_validate_dns_settings( + session, + base_url, + merged_network, + individual_settings, + validation_data, + ) + if not dns_precheck_result: + return validation_data + + ntp_precheck_result, validation_data = self._precheck_validate_ntp_settings( + session, + base_url, + merged_network, + individual_settings, + validation_data, + ) + if not ntp_precheck_result: + return validation_data + + # All checks have passed, mark as ready for onboarding + validation_data["status"] = "PASSED" + else: + validation_data["error"] = err_msg + validation_data["recommendation"] = rec_msg + else: + validation_data["error"] = f"HTTP {response.status_code} - Authentication or connectivity failed" + validation_data["recommendation"] = "Verify iLO credentials and network connectivity" + + except Exception as e: + validation_data["error"] = "Authentication or connectivity failed with iLO" + validation_data["recommendation"] = "Review the input file for credentials and re-run the command" + + return validation_data + + def _precheck_validate_dns_settings(self, session, base_url, merged_network, individual_settings, validation_data): + """Validate a DNS setting in iLO for precheck + + :param session: session object to iLO + :type session: object + :param base_url: base URL for iLO REST API + :type base_url: str + :param merged_network: merged network settings + :type merged_network: dict + :param individual_settings: individual settings + :type individual_settings: dict + :param validation_data: validation data dictionary + :type validation_data: dict + :returns: tuple of (DNS precheck status, updated validation data) + :rtype: tuple + """ + dns_precheck_status = False + current_dns_servers, dhcp_dns_enabled, dhcp_check_attempted = self._get_current_dns_servers(session, base_url) + if not dhcp_check_attempted: + LOGGER.error("Unable to retrieve DNS settings from iLO during precheck") + validation_data["error"] = "Unable to retrieve DNS settings from iLO" + validation_data["recommendation"] = "Check iLO connectivity and retry the operation" + return dns_precheck_status, validation_data + + if individual_settings.get("skipDns", False): + if not current_dns_servers and not dhcp_dns_enabled: + if merged_network.get("dns", []): + LOGGER.info("skipDNS flag is set, no DNS configured in iLO") + validation_data["recommendation"] = ( + "Remove skipDns or Configure DNS in iLO before attempting" + " to Onboard to HPE Compute Ops Management" + ) + else: + LOGGER.info("skipDNS flag is set, no DNS settings in input. Also no DNS configured in iLO") + validation_data["recommendation"] = ( + "Configure DNS in iLO before attempting to Onboard to HPE Compute Ops Management" + ) + validation_data["error"] = "No DNS configuration found in iLO" + return dns_precheck_status, validation_data + elif not merged_network.get("dns", []): + if not current_dns_servers and not dhcp_dns_enabled: + LOGGER.info("No DNS settings in input and no DNS configured in iLO") + validation_data["error"] = "No DNS configuration found in iLO" + validation_data["recommendation"] = ( + "Configure DNS in iLO before attempting to Onboard to HPE Compute Ops Management" + ) + return dns_precheck_status, validation_data + + dns_precheck_status = True + LOGGER.info("DNS precheck passed") + return dns_precheck_status, validation_data + + def _precheck_validate_ntp_settings(self, session, base_url, merged_network, individual_settings, validation_data): + """Validate NTP settings in iLO for precheck + + :param session: session object to iLO + :type session: object + :param base_url: base URL for iLO REST API + :type base_url: str + :param merged_network: merged network settings + :type merged_network: dict + :param individual_settings: individual settings + :type individual_settings: dict + :param validation_data: validation data dictionary + :type validation_data: dict + :returns: tuple of (NTP precheck status, updated validation data) + :rtype: tuple + """ + ntp_precheck_status = True + eth_interfaces_url = f"{base_url}/redfish/v1/Managers/1/EthernetInterfaces" + dhcp_ntp_is_enabled, _, dhcp_check_attempted = self._check_dhcp_ntp_config_on_interfaces( + session, + base_url, + eth_interfaces_url, + ) + ntp_servers = self._get_current_ntp_servers(session, base_url) + if not dhcp_check_attempted or ntp_servers is None: + LOGGER.error("Unable to retrieve NTP settings from iLO during precheck") + ntp_precheck_status = False + validation_data["error"] = "Unable to retrieve NTP settings from iLO" + validation_data["recommendation"] = "Check iLO connectivity and retry the operation" + return ntp_precheck_status, validation_data + + if individual_settings.get("skipNtp", False): + if not dhcp_ntp_is_enabled and not ntp_servers: + if merged_network.get("ntp", []): + LOGGER.info("skipNTP flag is set and no NTP configured in iLO") + validation_data["recommendation"] = ( + "Remove skipNtp or Configure NTP in iLO before attempting" + " to Onboard to HPE Compute Ops Management" + ) + else: + LOGGER.info("skipNTP flag is set, no NTP settings in input. Also no NTP configured in iLO") + validation_data["recommendation"] = ( + "Configure NTP in iLO before attempting to Onboard to HPE Compute Ops Management" + ) + validation_data["error"] = "No NTP configuration found in iLO" + return ntp_precheck_status, validation_data + elif not merged_network.get("ntp", []): + if not dhcp_ntp_is_enabled and not ntp_servers: + LOGGER.info("No NTP settings in input and no NTP configured in iLO") + validation_data["error"] = "No NTP configuration found in iLO" + validation_data["recommendation"] = ( + "Configure NTP in iLO before attempting to Onboard to HPE Compute Ops Management" + ) + return ntp_precheck_status, validation_data + + LOGGER.info("NTP precheck passed") + return ntp_precheck_status, validation_data + + def _validate_bulk_connect_input(self, config): + """ + Validate the input JSON from file for below: + - key named "commonSettings" must be present + - commonSettings must have computeOpsManagement key with atleast either of activationKey or workspace_id + - commonSettings must have iloAuthentication key that contains iloUser and iloPassword attributes. + - key named "targets" must be present + - key named "ilos" must be present + - atleast one iLO ip should be present either in individual or ranges section. + """ + if not isinstance(config, dict): + raise InvalidCommandLineError("Invalid configuration format: expected a JSON object at the top level") + + if "commonSettings" not in config: + raise InvalidCommandLineError("Missing 'commonSettings' section in configuration") + + common_settings = config["commonSettings"] + if not isinstance(common_settings, dict): + raise InvalidCommandLineError("'commonSettings' must be a JSON object") + + com_settings = common_settings.get("computeOpsManagement", {}) + if not isinstance(com_settings, dict): + raise InvalidCommandLineError("'computeOpsManagement' in 'commonSettings' must be a JSON object") + + activation_key = com_settings.get("activationKey") + workspace_id = com_settings.get("workspace_id") + if not activation_key and not workspace_id: + raise InvalidCommandLineError( + "In 'commonSettings.computeOpsManagement', either 'activationKey' or 'workspace_id' must be provided" + ) + + if "targets" not in config: + raise InvalidCommandLineError("Missing 'targets' section in configuration") + + targets = config["targets"] + if not isinstance(targets, dict): + raise InvalidCommandLineError("'targets' must be a JSON object") + + if "ilos" not in targets: + raise InvalidCommandLineError("Missing 'ilos' section in 'targets'") + + ilos = targets["ilos"] + if not isinstance(ilos, dict): + raise InvalidCommandLineError("'ilos' in 'targets' must be a JSON object") + + individual_ilos = ilos.get("individual", []) + ranges = ilos.get("ranges", []) + + if not isinstance(individual_ilos, list): + raise InvalidCommandLineError("'individual' in 'ilos' must be an array") + + # Check if common iLO authentication is provided + common_auth_provided = False + if "iloAuthentication" in common_settings: + ilo_auth = common_settings["iloAuthentication"] + if isinstance(ilo_auth, dict) and ilo_auth.get("iloUser") and ilo_auth.get("iloPassword"): + common_auth_provided = True + + # If common authentication is not provided, verify all targets have individual authentication + if not common_auth_provided: + # Check that all individual iLOs have authentication + for ilo in individual_ilos: + individual_auth = ilo.get("iloAuthentication", {}) + if not (isinstance(individual_auth, dict) and + individual_auth.get("iloUser") and + individual_auth.get("iloPassword")): + raise InvalidCommandLineError( + "The operation failed as iLO credential is not provided in the input json" + ) + + # Check that all ranges have authentication + for range_config in ranges: + range_auth = range_config.get("iloAuthentication", {}) + if not (isinstance(range_auth, dict) and + range_auth.get("iloUser") and + range_auth.get("iloPassword")): + raise InvalidCommandLineError( + "The operation failed as iLO credential is not provided in the input json" + ) + else: + # Common authentication is provided, ensure it's properly formatted + ilo_auth = common_settings["iloAuthentication"] + if not isinstance(ilo_auth, dict): + raise InvalidCommandLineError("'iloAuthentication' in 'commonSettings' must be a JSON object") + + if "iloUser" not in ilo_auth or "iloPassword" not in ilo_auth: + raise InvalidCommandLineError("Missing 'iloUser' or 'iloPassword' in 'iloAuthentication'") + + if not isinstance(ranges, list): + raise InvalidCommandLineError("'ranges' in 'ilos' must be an array") + + total_individual_ilos = len(individual_ilos) + total_range_objects = len(ranges) + for ilo in individual_ilos: + if not isinstance(ilo, dict) or "ip" not in ilo: + raise InvalidCommandLineError("Each entry in 'individual' must be an object with an 'ip' field") + + for range_config in ranges: + if not isinstance(range_config, dict) or "start" not in range_config or "end" not in range_config: + raise InvalidCommandLineError("Each entry in 'ranges' must be an object with 'start' and 'end' fields") + + if total_individual_ilos == 0 and total_range_objects == 0: + raise InvalidCommandLineError("At least one iLO IP must be specified in 'individual' or 'ranges'") + + def _validate_input_file(self, config_file) -> Dict[str, Any]: + # Load and validate configuration (do once, not in loop) + try: + if not os.path.exists(config_file): + error_msg = f"Configuration file not found: {config_file}" + LOGGER.error(error_msg) + raise InvalidCommandLineError(error_msg) + + with open(config_file, "r") as f: + config = json.load(f) + LOGGER.info(f"Loaded configuration from: {config_file}") + + # Validate configuration + self._validate_bulk_connect_input(config) + LOGGER.info("Configuration validation passed") + return config + except (json.JSONDecodeError, InvalidCommandLineError) as e: + error_msg = f"Error in configuration file '{config_file}': {str(e)}" + LOGGER.error(error_msg) + raise InvalidCommandLineError(error_msg) + + def bulk_connect(self, config_file, output_file=None, allow_ilo_reset=False): + """Perform bulk connect operations for multiple iLOs + + :param config_file: path to JSON configuration file + :type config_file: str + :param output_file: optional output file for report + :type output_file: str or None + :param allow_ilo_reset: allow iLO reset during operations + :type allow_ilo_reset: bool + """ + + # Suppress urllib3 warnings (connection retry warnings, InsecureRequestWarning, etc.) + urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) + urllib3.disable_warnings(urllib3.exceptions.ConnectionError) + urllib3.disable_warnings() + + # Get progress bar implementation + tqdm = self._get_progress_bar_implementation() + + # Setup logging + current_time = datetime.now() + LOGGER.info(f"Starting bulk_connect: config={config_file}, output={output_file}, allow_reset={allow_ilo_reset}") + + # Load and validate configuration (do once, not in loop) + try: + config = self._validate_input_file(config_file) + except (Exception) as e: + raise e + + # Generate default output filename if not provided + if not output_file: + output_file = f"onboard_{current_time.strftime('%m%d%Y_%H%M%S')}.json" + + LOGGER.info(f"Report will be saved to: {output_file}") + + # Initialize report with new format + report = { + "iloOnboard": [], + "resultCount": {"ilos": "0", "connected": "0", "failed": "0", "notAttempted": "0", "pendingiloReset": "0"}, + } + + # Counters for result tracking + counters = {"total": 0, "connected": 0, "failed": 0, "notAttempted": 0, "pendingiloReset": 0} + + # Progressive update for pbar implementation + # This gets updated in every iteration of the for loop below + pbar_postfix = {} + + # Extract common settings once (not per iLO) + common_settings = config.get("commonSettings", {}) + ilo_auth = ( + common_settings.get("iloAuthentication", {}) + if isinstance(common_settings.get("iloAuthentication"), dict) + else {} + ) + com_settings = ( + common_settings.get("computeOpsManagement", {}) + if isinstance(common_settings.get("computeOpsManagement"), dict) + else {} + ) + network_settings = ( + common_settings.get("network", {}) if isinstance(common_settings.get("network"), dict) else {} + ) + proxy_settings = common_settings.get("proxy", {}) if isinstance(common_settings.get("proxy"), dict) else {} + + # Get target IPs + try: + target_ips = self._get_all_target_ips(config.get("targets", {})) + counters["total_targets"] = len(target_ips) + LOGGER.info(f"Processing {len(target_ips)} target iLO(s)") + except Exception as e: + error_msg = f"Error processing target IPs: {str(e)}" + LOGGER.error(error_msg) + raise InvalidCommandLineError(error_msg) + + # Process each target IP sequentially with progress bar + with tqdm(total=len(target_ips), desc="Connecting iLOs", unit="iLO") as pbar: + for ip_info in target_ips: + ip = ip_info["ip"] + individual_settings = ip_info.get("settings", {}) if isinstance(ip_info.get("settings"), dict) else {} + + pbar.set_description(f"Processing {ip}") + LOGGER.info(f"Starting connection for iLO: {ip}") + + result = { + "ip": ip, + "status": "pending", + "message": "", + "timestamp": datetime.now().isoformat(), + "details": {}, + } + + # add precheck validation here + validation_result = self._validate_single_ilo_for_precheck( + ip, ilo_auth, com_settings, network_settings, individual_settings + ) + + if validation_result["status"] == "PASSED": + try: + # Connect to individual iLO and perform COM connection + success, ilo_was_reset, needs_manual_reset, ilo_result = self._connect_single_ilo_to_com( + ip=ip, + ilo_auth=ilo_auth, + com_settings=com_settings, + network_settings=network_settings, + proxy_settings=proxy_settings, + individual_settings=individual_settings, + allow_ilo_reset=allow_ilo_reset, + validation_result=validation_result, + ) + except Exception as e: + # Handle unexpected exceptions + report["iloOnboard"].append( + { + "ip": ip, + "managementProcessorModel": "", + "iloVersion": "", + "onboardStatus": "not attempted", + "onboardError": f"{str(e)}", + "recommendation": "Review error details and verify iLO configuration", + } + ) + counters["total"] += 1 + counters["failed"] += 1 + LOGGER.error(f"ERROR {ip}: Error - {str(e)}") + pbar_postfix["status"] = "ERROR" + pbar_postfix["failed"] = counters["failed"] + pbar.set_postfix(**pbar_postfix) + else: + # Add to report + report["iloOnboard"].append( + { + "ip": ilo_result["ip"], + "managementProcessorModel": ilo_result["managementProcessorModel"], + "iloVersion": ilo_result["iloVersion"], + "onboardStatus": ilo_result["onboardStatus"], + "onboardError": ilo_result["onboardError"], + "recommendation": ilo_result["recommendation"], + "preCheckResult": validation_result["status"], + "preCheckError": validation_result["error"], + } + ) + + # Update counters + counters["total"] += 1 + if ilo_result["onboardStatus"] == "connected": + counters["connected"] += 1 + + if success: + result["status"] = "success" + result["message"] = "Successfully connected to COM" + result["details"] = { + "authentication": "successful", + "com_connection": "established", + "activation_method": "pin" if com_settings.get("activationKey") else "workspace_id", + "ilo_was_reset": ilo_was_reset, + } + LOGGER.info(f"SUCCESS {ip}: Connected successfully") + pbar_postfix["status"] = "SUCCESS" + pbar_postfix["connected"] = counters["connected"] + pbar.set_postfix(**pbar_postfix) + elif ilo_result["onboardStatus"] == "not attempted": + counters["notAttempted"] += 1 + LOGGER.warning(f"NOT ATTEMPTED {ip}: {ilo_result['onboardError']}") + pbar_postfix["status"] = "NOT ATTEMPTED" + pbar_postfix["notAttempted"] = counters["notAttempted"] + pbar.set_postfix(**pbar_postfix) + elif ( + "reset" in ilo_result.get("onboardError", "").lower() + or "waiting" in ilo_result.get("onboardError", "").lower() + ): + counters["pendingiloReset"] += 1 + LOGGER.warning(f"PENDING RESET {ip}") + pbar_postfix["status"] = "PENDING RESET" + pbar_postfix["pendingiloReset"] = counters["pendingiloReset"] + pbar.set_postfix(**pbar_postfix) + else: + counters["failed"] += 1 + LOGGER.warning(f"FAILED {ip}: {ilo_result['onboardError']}") + pbar_postfix["status"] = "FAILED" + pbar_postfix["failed"] = counters["failed"] + pbar.set_postfix(**pbar_postfix) + result["status"] = "failed" + if needs_manual_reset: + result["message"] = "NTP configured but requires manual iLO reset" + result["details"] = { + "error": "NTP configured but manual reset required", + "ilo_was_reset": ilo_was_reset, + "needs_manual_reset": True, + } + else: + result["message"] = "Failed to connect to COM" + result["details"] = { + "error": "Connection failed during COM setup", + "ilo_was_reset": ilo_was_reset, + } + else: + # Precheck failed, do not attempt connection + report["iloOnboard"].append( + { + "ip": ip, + "managementProcessorModel": validation_result["managementProcessorModel"], + "iloVersion": validation_result["iloVersion"], + "onboardStatus": "not attempted", + "onboardError": "", + "recommendation": validation_result["recommendation"], + "preCheckResult": validation_result["status"], + "preCheckError": validation_result["error"], + } + ) + counters["total"] += 1 + counters["notAttempted"] += 1 + LOGGER.warning(f"NOT ATTEMPTED {ip}: Precheck failed - {validation_result['error']}") + pbar_postfix["status"] = "NOT ATTEMPTED" + pbar_postfix["notAttempted"] = counters["notAttempted"] + pbar.set_postfix(**pbar_postfix) + pbar.update(1) + + # Finalize report + report["resultCount"] = { + "ilos": str(counters["total"]), + "connected": str(counters["connected"]), + "failed": str(counters["failed"]), + "notAttempted": str(counters["notAttempted"]), + "pendingiloReset": str(counters["pendingiloReset"]), + } + + end_time = datetime.now() + duration_seconds = (end_time - current_time).total_seconds() + LOGGER.info( + f"Bulk connect completed in {duration_seconds:.2f}s: " + f"{counters['connected']} connected, {counters['failed']} failed, " + f"{counters['notAttempted']} not attempted, {counters['pendingiloReset']} pending reset" + ) + + # Write report to JSON file + try: + self._save_json_report(report, output_file) + except Exception as e: + LOGGER.error(f"Failed to save report: {str(e)}") + print(f"ERROR: Could not save report to {output_file}: {str(e)}") + + # Display summary both to stdout and log file + print(f"ComputeOpsManagement connection successful for {counters['connected']} server(s).") + print(f"ComputeOpsManagement connection failed for {counters['failed']} server(s).") + print(f"The operation completed. Details available in the output {output_file} file") + + LOGGER.info(f"Report saved to: {output_file}") + return ReturnCodes.SUCCESS + + def _get_all_target_ips(self, targets): + """Extract all target IPs from the configuration + + :param targets: targets section from config + :type targets: dict + :returns: list of IP information dictionaries + :rtype: list + """ + import ipaddress + + target_ips = [] + ilos = targets.get("ilos", {}) + + # Process individual IPs + individual_ilos = ilos.get("individual", []) + for ilo in individual_ilos: + ip_info = {"ip": ilo["ip"], "settings": ilo} + target_ips.append(ip_info) + + # Process IP ranges + ranges = ilos.get("ranges", []) + for range_config in ranges: + start_ip = ipaddress.IPv4Address(range_config["start"]) + end_ip = ipaddress.IPv4Address(range_config["end"]) + + # Validate range size to prevent memory issues + range_size = int(end_ip) - int(start_ip) + 1 + if range_size > 1000: + raise InvalidCommandLineError( + f"IP range {range_config['start']} to {range_config['end']} contains {range_size} IPs. " + f"Maximum allowed range size is 1000 IPs to prevent memory issues." + ) + + current_ip = start_ip + while current_ip <= end_ip: + ip_info = {"ip": str(current_ip), "settings": range_config} + target_ips.append(ip_info) + current_ip += 1 + + return target_ips + + def _get_all_target_ips_optimized(self, targets): + """Optimized generator to extract all target IPs from the configuration + + :param targets: targets section from config + :type targets: dict + :yields: IP information dictionaries + :rtype: iterator + """ + import ipaddress + + ilos = targets.get("ilos", {}) + + # Process individual IPs + individual_ilos = ilos.get("individual", []) + for ilo in individual_ilos: + yield {"ip": ilo["ip"], "settings": ilo} + + # Process IP ranges + ranges = ilos.get("ranges", []) + for range_config in ranges: + start_ip = ipaddress.IPv4Address(range_config["start"]) + end_ip = ipaddress.IPv4Address(range_config["end"]) + + # Validate range size to prevent memory issues + range_size = int(end_ip) - int(start_ip) + 1 + if range_size > 1000: + raise InvalidCommandLineError( + f"IP range {range_config['start']} to {range_config['end']} contains {range_size} IPs. " + f"Maximum allowed range size is 1000 IPs to prevent memory issues." + ) + + current_ip = start_ip + while current_ip <= end_ip: + yield {"ip": str(current_ip), "settings": range_config} + current_ip += 1 + + def _connect_single_ilo_to_com( + self, + ip, + ilo_auth, + com_settings, + network_settings, + proxy_settings, + individual_settings, + allow_ilo_reset=False, + validation_result=None, + ): + """Connect a single iLO to COM using HTTP-only connection + + :returns: tuple (success, ilo_was_reset, needs_manual_reset, result_dict) + :rtype: tuple + """ + import requests + from requests.auth import HTTPBasicAuth + + # Initialize return structure + result = { + "ip": ip, + "managementProcessorModel": "", + "iloVersion": "", + "onboardStatus": "not attempted", + "onboardError": "", + "recommendation": "", + } + + ilo_was_reset = False + + try: + # Create base URL and validate credentials + base_url = f"https://{ip}" + username, password = self._get_ilo_credentials(ilo_auth, individual_settings) + + if not password: + result["onboardError"] = "No password provided" + result["recommendation"] = "Provide valid iLO credentials" + LOGGER.warning(f"{ip}: No password provided") + return False, ilo_was_reset, False, result + + # Test connection with simple HTTP GET using requests + response = requests.get( + f"{base_url}/redfish/v1/", auth=HTTPBasicAuth(username, password), verify=False, timeout=30 + ) + + if response.status_code != 200: + result["onboardError"] = f"HTTP {response.status_code} - Authentication failed" + result["recommendation"] = "Verify iLO credentials and network connectivity" + LOGGER.error(f"{ip}: Authentication failed - HTTP {response.status_code}") + return False, ilo_was_reset, False, result + + LOGGER.info(f"{ip}: Authenticated successfully") + + # Get iLO version information + model = "" + model_version = "" + ilo_version = "" + if validation_result: + model = validation_result.get("managementProcessorModel", "iLO0") + model_version = int((model.strip()).split("iLO")[-1]) + ilo_version = float(validation_result.get("iloVersion", "0.0")) + else: + model_version, ilo_version = self._get_ilo_version_from_response( + response.json(), + base_url, + username, + password + ) + model = f"iLO{str(model_version)}" if model_version else "" + + result["managementProcessorModel"] = model + result["iloVersion"] = ilo_version + + session = self._create_session_with_retries(username, password) + + # Configure proxy if needed - stop and move to next iLO on failure + try: + self._configure_proxy_for_ilo_requests(session, base_url, proxy_settings, individual_settings) + except Exception as proxy_err: + result["onboardStatus"] = "not attempted" + result["onboardError"] = f"Proxy configuration failed: {str(proxy_err)}" + result["recommendation"] = "Verify proxy settings and network connectivity, then retry" + LOGGER.error(f"{ip}: Proxy configuration failed - {str(proxy_err)}") + return False, ilo_was_reset, False, result + + # Configure network settings if needed and capture reset status - stop and move to next iLO on failure + try: + ilo_was_reset, ntp_configured_needs_reset = self._configure_network_settings_requests( + session, base_url, network_settings, individual_settings, allow_ilo_reset + ) + except Exception as network_err: + result["onboardStatus"] = "not attempted" + result["onboardError"] = f"Network configuration failed: {str(network_err)}" + result["recommendation"] = "Verify DNS/NTP settings and network connectivity, then retry" + LOGGER.error(f"{ip}: Network configuration failed - {str(network_err)}") + return False, ilo_was_reset, False, result + + # If NTP was configured but reset is not allowed, stop onboarding + if ntp_configured_needs_reset: + result["onboardStatus"] = "pending reset" + result["onboardError"] = "NTP configuration requires manual iLO reset" + result["recommendation"] = ( + "Manually reset the iLO for NTP settings to take effect, " + "then re-run the onboarding command. " + "Alternatively, use --allow_ilo_reset flag to automatically reset iLO." + ) + LOGGER.warning(f"{ip}: NTP configured but requires manual iLO reset") + return False, ilo_was_reset, True, result + + # If iLO was reset and reset is allowed, wait for it to come back online + if ilo_was_reset and allow_ilo_reset: + LOGGER.info(f"{ip}: iLO was reset, waiting for it to come back online...") + if self._wait_for_ilo_after_reset(ip, username, password): + LOGGER.info(f"{ip}: iLO is back online") + # Create new session after reset + session = requests.Session() + session.auth = HTTPBasicAuth(username, password) + session.verify = False + else: + LOGGER.error(f"{ip}: iLO did not come back online within timeout") + return False, ilo_was_reset, False, result + elif ilo_was_reset and not allow_ilo_reset: + LOGGER.warning(f"{ip}: iLO was reset but --allow_ilo_reset not specified") + return False, ilo_was_reset, True, result + + # Determine if PIN-based onboarding is supported + workspace_id = com_settings.get("workspace_id") + activation_key = com_settings.get("activationKey") + pin_supported = self._is_pin_onboarding_supported(ilo_version, model_version) + + credential = activation_key if pin_supported and activation_key else workspace_id + if credential: + success, fail_reason = self._enable_com_connection_requests(session, base_url, credential) + if success: + result["onboardStatus"] = "connected" + LOGGER.info(f"{ip}: Successfully connected to COM") + if not ilo_was_reset and "Configure NTP in iLO" in validation_result["recommendation"]: + result["recommendation"] = ( + "Though iLO is onboarded to HPE Compute Ops Management, " + "it is recommended to configure NTP in iLO" + ) + return True, ilo_was_reset, False, result + else: + result["onboardStatus"] = "not connected" + if fail_reason == "InvalidActivationKey": + result["onboardError"] = "The activation key entered is invalid" + result["recommendation"] = ( + "Please check the activation key value. " "If the problem persists, contact HPE Support" + ) + elif fail_reason == "ExpiredActivationKey": + result["onboardError"] = "The activation key entered is expired" + result["recommendation"] = ( + "Please enter a valid activation key. " "If the problem persists, contact HPE Support" + ) + elif fail_reason == "ActivationKeyRequired": + result["onboardError"] = "An activation key is required" + result["recommendation"] = "Please provide a valid activation key" + else: + result["onboardError"] = ( + f"Failed to enable COM connection: {fail_reason}" + if fail_reason + else "Failed to enable COM connection" + ) + result["recommendation"] = "Verify activation key/workspace_id and network settings, then retry" + LOGGER.warning(f"{ip}: Failed to enable COM - {result['onboardError']}") + return False, ilo_was_reset, False, result + + except requests.exceptions.ConnectionError: + result["onboardError"] = "iLO is not reachable" + result["recommendation"] = "Verify network connectivity to iLO" + LOGGER.error(f"{ip}: Connection error - iLO not reachable") + return False, ilo_was_reset, False, result + except requests.exceptions.Timeout: + result["onboardError"] = "Connection timeout" + result["recommendation"] = "Verify network connectivity and iLO responsiveness" + LOGGER.error(f"{ip}: Connection timeout") + return False, ilo_was_reset, False, result + except Exception as e: + result["onboardError"] = str(e) + result["recommendation"] = "Review error details and verify iLO configuration" + LOGGER.error(f"{ip}: Unexpected error - {str(e)}") + return False, ilo_was_reset, False, result + + def _create_session_with_retries(self, username, password): + """Create a requests session with retry strategy + + :param username: iLO username + :type username: str + :param password: iLO password + :type password: str + :returns: configured requests session + :rtype: requests.Session + """ + from requests.auth import HTTPBasicAuth + from requests.adapters import HTTPAdapter + + # Create a session object with proper connection settings + session = requests.Session() + session.auth = HTTPBasicAuth(username, password) + session.verify = False + + # Define retry strategy for connection errors + retry_strategy = Retry( + total=3, # Total number of retries + backoff_factor=1, # Wait 1, 2, 4 seconds between retries + status_forcelist=[500, 502, 503, 504], # Retry on these HTTP status codes + allowed_methods=["GET", "POST", "PATCH"], # Retry these methods + raise_on_status=False, # Don't raise exceptions on retry exhaustion + ) + + # Create adapter with retry strategy + adapter = HTTPAdapter( + max_retries=retry_strategy, + pool_connections=1, # Single connection pool + pool_maxsize=1, # Single connection in pool to avoid conflicts + ) + + # Mount adapter for both http and https + session.mount("http://", adapter) + session.mount("https://", adapter) + + return session + + def _wait_for_ilo_after_reset(self, ip, username, password, timeout_minutes=3): + """Wait for iLO to come back online after reset + + :param ip: iLO IP address + :type ip: str + :param username: iLO username + :type username: str + :param password: iLO password + :type password: str + :param timeout_minutes: timeout in minutes to wait for iLO + :type timeout_minutes: int + :returns: True if iLO is back online, False if timeout + :rtype: bool + """ + import requests + from requests.auth import HTTPBasicAuth + import time + + base_url = f"https://{ip}" + timeout_seconds = timeout_minutes * 60 + check_interval = 10 # Check every 10 seconds as requested + max_attempts = int(timeout_seconds / check_interval) + start_time = time.time() + + LOGGER.info("Continuing to wait ...") + + for attempt in range(max_attempts): + try: + response = requests.get( + f"{base_url}/redfish/v1/", auth=HTTPBasicAuth(username, password), verify=False, timeout=30 + ) + + if response.status_code == 200: + elapsed_time = time.time() - start_time + LOGGER.info(f"{ip}: iLO back online after {elapsed_time:.1f}s") + return True + + except (requests.exceptions.ConnectionError, requests.exceptions.Timeout): + # Expected during reset - iLO is still rebooting + pass + except Exception as e: + LOGGER.warning(f"{ip}: Unexpected error while waiting - {str(e)}") + + # Wait before next check (skip on last attempt) + if attempt < max_attempts - 1: + time.sleep(check_interval) + + elapsed_time = time.time() - start_time + LOGGER.error(f"{ip}: Timeout - iLO did not come back online within {timeout_minutes}m ({elapsed_time:.1f}s)") + return False + + def _get_ilo_version_from_response(self, redfish_data, base_url=None, username=None, password=None): + """Get iLO version from Redfish response data.""" + model_version, ilo_version = 0, 0.0 + + try: + if all([redfish_data.get("Managers"), base_url, username, password]): + model, firmware_version = self._get_ilo_model_and_version(base_url, username, password) + + if model and "iLO" in model: + model_version = int(model.replace("iLO", "")) + + if firmware_version: + parts = firmware_version.split() + if len(parts) >= 3: + # Handle old format: "iLO 6 v1.66" -> parts[2] = "v1.66" + if parts[0].lower() == "ilo" and parts[2].startswith('v'): + version_str = parts[2].lstrip('v') # Remove 'v' prefix from old format + # Handle new format: "iLO 7 1.19.00 Nov 24 2025" -> parts[2] = "1.19.00" + elif parts[0].lower() == "ilo" and not parts[2].startswith('v'): + version_str = parts[2] # Use version number as-is from new format + # Handle direct version format: "1.19.00 Nov 24 2025" -> parts[0] = "1.19.00" + else: + version_str = parts[0] # Use first part for direct version format + + version_parts = version_str.split(".") + ilo_version = float(".".join(version_parts[:2])) if len(version_parts) >= 2 else 0.0 + else: + LOGGER.warning("Managers section not found in Redfish response or invalid credentials") + except Exception as e: + LOGGER.warning(f"Could not determine iLO version from response: {str(e)}") + raise e + + return model_version, ilo_version + + def _is_pin_onboarding_supported(self, ilo_version, major_version): + """Determine if PIN-based onboarding is supported + + :param ilo_version: iLO firmware version + :type ilo_version: float + :param major_version: iLO major version + :type major_version: int + :returns: True if PIN onboarding is supported + :rtype: bool + """ + # PIN-based onboarding support: + # - iLO 6: available from version 1.64 onwards + # - iLO 5: available from version 3.09 onwards + # - iLO 7: available from version 1.12 onwards + if major_version == 6 and ilo_version >= 1.64: + return True + elif major_version == 5 and ilo_version >= 3.09: + return True + elif major_version == 7 and ilo_version >= 1.12: + return True + return False + + def _check_ilo_version_compatibility(self, model, ilo_version, ip): + """Check if iLO version is compatible with COM requirements + + :param model: iLO major version number + :type model: int + :param ilo_version: iLO firmware version + :type ilo_version: float + :param ip: iLO IP address for logging + :type ip: str + :returns: tuple of (is_compatible, error_message, recommendation) + :rtype: tuple + """ + # Define minimum supported versions + ILO5_MIN_SUPPORTED = 2.47 + ILO6_MIN_SUPPORTED = 1.64 + ILO7_MIN_SUPPORTED = 1.12 + + if model == 5 and ilo_version < ILO5_MIN_SUPPORTED: + error_msg = f"iLO version should be greater than {ILO5_MIN_SUPPORTED}" + recommendation = "Upgrade to the latest iLO version to use HPE Compute Ops Management" + LOGGER.warning(f"{ip}: iLO version {ilo_version} incompatible - requires 2.47 or higher") + return False, error_msg, recommendation + elif model == 6 and ilo_version < ILO6_MIN_SUPPORTED: + error_msg = f"iLO version should be greater than {ILO6_MIN_SUPPORTED}" + recommendation = "Upgrade to the latest iLO version to use HPE Compute Ops Management" + LOGGER.warning(f"{ip}: iLO version {ilo_version} incompatible - requires 1.64 or higher") + return False, error_msg, recommendation + elif model == 7 and ilo_version < ILO7_MIN_SUPPORTED: + error_msg = f"iLO version should be greater than {ILO7_MIN_SUPPORTED}" + recommendation = "Upgrade to the latest iLO version to use HPE Compute Ops Management" + LOGGER.warning(f"{ip}: iLO version {ilo_version} incompatible - requires 1.12 or higher") + return False, error_msg, recommendation + + # Compatible version + return True, "", "" + + def _get_ilo_info(self, base_url, username, password): + """Get iLO model and version information""" + import requests + from requests.auth import HTTPBasicAuth + + try: + response = requests.get( + f"{base_url}/redfish/v1/Managers/1/", auth=HTTPBasicAuth(username, password), verify=False, timeout=30 + ) + + if response.status_code == 200: + data = response.json() + model = data.get("Model", "iLO5") + firmware_version = data.get("FirmwareVersion", "3.09") + + try: + version_parts = firmware_version.split(".") + major_version = int(model.replace("iLO", "")) if "iLO" in model else 5 + + if len(version_parts) >= 2: + version_float = float(f"{version_parts[0]}.{version_parts[1]}") + else: + version_float = float(version_parts[0]) + except: + version_float = 3.09 + major_version = 5 + + return model, firmware_version, version_float, major_version + except Exception as e: + LOGGER.warning(f"Failed to get iLO info: {str(e)}") + + return "iLO5", "3.09", 3.09, 5 + + def _get_ilo_model_and_version(self, base_url, username, password): + """Get iLO model and version information from the Redfish API + + :param base_url: base URL for iLO + :type base_url: str + :param username: iLO username + :type username: str + :param password: iLO password + :type password: str + :returns: tuple of (model, firmware_version) + :rtype: tuple + """ + from requests.auth import HTTPBasicAuth + + try: + mgr_response = requests.get( + f"{base_url}/redfish/v1/Managers/1/", + auth=HTTPBasicAuth(username, password), + verify=False, + timeout=30, + ) + if mgr_response.status_code == 200: + mgr_data = mgr_response.json() + model = mgr_data.get("Model", "") + firmware_version = mgr_data.get("FirmwareVersion", "") + return model, firmware_version + elif mgr_response.status_code == 401: + LOGGER.warning("Authentication failed while retrieving iLO model and version") + raise Exception("Authentication failed") + else: + return "", "" + except Exception as e: + LOGGER.warning(f"Failed to get iLO model and version: {str(e)}") + raise e + + def _check_dns_configuration(self, session, base_url, network_settings, individual_settings): + """Check if DNS is configured in iLO""" + try: + if individual_settings.get("skipDns", False): + return True + + response = session.get(f"{base_url}/redfish/v1/Managers/1/EthernetInterfaces/1", timeout=30) + if response.status_code == 200: + data = response.json() + oem_hpe = data.get("Oem", {}).get("Hpe", {}) + + ipv4_dns = oem_hpe.get("IPv4", {}).get("DNSServers", []) + ipv6_dns = oem_hpe.get("IPv6", {}).get("DNSServers", []) + + dns_servers = [dns for dns in (ipv4_dns + ipv6_dns) if dns and dns.strip() and dns != "0.0.0.0"] + + return len(dns_servers) > 0 + except Exception as e: + LOGGER.warning(f"Failed to check DNS configuration: {str(e)}") + + return False + + def _check_ntp_configuration(self, session, base_url, network_settings, individual_settings): + """Check if NTP is configured in iLO""" + try: + if individual_settings.get("skipNtp", False): + return True + + response = session.get(f"{base_url}/redfish/v1/Managers/1/DateTime/", timeout=30) + if response.status_code == 200: + data = response.json() + ntp_servers = data.get("NTPServers", []) + static_ntp = data.get("StaticNTPServers", []) + + all_ntp = [ntp for ntp in (ntp_servers + static_ntp) if ntp and ntp.strip()] + + return len(all_ntp) > 0 + except Exception as e: + LOGGER.warning(f"Failed to check NTP configuration: {str(e)}") + + return False + + def _check_com_connection_status(self, session, base_url): + """Check if COM is already connected + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :returns: True if COM is connected or connection is unstable, False if not connected, + None if status check failed for other reasons + :rtype: bool or None + """ + try: + response = session.get(f"{base_url}/redfish/v1/Managers/1/", timeout=30) + if response.status_code != 200: + LOGGER.warning(f"Failed to get manager resource, status: {response.status_code}") + return False + + manager_data = response.json() + cloud_connect = manager_data.get("Oem", {}).get("Hpe", {}).get("CloudConnect", {}) + current_status = cloud_connect.get("CloudConnectStatus", "") + + if current_status == "Connected": + LOGGER.info(f"COM is already connected for {base_url.split('//')[1]}") + return True + return False + except (ConnectionError, Timeout, RequestException) as e: + # Connection errors indicate unstable connection - treat as "already connected" to be safe + LOGGER.warning(f"Connection error while checking COM status: {str(e)}") + LOGGER.info( + "Treating as COM connected due to connection instability. Skipping configuration to avoid disruption." + ) + return True # Assume connected to avoid disruption during unstable connection + except Exception as e: + LOGGER.warning(f"Error checking COM connection status: {str(e)}") + return None # Cannot determine status + + def _configure_network_settings_requests( + self, session, base_url, network_settings, individual_settings, allow_ilo_reset + ): + """Configure network settings using requests session + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :param network_settings: common network settings + :type network_settings: dict + :param individual_settings: individual iLO settings + :type individual_settings: dict + :param allow_ilo_reset: allow iLO reset if needed + :type allow_ilo_reset: bool + :returns: tuple of (ilo_was_reset flag, ntp_configured_needs_reset flag) + :rtype: tuple(bool, bool) + """ + ilo_was_reset = False + ntp_configured_needs_reset = False + all_configs_successful = True # Track if all configurations succeed + + try: + # Skip if both DNS and NTP are explicitly skipped + if individual_settings.get("skipDns", False) and individual_settings.get("skipNtp", False): + return ilo_was_reset, ntp_configured_needs_reset + + # Check COM connection status before doing any patch operation + if self._check_com_connection_status(session, base_url): + LOGGER.info("COM is already connected. Skipping network configuration to avoid disruption.") + return ilo_was_reset, ntp_configured_needs_reset + + # Merge network settings (individual overrides common) + merged_network = network_settings.copy() if isinstance(network_settings, dict) else {} + individual_network = individual_settings.get("network", {}) + if individual_network and isinstance(individual_network, dict): + merged_network.update(individual_network) + + # Configure DNS - if this fails, mark configs as unsuccessful + if not individual_settings.get("skipDns", False) and merged_network.get("dns"): + dns_servers = merged_network["dns"] + try: + self._configure_dns(session, base_url, dns_servers) + except Exception as dns_err: + all_configs_successful = False + LOGGER.error(f"DNS configuration failed, will not reset iLO: {str(dns_err)}") + raise # Re-raise to stop processing + + # Configure NTP and capture if iLO was reset + # Only allow iLO reset if all previous configurations succeeded + if not individual_settings.get("skipNtp", False) and merged_network.get("ntp"): + ntp_servers = merged_network["ntp"] + # Pass allow_ilo_reset only if all configs successful so far + effective_allow_reset = allow_ilo_reset and all_configs_successful + if not effective_allow_reset and allow_ilo_reset: + LOGGER.warning("iLO reset disabled due to configuration errors, even though allow_ilo_reset=True") + + try: + ilo_was_reset, is_ntp_config_matching = self._configure_ntp( + session, base_url, ntp_servers, effective_allow_reset + ) + except Exception as ntp_err: + all_configs_successful = False + LOGGER.error(f"NTP configuration failed, will not reset iLO: {str(ntp_err)}") + raise # Re-raise to stop processing + + LOGGER.info(f"is_ntp_config_matching {is_ntp_config_matching}") + if is_ntp_config_matching: + return ilo_was_reset, ntp_configured_needs_reset + + # If allow_ilo_reset is False and NTP was configured, user must manually reset iLO + if not allow_ilo_reset and not ilo_was_reset: + ntp_configured_needs_reset = True + LOGGER.warning("=" * 80) + LOGGER.warning("NTP CONFIGURATION REQUIRES MANUAL iLO RESET") + LOGGER.warning("=" * 80) + LOGGER.warning("NTP configuration has been applied, but --allow_ilo_reset was not specified.") + LOGGER.warning("Please manually reset the iLO for NTP changes to take effect,") + LOGGER.warning("then retry the onboarding command.") + LOGGER.warning("=" * 80) + + except Exception as e: + error_msg = f"Failed to configure network settings: {str(e)}" + LOGGER.error(error_msg) + raise + + return ilo_was_reset, ntp_configured_needs_reset + + def _configure_dns(self, session, base_url, dns_servers): + """Configure DNS settings for the iLO with IPv4 and IPv6 support. + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :param dns_servers: list of DNS servers (IPv4 and/or IPv6) + :type dns_servers: list + """ + try: + # First, check if DNS servers are already configured as desired + current_dns_servers, _, _ = self._get_current_dns_servers(session, base_url) + + if current_dns_servers is not None: + # Normalize both lists for comparison (remove empty strings, sort) + desired_dns = sorted([srv.strip() for srv in dns_servers if srv and srv.strip()]) + current_dns = sorted([srv.strip() for srv in current_dns_servers if srv and srv.strip()]) + + if desired_dns == current_dns: + LOGGER.info(f"DNS already configured: {desired_dns}") + return + else: + LOGGER.info(f"Updating DNS from {current_dns} to {desired_dns}") + else: + LOGGER.info("Unable to retrieve current DNS configuration. Proceeding with DNS configuration.") + + # Separate IPv4 and IPv6 DNS servers + ipv4_dns_servers = [] + ipv6_dns_servers = [] + + # Optimize: Strip once and reuse the cleaned value + for dns_server in dns_servers: + if dns_server: + dns_clean = dns_server.strip() + if dns_clean: + if self._is_ipv6_address(dns_clean): + ipv6_dns_servers.append(dns_clean) + else: + ipv4_dns_servers.append(dns_clean) + + # Use EthernetInterfaces endpoint to configure DNS properly + dns_config_url = f"{base_url}/redfish/v1/Managers/1/EthernetInterfaces/1" + + # Build payload to disable DHCP for DNS servers and set static DNS for both IPv4 and IPv6 + payload = { + "DHCPv4": {"UseDNSServers": False}, + "DHCPv6": {"UseDNSServers": False}, + "Oem": {"Hpe": {"DHCPv4": {"UseDNSServers": False}, "DHCPv6": {"UseDNSServers": False}}}, + } + + # Add IPv4 DNS servers if present + if ipv4_dns_servers: + payload["Oem"]["Hpe"]["IPv4"] = {"DNSServers": ipv4_dns_servers} + + # Add IPv6 DNS servers if present + if ipv6_dns_servers: + payload["Oem"]["Hpe"]["IPv6"] = {"DNSServers": ipv6_dns_servers} + + # Create fresh session for DNS configuration to avoid connection reuse issues + dns_session = requests.Session() + dns_session.auth = session.auth + dns_session.verify = False + + LOGGER.info(f"Configuring DNS - IPv4: {ipv4_dns_servers}, IPv6: {ipv6_dns_servers}") + + response = dns_session.patch(dns_config_url, json=payload, timeout=30) + + if response.status_code not in [200, 204]: + raise Exception(f"Failed to configure DNS. HTTP {response.status_code}: {response.text}") + + LOGGER.info("DNS configuration successful") + except (ConnectionError, Timeout, RequestException) as e: + error_msg = f"Connection error while configuring DNS: {str(e)}" + LOGGER.error(error_msg) + raise Exception(error_msg) + except Exception as e: + error_msg = f"Error configuring DNS: {str(e)}" + LOGGER.error(error_msg) + raise + + def _is_ipv6_address(self, address): + """Check if the given address is an IPv6 address. + + :param address: IP address string + :type address: str + :returns: True if IPv6, False otherwise + :rtype: bool + """ + try: + from ipaddress import IPv6Address + + IPv6Address(address) + return True + except (ValueError, Exception): + return False + + def _get_current_dns_servers(self, session, base_url): + """Get current DNS server configuration from iLO (both IPv4 and IPv6). + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :returns: list of currently configured DNS servers (IPv4 and IPv6), or None if unable to retrieve + :rtype: list or None + """ + dhcp_dns_enabled = False + dhcp_check_attempted = False + try: + dns_config_url = f"{base_url}/redfish/v1/Managers/1/EthernetInterfaces/1" + + # Reuse existing session instead of creating a new one + response = session.get(dns_config_url, timeout=30) + if response.status_code == 200: + data = response.json() + + dhcp_check_attempted = True + dhcpv4_dns_enabled = data.get("DHCPv4", {}).get("UseDNSServers", False) + dhcpv6_dns_enabled = data.get("DHCPv6", {}).get("UseDNSServers", False) + + oem_hpe = data.get("Oem", {}).get("Hpe", {}) + + oem_dhcpv4_dns_enabled = oem_hpe.get("DHCPv4", {}).get("UseDNSServers", False) + oem_dhcpv6_dns_enabled = oem_hpe.get("DHCPv6", {}).get("UseDNSServers", False) + if any([dhcpv4_dns_enabled, dhcpv6_dns_enabled, oem_dhcpv4_dns_enabled, oem_dhcpv6_dns_enabled]): + LOGGER.info("DHCP DNS is enabled; static DNS servers may not be in use.") + dhcp_dns_enabled = True + + # Get both IPv4 and IPv6 DNS servers + ipv4_dns_servers = oem_hpe.get("IPv4", {}).get("DNSServers", []) + ipv6_dns_servers = oem_hpe.get("IPv6", {}).get("DNSServers", []) + + # Combine and filter using list comprehension (more efficient) + current_dns_servers = [srv for srv in ipv4_dns_servers + ipv6_dns_servers if srv] + + formatted_dns_servers = [ + srv.strip() for srv in current_dns_servers if srv and srv.strip() not in DNS_SERVER_EXCLUSION_LIST + ] + + return formatted_dns_servers, dhcp_dns_enabled, dhcp_check_attempted + else: + LOGGER.warning(f"Failed to get current DNS configuration. HTTP {response.status_code}") + return None, dhcp_dns_enabled, dhcp_check_attempted + except (ConnectionError, Timeout, RequestException) as e: + LOGGER.warning(f"Connection error while reading DNS configuration: {str(e)}") + return None, dhcp_dns_enabled, dhcp_check_attempted + except Exception as e: + LOGGER.warning(f"Error reading DNS configuration: {str(e)}") + return None, dhcp_dns_enabled, dhcp_check_attempted + + def _configure_ntp(self, session, base_url, ntp_servers, allow_ilo_reset): + """Configure NTP settings for the iLO. + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :param ntp_servers: list of NTP servers + :type ntp_servers: list + :param allow_ilo_reset: allow iLO reset if needed + :type allow_ilo_reset: bool + :returns: tuple of (ilo_was_reset flag, is_config_matching flag) + :rtype: tuple(bool, bool) + """ + ilo_was_reset = False + is_ntp_config_matching = False + dhcp_disabled_successfully = False + + LOGGER.info(f"Starting NTP configuration for {base_url.split('//')[1]}") + + try: + # First, check if DHCP NTP is enabled - if it is, static NTP list is irrelevant + dhcp_ntp_is_enabled = False + dhcp_check_attempted = False + + eth_interfaces_url = f"{base_url}/redfish/v1/Managers/1/EthernetInterfaces" + + dhcp_ntp_is_enabled, dhcp_disabled_successfully, dhcp_check_attempted = self._check_dhcp_ntp_config_on_interfaces( + session, + base_url, + eth_interfaces_url, + ) + + if dhcp_check_attempted and not dhcp_ntp_is_enabled: + LOGGER.info("DHCP NTP is already disabled on all ethernet interfaces") + current_ntp_servers = self._get_current_ntp_servers(session, base_url) + ntp_ips = [ntp_ip.strip() for ntp_ip in ntp_servers if ntp_ip and ntp_ip.strip()] + if sorted(current_ntp_servers) == sorted(ntp_ips): + LOGGER.info("NTP servers are already configured as desired") + is_ntp_config_matching = True + return ilo_was_reset, is_ntp_config_matching + + # If we couldn't check any interface due to errors + if not dhcp_check_attempted: + LOGGER.warning("Could not check DHCP NTP status - will attempt to disable") + + # Always apply NTP configuration regardless of current state + is_ntp_config_matching = False + + # Disable DHCP NTP if it's enabled + if dhcp_ntp_is_enabled: + LOGGER.info("Disabling DHCP NTP on ethernet interfaces") + + # Try to disable DHCP on ethernet interfaces + try: + eth_response = session.get(eth_interfaces_url, timeout=30) + if eth_response.status_code == 200: + eth_data = eth_response.json() + members = eth_data.get("Members", []) + + for member in members: + interface_url = base_url + member.get("@odata.id", "") + try: + interface_response = session.get(interface_url, timeout=30) + if interface_response.status_code == 200: + interface_data = interface_response.json() + if interface_data.get("Status", {}).get("State") == "Enabled": + dhcp_ntp_payload = { + "DHCPv4": {"UseNTPServers": False}, + "DHCPv6": {"UseNTPServers": False}, + "Oem": { + "Hpe": { + "DHCPv4": {"UseNTPServers": False}, + "DHCPv6": {"UseNTPServers": False}, + } + }, + } + + dhcp_response = session.patch(interface_url, json=dhcp_ntp_payload, timeout=30) + if dhcp_response.status_code in [200, 204]: + LOGGER.info("Successfully disabled DHCP NTP") + dhcp_disabled_successfully = True + time.sleep(3) + break + except Exception: + continue + + if not dhcp_disabled_successfully: + LOGGER.warning("Could not disable DHCP NTP via ethernet interfaces") + except Exception: + pass + else: + dhcp_disabled_successfully = True + + # Configure static NTP servers + if not is_ntp_config_matching: + # If DHCP wasn't disabled via ethernet interfaces, try NetworkProtocol + if not dhcp_disabled_successfully: + try: + network_protocol_url = f"{base_url}/redfish/v1/Managers/1/NetworkProtocol" + network_payload = { + "NTP": {"ProtocolEnabled": True}, + "Oem": {"Hpe": {"ConfigurationSettings": "Current"}}, + } + + network_response = session.patch(network_protocol_url, json=network_payload, timeout=30) + + if network_response.status_code in [200, 204]: + LOGGER.info("Successfully configured NetworkProtocol for static NTP") + dhcp_disabled_successfully = True + time.sleep(3) + except Exception: + pass + + # If still not successful after all attempts, raise error and stop + if not dhcp_disabled_successfully: + error_msg = ( + "Failed to disable DHCP NTP. Cannot apply static NTP configuration while DHCP NTP is enabled." + ) + LOGGER.error(error_msg) + raise Exception(error_msg) + + # Apply static NTP servers + ntp_datetime_url = f"{base_url}/redfish/v1/Managers/1/DateTime" + ntp_server_list = list(ntp_servers) + + if len(ntp_server_list) == 1: + ntp_server_list.append("") + + payload = {"StaticNTPServers": ntp_server_list} + LOGGER.info(f"Configuring static NTP servers: {ntp_servers}") + + try: + response = session.patch(ntp_datetime_url, json=payload, timeout=30) + + if response.status_code not in [200, 204]: + error_msg = f"Failed to configure NTP. HTTP {response.status_code}: {response.text}" + LOGGER.error(error_msg) + raise Exception(error_msg) + except (ConnectionError, Timeout, RequestException) as conn_err: + LOGGER.warning(f"Connection error during NTP configuration: {str(conn_err)}") + LOGGER.info("NTP configuration may have been applied despite connection error") + is_ntp_config_matching = False + + # NTP configuration may require iLO reset to take effect + # Only reset if allow_ilo_reset is True + if allow_ilo_reset: + LOGGER.info("Initiating iLO reset for NTP configuration") + reset_url = f"{base_url}/redfish/v1/Managers/1/Actions/Manager.Reset" + reset_payload = {"ResetType": "GracefulRestart"} + + try: + reset_response = session.post(reset_url, json=reset_payload, timeout=30) + + if reset_response.status_code in [200, 202, 204]: + LOGGER.info("iLO reset initiated successfully") + ilo_was_reset = True + time.sleep(30) + else: + LOGGER.warning(f"Failed to reset iLO - HTTP {reset_response.status_code}") + except (ConnectionError, Timeout, RequestException): + LOGGER.info("iLO appears to be resetting (connection closed)") + ilo_was_reset = True + time.sleep(30) + else: + LOGGER.info("NTP configured but iLO reset not allowed - manual reset may be required") + + except (ConnectionError, Timeout, RequestException) as e: + LOGGER.error(f"Connection error during NTP configuration: {str(e)}") + except Exception as e: + LOGGER.error(f"Error configuring NTP: {str(e)}") + + LOGGER.info(f"NTP configuration completed - reset={ilo_was_reset}, matching={is_ntp_config_matching}") + return ilo_was_reset, is_ntp_config_matching + + def _check_dhcp_ntp_config_on_interfaces(self, session, base_url, eth_interfaces_url): + """Check NTP configuration status on ethernet interfaces. + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :param members: list of ethernet interface members + :type members: list + :returns: tuple of (dhcp_ntp_is_enabled, dhcp_disabled_successfully, dhcp_check_attempted) + :rtype: tuple(bool, bool, bool) + """ + dhcp_ntp_is_enabled = False + dhcp_disabled_successfully = False + dhcp_check_attempted = False + + members = [] + try: + eth_response = session.get(eth_interfaces_url, timeout=30) + + if eth_response.status_code == 200: + eth_data = eth_response.json() + members = eth_data.get("Members", []) + except (ConnectionError, Timeout, RequestException) as eth_err: + LOGGER.warning(f"Error accessing ethernet interfaces: {str(eth_err)}") + return dhcp_ntp_is_enabled, dhcp_disabled_successfully, dhcp_check_attempted + + # Find the first enabled interface to check/disable DHCP NTP + for idx, member in enumerate(members): + interface_url = base_url + member.get("@odata.id", "") + + try: + interface_response = session.get(interface_url, timeout=30) + + if interface_response.status_code == 200: + interface_data = interface_response.json() + interface_state = interface_data.get("Status", {}).get("State") + + if interface_state == "Enabled": + dhcp_check_attempted = True + + dhcpv4_ntp_enabled = interface_data.get("DHCPv4", {}).get("UseNTPServers", False) + dhcpv6_ntp_enabled = interface_data.get("DHCPv6", {}).get("UseNTPServers", False) + oem_dhcpv4_ntp = ( + interface_data.get("Oem", {}) + .get("Hpe", {}) + .get("DHCPv4", {}) + .get("UseNTPServers", False) + ) + oem_dhcpv6_ntp = ( + interface_data.get("Oem", {}) + .get("Hpe", {}) + .get("DHCPv6", {}) + .get("UseNTPServers", False) + ) + + if any([dhcpv4_ntp_enabled, dhcpv6_ntp_enabled, oem_dhcpv4_ntp, oem_dhcpv6_ntp]): + # DHCP NTP is enabled + dhcp_ntp_is_enabled = True + LOGGER.info("DHCP NTP is enabled, will disable and configure static NTP") + else: + LOGGER.info("DHCP NTP already disabled") + dhcp_disabled_successfully = True + break + except (ConnectionError, Timeout, RequestException) as iface_err: + # Connection errors during interface access - implement retry with new session + LOGGER.warning(f"Connection error accessing interface: {type(iface_err).__name__}") + + # For RemoteDisconnected errors, retry once with a fresh session + if "RemoteDisconnected" in str(type(iface_err)) or "Connection aborted" in str(iface_err): + retry_session = None + try: + time.sleep(2) + retry_session = requests.Session() + retry_session.auth = session.auth + retry_session.verify = False + retry_response = retry_session.get(interface_url, timeout=30) + if retry_response.status_code == 200: + interface_data = retry_response.json() + interface_state = interface_data.get("Status", {}).get("State") + + if interface_state == "Enabled": + dhcp_check_attempted = True + dhcpv4_ntp_enabled = interface_data.get("DHCPv4", {}).get( + "UseNTPServers", False + ) + dhcpv6_ntp_enabled = interface_data.get("DHCPv6", {}).get( + "UseNTPServers", False + ) + oem_dhcpv4_ntp = ( + interface_data.get("Oem", {}) + .get("Hpe", {}) + .get("DHCPv4", {}) + .get("UseNTPServers", False) + ) + oem_dhcpv6_ntp = ( + interface_data.get("Oem", {}) + .get("Hpe", {}) + .get("DHCPv6", {}) + .get("UseNTPServers", False) + ) + + if any( + [dhcpv4_ntp_enabled, dhcpv6_ntp_enabled, oem_dhcpv4_ntp, oem_dhcpv6_ntp] + ): + dhcp_ntp_is_enabled = True + LOGGER.info("DHCP NTP enabled (retry check)") + else: + dhcp_disabled_successfully = True + break + except Exception: + pass + finally: + if retry_session: + retry_session.close() + continue + + return dhcp_ntp_is_enabled, dhcp_disabled_successfully, dhcp_check_attempted + + def _get_current_ntp_servers(self, session, base_url): + """Get current NTP server configuration from iLO. + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :returns: list of currently configured NTP servers, or None if unable to retrieve + :rtype: list or None + """ + try: + ntp_datetime_url = f"{base_url}/redfish/v1/Managers/1/DateTime" + response = session.get(ntp_datetime_url, timeout=30) + + if response.status_code == 200: + data = response.json() + current_ntp_servers = data.get("StaticNTPServers", []) + # Filter out empty strings and None values using list comprehension + current_ntp_servers = [srv for srv in current_ntp_servers if srv] + return current_ntp_servers + else: + LOGGER.warning(f"Failed to get NTP configuration - HTTP {response.status_code}") + return None + except (ConnectionError, Timeout, RequestException) as e: + LOGGER.warning(f"Connection error reading NTP configuration: {str(e)}") + return None + except Exception as e: + LOGGER.warning(f"Error reading NTP configuration: {str(e)}") + return None + + def _get_current_proxy_settings(self, session, base_url): + """Get current proxy configuration from iLO. + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :returns: dict with current proxy settings, or None if unable to retrieve + :rtype: dict or None + """ + try: + proxy_config_url = f"{base_url}/redfish/v1/Managers/1/NetworkProtocol/" + + # Reuse existing session instead of creating a new one + response = session.get(proxy_config_url, timeout=30) + if response.status_code == 200: + data = response.json() + web_proxy = data.get("Oem", {}).get("Hpe", {}).get("WebProxyConfiguration", {}) + + # Get raw values + raw_server = web_proxy.get("ProxyServer", "") + raw_port = web_proxy.get("ProxyPort") + raw_username = web_proxy.get("ProxyUserName", "") + + # Normalize all values: treat empty strings and None as "no proxy" + # Use 'is not None' check for port to correctly handle port 0 + current_proxy = { + "server": raw_server.strip() if raw_server else "", + "port": raw_port if raw_port is not None else None, + "username": raw_username.strip() if raw_username else "", + } + + return current_proxy + else: + LOGGER.warning(f"Failed to get current proxy configuration. HTTP {response.status_code}") + return None + except (ConnectionError, Timeout, RequestException) as e: + LOGGER.warning(f"Connection error while reading proxy configuration: {str(e)}") + return None + except Exception as e: + LOGGER.warning(f"Error reading proxy configuration: {str(e)}") + return None + + def _configure_proxy_for_ilo_requests(self, session, base_url, proxy_settings, individual_settings=None): + """Configure proxy settings using requests session + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :param proxy_settings: proxy settings + :type proxy_settings: dict + :param individual_settings: individual iLO settings + :type individual_settings: dict or None + """ + try: + # Check if proxy configuration should be skipped + if individual_settings and individual_settings.get("skipProxy", False): + LOGGER.info("Proxy configuration skipped per individual settings.") + return + + # Check COM connection status before doing any patch operation + if self._check_com_connection_status(session, base_url): + LOGGER.info("COM is already connected. Skipping proxy configuration to avoid disruption.") + return + + individual_proxy = individual_settings.get("proxy", {}) + # Only configure if proxy_settings exists and has meaningful content + # Check if there's actually a proxy server to configure + if not proxy_settings and not individual_proxy: + LOGGER.info("No proxy settings provided. Skipping proxy configuration.") + return + + # Apply proxy from individual or range in if present over common + # Else proxy from common settings + proxy_to_configure = individual_proxy if individual_proxy else proxy_settings + + # Check if server is provided and not empty + proxy_server = proxy_to_configure.get("server", "").strip() if proxy_to_configure.get("server") else "" + if not proxy_server: + LOGGER.info("No proxy server specified. Skipping proxy configuration.") + return + + # At this point, we have valid proxy settings to configure + self._configure_proxy(session, base_url, proxy_to_configure) + # Give iLO time to process proxy changes + time.sleep(2) + except Exception as e: + error_msg = f"Failed to configure proxy settings: {str(e)}" + LOGGER.error(error_msg) + raise + + def _configure_proxy(self, session, base_url, proxy_settings): + """Configure proxy settings for the iLO. + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :param proxy_settings: proxy settings + :type proxy_settings: dict + """ + try: + # First, check if proxy settings are already configured as desired + current_proxy = self._get_current_proxy_settings(session, base_url) + + if current_proxy is not None: + # Normalize desired proxy settings for comparison + # Treat empty strings and None as "no proxy configured" + desired_server = proxy_settings.get("server", "").strip() if proxy_settings.get("server") else "" + desired_port = proxy_settings.get("port") if proxy_settings.get("port") else None + desired_credentials = proxy_settings.get("credentials", {}) + # Normalize port to int for comparison if not None + if desired_port is not None: + try: + desired_port = int(desired_port) + except (ValueError, TypeError): + desired_port = None + desired_username = desired_credentials.get("username", "").strip() if desired_credentials else "" + + # Normalize current proxy settings (already normalized in _get_current_proxy_settings) + current_server = current_proxy.get("server", "") + current_port = current_proxy.get("port") + # Ensure current port is also int or None for consistent comparison + if current_port is not None: + try: + current_port = int(current_port) + except (ValueError, TypeError): + current_port = None + current_username = current_proxy.get("username", "") + + # Detect "no proxy" scenarios: + # iLO has no proxy if: server is empty string AND port is None + current_has_no_proxy = not current_server and current_port is None + desired_has_no_proxy = not desired_server and desired_port is None + + if current_has_no_proxy and desired_has_no_proxy: + LOGGER.info("Proxy not configured - skipping") + return + elif ( + desired_server == current_server + and desired_port == current_port + and desired_username == current_username + ): + LOGGER.info(f"Proxy already configured: {desired_server}:{desired_port}") + return + else: + LOGGER.info(f"Updating proxy configuration to {desired_server}:{desired_port}") + else: + LOGGER.info("Unable to retrieve current proxy configuration. Proceeding with proxy configuration.") + + proxy_config_url = f"{base_url}/redfish/v1/Managers/1/NetworkProtocol/" + + # Build proxy configuration payload + web_proxy_config = { + "ProxyServer": proxy_settings.get("server", ""), + "ProxyPort": proxy_settings.get("port"), + "ProxyUserName": "", + "ProxyPassword": "", + } + + # Only add username and password if they are provided (not None and not empty) + username = desired_credentials.get("username", "").strip() if desired_credentials else "" + password = desired_credentials.get("password", "").strip() if desired_credentials else "" + + if username: + web_proxy_config["ProxyUserName"] = username + if password: + web_proxy_config["ProxyPassword"] = password + + payload = {"Oem": {"Hpe": {"WebProxyConfiguration": web_proxy_config}}} + + response = session.patch(proxy_config_url, json=payload, timeout=30) + + if response.status_code not in [200, 204]: + raise Exception(f"Failed to configure proxy. HTTP {response.status_code}: {response.text}") + + LOGGER.info("Proxy configured successfully") + except Exception as e: + error_msg = f"Error configuring proxy: {str(e)}" + LOGGER.error(error_msg) + raise + + def _enable_com_connection_requests(self, session, base_url, activation_key): + """Enable COM connection using requests session + + :param session: requests Session object + :type session: requests.Session + :param base_url: base URL for iLO + :type base_url: str + :param activation_key: COM activation key or PIN + :type activation_key: str + :returns: tuple of (success, fail_reason) where success is True/False and fail_reason is string or None + :rtype: tuple + """ + + try: + # Get manager resource to check COM capabilities with timeout + response = session.get(f"{base_url}/redfish/v1/Managers/1/", timeout=30) + if response.status_code != 200: + LOGGER.warning(f"Failed to get manager resource - HTTP {response.status_code}") + return False, f"Failed to get manager resource: HTTP {response.status_code}" + + manager_data = response.json() + + # Check if COM is already connected + cloud_connect = manager_data.get("Oem", {}).get("Hpe", {}).get("CloudConnect", {}) + current_status = cloud_connect.get("CloudConnectStatus", "") + + if current_status == "Connected": + LOGGER.info("iLO is already connected") + return True, None + + # Check if COM connect actions are available + oem_actions = manager_data.get("Oem", {}).get("Hpe", {}).get("Actions", {}) + if "#HpeiLO.EnableCloudConnect" not in oem_actions: + LOGGER.warning("COM feature not available on this iLO") + return False, "COM feature not available" + + # Enable cloud connect with proper error handling and retry logic + action_uri = f"{base_url}/redfish/v1/Managers/1/Actions/Oem/Hpe/HpeiLO.EnableCloudConnect/" + body = {"ActivationKey": activation_key} if activation_key else {} + + LOGGER.info("Enabling COM connection") + + # Create a new session for the POST request to avoid connection reuse issues + post_session = requests.Session() + post_session.auth = session.auth + post_session.verify = False + + success = False + retry_count = 0 + max_retries = 3 + + while retry_count <= max_retries and not success: + try: + timeout_value = 90 + (retry_count * 30) # Increase timeout with retries + response = post_session.post(action_uri, json=body, timeout=timeout_value) + + if response.status_code in [200, 202]: + success = True + LOGGER.info("COM enable request successful") + else: + LOGGER.warning(f"COM enable failed - HTTP {response.status_code}") + if retry_count < max_retries: + retry_count += 1 + time.sleep(5) + else: + return False, "COM enable request failed" + + except (ConnectionError, Timeout, RequestException) as conn_err: + LOGGER.warning(f"Connection issue during COM enable: {str(conn_err)}") + + # Give iLO time to process the request + time.sleep(10) + + # Try to check if the request was actually accepted by querying status + try: + status_response = session.get(f"{base_url}/redfish/v1/Managers/1/", timeout=30) + if status_response.status_code == 200: + mgr_data = status_response.json() + cc_status = ( + mgr_data.get("Oem", {}) + .get("Hpe", {}) + .get("CloudConnect", {}) + .get("CloudConnectStatus", "") + ) + if cc_status in ["Initializing", "ConnectionInProgress", "Connected"]: + LOGGER.info(f"COM enable appears successful (status: {cc_status})") + success = True + break + except Exception: + pass + + if retry_count < max_retries: + retry_count += 1 + time.sleep(15) + else: + LOGGER.warning("Connection errors occurred, will check status to confirm") + success = True # Proceed to status checking + break + + if not success: + return False, "COM enable request failed" + + # Wait for connection to complete with improved status checking + max_wait = 480 # Increased from 180s (3 min) to 480s (8 min) + wait_interval = 12 + waited = 0 + + while waited < max_wait: + time.sleep(wait_interval) + waited += wait_interval + + try: + status_response = session.get(f"{base_url}/redfish/v1/Managers/1/", timeout=30) + if status_response.status_code == 200: + manager_data = status_response.json() + cloud_connect = manager_data.get("Oem", {}).get("Hpe", {}).get("CloudConnect", {}) + current_status = cloud_connect.get("CloudConnectStatus", "") + + if current_status == "Connected": + LOGGER.info("COM connection successful") + return True, None + elif current_status == "Initializing": + LOGGER.info("COM connection is initializing, waiting...") + continue # Keep waiting + elif current_status == "ConnectionFailed": + LOGGER.warning(f"COM connect terminal Status: {current_status}") + break + elif current_status in ["NotConnected", "NotEnabled"]: + fail_reason = cloud_connect.get("FailReason", "Unknown error") + + LOGGER.warning(f"COM Current Status: {current_status}") + LOGGER.warning("Continuing to wait ...") + continue # Keep waiting + # Add other terminal states if needed + else: + LOGGER.warning( + f"Failed to get manager resource during status check - HTTP {status_response.status_code}" + ) + except (ConnectionError, Timeout, RequestException): + LOGGER.warning("Connection error during COM status polling, will retry...") + continue + except Exception as check_err: + LOGGER.warning(f"Error during COM status polling: {str(check_err)}") + continue + + status_response = session.get(f"{base_url}/redfish/v1/Managers/1/", timeout=30) + if status_response.status_code == 200: + manager_data = status_response.json() + cloud_connect = manager_data.get("Oem", {}).get("Hpe", {}).get("CloudConnect", {}) + current_status = cloud_connect.get("CloudConnectStatus", "") + if current_status == "Connected": + LOGGER.info("COM connection successful") + return True, None + else: + fail_reason = cloud_connect.get("FailReason", "Unknown error") + LOGGER.warning(f"COM connection failed - final status: {current_status}") + return False, f"COM connection failed: {fail_reason}" + + except (ConnectionError, Timeout, RequestException) as e: + LOGGER.warning(f"Connection error during COM enable: {str(e)}") + # Try to verify if the connection actually succeeded + try: + verify_session = requests.Session() + verify_session.auth = session.auth + verify_session.verify = False + time.sleep(5) + + response = verify_session.get(f"{base_url}/redfish/v1/Managers/1/", timeout=30) + if response.status_code == 200: + mgr_data = response.json() + cc_status = ( + mgr_data.get("Oem", {}).get("Hpe", {}).get("CloudConnect", {}).get("CloudConnectStatus", "") + ) + if cc_status in ["Initializing", "ConnectionInProgress", "Connected"]: + LOGGER.info(f"COM enable appears successful despite connection error (status: {cc_status})") + success = True + else: + return False, "Connection error" + else: + return False, "Connection error" + except Exception: + return False, "Connection error" + + if not success: + return False, "Connection error" + + except Exception as e: + LOGGER.error(f"Unexpected error enabling COM: {str(e)}") + return False, str(e) + + def _save_json_report(self, report, output_file): + """Save report to JSON file with proper formatting + + :param report: report data to save + :type report: dict + :param output_file: output filename + :type output_file: str + :returns: True if successful, False otherwise + :rtype: bool + """ + try: + with open(output_file, "w") as f: + json.dump(report, f, indent=2, sort_keys=True) + + LOGGER.info(f"Report successfully saved to {output_file}") + + except Exception as e: + error_msg = f"Failed to save report to {output_file}: {str(e)}" + LOGGER.error(error_msg) + raise Exception(error_msg) + + def run(self, line, help_disp=False): + """Wrapper function for cloudconnect main function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except Exception: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + # Skip validations for multiconnect as it handles multiple connections differently + if not (options.command and options.command.lower() == "multiconnect"): + self.cmdbase.login_select_validation(self, options) + if not self.rdmc.app.redfishinst: + raise NoCurrentSessionEstablished("Please login to iLO and retry the command") + + ilo_ver = self.rdmc.app.getiloversion() + if ilo_ver < 5.247: + raise IncompatibleiLOVersionError( + "ComputeOpsManagement Feature is only available with iLO 5 version 2.47 or higher.\n" + ) + + # validation checks + self.cloudconnectvalidation(options) + if options.command: + if options.command.lower() == "connect": + if options.proxy: + self.proxy_config(options.proxy) + if options.activationkey: + self.connect_cloud(activationkey=options.activationkey) + elif not options.activationkey: + self.connect_cloud() + else: + raise InvalidCommandLineError( + "Activation Key %s is not alphanumeric or not of length 32." % str(options.activationkey) + ) + elif options.command.lower() == "disconnect": + self.disconnect_cloud() + elif options.command.lower() == "status": + if options.json: + self.cloud_status(json=True) + else: + self.cloud_status() + elif options.command.lower() == "multiconnect": + # Handle template generation + if options.template_input_file is not None: + self.generate_template_file(options.template_input_file) + # Handle bulk operations + elif options.file: + if getattr(options, "precheck", False): + result = self.bulk_connect_precheck(options.file, options.output_file) + if result != ReturnCodes.SUCCESS: + return result + else: + result = self.bulk_connect(options.file, options.output_file, options.allow_ilo_reset) + if result != ReturnCodes.SUCCESS: + return result + else: + raise InvalidCommandLineError( + "For multiconnect command, either --input_file_json_template" + " or --input_file must be specified. " + "Use -h for help." + ) + else: + raise InvalidCommandLineError("%s is not a valid option for this command." % str(options.command)) + else: + raise InvalidCommandLineError( + "Please provide either connect, disconnect, status, or multiconnect as additional subcommand." + " For help or usage related information, use -h or --help" + ) + # logout routine + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def cloudconnectvalidation(self, options): + """new command method validation function""" + # Check if Cloud Connect feature is enabled in iLO. + path = self.rdmc.app.typepath.defs.managerpath + resp = self.rdmc.app.get_handler(path, service=True, silent=True) + if resp.status == 200: + oem_actions = resp.dict["Oem"]["Hpe"]["Actions"] + # print(oem_actions) + if "#HpeiLO.EnableCloudConnect" not in oem_actions or "#HpeiLO.DisableCloudConnect" not in oem_actions: + raise CloudConnectFailedError("ComputeOpsManagement is disabled in this iLO.\n") + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + connect_help = "To connect to ComputeOpsManagement\n" + # connect sub-parser + connect_parser = subcommand_parser.add_parser( + "connect", + help=connect_help, + description=connect_help + "\n\tExample:\n\tcomputeopsmanagement connect or" + "\n\tcomputeopsmanagement connect --proxy http://proxy.abc.com:8080 or " + "\n\tcomputeopsmanagement connect --proxy None or " + "\n\tcomputeopsmanagement connect --activationkey 123456789EFGA or " + "\n\tcomputeopsmanagement connect --activationkey 123456789EFGA --proxy http://proxy.abc.com:8080 or " + "\n\tcomputeopsmanagement connect --activationkey 123456789EFGA --proxy None", + formatter_class=RawDescriptionHelpFormatter, + ) + connect_parser.add_argument( + "--activationkey", + dest="activationkey", + help="activation key is optional for connecting", + required=False, + type=str, + default=None, + ) + connect_parser.add_argument( + "--proxy", + dest="proxy", + help="to set or clear proxy while connecting", + type=str, + default=None, + ) + self.cmdbase.add_login_arguments_group(connect_parser) + status_help = "To check the ComputeOpsManagement connection status\n" + status_parser = subcommand_parser.add_parser( + "status", + help=status_help, + description=status_help + "\n\tExample:\n\tcomputeopsmanagement status or " + "\n\tcomputeopsmanagement status -j", + formatter_class=RawDescriptionHelpFormatter, + ) + status_parser.add_argument( + "-j", + "--json", + dest="json", + help="to print in json format", + action="store_true", + default=False, + ) + self.cmdbase.add_login_arguments_group(status_parser) + disconnect_help = "To disconnect from ComputeOpsManagement\n" + disconnect_parser = subcommand_parser.add_parser( + "disconnect", + help=disconnect_help, + description=disconnect_help + "\n\tExample:\n\tcomputeopsmanagement disconnect", + ) + self.cmdbase.add_login_arguments_group(disconnect_parser) + + # Multiconnect subcommand + multiconnect_help = "Bulk connect multiple iLOs using JSON configuration" + multiconnect_parser = subcommand_parser.add_parser( + "multiconnect", + help=multiconnect_help, + description=multiconnect_help + + """ + EXAMPLES: + # Generate template JSON file + computeopsmanagement multiconnect --input_file_json_template + # Generate precheck report without making applying changes + computeopsmanagement multiconnect --input_file servers.json --precheck + computeopsmanagement multiconnect --input_file servers.json --precheck --output report.json + # Onboard multiple servers from JSON file to COM + computeopsmanagement multiconnect --input_file servers.json + computeopsmanagement multiconnect --input_file servers.json --allow_ilo_reset + computeopsmanagement multiconnect --input_file servers.json --allow_ilo_reset --output report.json + """, + formatter_class=RawDescriptionHelpFormatter, + ) + + multiconnect_parser.add_argument( + "--input_file", + "-i", + dest="file", + help="Path to JSON file containing bulk server configuration", + required=False, + type=str, + default=None, + ) + + multiconnect_parser.add_argument( + "--input_file_json_template", + "-t", + dest="template_input_file", + help=f"Generate template JSON input file (writes {DEFAULT_TEMPLATE_FILE_NAME})", + action="store_const", + const=DEFAULT_TEMPLATE_FILE_NAME, + default=None, + ) + + # Create mutually exclusive group for precheck and allow_ilo_reset + exclusive_group = multiconnect_parser.add_mutually_exclusive_group() + + exclusive_group.add_argument( + "--allow_ilo_reset", + "-r", + dest="allow_ilo_reset", + help="If needed, allow iLO reset during bulk operations", + required=False, + action="store_true", + default=False, + ) + + multiconnect_parser.add_argument( + "--output", + "-of", + dest="output_file", + help="Output file for reports", + required=False, + type=str, + default=None, + ) + + exclusive_group.add_argument( + "--precheck", + "-c", + dest="precheck", + help="Perform precheck validation only (no onboarding or configuration)", + required=False, + action="store_true", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/DirectoryCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/DirectoryCommand.py new file mode 100644 index 0000000..a11018a --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/DirectoryCommand.py @@ -0,0 +1,876 @@ +### +# Copyright 2017 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Directory Command for rdmc""" + +import re +import sys +from argparse import Action, RawDescriptionHelpFormatter + +from redfish.ris.rmc_helper import IdTokenError, IloResponseError + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ResourceExists, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ResourceExists, + ReturnCodes, + ) + +__subparsers__ = ["ldap", "kerberos", "test"] + +PRIVKEY = { + 1: ("Login", "AssignedPrivileges"), + 2: ("RemoteConsolePriv", "OemPrivileges"), + 3: ("ConfigureUsers", "AssignedPrivileges"), + 4: ("ConfigureManager", "AssignedPrivileges"), + 5: ("VirtualMediaPriv", "OemPrivileges"), + 6: ("VirtualPowerAndResetPriv", "OemPrivileges"), + 7: ("HostNICConfigPriv", "OemPrivileges"), + 8: ("HostBIOSConfigPriv", "OemPrivileges"), + 9: ("HostStorageConfigPriv", "OemPrivileges"), + 10: ("SystemRecoveryConfigPriv", "OemPrivileges"), + 11: ("ConfigureSelf", "AssignedPrivileges"), + 12: ("ConfigureComponents", "AssignedPrivileges"), +} + + +class _DirectoryParse(Action): + def __init__(self, option_strings, dest, nargs, **kwargs): + super(_DirectoryParse, self).__init__(option_strings, dest, nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_strings): + """Helper for parsing options""" + if option_strings.endswith("disable"): + setattr(namespace, self.dest, False) + elif option_strings.endswith("enable"): + setattr(namespace, self.dest, True) + elif option_strings.endswith("enablelocalauth"): + setattr(namespace, self.dest, False) + elif option_strings.endswith("disablelocalauth"): + setattr(namespace, self.dest, True) + elif option_strings == "--removerolemap": + setattr(namespace, self.dest, {"remove": []}) + for role in next(iter(values)).split("#"): + role = role.replace('"', "") + if role: + namespace.roles["remove"].append(role) + elif option_strings == "--addrolemap": + setattr(namespace, self.dest, {"add": []}) + for role in next(iter(values)).split("#"): + role = role.replace('"', "") + if role and re.match(".*:.*", role): + privs = role.split(":")[0].split(";") + if len(privs) > 1: + for priv in privs: + try: + if priv and int(priv) > 12: + try: + parser.error("Invalid privilege number added %s." % priv) + except SystemExit: + raise InvalidCommandLineErrorOPTS("") + except ValueError: + try: + parser.error("Privileges must be added as numbers.") + except SystemExit: + raise InvalidCommandLineErrorOPTS("") + namespace.roles["add"].append(role) + else: + try: + parser.error("Supply roles to add in form :") + except SystemExit: + raise InvalidCommandLineErrorOPTS("") + elif option_strings == "--addsearch": + setattr(namespace, self.dest, {"add": []}) + for search in next(iter(values)).split(";"): + if search: + namespace.search["add"].append(search) + + elif option_strings == "--removesearch": + setattr(namespace, self.dest, {"remove": []}) + for search in next(iter(values)).split(";"): + if search: + namespace.search["remove"].append(search) + + +class DirectoryCommand: + """Update directory settings on the server""" + + def __init__(self): + self.ident = { + "name": "directory", + "usage": None, + "description": "\tAdd credentials, service address, two search strings, and enable" + "\n\tLDAP directory service, remote role groups (mapping), local custom role\n\t" + "IDs with privileges.\n\n\tTo view help on specific sub-commands" + " run: directory -h\n\n\tExample: directory ldap -h\n", + "summary": "Update directory settings, add/delete directory roles, and test directory " + "settings on the currently logged in server.", + "aliases": ["ad", "activedirectory"], + "auxcommands": ["IloAccountsCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main directory Function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + ident_subparser = False + for cmnd in __subparsers__: + if cmnd in line: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + ident_subparser = True + break + if not ident_subparser: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.directoryvalidation(options) + + if self.rdmc.app.getiloversion() < 5.140: + raise IncompatibleiLOVersionError("Directory settings are only available on " "iLO 5 1.40 or greater.") + results = None + if options.command.lower() == "ldap" or ( + (True if options.ldap_kerberos == "ldap" else False) if hasattr(options, "ldap_kerberos") else False + ): + try: + results = self.rdmc.app.select(selector="AccountService.", path_refresh=True)[0].dict + path = results[self.rdmc.app.typepath.defs.hrefstring] + oem = results["Oem"][self.rdmc.app.typepath.defs.oemhp] + local_auth = results["LocalAccountAuth"] + results = results["LDAP"] + if "RemoteRoleMapping" not in results: + rolemap = {"RemoteRoleMapping": {}} + results.update(rolemap) + name = "LDAP" + except (KeyError, IndexError): + raise NoContentsFoundForOperationError("Unable to gather LDAP settings.") + + elif options.command.lower() == "kerberos" or ( + (True if options.ldap_kerberos == "kerberos" else False) if hasattr(options, "ldap_kerberos") else False + ): + try: + results = self.rdmc.app.select(selector="AccountService.", path_refresh=True)[0].dict + path = results[self.rdmc.app.typepath.defs.hrefstring] + oem = results["Oem"][self.rdmc.app.typepath.defs.oemhp] + local_auth = results["LocalAccountAuth"] + results = results["ActiveDirectory"] + if "RemoteRoleMapping" not in results: + rolemap = {"RemoteRoleMapping": {}} + results.update(rolemap) + name = "ActiveDirectory" + except (KeyError, IndexError): + raise NoContentsFoundForOperationError("Unable to gather Kerberos settings.") + + if results: + keytab = None + payload = {} + if hasattr(options, "keytab"): + keytab = options.keytab + try: + directory_settings = self.directory_helper(results, options) + if "ldap" in line and options.roles and "add" in options.roles: + role_val = options.roles["add"] + for r in role_val: + if ";" in r.split(":")[0]: + name = "ActiveDirectory" + break + except IndexError: + directory_settings = self.directory_helper(results, options) + + if directory_settings: + payload[name] = directory_settings + + if hasattr(options, "authmode"): + if options.authmode: + payload.update( + {"Oem": {"Hpe": {"DirectorySettings": {"LdapAuthenticationMode": options.authmode}}}} + ) + + if not payload and not keytab: + if getattr(options, "json", False): + # self.rdmc.ui.print_out_json({name: results, 'LocalAccountAuth': local_auth, + # "Oem": {"Hpe": oem}}) + text_content = self.print_s(results, oem, local_auth, name) + self.rdmc.ui.print_out_json(text_content) + else: + self.print_settings(results, oem, local_auth, name) + + if payload: + priv_patches = {} + try: + if hasattr(options, "localauth"): + if options.localauth: + payload["LocalAccountAuth"] = "Enabled" if options.localauth else "Disabled" + elif local_auth: + payload["LocalAccountAuth"] = "Enabled" if local_auth else "Disabled" + except (NameError, AttributeError): + payload["LocalAccountAuth"] = "Disabled" + try: + maps = {} + if payload.get("LDAP"): + maps = payload["LDAP"].get("RemoteRoleMapping", {}) + elif payload.get("ActiveDirectory"): + maps = payload["ActiveDirectory"].get("RemoteRoleMapping", {}) + # Check if we need to modify roles after creating + for mapping in maps: + privs = mapping["LocalRole"].split(";") + if len(privs) > 1: + privs = [int(priv) for priv in privs if priv] + + if 10 in privs: + user_privs = self.auxcommands["iloaccounts"].getsesprivs() + if "SystemRecoveryConfigPriv" not in list(user_privs.keys()): + raise IdTokenError( + "The currently logged in account " + "must have the System Recovery Config privilege to " + "add the System Recovery Config privilege to a local " + "role group." + ) + + priv_patches[mapping["RemoteGroup"]] = privs + mapping["LocalRole"] = "ReadOnly" + except Exception as excp: + self.rdmc.ui.error(excp) + self.rdmc.ui.printer("Changing settings...\n") + try: + self.rdmc.app.patch_handler(path, payload) + except IloResponseError as excp: + if not results["ServiceEnabled"]: + self.rdmc.ui.error( + "You must enable this directory service before or " + "during assignment of username and password. Try adding the flag " + "--enable.\n", + excp, + ) + else: + raise IloResponseError + if priv_patches: + self.update_mapping_privs(priv_patches) + if keytab: + path = oem["Actions"][next(iter(oem["Actions"]))]["target"] + self.rdmc.ui.printer("Adding keytab...\n") + self.rdmc.app.post_handler(path, {"ImportUri": keytab}) + elif options.command.lower() == "test": + self.test_directory(options, json=getattr(options, "json", False)) + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def update_mapping_privs(self, roles_to_update): + """Helper function to update created role mappings to match user privileges. + + :param roles_to_update: Dictionary of privileges to update. + :type roles_to_update: dict + """ + self.rdmc.ui.printer("Updating privileges of created role maps...\n") + try: + results = self.rdmc.app.select(selector="AccountService.", path_refresh=True)[0].dict + roles = self.rdmc.app.getcollectionmembers(self.rdmc.app.getidbytype("RoleCollection.")[0]) + except (KeyError, IndexError): + raise NoContentsFoundForOperationError( + "Unable to gather Role settings. Roles may not " "be updated to match privileges requested." + ) + for rolemap in results["LDAP"]["RemoteRoleMapping"]: + for role in roles: + if role["RoleId"] == rolemap["LocalRole"]: + role["RemoteGroup"] = rolemap["RemoteGroup"] + break + for role in roles: + privs = {"AssignedPrivileges": [], "OemPrivileges": []} + + for update_role in list(roles_to_update.keys()): + if role.get("RemoteGroup", None) == update_role: + for priv in roles_to_update[update_role]: + privs[PRIVKEY[priv][1]].append(PRIVKEY[priv][0]) + try: + self.rdmc.app.patch_handler(role["@odata.id"], privs) + self.rdmc.ui.printer("Updated privileges for %s\n" % update_role) + except IloResponseError as excp: + self.rdmc.ui.error("Unable to update privileges for %s\n" % update_role, excp) + break + + def directory_helper(self, settings, options): + """Helper function to set the payload based on options and arguments + + :param settings: dictionary to change + :type settings: dict. + :param options: list of options + :type options: list. + """ + + payload = {} + serviceaddress = None + + if hasattr(options, "serviceaddress"): + if isinstance(options.serviceaddress, str): + serviceaddress = options.serviceaddress + if serviceaddress == '""' or serviceaddress == "''": + serviceaddress = "" + if hasattr(options, "port"): + if isinstance(options.port, str): + if serviceaddress is None: + serviceaddress = settings["ServiceAddresses"][0] + serviceaddress = serviceaddress + ":" + options.port + if hasattr(options, "realm"): + if isinstance(options.realm, str): + if serviceaddress is None: + serviceaddress = settings["ServiceAddresses"][0] + if options.realm == '""' or options.realm == "''": + options.realm = "" + serviceaddress = serviceaddress + "@" + options.realm + if serviceaddress is not None: + payload["ServiceAddresses"] = [serviceaddress] + + if hasattr(options, "enable"): + if options.enable is not None: + payload["ServiceEnabled"] = options.enable + + if hasattr(options, "ldap_username") and hasattr(options, "ldap_password"): + if options.ldap_username and options.ldap_password: + payload.update( + { + "Authentication": { + "Username": options.ldap_username, + "Password": options.ldap_password, + } + } + ) + + if hasattr(options, "roles"): + if options.roles: + payload["RemoteRoleMapping"] = self.role_helper(options.roles, settings["RemoteRoleMapping"]) + + if hasattr(options, "search"): + if options.search: + payload.update( + { + "LDAPService": { + "SearchSettings": self.search_helper( + options.search, + settings["LDAPService"]["SearchSettings"], + ) + } + } + ) + return payload + + def test_directory(self, options, json=False): + """Function to perform directory testing + + :param options: namespace of custom parser attributes which contain the original command + arguments for 'start/stop/viewresults' + :type options: namespace + :param json: Bool to print in json format or not. + :type json: bool. + """ + results = self.rdmc.app.select(selector="HpeDirectoryTest.", path_refresh=True)[0].dict + if options.start_stop_view.lower() == "start": + path = None + for item in results["Actions"]: + if "StartTest" in item: + path = results["Actions"][item]["target"] + break + if not path: + raise NoContentsFoundForOperationError("Unable to start directory test.") + self.rdmc.ui.printer( + "Starting the directory test. Monitor results with " 'command: "directory test viewresults".\n' + ) + self.rdmc.app.post_handler(path, {}) + elif options.start_stop_view.lower() == "stop": + path = None + for item in results["Actions"]: + if "StopTest" in item: + path = results["Actions"][item]["target"] + break + if not path: + raise NoContentsFoundForOperationError("Unable to stop directory test.") + self.rdmc.ui.printer("Stopping the directory test.\n") + self.rdmc.app.post_handler(path, {}) + elif options.start_stop_view.lower() == "viewresults": + if getattr(options, "json", False): + self.rdmc.ui.print_out_json(results["TestResults"]) + else: + for test in results["TestResults"]: + self.rdmc.ui.printer("Test: %s\n" % test["TestName"]) + self.rdmc.ui.printer("------------------------\n") + self.rdmc.ui.printer("Status: %s\n" % test["Status"]) + self.rdmc.ui.printer("Notes: %s\n\n" % test["Notes"]) + + def print_s(self, settings, oem_settings, local_auth_setting, name): + setting = "Kerberos" if name == "ActiveDirectory" else name + enable = str(settings["ServiceEnabled"]) + serviceaddress = settings["ServiceAddresses"][0] + address = serviceaddress if serviceaddress else "Not Set" + username = settings["Authentication"]["Username"] + dis_name = username if username else "Not Set" + auth = local_auth_setting + content = { + "Settings": setting, + "Enabled": enable, + "Service Address": address, + "Distinguished Name": dis_name, + "Local Account Authorization": auth, + } + if name.lower() == "activedirectory": + address_settings = oem_settings["KerberosSettings"] + port = address_settings["KDCServerPort"] + realm = address_settings["KerberosRealm"] if address_settings["KerberosRealm"] else "Not Set" + content.update({"Port": port, "Realm": realm}) + + else: + address_settings = oem_settings["DirectorySettings"] + port = address_settings["LdapServerPort"] + authmode = address_settings["LdapAuthenticationMode"] + content.update({"Port": port, "Authentication Mode": authmode}) + addsearch = [] + try: + count = 1 + for search in settings["LDAPService"]["SearchSettings"]["BaseDistinguishedNames"]: + # search = (count, search) + search = "Search %s: %s" % (count, search) + addsearch.append(search) + count += 1 + content.update({"Search Settings": addsearch}) + except KeyError: + search = "No Search Settings" + content.update({"Search Settings": {search}}) + addval = [] + if "RemoteRoleMapping" in settings: + if len(settings["RemoteRoleMapping"]) > 0: + for role in settings["RemoteRoleMapping"]: + roleadd = { + "Local Role": role["LocalRole"], + "Remote Group": role["RemoteGroup"], + } + addval.append(roleadd) + content.update({"Remote Role Mapping(s)": addval}) + else: + addval = {"No Remote Role Mappings"} + content.update({"Remote Role Mapping(s)": addval}) + else: + content.update({"No Remote Role Mappings"}) + return content + + def print_settings(self, settings, oem_settings, local_auth_setting, name): + """Pretty print settings of LDAP or Kerberos + + :param settings: settings to print + :type settings: dict. + :param oem_settings: oem_settings to print + :type oem_settings: dict. + :param local_auth_settings: local authorization setting + :type local_auth_settings: str. + :param name: type of setting (activedirectory or ldap) + :type name: str. + """ + + self.rdmc.ui.printer("%s settings:\n" % ("Kerberos" if name == "ActiveDirectory" else name)) + self.rdmc.ui.printer("--------------------------------\n") + self.rdmc.ui.printer("Enabled: %s\n" % str(settings["ServiceEnabled"])) + + serviceaddress = settings["ServiceAddresses"][0] + + self.rdmc.ui.printer("Service Address: %s\n" % (serviceaddress if serviceaddress else "Not Set")) + username = settings["Authentication"]["Username"] + self.rdmc.ui.printer("Distinguished Name: %s\n" % (username if username else "Not Set")) + + self.rdmc.ui.printer("Local Account Authorization: %s\n" % local_auth_setting) + + if name.lower() == "activedirectory": + address_settings = oem_settings["KerberosSettings"] + self.rdmc.ui.printer("Port: %s\n" % address_settings["KDCServerPort"]) + + self.rdmc.ui.printer( + "Realm: %s\n" % (address_settings["KerberosRealm"] if address_settings["KerberosRealm"] else "Not Set") + ) + else: + address_settings = oem_settings["DirectorySettings"] + self.rdmc.ui.printer("Port: %s\n" % address_settings["LdapServerPort"]) + self.rdmc.ui.printer("Authentication Mode: %s\n" % address_settings["LdapAuthenticationMode"]) + + try: + self.rdmc.ui.printer("Search Settings:\n") + count = 1 + for search in settings["LDAPService"]["SearchSettings"]["BaseDistinguishedNames"]: + self.rdmc.ui.printer("\tSearch %s: %s\n" % (count, search)) + count += 1 + except KeyError: + self.rdmc.ui.printer("\tNo Search Settings\n") + + if "RemoteRoleMapping" in settings: + self.rdmc.ui.printer("Remote Role Mapping(s):\n") + if len(settings["RemoteRoleMapping"]) > 0: + for role in settings["RemoteRoleMapping"]: + self.rdmc.ui.printer("\tLocal Role: %s\n" % role["LocalRole"]) + self.rdmc.ui.printer("\tRemote Group: %s\n" % role["RemoteGroup"]) + else: + self.rdmc.ui.printer("\tNo Remote Role Mappings.\n") + else: + self.rdmc.ui.printer("No Remote Role Mappings.\n") + + def role_helper(self, new_roles, curr_roles): + """Helper to prepare adding and removing roles for patching + + :param new_roles: dictionary of new roles to add or remove + :type new_roles: dict. + :param curr_roles: list of current roles on the system + :type curr_roles: list. + """ + final_roles = curr_roles + if "add" in new_roles: + for role in new_roles["add"]: + role = role.split(":", 1) + if not self.duplicate_group(role[1], curr_roles): + if len(curr_roles) > 0: + final_roles.append({"LocalRole": role[0], "RemoteGroup": role[1]}) + else: + final_roles = [{"LocalRole": role[0], "RemoteGroup": role[1]}] + curr_roles = final_roles + + else: + raise ResourceExists('Group DN "%s" already exists.' % role[1].split(":")[0]) + if "remove" in new_roles: + removed = False + for role in new_roles["remove"]: + removed = False + for item in reversed(final_roles): + if item["LocalRole"] == role: + if len(curr_roles) > 1: + del final_roles[final_roles.index(item)] + removed = True + break + elif len(curr_roles) == 1: + del final_roles[final_roles.index(item)] + final_roles = [{}] + removed = True + break + if not removed: + raise InvalidCommandLineError("Unable to find local role %s to delete" % role) + return final_roles + + def duplicate_group(self, group_dn, curr_roles): + """Checks if new role is a duplicate + + :param group_dn: group domain name from user + :type group_dn: str. + :param curr_roles: list of current roles + :type curr_roles: list. + """ + group_dn = group_dn.split(":")[0] + for item in curr_roles: + comp_dn = item["RemoteGroup"].split(":")[0] + if comp_dn == group_dn: + return True + return False + + def search_helper(self, new_searches, curr_searches): + """Helper to prepare search strings for patching + + :param new_serches: dictionary of new searches to add + :type new_searches: dict. + :param curr_searches: list of current searches + :type curr_searches: dict. + """ + final_searches = curr_searches + + if "add" in new_searches: + if "BaseDistinguishedNames" in final_searches: + for search in new_searches["add"]: + for key, value in final_searches.items(): + for v in value: + if search.lower() == v.lower(): + raise ResourceExists('Search Setting "%s" already exists.\n' % search) + + final_searches["BaseDistinguishedNames"].append(search) + else: + final_searches["BaseDistinguishedNames"] = new_searches["add"] + elif "remove" in new_searches: + to_remove = [] + + if "BaseDistinguishedNames" not in curr_searches: + raise NoContentsFoundForOperationError("No search strings to remove") + + for search in new_searches["remove"]: + if search in curr_searches["BaseDistinguishedNames"]: + to_remove.append(search) + else: + raise InvalidCommandLineError("Unable to find search %s to delete" % search) + for item in to_remove: + final_searches["BaseDistinguishedNames"].remove(item) + + if not final_searches["BaseDistinguishedNames"]: + sys.stdout.write("Attempting to delete all searches.\n") + final_searches["BaseDistinguishedNames"].append("") + + return final_searches + + def directoryvalidation(self, options): + """directory validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def options_argument_group(self, parser): + """Additional argument + + :param parser: The parser to add the removeprivs option group to + :type parser: ArgumentParser/OptionParser + """ + + parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + default_parser = subcommand_parser.add_parser("default") + + default_parser.add_argument( + "ldap_kerberos", + help="Specify LDAP or Kerberos configuration settings", + metavar="LDAP_KERBEROS", + nargs="?", + type=str, + default=None, + ) + self.cmdbase.add_login_arguments_group(default_parser) + + privilege_help = ( + "\n\nPRIVILEGES:\n\t1: Login\n\t2: Remote Console\n\t" + "3: User Config\n\t4: iLO (Manager) Config\n\t5: Virtual Media\n\t" + "6: Virtual Power and Reset\n\t7: Host NIC Config\n\t8: Host Bios Config\n\t9: " + "Host Storage Config\n\t10: System Recovery Config\n\t11: Self Password Change\n\t" + "12: Configure Components\n\n\tLOCAL ROLES:\n\tReadOnly\n\tOperator\n\tAdministrator" + "\n\n\tNOTE: The Self Password Change privilege is automatically added to roles with " + "the Login privilege." + ) + ldap_help = "Show, add or modify properties pertaining to iLO LDAP Configuration." + ldap_parser = subcommand_parser.add_parser( + __subparsers__[0], + help=ldap_help, + description=ldap_help + "\n\n\tSimply show LDAP configuration:\n\t\tdirectory ldap\n\n" + "To modify the LDAP username, password, service address, search strings or " + "enable/disable LDAP.\n\t\tdirectory ldap " + '--serviceaddress x.x.y.z --addsearch "string1;string2" --enable.\n\n\tTo add role ' + 'mapping.\n\t\tdirectory ldap --addrolemap "LocalRole1:' + 'RemoteGroup3#LocalRole2:RemoteGroup4:SID".\n\n\tTo remove role mapping.\n\t\t' + "directory ldap --removerolemap LocalRole1#LocalRole2." + privilege_help, + formatter_class=RawDescriptionHelpFormatter, + ) + ldap_parser.add_argument( + "ldap_username", + help="The LDAP username used in verifying AD (optional outside of '--enable' and" "'--disable')", + metavar="USERNAME", + nargs="?", + type=str, + default=None, + ) + ldap_parser.add_argument( + "ldap_password", + help="The LDAP password used in verifying AD (optional outside of '--enable' and" "'--disable')", + metavar="PASSWORD", + nargs="?", + type=str, + default=None, + ) + ldap_parser.add_argument( + "--enable", + "--disable", + dest="enable", + type=str, + nargs="*", + action=_DirectoryParse, + help="Optionally add this flag to enable LDAP services.", + default=None, + ) + ldap_parser.add_argument( + "--addsearch", + "--removesearch", + dest="search", + nargs="*", + action=_DirectoryParse, + help="Optionally add this flag to add or remove search strings for " "generic LDAP services.", + type=str, + default={}, + ) + ldap_parser.add_argument( + "--serviceaddress", + dest="serviceaddress", + help="Optionally include this flag to set the service address of the LDAP Services.", + default=None, + ) + ldap_parser.add_argument( + "--port", + dest="port", + help="Optionally include this flag to set the port of the LDAP services.", + default=None, + ) + ldap_parser.add_argument( + "--addrolemap", + "--removerolemap", + dest="roles", + nargs="*", + action=_DirectoryParse, + help="Optionally add this flag to add or remove Role Mapping(s) for the LDAP." + " Remove EX: --removerolemap LocalRole1#LocalRole2 " + 'Add EX: --addrolemap "LocalRole1:RemoteGroup3#LocalRole2:RemoteGroup4\n\n"' + 'SID EX: --addrolemap "LocalRole1:RemoteGroup2:SID#LocalRole2:RemoteGroup5:SID"' + "\n\nNOTE 1: Create a custom local role group (and subsequently assign to a role map)" + "by adding the numbers associated with privilege(s) desired separated by a semicolon" + "(;)\n\nNOTE 2: SID is optional", + type=str, + default={}, + ) + ldap_parser.add_argument( + "--enablelocalauth", + "--disablelocalauth", + dest="localauth", + nargs="*", + type=str, + action=_DirectoryParse, + help="Optionally include this flag if you wish to enable or disable authentication " "for local accounts.", + default=None, + ) + ldap_parser.add_argument( + "--authentication", + dest="authmode", + choices=["DefaultSchema", "ExtendedSchema"], + help="Optionally include this flag if you would like to choose a LDAP authentication " + "mode Valid choices are: DefaultSchema (Directory Default Schema or Schema-free) or " + "ExtendedSchema (HPE Extended Schema).", + default=None, + ) + self.cmdbase.add_login_arguments_group(ldap_parser) + + self.options_argument_group(ldap_parser) + + kerberos_help = "Show, add or modify properties pertaining to AD Kerberos Configuration." + kerberos_parser = subcommand_parser.add_parser( + __subparsers__[1], + help=kerberos_help, + description=kerberos_help + "\n\nExamples:\n\nShow Kerberos specific AD/LDAP configuration " + "settings.\n\tdirectory kerberos\n\nShow current AD Kerberos configuration." + "\n\tdirectory kerberos\n\nAlter kerberos service address, AD realm and Port.\n\t" + "directory kerberos --serviceaddress x.x.y.z --port 8888 --realm adrealm1", + formatter_class=RawDescriptionHelpFormatter, + ) + kerberos_parser.add_argument( + "--serviceaddress", + dest="serviceaddress", + help="Optionally include this flag to set the Kerberos serviceaddress.", + default=None, + ) + kerberos_parser.add_argument( + "--port", + dest="port", + help="Optionally include this flag to set the Kerberos port.", + default=None, + ) + kerberos_parser.add_argument( + "--realm", + dest="realm", + help="Optionally include this flag to set the Kerberos realm.", + default=None, + ) + kerberos_parser.add_argument( + "--keytab", + dest="keytab", + help="Optionally include this flag to import a Kerberos Keytab by it's URI location.", + default="", + ) + kerberos_parser.add_argument( + "--enable", + "--disable", + dest="enable", + type=str, + nargs="*", + action=_DirectoryParse, + help="Optionally add this flag to enable or disable Kerberos services.", + default=None, + ) + self.cmdbase.add_login_arguments_group(kerberos_parser) + + self.options_argument_group(kerberos_parser) + + directory_test_help = ( + "Start, stop or view results of an AD/LDAP test which include: ICMP, " + "Domain Resolution, Connectivity, Authentication, Bindings, LOM Object and User " + "Context tests." + ) + directory_test_parser = subcommand_parser.add_parser( + __subparsers__[2], + help=directory_test_help, + description=directory_test_help + "\n\nExamples:\n\nStart a directory test:\n\tdirectory test " + "start\n\nStop a directory test:\n\tdirectory test stop\n\nView results of the last " + "directory test:\n\tdirectory test viewresults", + formatter_class=RawDescriptionHelpFormatter, + ) + directory_test_parser.add_argument( + "start_stop_view", + help="Start, stop, or view results on an AD/LDAP test.", + metavar="START, STOP, VIEWRESULTS", + default="viewresults", + ) + self.cmdbase.add_login_arguments_group(directory_test_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/DisableIloFunctionalityCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/DisableIloFunctionalityCommand.py new file mode 100644 index 0000000..ace8fbb --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/DisableIloFunctionalityCommand.py @@ -0,0 +1,204 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""iLO Functionality Command for rdmc""" + +import json + +try: + from rdmc_helper import ( + IncompatableServerTypeError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatableServerTypeError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class DisableIloFunctionalityCommand: + """Disables iLO functionality to the server""" + + def __init__(self): + self.ident = { + "name": "disableilofunctionality", + "usage": None, + "description": "Disable iLO functionality on the current logged in server." + "\n\texample: disableilofunctionality\n\n\tWARNING: this will" + " render iLO unable to respond to network operations.\n\n\t" + "Add the --force flag to ignore critical task checking.", + "summary": "disables iLO's accessibility via the network and resets " + "iLO. WARNING: This should be used with caution as it will " + "render iLO unable to respond to further network operations " + "(including REST operations) until iLO is re-enabled using the" + " RBSU menu.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main DisableIloFunctionalityCommand function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("disableilofunctionality command takes no arguments.") + + self.ilofunctionalityvalidation(options) + + select = "Manager." + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Manager. not found.") + + bodydict = results.resp.dict["Oem"][self.rdmc.app.typepath.defs.oemhp] + if bodydict["iLOFunctionalityRequired"]: + raise IncompatableServerTypeError( + "disableilofunctionality" + " command is not available. iLO functionality is required" + " and can not be disabled on this platform." + ) + + try: + for item in bodydict["Actions"]: + if "iLOFunctionality" in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = "iLOFunctionality" + + path = bodydict["Actions"][item]["target"] + body = {"Action": action} + break + except: + body = {"Action": "iLOFunctionality", "Target": "/Oem/Hp"} + + if self.ilodisablechecks(options): + self.rdmc.ui.warn( + "Disabling iLO functionality. iLO will be unavailable on the logged " + " in server until it is re-enabled manually.\n" + ) + + results = self.rdmc.app.post_handler(path, body, silent=True, service=True) + + if results.status == 200: + self.rdmc.ui.printer("[%d] The operation completed successfully.\n" % results.status) + else: + self.rdmc.ui.printer("[%d] iLO responded with the following info: \n" % results.status) + json_payload = json.loads(results._http_response.data) + try: + self.rdmc.ui.error("%s" % json_payload["error"]["@Message.ExtendedInfo"][0]["MessageId"]) + except: + self.rdmc.ui.error("An invalid or incomplete response was received: %s\n" % json_payload) + + else: + self.rdmc.ui.error( + "iLO is currently performing a critical task and " + "can not be safely disabled at this time. Please try again later.\n" + ) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def ilodisablechecks(self, options): + """Verify it is safe to actually disable iLO + + :param options: command line options + :type options: values, attributes of class obj + """ + + if options.force: + self.rdmc.ui.warn("Force Enabled: Ignoring critical operation/mode checking.\n") + return True + + else: + keyword_list = "idle", "complete" + + try: + results = self.rdmc.app.select(selector="UpdateService.")[0] + except: + raise NoContentsFoundForOperationError("UpdateService. not found.") + + try: + state = results.resp.dict["Oem"]["Hpe"]["State"].lower() + for val in keyword_list: + if val in state: + return True + return False + + except: + raise NoContentsFoundForOperationError("iLO state not identified") + + def ilofunctionalityvalidation(self, options): + """ilofunctionalityvalidation method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--force", + dest="force", + help="Ignore any critical task checking and force disable iLO.", + action="store_true", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ESKMCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ESKMCommand.py new file mode 100644 index 0000000..70e5d09 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ESKMCommand.py @@ -0,0 +1,134 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""ESKM Command for rdmc""" + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class ESKMCommand: + """Commands ESKM available actions""" + + def __init__(self): + self.ident = { + "name": "eskm", + "usage": None, + "description": "Clear the ESKM logs.\n\texample: eskm" + " clearlog\n\n\tTest the ESKM connections.\n\texample: eskm testconnections", + "summary": "Command for all ESKM available actions.", + "aliases": [], + "auxcommands": [], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main ESKMCommand function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if not len(args) == 1: + raise InvalidCommandLineError("eskm command only takes one parameter.") + + self.eskmvalidation(options) + + select = self.rdmc.app.typepath.defs.hpeskmtype + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("ESKM not found.") + + if args[0].lower() == "clearlog": + actionitem = "ClearESKMLog" + elif args[0].lower() == "testconnections": + actionitem = "TestESKMConnections" + else: + raise InvalidCommandLineError("Invalid command.") + + bodydict = results.resp.dict + try: + for item in bodydict["Actions"]: + if actionitem in item: + if self.rdmc.app.typepath.defs.isgen10: + actionitem = item.split("#")[-1] + + path = bodydict["Actions"][item]["target"] + break + except: + pass + + body = {"Action": actionitem} + self.rdmc.app.post_handler(path, body) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def eskmvalidation(self, options): + """eskmvalidation method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/EthernetCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/EthernetCommand.py new file mode 100644 index 0000000..3faec34 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/EthernetCommand.py @@ -0,0 +1,1242 @@ +### +# Copyright 2019 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Certificates Command for rdmc""" + +import copy +import json +import re +from argparse import RawDescriptionHelpFormatter +from collections import OrderedDict + +import redfish.ris +from redfish.ris.ris import RisInstanceNotFoundError +from redfish.ris.rmc_helper import ( + IloResponseError, + IncompatibleiLOVersionError, + InvalidPathError, +) +from redfish.ris.utils import diffdict, json_traversal, json_traversal_delete_empty + +try: + from rdmc_helper import ( + UI, + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + InvalidPropertyError, + NoDifferencesFoundError, + RdmcError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + UI, + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + InvalidPropertyError, + NoDifferencesFoundError, + RdmcError, + ReturnCodes, + ) + +__eth_file__ = "eth.json" +__subparsers__ = ["save", "load"] + + +class EthernetCommand: + """ Commands for Ethernet Management Controller and RDE supported NIC card \ + configuration on server """ + + def __init__(self): + self.ident = { + "name": "ethernet", + "usage": None, + "description": "Save or load from a JSON formatted file containing " + "properties pertaining to a system's iLO ethernet management controller. " + "Additionally enable/disable individual management controller NICs (including VNIC), " + "IPv4 and IPv6 addressing configuration and domain name servers." + "\n\tBy default the JSON file will be named 'eth.json'.\n\t" + "\n\t***Credentials for an iLO administrator level account must be provided as" + " well as the iLO base URL.***" + "\n\n\tSave ethernet management controller data.\n\texample: ethernet save " + "\n\n\tLoad ethernet management controller data.\n\texample: ethernet load" + "\n\n\tSave ethernet management controller data to a different file, silently\n\t" + "ethernet save -f --silent" + "\n\n\tLoad etherent management controller data from a different file, silently\n\t" + "ethernet load -f --silent" + "\n\n\tEnable network interfaces by listing each interface to be enabled. **Note**: " + "Non-existent interfaces will be omitted from configuration.\n\t " + "ethernet --enable_nic 1,2,3" + "\n\n\tDisable network interfaces by listing each interface to be disabled. **Note**: " + "Non-existent interfaces will be omitted from configuration.\n\t " + "ethernet --disable_nic 1,2,3" + "\n\n\tEnable virtual network interface of management network.\n\t" + "ethernet --enable_vnic" + "\n\n\tDisable virtual network interface of management network.\n\t " + "ethernet --disable_vnic" + "\n\n\tEnable Enhanced Download Performance.\n\t" + "ethernet --enable_enhanced_downloads" + "\n\n\tDisable Enhanced Download Performance.\n\t " + "ethernet --disable_enhanced_downloads" + "\n\n\tConfigure Domain Name Servers (DNS) in a list: \n\t" + "ethernet --nameservers 8.8.8.8,1.1.1.1,2.2.2.2 OR ethernet --nameservers " + "dns_resolver1.aws.com,dns_resolver2.aws.com" + "\n\n\tConfigure Static IPv4 Settings. Provide a list of network settings\n\t" + "ethernet --network_ipv4 ,," + "\n\n\tConfigure Proxy Settings. Provide a proxy server and port\n\t" + "ethernet --proxy http://proxy.company.net:8080" + "\n\n\tClear Proxy Settings. \n\t" + "ethernet --proxy None" + "\n\n\tConfigure Static IPv6 Settings. Provide a list of network settings\n\t" + "ethernet --network_ipv6 ,,", + "summary": "Command for configuring Ethernet Management Controller Interfaces and " "associated properties", + "aliases": [], + "auxcommands": ["IloResetCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self.save = None + self.load = None + self.eth_file = None # set in validation + self.eth_args = None + self.saved_inputline = None # set in validation + + def run(self, line, help_disp=False): + """Main Ethernet Management Controller Interfaces Run + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + ident_subparser = False + for cmnd in __subparsers__: + if cmnd in line: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + ident_subparser = True + break + if not ident_subparser: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True) + except: + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.ethernetvalidation(options) + + if "default" in options.command.lower(): + # flags are used to showing data with respect to enhance download + flag = True + return_flag = True + if options.enable_enhanced_downloads or options.disable_enhanced_downloads: + ilo_ver = self.rdmc.app.getiloversion() + self.rdmc.ui.printer("iLO Version is " + str(ilo_ver) + "\n") + if ilo_ver < 5.263: + raise IncompatibleiLOVersionError("Enhance download required minimum " "iLO Version iLO5.263") + # net_path = self.rdmc.app.getidbytype('NetworkProtocol.') + net_path = self.rdmc.app.typepath.defs.managerpath + "NetworkProtocol" + results = self.rdmc.app.get_handler(net_path, silent=True, service=True).dict + body = dict() + body["Oem"] = {} + body["Oem"]["Hpe"] = {} + if options.enable_enhanced_downloads: + if results["Oem"]["Hpe"]["EnhancedDownloadPerformanceEnabled"]: + self.rdmc.ui.printer( + "Enhanced Download Performance " "already enabled!! \n", + verbose_override=True, + ) + return_flag = False + else: + self.rdmc.ui.printer("Enabling enhanced " "downloads...\n", verbose_override=True) + body["Oem"]["Hpe"]["EnhancedDownloadPerformanceEnabled"] = True + flag = False + else: + if not results["Oem"]["Hpe"]["EnhancedDownloadPerformanceEnabled"]: + self.rdmc.ui.printer( + "Enhanced Download Performance " "already disabled!!\n\n", + verbose_override=True, + ) + return_flag = False + else: + self.rdmc.ui.printer("Disabling enhanced " "downloads...\n", verbose_override=True) + body["Oem"]["Hpe"]["EnhancedDownloadPerformanceEnabled"] = False + flag = False + if options.enable_vnic or options.disable_vnic: + self.rdmc.app.patch_handler(net_path, body, service=False, silent=True) + elif return_flag: + self.rdmc.app.patch_handler(net_path, body, service=False, silent=False) + if options.enable_vnic or options.disable_vnic: + results = self.rdmc.app.get_handler( + self.rdmc.app.typepath.defs.managerpath, service=True, silent=True + ).dict + body = dict() + body["Oem"] = {} + body["Oem"]["Hpe"] = {} + if options.enable_vnic: + if results["Oem"]["Hpe"]["VirtualNICEnabled"]: + self.rdmc.ui.printer("Virtual NIC already enabled!!\n", verbose_override=True) + return ReturnCodes.SUCCESS + self.rdmc.ui.printer("Enabling Virtual NIC...\n", verbose_override=True) + body["Oem"]["Hpe"]["VirtualNICEnabled"] = True + else: + if not results["Oem"]["Hpe"]["VirtualNICEnabled"]: + self.rdmc.ui.printer("Virtual NIC already disabled!!\n", verbose_override=True) + return ReturnCodes.SUCCESS + self.rdmc.ui.printer("Disabling Virtual NIC...\n", verbose_override=True) + body["Oem"]["Hpe"]["VirtualNICEnabled"] = False + self.rdmc.app.patch_handler( + self.rdmc.app.typepath.defs.managerpath, + body, + service=False, + silent=False, + ) + self.rdmc.ui.printer("Warning: Resetting iLO...\n") + self.auxcommands["iloreset"].run("") + self.rdmc.ui.printer("You will need to re-login to access this system...\n") + elif flag and return_flag: + data = self.get_data(options) + get_data = True + for inst in data: + if "ethernetinterface" in inst.lower() or "ethernetnetworkinterface" in inst.lower(): + for path in data[inst]: + if ( + "managers/1/ethernetinterfaces/1" in path.lower() + or "managers/1/ethernetnetworkinterfaces/1" in path.lower() + ): # only process managers interfaces + if options.enable_nic or options.disable_nic: + get_data = False + self.enable_disable_nics(data[inst][path], options) + + if ( + options.network_ipv4 or options.network_ipv6 or options.nameservers or options.proxy + ) and "virtual" not in data[inst][path]["Name"].lower(): + get_data = False + self.configure_static_settings(data[inst][path], options) + if options.proxy: + return ReturnCodes.SUCCESS + if "networkprotocol" in inst.lower(): + for path in data[inst]: + if "managers/1/networkprotocol" in path.lower(): + if options.enable_enhanced_downloads or options.disable_enhanced_downloads: + get_data = False + if get_data: + self.output_data(data, options, get_data) + else: + self.load_main_function(options, data) + + if "save" in options.command.lower(): + self.save = True + self.load = False + if "save" in options.command.lower() and not options.ethfilename: + self.rdmc.ui.printer("Saving configurations to default file '%s'.\n" % self.eth_file) + self.output_data(self.get_data(options), options, False) + + elif "load" in options.command.lower(): + self.save = False + self.load = True + if options.force_network_config: + self.load_main_function(options) + else: + self.rdmc.ui.printer("Skipping network configurations as " "--force_network_config is not included.\n") + + return ReturnCodes.SUCCESS + + def get_data(self, options): + """Obtain data for Ethernet Management Interfaces, Ethernet System Interfaces, \ + Manager Network Services and iLO Date Time Types and subsequently save to a dictionary \ + :param options: command line options + :type options: attribute + """ + + network_data_collection = eth_iface = manager_network_services = ilo_date_time = OrderedDict() + + if self.rdmc.app.redfishinst.is_redfish: + try: + iface_results = self.rdmc.app.select(selector="ethernetinterface.") + except RisInstanceNotFoundError: + self.rdmc.ui.printer("Type 'EthernetInterface.' not available.\n") + else: + try: + iface_results = self.rdmc.app.select(selector="ethernetnetworkinterface.") + except RisInstanceNotFoundError: + self.rdmc.ui.printer("Type 'EthernetNetworkInterface.' not available.\n") + + for inst in iface_results: + try: + eth_iface[inst.type].update({inst.path: inst.dict}) + except KeyError: + eth_iface[inst.type] = {inst.path: inst.dict} + continue + except AttributeError: + self.rdmc.ui.printer("Missing instance data for type '%s' : '%s'" % (inst.type, inst.path)) + if self.rdmc.app.typepath.defs.isgen9: + selector_content = self.rdmc.app.select(selector="ManagerNetworkService.0.11.1.networkprotocol") + else: + selector_content = self.rdmc.app.select(selector="networkprotocol.") + for inst in selector_content: + try: + manager_network_services[inst.type].update({inst.path: inst.dict}) + except KeyError: + manager_network_services[inst.type] = {inst.path: inst.dict} + continue + except AttributeError: + self.rdmc.ui.printer("Missing instance data for type '%s' : '%s'" % (inst.type, inst.path)) + + for inst in self.rdmc.app.select(selector="datetime."): + try: + ilo_date_time[inst.type].update({inst.path: inst.dict}) + except KeyError: + ilo_date_time[inst.type] = {inst.path: inst.dict} + continue + except AttributeError: + self.rdmc.ui.printer("Missing instance data for type '%s' : '%s'" % (inst.type, inst.path)) + + network_data_collection.update(eth_iface) + network_data_collection.update(manager_network_services) + network_data_collection.update(ilo_date_time) + + return network_data_collection + + def output_data(self, data, options, get_only=False): + """ Output data in requested json file or on console + :param data: dictionary containing ethernet configuration data: + :type data: dictionary + :param options: command line options + :type options: attribute + :param get_only: parameter to only output to console (do not save data to file). \ + Essentially a targeted \'get\' command. + :type: boolean + """ + + outdata = list() + + outdata.append(self.rdmc.app.create_save_header()) + + for _type in data: + for path in data.get(_type): + temp = dict() + try: + temp[_type.split("#")[-1]].update({path: self.rdmc.app.removereadonlyprops(data[_type][path])}) + outdata.append(temp) + except KeyError: + temp[_type.split("#")[-1]] = {path: self.rdmc.app.removereadonlyprops(data[_type][path])} + outdata.append(temp) + except Exception: + pass + + if self.eth_file and not get_only: + if options.encryption: + with open(self.eth_file, "wb") as outfile: + outfile.write( + Encryption().encrypt_file( + json.dumps(outdata, indent=2, cls=redfish.ris.JSONEncoder), + options.encryption, + ) + ) + else: + with open(self.eth_file, "w") as outfile: + outfile.write(json.dumps(outdata, indent=2, cls=redfish.ris.JSONEncoder)) + else: + if options.json: + self.rdmc.ui.print_out_json_ordered(outdata) + else: + UI().print_out_human_readable(outdata) + + def enable_disable_nics(self, data, options): + if options.disable_nic: + for ident in re.split("[, ]", options.disable_nic): + try: + if int(data.get("Id")) == int(ident): + data.update({"InterfaceEnabled": False}) + break + except ValueError: + if data.get("Name") == ident: + data.update({"InterfaceEnabled": False}) + break + + if options.enable_nic: + for ident in re.split("[, ]", options.enable_nic): + try: + if int(data.get("Id")) == int(ident): + data.update({"InterfaceEnabled": True}) + break + except ValueError: + if data.get("Name") == ident: + data.update({"InterfaceEnabled": True}) + break + + def configure_static_settings(self, data, options): + if options.network_ipv4: + usr_data = re.split("[, ]", options.network_ipv4) + if len(usr_data) > 2: + data["IPv4Addresses"][0] = { + "Address": usr_data[0], + "Gateway": usr_data[1], + "SubnetMask": usr_data[2], + } + data["DHCPv4"].update({"DHCPEnabled": False}) + data["Oem"][self.rdmc.app.typepath.defs.oemhp]["DHCPv4"].update({"Enabled": False}) + else: + raise InvalidCommandLineErrorOPTS( + "An invalid number of arguments provided to " + " quick networking configuration. Check '--network_ipv4' entry." + ) + else: + del data["IPv4Addresses"] + + if options.network_ipv6: + usr_data = re.split("[, ]", options.network_ipv6) + if len(usr_data) > 2: + next(iter(data["IPv6Addresses"])).update({"Address": usr_data[0], "PrefixLength": usr_data[2]}) + data["IPv6DefaultGateway"] = options.network_ipv6[1] + data["DHCPv6"].update({"DHCPEnabled": False}) + data["Oem"][self.rdmc.app.typepath.defs.oemhp]["DHCPv6"].update({"Enabled": False}) + else: + raise InvalidCommandLineErrorOPTS( + "An invalid number of arguments provided to " + " quick networking configuration. Check '--network_ipv6' entry." + ) + else: + del data["IPv6Addresses"] + + if options.nameservers: + usr_data = re.split("[, ]", options.nameservers) + if len(usr_data) <= 3: + ipv6_list = list() + ipv4_list = list() + static_list = list() + for elem in usr_data: + oem_ipv4 = data["Oem"][self.rdmc.app.typepath.defs.oemhp]["IPv4"] + oem_ipv6 = data["Oem"][self.rdmc.app.typepath.defs.oemhp]["IPv6"] + if "::" in elem: + ipv6_list.append(elem) + elif "." in elem: + ipv4_list.append(elem) + static_list.append(elem) + oem_ipv6["DNSServers"] = ipv6_list + oem_ipv4["DNSServers"] = ipv4_list + data["StaticNameServers"] = static_list + else: + raise InvalidCommandLineErrorOPTS("Name Servers argument has to be less than or equal to 2") + else: + del data["StaticNameServers"] + del data["Oem"][self.rdmc.app.typepath.defs.oemhp]["IPv4"]["DNSServers"] + del data["Oem"][self.rdmc.app.typepath.defs.oemhp]["IPv6"]["DNSServers"] + + if options.proxy and options.proxy != "None": + body = dict() + body["Oem"] = {} + body["Oem"]["Hpe"] = {} + body["Oem"]["Hpe"]["WebProxyConfiguration"] = {} + proxy_body = body["Oem"]["Hpe"]["WebProxyConfiguration"] + proxy_body["ProxyServer"] = None + proxy_body["ProxyUserName"] = None + proxy_body["ProxyPassword"] = None + if "https" in options.proxy: + proxy_body["ProxyPort"] = 443 + else: + proxy_body["ProxyPort"] = 80 + if "@" in options.proxy: + proxy = options.proxy.split("@") + proxy_usr_pass = proxy[0] + proxy_srv_port = proxy[1] + if "//" in proxy_usr_pass: + proxy_usr_pass = proxy_usr_pass.split("//")[1] + if ":" in proxy_srv_port: + proxy = proxy_srv_port.split(":") + proxy_body["ProxyServer"] = proxy[0] + proxy_body["ProxyPort"] = int(proxy[1]) + else: + proxy_body["ProxyServer"] = proxy_srv_port + if ":" in proxy_usr_pass: + proxy = proxy_usr_pass.split(":") + proxy_body["ProxyPassword"] = proxy[1] + proxy_body["ProxyUserName"] = proxy[0] + else: + proxy_body["ProxyUserName"] = proxy_usr_pass + else: + proxy_srv_port = options.proxy + if "//" in proxy_srv_port: + proxy_srv_port = proxy_srv_port.split("//")[1] + if ":" in proxy_srv_port: + proxy = proxy_srv_port.split(":") + proxy_body["ProxyServer"] = proxy[0] + proxy_body["ProxyPort"] = int(proxy[1]) + else: + proxy_body["ProxyServer"] = proxy_srv_port + path = self.rdmc.app.getidbytype("NetworkProtocol.") + + if path and body: + self.rdmc.ui.printer("Enabling Proxy configuration...\n", verbose_override=True) + self.rdmc.app.patch_handler(path[0], body, service=False, silent=False) + elif options.proxy and options.proxy == "None": + body = dict() + body["Oem"] = {} + body["Oem"]["Hpe"] = {} + body["Oem"]["Hpe"]["WebProxyConfiguration"] = {} + proxy_body = body["Oem"]["Hpe"]["WebProxyConfiguration"] + proxy_body["ProxyServer"] = "" + proxy_body["ProxyPort"] = None + proxy_body["ProxyUserName"] = "" + proxy_body["ProxyPassword"] = None + path = self.rdmc.app.getidbytype("NetworkProtocol.") + + if path and body: + self.rdmc.ui.printer("Clearing Proxy configuration...\n", verbose_override=True) + self.rdmc.app.patch_handler(path[0], body, service=False, silent=False) + + json_traversal_delete_empty(data, None, None) + + def load_main_function(self, options, data=None): + """ + Load main function. Handles SSO and SSL/TLS Certificates, kickoff load + helper, load of patch file, get server status and issues system reboot + and iLO reset after completion of all post and patch commands. + :param options: command line options + :type options: attribute + :param data: data + :type data: optional + """ + + if self.rdmc.app.redfishinst.is_redfish: + _ = "ethernetinterface." + else: + _ = "ethernetnetworkinterface." + + if not data: + data = {} + try: + if options.encryption: + with open(self.eth_file, "rb") as file_handle: + data = json.loads(Encryption().decrypt_file(file_handle.read(), options.encryption)) + else: + with open(self.eth_file, "rb") as file_handle: + data = json.loads(file_handle.read()) + except: + raise InvalidFileInputError( + "Invalid file formatting found. Verify the file has a " "valid JSON format." + ) + if "load" in options.command.lower(): + for d in data: + for ilotype, subsect in d.items(): + _type = ilotype.split(".")[0] + for _path in subsect: + if not subsect[_path]: + continue + elif "ethernetinterface" in _type.lower() or "ethernetnetworkinterface" in _type.lower(): + if "managers" in _path.lower(): + self.load_ethernet_aux(_type, _path, d[ilotype][_path]) + elif "systems" in _path.lower(): + self.rdmc.ui.warn("Systems Ethernet Interfaces '%s' " "cannot be modified." % _path) + continue + elif "datetime" in _type.lower(): + if "StaticNTPServers" in list(subsect.get(_path).keys()): + # must set NTP Servers to static in OEM then reset iLO for StaticNTPServers + # property to appear in iLODateTime + if self.rdmc.app.redfishinst.is_redfish: + eth_config_type = "ethernetinterface" + else: + eth_config_type = "ethernetnetworkinterface" + for key in list(data.keys()): + if key.split(".")[0].lower() == eth_config_type: + eth_config_type = key + for _path in data[eth_config_type]: + if "managers" in _path.lower(): + try: + oemhp = self.rdmc.app.typepath.defs.oemhp + data[eth_config_type][_path]["DHCPv4"]["UseNTPServers"] = True + data[eth_config_type][_path]["DHCPv6"]["UseNTPServers"] = True + data[eth_config_type][_path]["Oem"][oemhp]["DHCPv4"][ + "UseNTPServers" + ] = True + data[eth_config_type][_path]["Oem"][oemhp]["DHCPv6"][ + "UseNTPServers" + ] = True + self.load_ethernet_aux( + eth_config_type, + _path, + data[eth_config_type][_path], + ) + except KeyError: + self.rdmc.ui.printer( + "Unable to configure " "'UseNTPServers' for '%s'.\n" % _path + ) + self.rdmc.ui.printer( + "iLO must be reset in order for " + "changes to static network time protocol servers to " + "take effect.\n" + ) + elif "default" in options.command.lower(): + for ilotype, subsect in data.items(): + _type = ilotype.split(".")[0] + for _path in subsect: + if not subsect[_path]: + continue + elif "ethernetinterface" in _type.lower() or "ethernetnetworkinterface" in _type.lower(): + if "managers" in _path.lower(): + self.load_ethernet_aux(_type, _path, data[ilotype][_path]) + elif "systems" in _path.lower(): + self.rdmc.ui.warn("Systems Ethernet Interfaces '%s' " "cannot be modified." % _path) + continue + elif "datetime" in _type.lower(): + if "StaticNTPServers" in list(subsect.get(_path).keys()): + # must set NTP Servers to static in OEM then reset iLO for StaticNTPServers + # property to appear in iLODateTime + if self.rdmc.app.redfishinst.is_redfish: + eth_config_type = "ethernetinterface" + else: + eth_config_type = "ethernetnetworkinterface" + for key in list(data.keys()): + if key.split(".")[0].lower() == eth_config_type: + eth_config_type = key + for _path in data[eth_config_type]: + if "managers" in _path.lower(): + try: + oemhp = self.rdmc.app.typepath.defs.oemhp + data[eth_config_type][_path]["DHCPv4"]["UseNTPServers"] = True + data[eth_config_type][_path]["DHCPv6"]["UseNTPServers"] = True + data[eth_config_type][_path]["Oem"][oemhp]["DHCPv4"][ + "UseNTPServers" + ] = True + data[eth_config_type][_path]["Oem"][oemhp]["DHCPv6"][ + "UseNTPServers" + ] = True + self.load_ethernet_aux( + eth_config_type, + _path, + data[eth_config_type][_path], + ) + except KeyError: + self.rdmc.ui.printer( + "Unable to configure " "'UseNTPServers' for '%s'.\n" % _path + ) + self.rdmc.ui.printer( + "iLO must be reset in order for " + "changes to static network time protocol servers to " + "take effect.\n" + ) + + def load_ethernet_aux(self, _type, _path, ethernet_data): + """helper function for parsing and bucketting ethernet properties. + :param _type: Redfish type used for querying the current server data + :type _type: string + :param _path: URI path to be patched. + :type _path: string + :param ethernet_data: JSON containing the ethernet instance (and valid, associated + properties) to be patched. + :type ethernet_data: JSON + + """ + + support_ipv6 = True + dhcpv4curr = dhcpv4conf = oem_dhcpv4conf = dict() + _ = dhcpv6conf = oem_dhcpv6conf = dict() + errors = [] + + ident_eth = False + if "EthernetInterface" in _type: + for curr_sel in self.rdmc.app.select( + _type.split(".")[0] + ".", + ( + self.rdmc.app.typepath.defs.hrefstring, + self.rdmc.app.typepath.defs.managerpath + "*", + ), + path_refresh=True, + ): + if curr_sel.path == _path: + ident_eth = True + break + # 'links/self/href' required when using iLO 4 (rest). + elif "EthernetNetworkInterface" in _type: + for curr_sel in self.rdmc.app.select( + _type.split(".")[0] + ".", + ( + "links/self/" + self.rdmc.app.typepath.defs.hrefstring, + self.rdmc.app.typepath.defs.managerpath + "*", + ), + path_refresh=True, + ): + if curr_sel.path == _path: + ident_eth = True + break + else: + raise Exception("Invalid type in management NIC load operation: '%s'" % _type) + + if not ident_eth: + raise InvalidPathError("Path: '%s' is invalid/not identified on this server.\n" % _path) + + ident_name = curr_sel.dict.get("Name") + ident_id = curr_sel.dict.get("Id") + # ENABLING ETHERNET INTERFACE SECTION + try: + # Enable the Interface if called for and not already enabled + if ethernet_data.get("InterfaceEnabled") and not curr_sel.dict.get("InterfaceEnabled"): + self.rdmc.app.patch_handler(_path, {"InterfaceEnabled": True}, silent=True) + self.rdmc.ui.printer("NIC Interface Enabled.\n") + # Disable the Interface if called for and not disabled already + # No need to do anything else, just return + elif not ethernet_data.get("InterfaceEnabled") and not curr_sel.dict.get("InterfaceEnabled"): + self.rdmc.app.patch_handler(_path, {"InterfaceEnabled": False}, silent=True) + self.rdmc.ui.warn("NIC Interface Disabled. All additional configurations " "omitted.") + return + except (KeyError, NameError, TypeError, AttributeError): + # check OEM for NICEnabled instead + if ( + not curr_sel.dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["NICEnabled"] + and ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]["NICEnabled"] + ): + self.rdmc.app.patch_handler( + _path, + {"Oem": {self.rdmc.app.typepath.defs.oemhp: {"NICEnabled": True}}}, + silent=True, + ) + self.rdmc.ui.printer("NIC Interface Enabled.\n") + elif ( + not curr_sel.dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["NICEnabled"] + and not ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]["NICEnabled"] + ): + self.rdmc.app.patch_handler( + _path, + {"Oem": {self.rdmc.app.typepath.defs.oemhp: {"NICEnabled": False}}}, + silent=True, + ) + self.rdmc.ui.printer("NIC Interface Disabled.\n") + return + # except IloResponseError should just be raised and captured by decorator. No point in + # performing any other operations if the interface can not be set. + + # END ENABLING ETHERNET INTEFACE SECTION + # --------------------------------------- + # DETERMINE DHCPv4 and DHCPv6 States and associated flags + + if "NICSupportsIPv6" in list(curr_sel.dict["Oem"][self.rdmc.app.typepath.defs.oemhp].keys()): + support_ipv6 = curr_sel.dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["NICSupportsIPv6"] + + # obtain DHCPv4 Config and OEM + try: + if "DHCPv4" in list(curr_sel.dict.keys()) and "DHCPv4" in list(ethernet_data.keys()): + dhcpv4curr = copy.deepcopy(curr_sel.dict["DHCPv4"]) + dhcpv4conf = copy.deepcopy(ethernet_data["DHCPv4"]) + except (KeyError, NameError, TypeError, AttributeError): + errors.append("Unable to find Redfish DHCPv4 Settings.\n") + finally: + try: + oem_dhcpv4conf = copy.deepcopy(ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]["DHCPv4"]) + + except (KeyError, NameError): + errors.append("Unable to find OEM Keys for DHCPv4 or IPv4") + + try: + if support_ipv6: + if "DHCPv6" in list(curr_sel.dict.keys()) and "DHCPv6" in list(ethernet_data.keys()): + dhcpv6conf = copy.deepcopy(ethernet_data["DHCPv6"]) + else: + self.rdmc.ui.warn("NIC Does not support IPv6.") + except (KeyError, NameError, TypeError, AttributeError): + errors.append("Unable to find Redfish DHCPv6 Settings.\n") + finally: + try: + oem_dhcpv6conf = copy.deepcopy(ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]["DHCPv6"]) + except (KeyError, NameError): + errors.append("Unable to find OEM Keys for DHCPv6 or IPv6") + + try: + # if DHCP Enable request but not currently enabled + if dhcpv4conf.get("DHCPEnabled") and not curr_sel.dict["DHCPv4"]["DHCPEnabled"]: + self.rdmc.app.patch_handler(_path, {"DHCPv4": {"DHCPEnabled": True}}, silent=True) + self.rdmc.ui.printer("DHCP Enabled.\n") + # if DHCP Disable request but currently enabled + elif not dhcpv4conf["DHCPEnabled"] and curr_sel.dict["DHCPv4"]["DHCPEnabled"]: + self.rdmc.app.patch_handler(_path, {"DHCPv4": {"DHCPEnabled": False}}, silent=True) + dhcpv4conf["UseDNSServers"] = False + dhcpv4conf["UseNTPServers"] = False + dhcpv4conf["UseGateway"] = False + dhcpv4conf["UseDomainName"] = False + self.rdmc.ui.printer("DHCP Disabled.\n") + except (KeyError, NameError, TypeError, AttributeError): + # try with OEM + try: + if ( + oem_dhcpv4conf.get("Enabled") + and not curr_sel.dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["DHCPv4"]["Enabled"] + ): + self.rdmc.app.patch_handler( + _path, + {"Oem": {self.rdmc.app.typepath.defs.oemhp: {"DHCPv4": {"DHCPEnabled": True}}}}, + silent=True, + ) + self.rdmc.ui.printer("DHCP Enabled.\n") + if "IPv4Addresses" in ethernet_data: + del ethernet_data["IPv4Addresses"] + elif ( + not oem_dhcpv4conf.get("Enabled") + and curr_sel.dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["DHCPv4"]["Enabled"] + ): + oem_dhcpv4conf["UseDNSServers"] = False + oem_dhcpv4conf["UseNTPServers"] = False + oem_dhcpv4conf["UseGateway"] = False + oem_dhcpv4conf["UseDomainName"] = False + self.rdmc.ui.printer("DHCP Disabled.\n") + except (KeyError, NameError) as exp: + errors.append("Failure in parsing or removing data in OEM DHCPv4: %s.\n" % exp) + + try: + # if the ClientIDType is custom and we are missing the ClientID then this property can + # not be set. + if "ClientIdType" in list(dhcpv4conf.keys()): + if dhcpv4conf["ClientIdType"] == "Custom" and "ClientID" not in list(dhcpv4conf.keys()): + del ethernet_data["DHCPv4"]["ClientIdType"] + elif "ClientIdType" in list(oem_dhcpv4conf.keys()): + if oem_dhcpv4conf["ClientIdType"] == "Custom" and "ClientID" not in list(oem_dhcpv4conf.keys()): + del ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]["DHCPv4"]["ClientIdType"] + except (KeyError, NameError, TypeError, AttributeError): + try: + if "ClientIdType" in list(oem_dhcpv4conf.keys()): + if oem_dhcpv4conf["ClientIdType"] == "Custom" and "ClientID" not in list(oem_dhcpv4conf.keys()): + del ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]["DHCPv4"]["ClientIdType"] + except (KeyError, NameError) as exp: + errors.append("Unable to remove property %s.\n" % exp) + + # special considerations go here for things that need to stay despite diffdict + # EX: IPv4 addresses (aka bug). Changing only one property within the + # IPv4StaticAddresses or IPv4Addresses causes an issue during load. Must include IP, + # subnet mask and gateway (they can not be patched individually). + # spec_dict = {'Oem': {self.rdmc.app.typepath.defs.oemhp: {}}} + spec_dict = dict() + if "IPv4Addresses" in ethernet_data: + spec_dict["IPv4Addresses"] = copy.deepcopy(ethernet_data["IPv4Addresses"]) + try: + if "IPv4Addresses" in ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]: + spec_dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["IPv4Addresses"] = copy.deepcopy( + ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]["IPv4StaticAddresses"] + ) + except (KeyError, NameError, TypeError, AttributeError): + pass + + # diff and overwrite the original payload + ethernet_data = diffdict(ethernet_data, curr_sel.dict) + ethernet_data.update(spec_dict) + + # verify dependencies on those flags which are to be applied are eliminated + try: + # delete Domain name and FQDN if UseDomainName for DHCPv4 or DHCPv6 + # is present. can wait to apply at the end + if dhcpv4conf.get("UseDomainName"): # or dhcpv6conf['UseDomainName']: + if "DomainName" in ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]: + del ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]["DomainName"] + if "FQDN" in ethernet_data: + del ethernet_data["FQDN"] + except (KeyError, NameError, TypeError, AttributeError): + # try again with OEM + try: + if oem_dhcpv4conf.get("UseDomainName") or oem_dhcpv6conf.get("UseDomainName"): + if ( + "Oem" in ethernet_data + and "DomainName" in ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp] + ): + del ethernet_data["Oem"][self.rdmc.app.typepath.defs.oemhp]["DomainName"] + if "FQDN" in ethernet_data: + del ethernet_data["FQDN"] + except (KeyError, NameError) as exp: + errors.append("Unable to remove property %s.\n" % exp) + + try: + # delete DHCP4 DNSServers from IPV4 dict if UseDNSServers Enabled + # can wait to apply at the end + if dhcpv4conf.get("UseDNSServers"): # and ethernet_data.get('NameServers'): + json_traversal_delete_empty(data=ethernet_data, remove_list=["NameServers"]) + except (KeyError, NameError, TypeError, AttributeError): + pass + finally: + try: + if oem_dhcpv4conf.get("UseDNSServers"): + # del_sections('DNSServers', ethernet_data) + json_traversal_delete_empty(data=ethernet_data, remove_list=["DNSServers"]) + except (KeyError, NameError) as exp: + errors.append("Unable to remove property %s.\n" % exp) + try: + if dhcpv4conf.get("UseWINSServers"): + json_traversal_delete_empty(data=ethernet_data, remove_list=["WINServers"]) + except (KeyError, NameError, TypeError, AttributeError): + pass + finally: + try: + if oem_dhcpv4conf.get("UseWINSServers"): + json_traversal_delete_empty( + data=ethernet_data, + remove_list=["WINServers", "WINSRegistration"], + ) + except (KeyError, NameError) as exp: + errors.append("Unable to remove property %s.\n" % exp) + + try: + if dhcpv4conf.get("UseStaticRoutes"): + json_traversal_delete_empty(data=ethernet_data, remove_list=["StaticRoutes"]) + except (KeyError, NameError, TypeError, AttributeError): + pass + finally: + try: + if oem_dhcpv4conf.get("UseStaticRoutes"): + json_traversal_delete_empty(data=ethernet_data, remove_list=["StaticRoutes"]) + except (KeyError, NameError) as exp: + errors.append("Unable to remove property %s.\n" % exp) + + try: + # if using DHCPv4, remove static addresses + if dhcpv4conf.get("DHCPEnabled"): + json_traversal_delete_empty( + data=ethernet_data, + remove_list=["IPv4Addresses", "IPv4StaticAddresses"], + ) + except (KeyError, NameError, TypeError, AttributeError): + pass + finally: + try: + if oem_dhcpv4conf.get("Enabled"): + json_traversal_delete_empty( + data=ethernet_data, + remove_list=["IPv4Addresses", "IPv4StaticAddresses"], + ) + except (KeyError, NameError) as exp: + errors.append("Unable to remove property %s.\n" % exp) + + try: + # if not using DHCPv6, remove static addresses from payload + if dhcpv6conf.get("OperatingMode") == "Disabled": + json_traversal_delete_empty( + data=ethernet_data, + remove_list=["IPv6Addresses", "IPv6StaticAddresses"], + ) + except (KeyError, NameError, TypeError, AttributeError): + pass + finally: + try: + if not oem_dhcpv6conf.get("StatefulModeEnabled"): + json_traversal_delete_empty( + data=ethernet_data, + remove_list=["IPv6Addresses", "IPv6StaticAddresses"], + ) + except (KeyError, NameError) as exp: + errors.append("Unable to remove property %s.\n" % exp) + + flags = ethernet_data + if "StaticNameServers" in flags: + if dhcpv4curr["UseDNSServers"]: + flags["DHCPv4"] = {"UseDNSServers": False} + + # verify dependencies on those flags which are to be applied are eliminated + if ( + "IPv4Addresses" in flags + and "Address" in flags["IPv4Addresses"][0] + and "Address" in curr_sel.dict["IPv4Addresses"][0] + and flags["IPv4Addresses"][0]["Address"] == curr_sel.dict["IPv4Addresses"][0]["Address"] + and "Gateway" in flags["IPv4Addresses"][0] + and "Gateway" in curr_sel.dict["IPv4Addresses"][0] + and flags["IPv4Addresses"][0]["Gateway"] == curr_sel.dict["IPv4Addresses"][0]["Gateway"] + and "SubnetMask" in flags["IPv4Addresses"][0] + and "SubnetMask" in curr_sel.dict["IPv4Addresses"][0] + and flags["IPv4Addresses"][0]["SubnetMask"] == curr_sel.dict["IPv4Addresses"][0]["SubnetMask"] + ): + del flags["IPv4Addresses"] + try: + if not flags: + self.rdmc.ui.warn("No change in configurations in " + _path) + else: + self.rdmc.app.patch_handler(_path, flags, silent=True) + except IloResponseError as excp: + errors.append("iLO Responded with the following errors setting DHCP: %s.\n" % excp) + + try: + if "AutoNeg" not in list(ethernet_data.keys()): + json_traversal_delete_empty(data=ethernet_data, remove_list=["FullDuplex", "SpeedMbps"]) + + # if Full Duplex exists, check if FullDuplexing enabled. If so, + # remove Speed setting. + elif "FullDuplex" in list(ethernet_data.keys()): + json_traversal_delete_empty(data=ethernet_data, remove_list=["FullDuplex", "SpeedMbps"]) + except (KeyError, NameError) as exp: + errors.append("Unable to remove property %s.\n" % exp) + + try: + if "FrameSize" in list(ethernet_data.keys()): + json_traversal_delete_empty(data=ethernet_data, remove_list=["FrameSize"]) + except (KeyError, NameError) as exp: + errors.append("Unable to remove property %s.\n" % exp) + + if ethernet_data: + self.patch_eth(_path, ethernet_data, errors) + + if errors and "Virtual" not in ident_name: + raise RdmcError( + "Ethernet configuration errors were found collectively on adapter: " + "'%s, %s'\ntype: %s\nerrors: %s" % (ident_name, ident_id, _type, errors) + ) + + def patch_eth(self, _path, eth_data, errors=[]): + """helper function for patching ethernet properties. Retry functionality with the ability + to remove the offending property. + :param _path: URI path to be patched. + :type _path: string + :param eth_data: JSON containing the ethernet instance (and valid, associated + properties) to be patched. + :type eth_data: JSON + :param errors: list of errors catalogued between attempts + :type errors: list + + """ + + try: + if eth_data: + # eth_data = json.dumps(eth_data) + # import ast + # eth_data = ast.literal_eval(eth_data) + tmp = self.rdmc.app.patch_handler(_path, eth_data, silent=False, service=False) + if tmp.status == 400: + raise InvalidPropertyError(tmp.dict["error"][next(iter(tmp.dict["error"]))]) + else: + raise NoDifferencesFoundError( + "No differences between existing iLO ethernet " + "configuration and new ethernet configuration.\nPath: %s\n" % _path + ) + + except InvalidPropertyError as excp: + errors.append("iLO Responded with the following error: %s.\n" % excp) + + def drill_to_data(data, list_o_keys): + if len(list_o_keys) > 1: + k = list_o_keys.pop(0) + else: + del data[k] + if isinstance(data, dict): + drill_to_data(data[k], list_o_keys) + + if hasattr(excp, "message"): + for key in excp.message[0]["MessageArgs"]: + try: + eth_data.pop(key) + except (AttributeError, KeyError, StopIteration): + try: + drill_to_data( + eth_data, + list_o_keys=json_traversal(eth_data, key, ret_key_path=True), + ) + except: + errors.append("Unable to find '%s'" % key) + return + self.patch_eth(_path, eth_data) + + except NoDifferencesFoundError as excp: + errors.append("%s" % excp) + + def ethernetvalidation(self, options): + """ethernet validation function + :param options: command line options + :type options: list. + """ + + self.cmdbase.login_select_validation(self, options) + + if options.ethfilename: + if len(options.ethfilename) < 2: + self.eth_file = options.ethfilename[0] + else: + raise InvalidCommandLineError("Only a single ethernet file may be specified.") + else: + self.eth_file = __eth_file__ + + @staticmethod + def options_argument_group(parser): + """Define option arguments group + :param parser: The parser to add the login option group to + :type parser: ArgumentParser/OptionParser + """ + + parser.add_argument( + "--encryption", + dest="encryption", + help="Optionally include this flag to encrypt/decrypt a file" " using the key provided.", + default=None, + ) + parser.add_argument( + "-f", + "--ethfile", + dest="ethfilename", + help="""Optionally specify a JSON file to store or load ethernet configuration data.""", + action="append", + default=None, + ) + + def definearguments(self, customparser): + """Wrapper function for certificates command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + self.options_argument_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + default_parser = subcommand_parser.add_parser( + "default", + help="Obtain iLO management networking interface details and configure basic " + "properties such as enablement/disablement, domain name servers, ipv4 and ipv6 " + "networking configuration.", + formatter_class=RawDescriptionHelpFormatter, + ) + default_parser.add_argument( + "--enable_nic", + dest="enable_nic", + help="Enable network interfaces. List each interface to be enabled by ID: " + "Ex: ethernet --enable_nic 1,2,3. **Note**: Non-existent interfaces will be omitted from " + "configuration.", + type=str, + default=None, + ) + default_parser.add_argument( + "--disable_nic", + dest="disable_nic", + help="Disable network interfaces. List each interface to be disabled by ID: " + "Ex: ethernet --disable_nic 1,2,3. **Note**: Non-existent interfaces will be omitted from " + "configuration.", + type=str, + default=None, + ) + default_parser.add_argument( + "--enable_vnic", + dest="enable_vnic", + help="""Enable virtual network interfaces of management network. + Ex: ethernet --enable_vnic""", + action="store_true", + default=False, + ) + default_parser.add_argument( + "--disable_vnic", + dest="disable_vnic", + help="""Disable virtual network interfaces of management network. + Ex: ethernet --disable_vnic""", + action="store_true", + default=False, + ) + default_parser.add_argument( + "--disable_enhanced_downloads", + dest="disable_enhanced_downloads", + help="""Disable enhanced download for virtual media and firmware update. + Ex: ethernet --disable_enhanced_downloads""", + action="store_true", + default=False, + ) + default_parser.add_argument( + "--enable_enhanced_downloads", + dest="enable_enhanced_downloads", + help="""Enable enhanced download for virtual media and firmware update. + Ex: ethernet --enable_enhanced_downloads""", + action="store_true", + default=False, + ) + default_parser.add_argument( + "--nameservers", + dest="nameservers", + help="Configure physical and shared management network interface domain name " + "servers (DNS) in a list as follows: " + "Ex: ethernet --nameservers 8.8.8.8,1.1.1.1,2.2.2.2 ethernet --nameservers dns_resolver1.aws.com, " + "dns_resolver2.aws.com", + default=None, + ) + default_parser.add_argument( + "--proxy", + dest="proxy", + type=str, + help="Configure or clear proxy server for the network " + "Ex:\nTo set proxy\nethernet --proxy http://proxy.abc.net:8080 or \n" + "To clear proxy settings\n" + "ethernet --proxy None", + default=None, + ) + default_parser.add_argument( + "--network_ipv4", + dest="network_ipv4", + help="Configure physical and shared management network interface static IPv4 " + "settings. Settings provided in a list as follows: " + ", , . Ex: ethernet --network_ipv4 " + "192.168.1.10, 192.168.1.1, 255.255.0.0", + default=None, + ) + default_parser.add_argument( + "--network_ipv6", + dest="network_ipv6", + help="Configure physical and shared management network interface static IPv6 " + "settings. Settings provided in a list as follows: " + ", , . Ex: ethernet --network_ipv6 " + "0:0:0:0:ffff:c0a8:10e, 0:0:0:0:0:ffff:c0a8:101, 64. **Note**: IPv6 network mask" + "is restricted to '64' bits.", + default=None, + ) + default_parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the " "displayed output to JSON format.", + default=False, + ) + self.cmdbase.add_login_arguments_group(default_parser) + save_help = "Save a Network Configuration." + save_parser = subcommand_parser.add_parser( + __subparsers__[0], + help=save_help, + description="{0}\n\texample: ethernet save\n\n\tSave iLO ethernet network management interface " + "settings to a non-default file name.\n\tEx: ethernet save -f networking.json\n\n\tSave " + "an encrypted iLO networking configuration file\n\texample: ethernet save --encryption " + "".format(save_help), + formatter_class=RawDescriptionHelpFormatter, + ) + self.cmdbase.add_login_arguments_group(save_parser) + self.options_argument_group(save_parser) + load_help = "Load a Network Configuration." + load_parser = subcommand_parser.add_parser( + __subparsers__[1], + help=load_help, + description="{0}\n\texample: ethernet load\n\n\tLoad iLO ethernet networking management interface " + "settings from a non-default file name.\n\tEx: ethernet load -f " + "networking.json\n\n\tLoad an encrypted iLO networking configuration file\n\texample: " + "ethernet load --encryption ".format(load_help), + formatter_class=RawDescriptionHelpFormatter, + ) + load_parser.add_argument( + "--force_network_config", + dest="force_network_config", + help="Use this flag to force set network configuration." + "Network settings will be skipped if the flag is not included.", + action="store_true", + default=None, + ) + self.cmdbase.add_login_arguments_group(load_parser) + self.options_argument_group(load_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/FactoryDefaultsCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/FactoryDefaultsCommand.py new file mode 100644 index 0000000..ff98397 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/FactoryDefaultsCommand.py @@ -0,0 +1,135 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Factory Defaults Command for rdmc""" + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class FactoryDefaultsCommand: + """Reset server to factory default settings""" + + def __init__(self): + self.ident = { + "name": "factorydefaults", + "usage": None, + "description": "Reset iLO to factory defaults in the current logged in " + "server.\n\texample: factorydefaults", + "summary": "Resets iLO to factory defaults. WARNING: user data will " "be removed use with caution.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main factorydefaults function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("factorydefaults command takes no arguments.") + + self.factorydefaultsvalidation(options) + + self.rdmc.ui.warn("Resetting iLO to factory default settings.\n" "Current session will be terminated.\n") + + select = "Manager." + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Manager. not found.") + + bodydict = results.resp.dict["Oem"][self.rdmc.app.typepath.defs.oemhp] + + try: + for item in bodydict["Actions"]: + if "ResetToFactoryDefaults" in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = "ResetToFactoryDefaults" + + path = bodydict["Actions"][item]["target"] + body = {"Action": action, "ResetType": "Default"} + break + except: + body = { + "Action": "ResetToFactoryDefaults", + "Target": "/Oem/Hp", + "ResetType": "Default", + } + + self.rdmc.app.post_handler(path, body, service=True) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def factorydefaultsvalidation(self, options): + """factory defaults validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/FirmwareIntegrityCheckCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/FirmwareIntegrityCheckCommand.py new file mode 100644 index 0000000..0148ca5 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/FirmwareIntegrityCheckCommand.py @@ -0,0 +1,170 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Firmware Update Command for rdmc""" + +import time + +try: + from rdmc_helper import ( + IloLicenseError, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + TimeOutError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IloLicenseError, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + TimeOutError, + ) + + +class FirmwareIntegrityCheckCommand: + """Reboot server that is currently logged in""" + + def __init__(self): + self.ident = { + "name": "fwintegritycheck", + "usage": None, + "description": "Perform a firmware " + "integrity check on the current logged in server.\n\t" + "example: fwintegritycheck\n\n\tPerform a firmware integrity check and " + "return results of the check.\n\texmaple: fwintegritycheck --results", + "summary": "Perform a firmware integrity check on the currently logged in server.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main firmware update worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("fwintegritycheck command takes no arguments") + + self.firmwareintegritycheckvalidation(options) + if self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("fwintegritycheck command is " "only available on iLO 5.") + + licenseres = self.rdmc.app.select(selector="HpeiLOLicense.") + try: + licenseres = licenseres[0] + except: + pass + if not licenseres.dict["LicenseFeatures"]["FWScan"]: + raise IloLicenseError("This command is not available with this iLO license.\n") + + select = self.rdmc.app.typepath.defs.hpilofirmwareupdatetype + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + bodydict = results.resp.dict + if "#HpeiLOUpdateServiceExt.StartFirmwareIntegrityCheck" in bodydict["Oem"]["Hpe"]["Actions"]: + path = bodydict["Oem"]["Hpe"]["Actions"]["#HpeiLOUpdateServiceExt.StartFirmwareIntegrityCheck"]["target"] + else: + raise InvalidCommandLineError("Firmware integrity check action not found.") + + self.rdmc.app.post_handler(path, {}) + + if options.results: + results_string = "Awaiting results of firmware integrity check..." + self.rdmc.ui.printer(results_string) + polling = 50 + found = False + while polling > 0: + if not polling % 5: + self.rdmc.ui.printer(".") + get_results = self.rdmc.app.get_handler(bodydict["@odata.id"], service=True, silent=True) + if get_results: + curr_time = time.strptime(bodydict["Oem"]["Hpe"]["CurrentTime"], "%Y-%m-%dT%H:%M:%SZ") + scan_time = time.strptime( + get_results.dict["Oem"]["Hpe"]["FirmwareIntegrity"]["LastScanTime"], + "%Y-%m-%dT%H:%M:%SZ", + ) + + if scan_time > curr_time: + self.rdmc.ui.printer( + "\nScan Result: %s\n" + % get_results.dict["Oem"]["Hpe"]["FirmwareIntegrity"]["LastScanResult"] + ) + found = True + break + + polling -= 1 + time.sleep(1) + if not found: + self.rdmc.ui.error("\nPolling timed out before scan completed.\n") + TimeOutError("") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def firmwareintegritycheckvalidation(self, options): + """Firmware update method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--results", + dest="results", + help="Optionally include this flag to show results of firmware integrity check.", + default=False, + action="store_true", + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/FirmwareUpdateCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/FirmwareUpdateCommand.py new file mode 100644 index 0000000..f363acd --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/FirmwareUpdateCommand.py @@ -0,0 +1,325 @@ +### +# Copyright 2016-2023 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Firmware Update Command for rdmc""" + +import time + +from redfish.ris.resp_handler import ResponseHandler + +try: + from rdmc_helper import ( + FirmwareUpdateError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + FirmwareUpdateError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class FirmwareUpdateCommand: + """Reboot server that is currently logged in""" + + def __init__(self): + self.ident = { + "name": "firmwareupdate", + "usage": None, + "description": "Apply a firmware " + "update to the current logged in server.\n\texample: " + "firmwareupdate /images/image.bin\n\t" + "update based on targets GPU .\n\texample: " + "firmwareupdate --targets ", + "summary": "Perform a firmware update on the currently logged in server.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main firmware update worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + # Third Party FW + + file_name = args[0].lower() + if args and (".pup" in file_name or ".fup" in file_name or ".hpb" in file_name or ".HPb" in file_name): + raise InvalidCommandLineErrorOPTS("For flashing 3rd party firmwares, please use flashfwpkg command") + + # Supports only .bin/fwpkg/full/vme Firmware files. + if ( + args + and (".bin" in file_name or ".fwpkg" in file_name or ".full" in file_name or ".vme" in file_name) + or ".lpk" in file_name + ): + if args[0].startswith('"') and args[0].endswith('"'): + args[0] = args[0][1:-1] + else: + raise InvalidCommandLineErrorOPTS("Please input URL pointing to a .bin or .fwpkg or .full or .vme firmware") + + self.firmwareupdatevalidation(options) + + action = None + uri = "FirmwareURI" + + select = self.rdmc.app.typepath.defs.hpilofirmwareupdatetype + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + update_path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + bodydict = results.resp.dict + + try: + if options.targets: + for item in bodydict["Oem"]["Hpe"]["Actions"]: + if "AddFromUri" in item: + action = item.split("#")[-1] + uri = "ImageURI" + if action: + put_path = bodydict["Oem"]["Hpe"]["Actions"][item]["target"] + break + else: + for item in bodydict["Actions"]: + if self.rdmc.app.typepath.defs.isgen10: + if "SimpleUpdate" in item: + action = item.split("#")[-1] + + uri = "ImageURI" + # options.tpmenabled = False + elif "InstallFromURI" in item: + action = "InstallFromURI" + + if action: + put_path = bodydict["Actions"][item]["target"] + break + except: + put_path = update_path + action = "Reset" + + if "AddFromUri" in action: + body = { + "Action": action, + uri: args[0], + "TPMOverrideFlag": options.tpmenabled, + } + elif "SimpleUpdate" in action: + body = {"Action": action, uri: args[0]} + if options.targets: + targets = [] + target_list = options.targets.split(",") + for target in target_list: + target_url = "/redfish/v1/UpdateService/FirmwareInventory/" + str(target) + "/" + targets.append(target_url) + verified = self.verify_targets(options.targets) + if not verified: + self.rdmc.ui.error("Provided target was not available, Please provide valid target id\n") + return ReturnCodes.INVALID_TARGET_ERROR + body["UpdateTarget"] = options.updatetarget + body["UpdateRepository"] = options.updaterepository + body["Targets"] = targets + self.rdmc.ui.printer('payload: "%s"\n' % body) + self.rdmc.app.post_handler(put_path, body, silent=True, service=True) + + self.rdmc.ui.printer("\nStarting upgrade process...\n\n") + + self.showupdateprogress(update_path) + # logoutobj.run("") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def verify_targets(self, target): + target_list = target.split(",") + for target in target_list: + try: + target_url = "/redfish/v1/UpdateService/FirmwareInventory/" + target + dd = self.rdmc.app.get_handler(target_url, service=True, silent=True) + if dd.status == 404: + return False + except: + return False + return True + + def showupdateprogress(self, path): + """handler function for updating the progress + + :param path: path to update service. + :tyep path: str + """ + counter = 0 + written = False + uploadingpost = False + spinner = ["|", "/", "-", "\\"] + position = 0 + + while True: + if counter == 100: + raise FirmwareUpdateError( + "Error occurred while updating the firmware. Check if TPM is enabled. " + "And if TPM enabled provide --tpmenabled flag" + ) + else: + counter += 1 + + results = self.rdmc.app.get_handler(path, silent=True) + results = results.dict + try: + results = results["Oem"]["Hpe"] + except: + pass + + if not results: + raise FirmwareUpdateError("Unable to contact Update Service. " "Please re-login and try again.") + + if results["State"].lower().startswith("idle"): + time.sleep(2) + elif results["State"].lower().startswith("uploading"): + counter = 0 + + if not uploadingpost: + uploadingpost = True + else: + if not written: + written = True + self.rdmc.ui.printer("\n iLO is uploading the necessary files. Please wait...\n") + + time.sleep(0.5) + elif results["State"].lower().startswith(("progressing", "updating", "verifying", "writing", "backingup")): + counter = 0 + + for _ in range(2): + if position < 4: + self.rdmc.ui.printer("Updating: " + spinner[position] + "\r") + position += 1 + time.sleep(0.1) + else: + position = 0 + self.rdmc.ui.printer("Updating: " + spinner[position] + "\r") + position += 1 + time.sleep(0.1) + elif results["State"].lower().startswith("complete"): + self.rdmc.ui.printer( + "\n\nFirmware update has completed and iLO" + " may reset. \nIf iLO resets the" + " session will be terminated.\nPlease wait" + " for iLO to initialize completely before" + " logging in again.\nA reboot may be required" + " for firmware changes to take effect.\n" + ) + break + elif results["State"].lower().startswith("error"): + error = self.rdmc.app.get_handler(path, silent=True) + self.printerrmsg(error) + + def printerrmsg(self, error): + """raises and prints the detailed error message if possible""" + output = "Error occurred while updating the firmware." + + try: + error = error.dict["Oem"]["Hpe"]["Result"]["MessageId"].split(".") + # TODO: Update to new ResponseHandler Method 'return_reg' + errmessages = ResponseHandler( + self.rdmc.app.validation_manager, + self.rdmc.app.typepath.defs.messageregistrytype, + ).get_error_messages() + for messagetype in list(errmessages.keys()): + if error[0] == messagetype: + if errmessages[messagetype][error[-1]]["NumberOfArgs"] == 0: + output = "Firmware update error. %s" % errmessages[messagetype][error[-1]]["Message"] + else: + output = "Firmware update error. %s" % errmessages[messagetype][error[-1]]["Description"] + break + except: + pass + + raise FirmwareUpdateError(output) + + def firmwareupdatevalidation(self, options): + """Firmware update method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--tpmenabled", + dest="tpmenabled", + action="store_true", + help="Use this flag if the server you are currently logged into" " has a TPM chip installed.", + default=False, + ) + customparser.add_argument( + "--updatetarget", + dest="updatetarget", + action="store_true", + help="UpdateTarget set as true/false" "if true firmware updates", + default=True, + ) + customparser.add_argument( + "--updaterepository", + dest="updaterepository", + action="store_false", + help="UpdateRepository set as true or false" " if true it will add repository.", + default=True, + ) + customparser.add_argument( + "--targets", + help="If targets value specify a comma separated\t" "firmwareinventory id only", + metavar="targets_indices", + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IPProfilesCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IPProfilesCommand.py new file mode 100644 index 0000000..eb7a482 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IPProfilesCommand.py @@ -0,0 +1,701 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""RawGet Command for rdmc""" + +import base64 +import gzip +import json +import os +import re +import time +from ctypes import create_string_buffer +from datetime import datetime +import six +from six import BytesIO + +if six.PY3: + from datetime import timezone + + +import redfish +from redfish.hpilo.risblobstore2 import BlobStore2 + +try: + from rdmc_helper import ( + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + NoContentsFoundForOperationError, + PathUnavailableError, + ReturnCodes, + UnableToDecodeError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + NoContentsFoundForOperationError, + PathUnavailableError, + ReturnCodes, + UnableToDecodeError, + ) + + +class IPProfilesCommand: + """Raw form of the get command""" + + def __init__(self): + self.ident = { + "name": "ipprofiles", + "usage": None, + "description": "Decodes and lists " + "ipprofiles. This is default option. No argument required" + "\n\tExample: ipprofiles" + "\n\n\tAdds a new ipprofile from the provided json file." + "\n\tNOTE: Path can be absolute or from the " + "same path you launch iLOrest." + "\n\tipprofiles " + "\n\n\tDelete an ipprofile or list of profiles.\n\t" + "Provide the unique key that corresponds to the ipprofile" + " data you want to delete.\n\tSeveral IDs can be comma-separated" + " with no space in between to delete more than one profile. " + "\n\tipprofiles --delete ID1,ID2,ID3..." + "\n\n\tCopies ip profile with the specified ID into the ip job queue." + "and starts it.\n\texample: ipprofiles --start=", + "summary": "This is used to manage hpeipprofile data store.", + "aliases": [], + "auxcommands": ["BootOrderCommand", "RebootCommand"], + } + self.path = "" + self.ipjobs = "" + self.running_jobs = "" + self.hvt_output = "" + self.ipjobtype = ["langsel", "hvt", "ssa", "install", "rbsu"] + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main raw get worker function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.getpaths() + + self.validation(options) + + self.ipprofileworkerfunction(options, args) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def ipprofileworkerfunction(self, options, args): + """ + Ipprofile manager worker function. It calls appropriate function. + :param options: command line options + :type options: list. + :param args: command line args + :type args: string. + """ + + if options.running_jobs: + self.get_running_job() + return ReturnCodes.SUCCESS + if options.get_hvt: + if options.filename: + self.rdmc.ui.warn("iprprofiles -D option does not support -f, please remove -f and rerun\n") + return ReturnCodes.SUCCESS + self.get_hvt_output() + return ReturnCodes.SUCCESS + if options.del_key: + self.deletekeyfromipprofiledata(options) + return ReturnCodes.SUCCESS + + if options.start_ip: + self.addprofileandstartjob(options) + return ReturnCodes.SUCCESS + + if len(args) == 1: + self.encodeandpatchipprofiledata(args) + return ReturnCodes.SUCCESS + + self.getipprofiledataanddecode(options) + + def getipprofiledataanddecode(self, options): + """ + Retrieves and decodes, if encoded, data from hpeprofile data store + :param options: command line options + :type options: list. + :return returncode: int + """ + + results = self.rdmc.app.get_handler(self.path, silent=True) + if results.status == 404: + raise PathUnavailableError( + "The Intelligent Provisioning resource " + "is not available on this system. You may need" + " to run IP at least once to add the resource." + ) + + if results and results.status == 200: + j2python = json.loads(results.read) + for _, val in enumerate(j2python.keys()): + if isinstance(val, six.string_types): + result = self.decode_base64_string(str(j2python[val])) + if result is not None: + j2python[val] = result + + results.read = json.dumps(j2python, ensure_ascii=False, sort_keys=True) + if results.dict: + if options.filename: + output = json.dumps( + results.dict, + indent=2, + cls=redfish.ris.JSONEncoder, + sort_keys=True, + ) + + filehndl = open(options.filename[0], "w") + filehndl.write(output) + filehndl.close() + + self.rdmc.ui.printer("Results written out to '%s'.\n" % options.filename[0]) + else: + self.rdmc.ui.print_out_json(results.dict) + else: + self.rdmc.ui.warn("No IP profiles found\n") + + def get_running_job(self): + """ + Retrieves and decodes, running job + :return returncode: int + """ + + results = self.rdmc.app.get_handler(self.running_jobs, silent=True) + if results.status == 404: + raise PathUnavailableError( + "The Intelligent Provisioning resource " + "is not available on this system. You may need" + " to run IP at least once to add the resource." + ) + + if results and results.status == 200: + j2python = json.loads(results.read) + for _, val in enumerate(list(j2python.keys())): + if isinstance(val, six.string_types): + result = self.decode_base64_string(str(j2python[val])) + if result is not None: + j2python[val] = result + + results.read = json.dumps(j2python, ensure_ascii=False) + if results.dict: + self.rdmc.ui.print_out_json(results.dict) + else: + self.rdmc.ui.warn("No IP profiles found\n") + + def get_hvt_output(self): + """ + Retrieves and decodes, running job + :return returncode: int + """ + return_value = {} + results = self.rdmc.app.get_handler(self.hvt_output, silent=True) + if results.status == 404: + raise PathUnavailableError( + "The Intelligent Provisioning resource " + "is not available on this system. You may need" + " to run IP at least once to add the resource." + ) + + if results and results.status == 200: + j2python = json.loads(results.read) + for _, val in enumerate(list(j2python.keys())): + if isinstance(val, six.string_types) and "@" not in val: + return_value = json.loads(self.decode_base64_string(str(j2python[val]))) + self.rdmc.ui.print_out_json(return_value) + else: + self.rdmc.ui.error("No IP profiles found\n") + + def encodeandpatchipprofiledata(self, args): + """ + Reads file in the given path, encode it, + and apply it on iLO hpeipprofiles data store. + :param args: command line args + :type args: string. + :retirn returncode: int + """ + + contentsholder = self.encode_base64_string(args) + + if "path" in contentsholder and "body" in contentsholder: + self.rdmc.app.patch_handler(contentsholder["path"], contentsholder["body"]) + + return ReturnCodes.SUCCESS + + def deletekeyfromipprofiledata(self, options): + """ + Provide a string which represents a valid key in + hpeipprofiles data store. + :param options: command line options + :type options: list. + :return returncode: int + """ + + get_results = self.rdmc.app.get_handler(self.path, silent=True) + + j2python = json.loads(get_results.read) + all_keys = options.del_key[0].split(",") + for key in all_keys: + if isinstance(key, six.string_types) and j2python.get(key.strip(), False): + del j2python[key.strip()] + else: + raise InvalidFileFormattingError("%s was not found .\n" % key) + + payload = {"path": self.path, "body": j2python} + + self.rdmc.app.put_handler(payload["path"], payload["body"]) + + return ReturnCodes.SUCCESS + + def addprofileandstartjob(self, options): + """ + Adds ip profile into the job queue and start it. + :return returncode: int + """ + + ipprovider = self.hasipprovider() + if ipprovider is None: + raise PathUnavailableError("System does not support this feature of IP.\n") + + ipjob = self.hasipjobs() + if not ipjob: + raise InvalidFileFormattingError("System does not have any IP" " profile to copy to the job queue.\n") + + current_state = self.inipstate(ipprovider) + if current_state is None: + raise PathUnavailableError("System does not support this feature of IP.\n") + + later_state = False + ipstate = current_state["InIP"] + if isinstance(ipstate, bool) and ipstate: + # make sure we are in IP state. Reset and monitor + self.resetinipstate(ipprovider, current_state) + # if we are in ip, monitor should be fast, use 15 seconds + later_status = self.monitorinipstate(ipprovider, 3) + if later_status: + self.rdmc.ui.printer("Server is in IP state. Powering on...\n") + self.copyjobtoipqueue(ipjob, options.start_ip) + self.rdmc.ui.printer("Copy operation was successful...\n") + return ReturnCodes.SUCCESS + + if not isinstance(ipstate, bool): + # inip is in an unknown state, so ... + # patch to false, reboot, then monitor...if it turns true later... + # then we are in IP state otherwise, manually check system... + self.resetinipstate(ipprovider, current_state) + later_status = self.monitorinipstate(ipprovider, 3) + if later_status: + self.copyjobtoipqueue(ipjob, options.start_ip) + self.rdmc.ui.printer("Copy operation was successful...\n") + return ReturnCodes.SUCCESS + + # Check if the server is On or Off, If it is Off, make it on + path = self.rdmc.app.typepath.defs.systempath + get_results = self.rdmc.app.get_handler(path, silent=True) + result = json.loads(get_results.read) + if result and result.get("PowerState") == "Off": + self.rdmc.ui.printer("Server is in OFF state. Powering on...\n") + self.auxcommands["bootorder"].run("--onetimeboot=Utilities " "--reboot=On --commit") + elif result and result.get("Oem").get("Hpe").get("PostState") != "FinishedPost": + self.rdmc.ui.printer("Server is in POST state. Restarting...\n") + # Forcefully switch off as it is stuck in POST + self.auxcommands["reboot"].run("ForceOff") + time.sleep(5) + # Switch on with bootorder set + self.auxcommands["bootorder"].run("--onetimeboot=Utilities " "--reboot=On --commit") + # Check to find out if server in POST + # count = 0 + # in_post = True + # path = self.rdmc.app.typepath.defs.systempath + # while (count < 5): + # count = count + 1 + # get_results = self.rdmc.app.get_handler(path, silent=True) + # result = json.loads(get_results.read) + # if result and result.get("Oem").get("Hpe").get("PostState") == "FinishedPost": + # in_post = False + # break + # else: + # # Sleep for 1 minutes for 5 times. + # self.rdmc.ui.printer("System in POST...waiting for 1 min...\n") + # time.sleep(60) + + # if in_post: + # raise NoContentsFoundForOperationError( + # "Server still in POST even after 5 minutes - Please check" + # ) + else: + self.rdmc.ui.printer("Server is in OS state. ColdBooting...\n") + try: + self.auxcommands["bootorder"].run("--onetimeboot=Utilities " "--reboot=ColdBoot --commit") + except: + raise InvalidFileFormattingError("System failed to reboot") + + # After reboot, login again + time.sleep(options.sleep_time) # Sleep until reboot + self.validation(options) + + later_state = self.monitorinipstate(ipprovider) + if later_state: + self.copyjobtoipqueue(ipjob, options.start_ip) + self.rdmc.ui.printer("Copy operation was successful...\n") + else: + raise InvalidFileFormattingError( + "\nSystem reboot took longer than 4 minutes." + "something is wrong. You need to physically check this system.\n" + ) + + return ReturnCodes.SUCCESS + + def resetinipstate(self, ipprovider, current_state): + """ + Regardless of previous value, sets InIP value to False + :param ipprovider: url path of heip. + :type ipprovider: string. + :param current_state: the value of InIP. + :type current_state: dict + """ + + current_state["InIP"] = False + + payload = {"path": ipprovider, "body": current_state} + + self.rdmc.app.put_handler(payload["path"], payload["body"], silent=True) + + def monitorinipstate(self, ipprovider, timer=48): + """ + Monitor InIP value every 5 seconds until it turns true or time expires. + :param ipprovider: url path of heip. + :type ipprovider: string. + :param timer: time it takes iLO to boot into F10 assuming we are in boot state. + :type timer: int + :return ipstate: boolean + """ + + retry = timer # 48 * 5 = 4 minutes + ipstate = False + progress = self.progressbar() + self.rdmc.ui.printer("\n") + while retry > 0: + time.sleep(5) + next(progress) + status = self.inipstate(ipprovider) + if isinstance(status["InIP"], bool) and status["InIP"]: + ipstate = True + break + retry = retry - 1 + self.rdmc.ui.printer("\n") + + return ipstate + + def progressbar(self): + """ + An on demand function use to output the progress while iLO is booting into F10. + """ + while True: + yield self.rdmc.ui.printer(">>>") + + def copyjobtoipqueue(self, ipjobs, jobkey): + """ + Copies HpeIpJob to Job queue. Function assumes there is a job to copy. + A check was already done to make sure we have a job to copy in hasipjobs() + :param ipjobs: url path of heip. + :type ipjobs: list of dictionary. + :param jobkey: key of job to copy. + :type jokbey: str. + """ + + get_results = self.rdmc.app.get_handler(self.path, silent=True) + + j2python = json.loads(get_results.read) + copy_job = {} + for ipj in j2python: + if jobkey == ipj: + _decode = self.decode_base64_string(j2python[ipj]) + if _decode is not None: + _critical_props = { + "log": '[{"msg": "WAITINGTOBEPROCESSED", "percent": 0}]', + "status": "waiting", + } + copy_job.update( + { + k: v.update(_critical_props) or v + for k, v in json.loads(_decode).items() + if k in self.ipjobtype + } + ) + else: + raise NoContentsFoundForOperationError("Not supported profile content") + break + if not copy_job: + raise NoContentsFoundForOperationError("The ID %s does not match any ipprofile" % jobkey) + payload = {"path": self.ipjobs, "body": copy_job} + + self.rdmc.app.put_handler(payload["path"], payload["body"]) + + def inipstate(self, ipprovider): + """ + A check is done to determine if this version of iLO has InIP profile. + :param ipprovider: url path of heip. + :type ipprovider: string. + :return is_inip: None or dict + """ + + if ipprovider.startswith("/redfish/"): + get_results = self.rdmc.app.get_handler(ipprovider, silent=True) + result = json.loads(get_results.read) + + is_inip = None + try: + if "InIP" in list(result.keys()): + is_inip = result + except KeyError: + pass + + return is_inip + + def hasipjobs(self): + """ + A check is done to determine if there is a job in HpeIpJobs we can + copy to IP job queue + :param options: command line options + :type options: list. + :return list_dict: list of dicts + """ + + results = self.rdmc.app.get_handler(self.path, silent=True) + + j2python = json.loads(results.read) + for _, val in enumerate(j2python.keys()): + if isinstance(val, six.string_types): + result = self.decode_base64_string(str(j2python[val])) + if result is not None: + j2python[val] = result + + list_dict = [] + + for key, value in j2python.items(): + if not re.match("@odata", key): + if len(key) >= 13 and key.isdigit(): + list_dict.append({key: value}) # list of dict with valid key/value + + return list_dict + + def hasipprovider(self): + """ + A check is done here to determine if this version of iLO has IP provider + profile path using the "Oem.Hpe.Links.HpeIpProvider" + + :return is_provider: None or string. + """ + path = self.rdmc.app.typepath.defs.systempath + get_results = self.rdmc.app.get_handler(path, silent=True) + + result = json.loads(get_results.read) + + is_ipprovider = None + try: + is_ipprovider = list(result["Oem"]["Hpe"]["Links"]["HpeIpProvider"].values())[0] + except KeyError: + pass + + return is_ipprovider + + def validation(self, options): + """IPProfiles validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def decode_base64_string(self, str_b64): + """ + Decodes a given string that was encoded with base64 and gzipped. + :param str_b64: a string that was base64 encoded and the gzipped + :type str_b64: string. + """ + + read_data = None + if isinstance(str_b64, six.string_types) and str_b64: + try: + decoded_str = base64.decodebytes(str_b64.encode("utf-8")) + inbuffer = BytesIO(decoded_str) + gzffile = gzip.GzipFile(mode="rb", fileobj=inbuffer) + read_data = "" + for line in gzffile.readlines(): + read_data = read_data + line.decode("utf-8") + except: + pass + + return read_data + + def encode_base64_string(self, args): + """ + Encode a given string with base64 and gzip it. + :param args: command line args + :type args: string. + """ + + payload = {} + filename = args[0] + if filename: + if not os.path.isfile(filename): + raise InvalidFileInputError( + "File '%s' doesn't exist. " "Please create file by running 'save' command." % filename + ) + + try: + with open(filename, "r") as fh: + contentsholder = json.loads(fh.read()) + except: + raise InvalidFileFormattingError("Input file '%s' was not " "format properly." % filename) + + try: + text = json.dumps(contentsholder).encode("utf-8") + buf = BytesIO() + gzfile = gzip.GzipFile(mode="wb", fileobj=buf) + gzfile.write(text) + gzfile.close() + + en_text = base64.encodebytes(buf.getvalue()).decode("utf-8") + + if six.PY3: + epoch = datetime.fromtimestamp(0, tz=timezone.utc) + now = datetime.now(tz=timezone.utc) + elif six.PY2: + epoch = datetime.utcfromtimestamp(0) + now = datetime.utcnow() + delta = now - epoch + time_stamp = delta.total_seconds() * 1000 + time_stamp = repr(time_stamp).split(".")[0] + + body_text = {time_stamp: en_text.strip()} + + payload["body"] = body_text + if isinstance(self.path, bytes): + self.path = self.path.decode("utf-8") + payload["path"] = self.path + except Exception as excp: + raise UnableToDecodeError("Error while encoding string %s." % excp) + + return payload + + def getpaths(self): + """Get paths for ipprofiles command""" + if not all(iter([self.path, self.ipjobs, self.running_jobs, self.hvt_output])): + dll = BlobStore2.gethprestchifhandle() + + profiles_path = create_string_buffer(50) + jobs_path = create_string_buffer(50) + running_jobs_path = create_string_buffer(50) + hvt_output_path = create_string_buffer(50) + + dll.get_ip_profiles(profiles_path) + dll.get_ip_jobs(jobs_path) + dll.get_running_jobs(running_jobs_path) + dll.get_hvt_output(hvt_output_path) + + self.path = profiles_path.value + self.ipjobs = jobs_path.value + self.running_jobs = running_jobs_path.value + self.hvt_output = hvt_output_path.value + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-r", + "--running", + dest="running_jobs", + default=False, + action="store_true", + help="""Show status of the currently running or last job executed""", + ) + customparser.add_argument( + "-D", + "--diags", + help="""Get result of last HVT (diagnostics) run as part of an ipprofile job""", + default=False, + action="store_true", + dest="get_hvt", + ) + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="""Write results to the specified file.""", + action="append", + default=None, + ) + customparser.add_argument( + "--delete", + dest="del_key", + action="append", + help="Look for the key or keys in the ipprofile manager and delete", + default=None, + ) + customparser.add_argument( + "-s", + "--start", + dest="start_ip", + help="Copies the specified ip profile into the job queue and starts it", + default=None, + ) + customparser.add_argument( + "-t", + "--sleeptime", + dest="sleep_time", + type=int, + help="Sleep time in seconds when server in OS mode and rebooted", + default=320, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloAccountsCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloAccountsCommand.py new file mode 100644 index 0000000..27e1f4c --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloAccountsCommand.py @@ -0,0 +1,780 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Add Account Command for rdmc""" + +import getpass +import os +from argparse import Action, RawDescriptionHelpFormatter + +from redfish.ris.ris import SessionExpired +from redfish.ris.rmc_helper import IdTokenError + +try: + from rdmc_helper import ( + Encryption, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + NoContentsFoundForOperationError, + ResourceExists, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + Encryption, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + NoContentsFoundForOperationError, + ResourceExists, + ReturnCodes, + ) + +__subparsers__ = ["add", "modify", "changepass", "delete", "addcert", "deletecert"] + + +class _AccountParse(Action): + def __init__(self, option_strings, dest, nargs, **kwargs): + super(_AccountParse, self).__init__(option_strings, dest, nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_strings): + """Account privileges option helper""" + + privkey = { + 1: "LoginPriv", + 2: "RemoteConsolePriv", + 3: "UserConfigPriv", + 4: "iLOConfigPriv", + 5: "VirtualMediaPriv", + 6: "VirtualPowerAndResetPriv", + 7: "HostNICConfigPriv", + 8: "HostBIOSConfigPriv", + 9: "HostStorageConfigPriv", + 10: "SystemRecoveryConfigPriv", + } + + for priv in next(iter(values)).split(","): + try: + priv = int(priv) + except ValueError: + try: + parser.error("Invalid privilege entered: %s. Privileges must " "be numbers." % priv) + except: + raise InvalidCommandLineErrorOPTS("") + try: + if not isinstance(namespace.optprivs, list): + namespace.optprivs = list() + if option_strings.startswith("--add"): + namespace.optprivs.append({privkey[priv]: True}) + elif option_strings.startswith("--remove"): + namespace.optprivs.append({privkey[priv]: False}) + except KeyError: + try: + parser.error( + "Invalid privilege entered: %s. Number does not " "match an available privilege." % priv + ) + except: + raise InvalidCommandLineErrorOPTS("") + + # account_parse.counter = 0 + + +class IloAccountsCommand: + """command to manipulate/add ilo user accounts""" + + def __init__(self): + self.ident = { + "name": "iloaccounts", + "usage": None, + "description": "\tView, Add, Remove, and Modify iLO accounts based on the " + "sub-command used.\n\n\tTo view help on specific sub-commands run: " + "iloaccounts -h\n\t" + "Example: iloaccounts add -h\n\tNote: \n\t\t1. UserName and LoginName are reversed " + "in the iLO GUI for Redfish compatibility.\n\t\t" + "2. While executing the command: iloaccounts add in a Linux machine, " + "an escape character needs to be added before a special character in the password.\n\t\t\t" + "Example: iloaccount add rest rest 12iso*help\n", + "summary": "Views/Adds/deletes/modifies an iLO account on the currently logged in server.", + "aliases": ["iloaccount"], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main iloaccounts function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + acct = mod_acct = None + try: + ident_subparser = False + for cmnd in __subparsers__: + if cmnd in line: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + ident_subparser = True + break + if not ident_subparser: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.iloaccountsvalidation(options) + + redfish = self.rdmc.app.monolith.is_redfish + path = self.rdmc.app.typepath.defs.accountspath + results = self.rdmc.app.get_handler(path, service=True, silent=True).dict + + if redfish: + results = results["Members"] + else: + if "Member" in results["links"]: + results = results["links"]["Member"] + else: + results = list() + + for indx, acct in enumerate(results): + acct = self.rdmc.app.get_handler( + acct[self.rdmc.app.typepath.defs.hrefstring], service=True, silent=True + ).dict + try: + if hasattr(options, "identifier"): + if acct["Id"] == options.identifier or acct["UserName"] == options.identifier: + if redfish: + path = acct["@odata.id"] + else: + path = acct["links"]["self"]["href"] + mod_acct = acct + elif options.command == "default": + results[indx] = acct + else: + raise KeyError + except KeyError: + continue + else: + if mod_acct: + acct = mod_acct + break + else: + acct = None + + if not results: + raise NoContentsFoundForOperationError("No iLO Management Accounts were found.") + + outdict = dict() + if options.command.lower() == "default": + if not options.json: + self.rdmc.ui.printer( + "\niLO Account info:\n\n[Id] UserName (LoginName): " "\nPrivileges\n-----------------\n\n", + verbose_override=True, + ) + for acct in sorted(results, key=lambda k: int(k["Id"])): + privstr = "" + privs = acct["Oem"][self.rdmc.app.typepath.defs.oemhp]["Privileges"] + + if ( + "ServiceAccount" in list(acct["Oem"][self.rdmc.app.typepath.defs.oemhp].keys()) + and acct["Oem"][self.rdmc.app.typepath.defs.oemhp]["ServiceAccount"] + ): + service = "ServiceAccount=True" + else: + service = "ServiceAccount=False" + if not options.json: + for priv in privs: + privstr += priv + "=" + str(privs[priv]) + "\n" + self.rdmc.ui.printer( + "[%s] %s (%s):\n%s\n%s\n" + % ( + acct["Id"], + acct["UserName"], + acct["Oem"][self.rdmc.app.typepath.defs.oemhp]["LoginName"], + service, + privstr, + ), + verbose_override=True, + ) + keyval = "[" + str(acct["Id"]) + "] " + acct["UserName"] + outdict[keyval] = privs + outdict[keyval]["ServiceAccount"] = service.split("=")[-1].lower() + if options.json: + self.rdmc.ui.print_out_json_ordered(outdict) + elif options.command.lower() == "changepass": + if not acct: + raise InvalidCommandLineError("Unable to find the specified account.") + if not options.acct_password: + self.rdmc.ui.printer("Please input the new password.\n", verbose_override=True) + tempinput = getpass.getpass() + self.credentialsvalidation("", "", tempinput, "", True, options) + options.acct_password = tempinput + + self.credentialsvalidation("", "", options.acct_password.split("\r")[0], "", True) + body = {"Password": options.acct_password.split("\r")[0]} + + if path and body: + self.rdmc.app.patch_handler(path, body) + else: + raise NoContentsFoundForOperationError("Unable to find the specified account.") + + elif options.command.lower() == "add": + if options.encode: + args[2] = Encryption.decode_credentials(args[2]) + if isinstance(args[2], bytes): + args[2] = args[2].decode("utf-8") + + privs = self.getprivs(options) + path = self.rdmc.app.typepath.defs.accountspath + + body = { + "UserName": options.identifier, + "Password": options.acct_password, + "Oem": {self.rdmc.app.typepath.defs.oemhp: {"LoginName": options.loginname}}, + } + if privs: + body["Oem"][self.rdmc.app.typepath.defs.oemhp].update({"Privileges": privs}) + self.credentialsvalidation(options.identifier, options.loginname, options.acct_password, acct, True) + if options.serviceacc: + body["Oem"][self.rdmc.app.typepath.defs.oemhp].update({"ServiceAccount": True}) + if options.role: + if self.rdmc.app.getiloversion() >= 5.140: + body["RoleId"] = options.role + else: + raise IncompatibleiLOVersionError("Roles can only be set in iLO 5" " 1.40 or greater.") + if path and body: + self.rdmc.app.post_handler(path, body) + self.rdmc.ui.printer("New iLO account %s is added successfully\n" % options.identifier) + elif options.command.lower() == "modify": + if not mod_acct: + raise InvalidCommandLineError("Unable to find the specified account.") + body = {} + + if options.optprivs: + body.update({"Oem": {self.rdmc.app.typepath.defs.oemhp: {"Privileges": {}}}}) + if any( + priv for priv in options.optprivs if "SystemRecoveryConfigPriv" in priv + ) and "SystemRecoveryConfigPriv" not in list(self.getsesprivs().keys()): + excp = IdTokenError() + note_msg = "" + if self.rdmc.app.getiloversion(skipschemas=True) >= 7: + note_msg = ( + "Note: If you have logged into iLO using appaccount, " + "this is an expected behaviour. Please log in through " + "--no_app_account with a privileged user credentials to proceed.\n" + ) + error_msg = ( + "The currently logged in account must have The System " + "Recovery Config privilege to add the System Recovery " + "Config privilege.\n" + note_msg + ) + excp.message = error_msg + raise excp + privs = self.getprivs(options) + body["Oem"][self.rdmc.app.typepath.defs.oemhp]["Privileges"] = privs + + if options.role and self.rdmc.app.getiloversion() >= 5.140: + body["RoleId"] = options.role + + if not body: + raise InvalidCommandLineError( + "Valid Privileges/RoleID have not been provided; " + " no changes have been made to this account: %s\n" % options.identifier + ) + self.rdmc.app.patch_handler(path, body) + + elif options.command.lower() == "delete": + if not acct: + raise InvalidCommandLineError("Unable to find the specified account.") + self.rdmc.app.delete_handler(path) + + elif "cert" in options.command.lower(): + certpath = "/redfish/v1/AccountService/UserCertificateMapping/" + privs = self.getsesprivs() + if self.rdmc.app.typepath.defs.isgen9: + IncompatibleiLOVersionError("This operation is only available on gen 10 " "and newer machines.") + elif not privs["UserConfigPriv"]: + excp = IdTokenError() + note_msg = "" + if self.rdmc.app.getiloversion(skipschemas=True) >= 7: + note_msg = ( + "Note: If you have logged into iLO using appaccount, " + "this is an expected behaviour. Please log in through " + "--no_app_account with a privileged user credentials to proceed.\n" + ) + error_msg = ( + "The currently logged in account must have The User " + "Config privilege to manage certificates for users.\n" + note_msg + ) + excp.message = error_msg + raise excp + else: + if options.command.lower() == "addcert": + if not acct: + raise InvalidCommandLineError("Unable to find the specified account.") + body = {} + username = acct["UserName"] + + fingerprintfile = options.certificate + if os.path.exists(fingerprintfile): + with open(fingerprintfile, "r") as fingerfile: + fingerprint = fingerfile.read() + else: + raise InvalidFileInputError("%s cannot be read." % fingerprintfile) + body = {"Fingerprint": fingerprint, "UserName": username} + self.rdmc.app.post_handler(certpath, body) + + elif options.command.lower() == "deletecert": + if not acct: + raise InvalidCommandLineError("Unable to find the specified account.") + certpath += acct["Id"] + self.rdmc.app.delete_handler(certpath) + + else: + raise InvalidCommandLineError("Invalid command.") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def getprivs(self, options): + """find and return the privileges to set + + :param options: command line options + :type options: list. + """ + sesprivs = self.getsesprivs() + setprivs = {} + availableprivs = self.getsesprivs(availableprivsopts=True) + + if "UserConfigPriv" not in list(sesprivs.keys()): + excp = IdTokenError() + note_msg = "" + if self.rdmc.app.getiloversion(skipschemas=True) >= 7: + note_msg = ( + "Note: If you have logged into iLO using appaccount, " + "this is an expected behaviour. Please log in through " + "--no_app_account with a privileged user credentials to proceed.\n" + ) + error_msg = ( + "The currently logged in account does not have the User Config " + "privilege and cannot add or modify user accounts.\n" + note_msg + ) + excp.message = error_msg + raise excp + + if options.optprivs: + for priv in options.optprivs: + priv = next(iter(list(priv.keys()))) + if priv not in availableprivs: + raise IncompatibleiLOVersionError("Privilege %s is not available on this " "iLO version." % priv) + + if all(priv.values() for priv in options.optprivs): + if ( + any(priv for priv in options.optprivs if "SystemRecoveryConfigPriv" in priv) + and "SystemRecoveryConfigPriv" not in sesprivs.keys() + ): + excp = IdTokenError() + note_msg = "" + if self.rdmc.app.getiloversion(skipschemas=True) >= 7: + note_msg = ( + "Note: If you have logged into iLO using appaccount, " + "this is an expected behaviour. Please log in through " + "--no_app_account with a privileged user credentials to proceed.\n" + ) + error_msg = ( + "The currently logged in account must have The System " + "Recovery Config privilege to add the System Recovery " + "Config privilege.\n" + note_msg + ) + excp.message = error_msg + raise excp + else: + setprivs = {} + for priv in options.optprivs: + setprivs.update(priv) + + return setprivs + + def getsesprivs(self, availableprivsopts=False): + """Finds and returns the curent session's privileges + + :param availableprivsopts: return available privileges + :type availableprivsopts: boolean. + """ + if self.rdmc.app.current_client: + sespath = self.rdmc.app.current_client.session_location + sespath = ( + self.rdmc.app.current_client.default_prefix + + sespath.split(self.rdmc.app.current_client.default_prefix)[-1] + ) + + ses = self.rdmc.app.get_handler(sespath, service=False, silent=True) + + if not ses: + raise SessionExpired("Invalid session. Please logout and " "log back in or include credentials.") + + sesprivs = { + "HostBIOSConfigPriv": True, + "HostNICConfigPriv": True, + "HostStorageConfigPriv": True, + "LoginPriv": True, + "RemoteConsolePriv": True, + "SystemRecoveryConfigPriv": True, + "UserConfigPriv": True, + "VirtualMediaPriv": True, + "VirtualPowerAndResetPriv": True, + "iLOConfigPriv": True, + } + if "Oem" in ses.dict: + sesoemhp = ses.dict["Oem"][self.rdmc.app.typepath.defs.oemhp] + if "Privileges" in list(sesoemhp.keys()): + sesprivs = sesoemhp["Privileges"] + availableprivs = list(sesprivs.keys()) + updated_privs = dict() + for priv, val in sesprivs.items(): + if val: + updated_privs[priv] = sesprivs[priv] + sesprivs = updated_privs + del updated_privs + else: + sesprivs = None + if availableprivsopts: + return availableprivs + else: + return sesprivs + + def credentialsvalidation( + self, + username="", + loginname="", + password="", + acct=None, + check_password=False, + options=None, + ): + """sanity validation of credentials + :param username: username to be added + :type username: str. + :param loginname: loginname to be added + :type loginname: str. + :param password: password to be added + :type password: str. + :param accounts: target federation account + :type accounts: dict. + :param check_password: flag to check password + :type check_password: bool. + """ + username_max_chars = 39 # 60 + loginname_max_chars = 39 # 60 + password_max_chars = 39 # PASSWORD MAX CHARS + password_min_chars = 8 # PASSWORD MIN CHARS + + password_min_chars = next(iter(self.rdmc.app.select("AccountService."))).dict["Oem"][ + self.rdmc.app.typepath.defs.oemhp + ]["MinPasswordLength"] + + if username != "" and loginname != "": + if len(username) > username_max_chars: + raise InvalidCommandLineError( + "Username exceeds maximum length" ". Use at most %s characters." % username_max_chars + ) + + if len(loginname) > loginname_max_chars: + raise InvalidCommandLineError( + "Login name exceeds maximum " "length. Use at most %s characters." % loginname_max_chars + ) + + try: + if acct: + if ( + acct["UserName"] == username + or acct["Oem"][self.rdmc.app.typepath.defs.oemhp]["LoginName"] == loginname + ): + raise ResourceExists("Username or login name is already in use.") + except KeyError: + pass + + if check_password: + if password == "" or password == "/r": + raise InvalidCommandLineError("An invalid password was entered.") + else: + if len(password) > password_max_chars: + raise InvalidCommandLineError( + "Password length is invalid." " Use at most %s characters." % password_max_chars + ) + if len(password) < password_min_chars: + raise InvalidCommandLineError( + "Password length is invalid." " Use at least %s characters." % password_min_chars + ) + + def iloaccountsvalidation(self, options): + """add account validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + @staticmethod + def options_argument_group(parser): + """Define optional arguments group + + :param parser: The parser to add the --addprivs option group to + :type parser: ArgumentParser/OptionParser + """ + group = parser.add_argument_group( + "GLOBAL OPTIONS", + "Options are available for all " "arguments within the scope of this command.", + ) + + group.add_argument( + "--addprivs", + dest="optprivs", + nargs="*", + action=_AccountParse, + type=str, + help="Optionally include this flag if you wish to specify " + "which privileges you want added to the iLO account. Pick " + "privileges from the privilege list in above help text. EX: --addprivs=1,2,4", + default=None, + metavar="Priv,", + ) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + privilege_help = ( + "\n\n\tPRIVILEGES:\n\t1: Login\n\t2: Remote Console\n\t3: User Config\n\t4:" + " iLO Config\n\t5: Virtual Media\n\t6: Virtual Power and Reset\n\n\tiLO 5 added " + "privileges:\n\t7: Host NIC Config\n\t8: Host Bios Config\n\t9: Host Storage Config" + "\n\t10: System Recovery Config" + ) + # default sub-parser + default_parser = subcommand_parser.add_parser( + "default", + help="Running without any sub-command will return all account information on the\n" + "currently logged in server.", + ) + default_parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + self.cmdbase.add_login_arguments_group(default_parser) + # add sub-parser + add_help = ( + "Adds an iLO user account to the currently logged in server with privileges\n" "specified in --addprivs." + ) + add_parser = subcommand_parser.add_parser( + __subparsers__[0], + help=add_help, + description=add_help + "\n\t*Note*:By default only the login privilege is added to the" + ' newly created account\n\twith role "ReadOnly"in iLO 5 and no privileges in iLO 4.' + + privilege_help + + "\n\n\tExamples:\n\n\tAdd an account with specific privileges:\n\t\tiloaccounts add " + "username accountname password --addprivs 1,2,4\n\n\tAdd an account and specify " + "privileges by role:\n\t\tiloaccounts add username accountname password --role " + "ReadOnly", + formatter_class=RawDescriptionHelpFormatter, + ) + # addprivs + add_parser.add_argument( + "identifier", + help="The username or ID of the iLO account to modify.", + metavar="USERNAMEorID#", + ) + add_parser.add_argument( + "loginname", + help="The loginname of the iLO account to add. This is NOT used to login to the newly " "created account.", + metavar="LOGINNAME", + ) + add_parser.add_argument( + "acct_password", + help="The password of the iLO account to add. If you do not include a password, you " + "will be prompted to enter one before an account is created. This is used to login to " + "the newly created account.", + metavar="PASSWORD", + nargs="?", + default="", + ) + add_parser.add_argument( + "--role", + dest="role", + choices=["Administrator", "ReadOnly", "Operator"], + help="Optionally include this option if you would like to specify Privileges by role." + " Roles are a set of privileges created based on the role of the account.", + default=None, + ) + add_parser.add_argument( + "--serviceaccount", + dest="serviceacc", + action="store_true", + help="Optionally include this flag if you wish to created account " "to be a service account.", + default=False, + ) + self.cmdbase.add_login_arguments_group(add_parser) + + self.options_argument_group(add_parser) + # modify sub-parser + modify_help = ( + "Modifies the provided iLO user account on the currently logged in server" + '\nadding privileges using "--addprivs" to include privileges and using\n' + '"--removeprivs" for removing privileges.' + ) + modify_parser = subcommand_parser.add_parser( + __subparsers__[1], + help=modify_help, + description=modify_help + privilege_help + "\n\n\tExamples:\n\n\tModify an iLO account's " + "privileges by adding:\n\tiloaccounts modify username --addprivs 3,5\n\n\t" + "Modify an iLO account's privileges by removal:\n\tiloaccounts modify username " + "--removeprivs 10\n\n\tOr modify an iLO account's privileges by both simultaneously " + "adding and removing privleges:\n\n\tiloaccounts modify username --addprivs 3,7 " + "--removeprivs 9,10", + formatter_class=RawDescriptionHelpFormatter, + ) + modify_parser.add_argument( + "identifier", + help="The username or ID of the iLO account to modify.", + metavar="USERNAMEorID#", + ) + self.cmdbase.add_login_arguments_group(modify_parser) + + self.options_argument_group(modify_parser) # addprivs + modify_parser.add_argument( + "--role", + dest="role", + choices=["Administrator", "ReadOnly", "Operator"], + help="Optionally include this option if you would like to specify Privileges by role." + " Roles are a set of privileges created based on the role of the account.", + default=None, + ) + modify_parser.add_argument( + "--removeprivs", + dest="optprivs", + nargs="*", + action=_AccountParse, + type=str, + help="Include this flag if you wish to specify " + "which privileges you want removed from the iLO account. Pick " + "privileges from the privilege list in the above help text. EX: --removeprivs=1,2,4", + default=None, + metavar="PRIV,", + ) + # changepass sub-parser + changepass_help = "Changes the password of the provided iLO user account on the currently " "logged in server." + changepass_parser = subcommand_parser.add_parser( + __subparsers__[2], + help=changepass_help, + description=changepass_help + "\n\nExamples:\n\nChange the password of an account:\n\t" + "iloaccounts changepass 2 newpassword", + formatter_class=RawDescriptionHelpFormatter, + ) + changepass_parser.add_argument( + "identifier", + help="The username or ID of the iLO account to change the password for.", + metavar="USERNAMEorID#", + ) + changepass_parser.add_argument( + "acct_password", + help="The password to change the selected iLO account to. If you do not include a " + "password, you will be prompted to enter one before an account is created. This is " + "used to login to the newly created account.", + metavar="PASSWORD", + nargs="?", + default="", + ) + self.cmdbase.add_login_arguments_group(changepass_parser) + + # delete sub-parser + delete_help = "Deletes the provided iLO user account on the currently logged in server." + delete_parser = subcommand_parser.add_parser( + __subparsers__[3], + help=delete_help, + description=delete_help + "\n\nExamples:\n\nDelete an iLO account:\n\t" "iloaccounts delete username", + formatter_class=RawDescriptionHelpFormatter, + ) + delete_parser.add_argument( + "identifier", + help="The username or ID of the iLO account to delete.", + metavar="USERNAMEorID#", + ) + self.cmdbase.add_login_arguments_group(delete_parser) + + # addcert sub-parser + addcert_help = "Adds a certificate to the provided iLO user account on the currently logged" " in server." + addcert_parser = subcommand_parser.add_parser( + __subparsers__[4], + help=addcert_help, + description=addcert_help + r"\n\nExamples:\n\nAdd a user certificate to the provided " + r"iLO account.\n\tiloaccounts addcert accountUserName C:\Users\user\cert.txt", + formatter_class=RawDescriptionHelpFormatter, + ) + addcert_parser.add_argument( + "identifier", + help="The username or ID of the iLO account to add a certificate to.", + metavar="USERNAMEorID#", + ) + addcert_parser.add_argument( + "certificate", + help="The certificate to add to the provided iLO account.", + metavar="X.509CERTIFICATE", + ) + self.cmdbase.add_login_arguments_group(addcert_parser) + + # deletecert sub-parser + deletecert_help = "Deletes a certificate to the provided iLO user account on the currently " "logged in server." + deletecert_parser = subcommand_parser.add_parser( + __subparsers__[5], + help=deletecert_help, + description=deletecert_help + "\n\nExamples:\n\nDelete a user certificate from the " + "provided iLO account.\n\tiloaccounts deletecert username", + formatter_class=RawDescriptionHelpFormatter, + ) + deletecert_parser.add_argument( + "identifier", + help="The username or ID of the iLO account to delete the certificate from.", + metavar="USERNAMEorID#", + ) + self.cmdbase.add_login_arguments_group(deletecert_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloBackupRestoreCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloBackupRestoreCommand.py new file mode 100644 index 0000000..badf63c --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloBackupRestoreCommand.py @@ -0,0 +1,310 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Factory Defaults Command for rdmc""" +import os +from argparse import RawDescriptionHelpFormatter + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + InvalidPasswordLengthError, + NoContentsFoundForOperationError, + ReturnCodes, + UploadError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + InvalidFileInputError, + InvalidPasswordLengthError, + UploadError, + ) + +from redfish.ris.ris import SessionExpired + + +class IloBackupRestoreCommand: + """Backup and restore server using iLO's .bak file""" + + def __init__(self): + self.ident = { + "name": "backuprestore", + "usage": None, + "description": "Create a .bak file. \n\tExample: backuprestore backup\n\n\t" + "Restore a server using a .bak file. \n\texample: backuprestore " + "restore\n\n\tNOTE: This command is designed to only restore\n\tthe " + "machine from which the backup file was created against.\n\tIf you would like to " + "take one configuration and apply it\n\tto multiple systems see the " + "serverclone command.\n\tThis command is only available in remote mode.", + "summary": "Backup and restore iLO to a server using a .bak file.", + "aliases": ["br"], + "auxcommands": ["LogoutCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main factorydefaults function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + # if not len(args) == 1: + # raise InvalidCommandLineError("backuprestore command takes one argument.") + + self.ilobackuprestorevalidation(options) + + if "blobstore" in self.rdmc.app.current_client.base_url: + raise InvalidCommandLineError("This command is only available remotely.") + + sessionkey = self.rdmc.app.current_client.session_key + # sessionkey = (sessionkey).encode('ascii', 'ignore') + + if options.command == "backup": + self.backupserver(options, sessionkey) + elif options.command == "restore": + self.restoreserver(options, sessionkey) + else: + raise InvalidCommandLineError("Only options are backup or restore\n") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def backupserver(self, options, skey): + """Create .bak file for a server + + :param options: command options + :type options: list. + :param skey: sessionkey of the currently logged in server + :type skey: str. + """ + select = "HpeiLOBackupRestoreService." + backupfile = None + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + service = results.resp.dict + else: + raise NoContentsFoundForOperationError("%s not found.It may not " "be available on this system.\n" % select) + + backuplocation = service["BackupFileLocation"] + backupname = backuplocation.split("/")[-1] + + postdata = [] + postdata.append(("sessionKey", skey)) + + if options.fpass: + if len(options.fpass) > 32: + raise InvalidPasswordLengthError("Length of password cannot be greater than 32 characters.") + postdata.append(("password", options.fpass)) + self.rdmc.ui.printer("Downloading backup file %s...\n" % backupname) + backupfile = self.rdmc.app.post_handler(backuplocation, postdata, service=True, silent=True) + + if backupfile: + if (backupfile.status != 200) and backupfile.read != "": + if "Invalid Session" in backupfile.read: + raise SessionExpired("Invalid session. Please logout and log back in or include credentials.") + self.rdmc.ui.printer("Download complete.\n") + outfile = open(backupname, "wb") + outfile.write(backupfile.ori) + outfile.close() + else: + raise NoContentsFoundForOperationError("Unable to download file.\n") + + def restoreserver(self, options, skey): + """Use a .bak file to restore a server + + :param options: command options + :type options: list. + :param skey: sessionkey of the currently logged in server + :type skey: str. + """ + + select = "HpeiLOBackupRestoreService." + + if options.filename: + filename = options.filename[0] + else: + files = [] + files = [f for f in os.listdir(".") if os.path.isfile(f) and f.endswith(".bak")] + if files and len(files) > 1: + raise InvalidFileInputError( + "More than one .bak file found in " + "the current directory. Please specify " + "a file using the -f option." + ) + elif not files: + raise InvalidFileInputError( + "No .bak file found in current " "directory. Please specify a file using the -f option." + ) + else: + filename = files[0] + + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + service = results.resp.dict + else: + raise NoContentsFoundForOperationError("%s not found.It may not " "be available on this system." % select) + restorelocation = service["HttpPushUri"] + postdata = [] + + with open(filename, "rb") as fle: + bakfile = fle.read() + postdata.append(("sessionKey", skey)) + if options.fpass: + postdata.append(("password", options.fpass)) + postdata.append(("file", (filename, bakfile, "application/octet-stream"))) + if isinstance(skey, bytes): + skey = skey.decode("utf-8") + resp = self.rdmc.app.post_handler( + restorelocation, + postdata, + service=False, + silent=True, + headers={"Cookie": "sessionKey=" + skey}, + ) + + if not resp.status == 200: + resp_ori = resp.ori + if isinstance(resp_ori, bytes): + resp_ori = resp_ori.decode("utf-8") + if resp_ori == "invalid_restore_password": + raise UploadError( + "Invalid or no password supplied during restore. Please " + "supply the password used during creation of the backup file." + ) + else: + raise UploadError("Error while uploading the backup file.") + else: + self.rdmc.ui.printer( + "Restore in progress. iLO will be unresponsive while the " + "restore completes.\nYour session will be terminated.\n" + ) + self.auxcommands["logout"].run("") + + def ilobackuprestorevalidation(self, options): + """factory defaults validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + subcommand_parser = customparser.add_subparsers(dest="command") + subcommand_parser.required = True + backup_help = "Create a backup of a server. This option is iLO5 Onwards" + # backup sub-parser + backup_parser = subcommand_parser.add_parser( + "backup", + help=backup_help, + description=backup_help + "\n\texample: backuprestore backup " "--f --filepass ", + formatter_class=RawDescriptionHelpFormatter, + ) + self.cmdbase.add_login_arguments_group(backup_parser) + + backup_parser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag to specify which backup file to restore. By " + "default the commmand will try to find a .bak file in the current " + "working directory.", + action="append", + default=None, + ) + backup_parser.add_argument( + "--filepass", + dest="fpass", + help="Optionally use the provided password when creating the " + "backup file. The same password must be used for restoring.", + default=None, + ) + + restore_help = "Restore a server with backup file. This option is for iLO5 Onwards" + # backup sub-parser + restore_parser = subcommand_parser.add_parser( + "restore", + help=restore_help, + description=restore_help + "\n\texample: backuprestore restore " "--f --filepass ", + formatter_class=RawDescriptionHelpFormatter, + ) + self.cmdbase.add_login_arguments_group(restore_parser) + + restore_parser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag to specify which backup file to restore. By " + "default the commmand will try to find a .bak file in the current " + "working directory.", + action="append", + default=None, + ) + restore_parser.add_argument( + "--filepass", + dest="fpass", + help="Optionally use the provided password when creating the " + "backup file. The same password must be used for restoring.", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloFederationCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloFederationCommand.py new file mode 100644 index 0000000..b545c50 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloFederationCommand.py @@ -0,0 +1,537 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Add Federation Command for rdmc""" + + +from argparse import Action, RawDescriptionHelpFormatter + +from redfish.ris.ris import SessionExpired +from redfish.ris.rmc_helper import IdTokenError + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ResourceExists, + ReturnCodes, + UsernamePasswordRequiredError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ResourceExists, + ReturnCodes, + UsernamePasswordRequiredError, + ) + +__subparsers__ = ["add", "modify", "changekey", "delete"] + + +class _FederationParse(Action): + def __init__(self, option_strings, dest, nargs, **kwargs): + super(_FederationParse, self).__init__(option_strings, dest, nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_strings): + """Federation privileges option helper""" + + privkey = { + 1: "LoginPriv", + 2: "RemoteConsolePriv", + 3: "UserConfigPriv", + 4: "iLOConfigPriv", + 5: "VirtualMediaPriv", + 6: "VirtualPowerAndResetPriv", + 7: "HostNICConfigPriv", + 8: "HostBIOSConfigPriv", + 9: "HostStorageConfigPriv", + 10: "SystemRecoveryConfigPriv", + } + + for priv in next(iter(values)).split(","): + try: + priv = int(priv) + except ValueError: + try: + parser.error("Invalid privilege entered: %s. Privileges must " "be numbers." % priv) + except: + raise InvalidCommandLineErrorOPTS("") + + try: + if not isinstance(namespace.optprivs, list): + namespace.optprivs = list() + if option_strings.startswith("--add"): + namespace.optprivs.append({privkey[priv]: True}) + elif option_strings.startswith("--remove"): + namespace.optprivs.append({privkey[priv]: False}) + except KeyError: + try: + parser.error( + "Invalid privilege entered: %s. Number does not " "match an available privlege." % priv + ) + except: + raise InvalidCommandLineErrorOPTS("") + + +class IloFederationCommand: + """Add a new ilo federation to the server""" + + def __init__(self): + self.ident = { + "name": "ilofederation", + "usage": None, + "description": "View, Add, Remove and Modify iLO Federation accoutns based on the " + "sub-command used.\nTo view help on specific sub-commands run: ilofederation " + " -h\n\nExample: ilofederation add -h\n" + "NOTE 1: By default only the login privilege is added to the newly created\n\t\t" + 'federation group with role "ReadOnly" in iLO 5 and no privileges in iLO 4.\n\t' + "NOTE 2: Federation credentials are case-sensitive.", + "summary": "Adds / deletes an iLO federation group on the currently logged in server.", + "aliases": [], + "auxcommands": [], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main addfederation function + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + body = dict() + try: + ident_subparser = False + for cmnd in __subparsers__: + if cmnd in line: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + ident_subparser = True + break + if not ident_subparser: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.ilofederationvalidation(options) + + redfish = self.rdmc.app.monolith.is_redfish + path = self.rdmc.app.typepath.defs.federationpath + results = self.rdmc.app.get_handler(path, service=True, silent=True).dict + + if redfish: + if "Members" in results: + results = results["Members"] + else: + results = list() + else: + if "Member" in results["links"]: + results = results["links"]["Member"] + else: + results = list() + + mod_fed = None + path = None + for indx, acct in enumerate(results): + fed = self.rdmc.app.get_handler( + acct[self.rdmc.app.typepath.defs.hrefstring], service=True, silent=True + ).dict + try: + if hasattr(options, "fedname"): + if fed["Name"].lower() == options.fedname.lower(): + if redfish: + path = acct["@odata.id"] + else: + path = acct["href"] + mod_fed = fed + elif options.command == "default": + results[indx] = fed + else: + raise KeyError + except KeyError: + continue + else: + if mod_fed: + break + + if mod_fed: + if options.command.lower() == "add": + raise ResourceExists("Federation name %s is already in use." % options.fedname) + else: + if options.command.lower() != "add" and options.command.lower() != "default": + raise InvalidCommandLineError("Unable to find the specified federation %s." % options.fedname) + + if options.command.lower() == "add": + privs = self.getprivs(options) + path = self.rdmc.app.typepath.defs.federationpath + + body = {"Name": options.fedname, "Key": options.fedkey} + if privs: + body.update({"Privileges": privs}) + self.addvalidation(options.fedname, options.fedkey, results) + + if path and body: + resp = self.rdmc.app.post_handler(path, body, silent=False, service=False) + + if resp and resp.dict: + if "resourcealreadyexist" in str(resp.dict).lower(): + raise ResourceExists("") + + elif options.command.lower() == "changekey": + try: + newkey = options.fedkey + except: + raise InvalidCommandLineError("Invalid number of parameters.") + + body = {"Key": newkey} + + if path and body: + self.rdmc.app.patch_handler(path, body, service=True) + else: + raise NoContentsFoundForOperationError("Unable to find " "the specified federation.") + elif options.command.lower() == "modify": + if options.optprivs: + body.update({"Privileges": {}}) + if any( + priv for priv in options.optprivs if "SystemRecoveryConfigPriv" in priv + ) and "SystemRecoveryConfigPriv" not in list(self.getsesprivs().keys()): + raise IdTokenError( + "The currently logged in federation must have The System " + "Recovery Config privilege to add the System Recovery " + "Config privilege." + ) + privs = self.getprivs(options) + body["Privileges"] = privs + + self.rdmc.app.patch_handler(path, body) + + elif options.command.lower() == "delete": + if path: + self.rdmc.app.delete_handler(path) + else: + if len(results) == 0: + self.rdmc.ui.printer("No iLO Federation accounts found.\n") + else: + self.rdmc.ui.printer("iLO Federation Id list with Privileges:\n") + if options.json: + outdict = dict() + for fed in sorted(results, key=lambda k: k["Name"]): + outdict[fed["Name"]] = fed["Privileges"] + self.rdmc.ui.print_out_json_ordered(outdict) + else: + for fed in sorted(results, key=lambda k: k["Name"]): + privstr = "" + privs = fed["Privileges"] + for priv in privs: + privstr += priv + "=" + str(privs[priv]) + "\n" + self.rdmc.ui.printer("\nName=%s:\n%s" % (fed["Name"], privstr)) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def getprivs(self, options): + """find and return the privileges to set + :param options: command line options + :type options: list. + """ + sesprivs = self.getsesprivs() + setprivs = {} + availableprivs = self.getsesprivs(availableprivsopts=True) + + if "UserConfigPriv" not in list(sesprivs.keys()): + raise IdTokenError( + "The currently logged in federation does not have the Config" + "Privilege and cannot add or modify federations." + ) + + if options.optprivs: + for priv in options.optprivs: + priv = next(iter(list(priv.keys()))) + if not options.user or not options.password: + if priv == "SystemRecoveryConfigPriv" and self.rdmc.app.current_client.base_url == "blobstore://.": + raise UsernamePasswordRequiredError( + "Privilege %s need username and password to be specified." % priv + ) + if priv not in availableprivs: + raise IncompatibleiLOVersionError("Privilege %s is not available on this " "iLO version." % priv) + + if all(priv.values() for priv in options.optprivs): + if ( + any(priv for priv in options.optprivs if "SystemRecoveryConfigPriv" in priv) + and "SystemRecoveryConfigPriv" not in sesprivs.keys() + ): + raise IdTokenError( + "The currently logged in account must have The System " + "Recovery Config privilege to add the System Recovery " + "Config privilege." + ) + else: + setprivs = {} + for priv in options.optprivs: + setprivs.update(priv) + + return setprivs + + def getsesprivs(self, availableprivsopts=False): + """Finds and returns the current session's privileges + :param availableprivsopts: return available privileges + :type availableprivsopts: boolean. + """ + if self.rdmc.app.current_client: + sespath = self.rdmc.app.current_client.session_location + sespath = ( + self.rdmc.app.current_client.default_prefix + + sespath.split(self.rdmc.app.current_client.default_prefix)[-1] + ) + + ses = self.rdmc.app.get_handler(sespath, service=False, silent=True) + + if ses.status != 200: + raise SessionExpired("Invalid session. Please logout and " "log back in or include credentials.") + + sesprivs = { + "HostBIOSConfigPriv": True, + "HostNICConfigPriv": True, + "HostStorageConfigPriv": True, + "LoginPriv": True, + "RemoteConsolePriv": True, + "SystemRecoveryConfigPriv": True, + "UserConfigPriv": True, + "VirtualMediaPriv": True, + "VirtualPowerAndResetPriv": True, + "iLOConfigPriv": True, + } + if "Oem" in ses.dict: + sesoemhp = ses.dict["Oem"][self.rdmc.app.typepath.defs.oemhp] + if "Privileges" in list(sesoemhp.keys()): + sesprivs = sesoemhp["Privileges"] + availableprivs = list(sesprivs.keys()) + keepprivs = dict() + for priv, val in sesprivs.items(): + if val: + keepprivs[priv] = sesprivs[priv] + sesprivs = keepprivs + else: + sesprivs = None + + if availableprivsopts: + return availableprivs + else: + return sesprivs + + def addvalidation(self, username, key, feds): + """add validation function + :param username: username to be added + :type username: str. + :param key: key to be added + :type key: str. + :param feds: list of federation accounts + :type feds: list. + """ + + if len(username) >= 32: + raise InvalidCommandLineError("User name exceeds maximum length.") + elif len(key) >= 32 or len(key) <= 7: + raise InvalidCommandLineError("Password is invalid length.") + + def ilofederationvalidation(self, options): + """addfederation validation function + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + @staticmethod + def options_addprivs_argument_group(parser): + """Define optional arguments group + :param parser: The parser to add the addprivs option group to + :type parser: ArgumentParser/OptionParser + """ + parser.add_argument( + "--addprivs", + dest="optprivs", + nargs="*", + action=_FederationParse, + type=str, + help="Optionally include this flag if you wish to specify " + "which privileges you want added to the iLO federation. This overrides the default of " + "duplicating privileges of the currently logged in federation on the new federation. " + "Pick privileges from the privilege list in the above help text. EX: --addprivs=1,2,4", + default=None, + ) + + @staticmethod + def options_removeprivs_argument_group(parser): + """Additional argument + :param parser: The parser to add the removeprivs option group to + :type parser: ArgumentParser/OptionParser + """ + + parser.add_argument( + "--removeprivs", + dest="optprivs", + nargs="*", + action=_FederationParse, + type=str, + help="Optionally include this flag if you wish to specify " + "which privileges you want removed from the iLO federation. This overrides the default" + " of duplicating privileges of the currently logged in federation on the new " + "federation. Pick privileges from the privilege list in the above help text. " + "EX: --removeprivs=1,2,4", + default=None, + ) + + def definearguments(self, customparser): + """Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + privilege_help = ( + "\n\nPRIVILEGES:\n\t1: Login\n\t2: Remote Console\n\t3: User Config\n\t4:" + " iLO Config\n\t5: Virtual Media\n\t6: Virtual Power and Reset\n\n\tiLO 5 added " + "privileges:\n\t7: Host NIC Config\n\t8: Host Bios Config\n\t9: Host Storage Config" + "\n\t10: System Recovery Config" + ) + # default sub-parser + default_parser = subcommand_parser.add_parser( + "default", + help="Running without any sub-command will return all federation group information " + " on the currently logged in server.", + ) + default_parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + self.cmdbase.add_login_arguments_group(default_parser) # add sub-parser + add_help = ( + "Adds an iLO federation group to the currently logged in server. Federation " + 'group privileges may be specified with\n"--addprivs". If a federation key ' + "is not " + "provided, the user will be prompted to provide one prior to account creation." + ) + add_parser = subcommand_parser.add_parser( + __subparsers__[0], + help=add_help, + description=add_help + + "\n\tilofederation add [FEDERATIONNAME] [FEDERATIONKEY] " + + privilege_help + + "\n\tilofederation add newilofedname thisfedkey --addprivs 1,3,4", + formatter_class=RawDescriptionHelpFormatter, + ) + add_parser.add_argument( + "fedname", + help="Federation name of the federation group to add.", + type=str, + metavar="FEDERATION NAME", + ) + add_parser.add_argument( + "fedkey", + help="Federation key of the federation group to add.", + type=str, + metavar="FEDERATION KEY", + ) + self.options_addprivs_argument_group(add_parser) + self.cmdbase.add_login_arguments_group(add_parser) + + modify_help = "Modify the privileges on an existing federation group." + modify_parser = subcommand_parser.add_parser( + __subparsers__[1], + help=modify_help, + description=modify_help + "\n\nTo add privileges:\n\tilofederation modify " + "[FEDNAME] --addprivs \n\nTo remove privileges:\n\t" + "ilofederation modify [FEDNAME] --removeprivs \n\n" + privilege_help, + formatter_class=RawDescriptionHelpFormatter, + ) + modify_parser.add_argument( + "fedname", + help="The federation name of the iLO account to modify.", + metavar="FEDERATION NAME", + type=str, + ) + self.options_addprivs_argument_group(modify_parser) + self.options_removeprivs_argument_group(modify_parser) + self.cmdbase.add_login_arguments_group(modify_parser) + + # changepass sub-parser + changekey_help = "Change the key of an iLO federation group on the currently logged in " "server." + changekey_parser = subcommand_parser.add_parser( + __subparsers__[2], + help=changekey_help, + description=changekey_help + "\n\nexample:ilofederation changekey [FEDNAME] [NEWFEDKEY]", + formatter_class=RawDescriptionHelpFormatter, + ) + changekey_parser.add_argument( + "fedname", + help="The iLO federation account to be updated with a new federation key (password).", + metavar="FEDERATION NAME", + type=str, + ) + changekey_parser.add_argument( + "fedkey", + help="The federation key (password) to be altered for the selected iLO federation " + " account. If you do not include a federation key, you will be prompted to enter one.", + metavar="FEDERATION KEY", + type=str, + nargs="?", + default="", + ) + self.cmdbase.add_login_arguments_group(changekey_parser) + + # delete sub-parser + delete_help = "Deletes the provided iLO user account on the currently logged in server." + delete_parser = subcommand_parser.add_parser( + __subparsers__[3], + help=delete_help, + description=delete_help + "\n\nexample: ilofederation delete fedname", + formatter_class=RawDescriptionHelpFormatter, + ) + delete_parser.add_argument( + "fedname", + help="The iLO federation account to delete.", + metavar="FEDERATION NAME", + type=str, + ) + self.cmdbase.add_login_arguments_group(delete_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloLicenseCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloLicenseCommand.py new file mode 100644 index 0000000..c1c6526 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloLicenseCommand.py @@ -0,0 +1,317 @@ +### +# Copyright 2016-2022 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Add License Command for rdmc""" + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + PathUnavailableError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineErrorOPTS, + PathUnavailableError, + IncompatibleiLOVersionError, + ) + +from redfish.ris import rmc_helper + + +class IloLicenseCommand: + """Add an iLO license to the server""" + + def __init__(self): + self.ident = { + "name": "ilolicense", + "usage": None, + "description": "Set an iLO license on the current logged in server.\n\t", + "summary": "Adds an iLO license key to the currently logged in server.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main ilolicense Function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("Provide required argument") + + self.addlicensevalidation(options) + + code = self.ilolicenseworkerfunction(options, args) + + self.cmdbase.logout_routine(self, options) + # Return code + return code + + def ilolicenseworkerfunction(self, options, args): + """ + Ilolicense worker function. It calls appropriate function. + :param options: command line options + :type options: list. + :param args: command line args + :type args: string. + """ + path = self.rdmc.app.typepath.defs.addlicensepath + if options.check_license: + self.check_license(options, path) + return ReturnCodes.SUCCESS + if options.confirm: + res = self.confirm_license(options, path) + if res: + return ReturnCodes.SUCCESS + else: + return ReturnCodes.ILO_LICENSE_ERROR + if options.check_state: + if self.rdmc.app.typepath.defs.isgen10: + result = self.get_license(options, path) + state = result.dict["ConfirmationRequest"]["EON"]["State"] + self.rdmc.ui.print_out_json("State: " + state) + if state == "confirmed": + self.rdmc.ui.printer("License is confirmed\n") + if state == "unconfirmed": + self.rdmc.ui.printer("License is not confirmed\n") + if state == "unlicensed": + self.rdmc.ui.printer("Server is unlicensed\n") + if state == "evaluation": + self.rdmc.ui.printer("Server is in evaluation mode\n") + return ReturnCodes.SUCCESS + else: + self.rdmc.ui.printer("Feature supported only for Gen 10 and above\n") + return ReturnCodes.SUCCESS + if options.license_key: + return_code = self.license_key(options, path, args) + return return_code + if options.uninstall_license: + return_code = self.uninstall_license(options, path) + return return_code + if ( + len(args) == 0 + and options.license_key is None + and options.check_license is None + and options.check_state is False + and options.uninstall_license is False + ): + result = self.get_license(options, path) + self.print_license_info(result.dict) + return ReturnCodes.SUCCESS + if ( + len(args) == 1 + and options.license_key is None + and options.check_license is None + and options.check_state is False + and options.uninstall_license is False + ): + return_code = self.license_key(options, path, args) + return return_code + + def print_license_info(self, results): + """ + Prints the license info + """ + for key, value in results.items(): + if "@odata" not in key: + if type(value) is dict: + self.print_license_info(value) + else: + self.rdmc.ui.printer(key + ":" + str(value) + "\n") + + def uninstall_license(self, options, path): + """ + Deletes the license + """ + if self.rdmc.app.typepath.defs.isgen10: + path = path + "1/" + else: + path = path + "/1" + try: + results = self.rdmc.app.delete_handler(path, silent=True) + if results.status == 404: + raise PathUnavailableError("License is not installed") + # return ReturnCodes.SUCCESS + if results.status == 403: + self.rdmc.ui.error("Insufficient Privilege to uninstall license") + return ReturnCodes.RIS_MISSING_ID_TOKEN + if results.status == 400: + self.rdmc.ui.printer("There is no license available to uninstall\n") + return ReturnCodes.SUCCESS + if results.status == 200: + self.rdmc.ui.printer("Uninstalled license successfully\n") + return ReturnCodes.SUCCESS + except rmc_helper.IloLicenseError: + self.rdmc.ui.error("Error Occured while Uninstall") + return ReturnCodes.ILO_LICENSE_ERROR + except IncompatibleiLOVersionError: + self.rdmc.ui.error("iLO FW version on this server doesnt support this operation") + return ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + + def license_key(self, options, path, args): + """ + Installs the license + """ + if options.license_key is not None: + if options.license_key[0] is not None: + body = {"LicenseKey": "%s" % options.license_key[0]} + else: + self.rdmc.ui.printer("Provide license key to install\n") + if len(args) == 1 and options.license_key is None: + body = {"LicenseKey": "%s" % args[0]} + try: + results = self.rdmc.app.post_handler(path, body) + if results.status == 201: + return ReturnCodes.SUCCESS + except rmc_helper.IloLicenseError: + self.rdmc.ui.error("Error Occured while install") + return ReturnCodes.ILO_LICENSE_ERROR + except rmc_helper.IdTokenError: + self.rdmc.ui.error("Insufficient Privilege to update license") + return ReturnCodes.RIS_MISSING_ID_TOKEN + + def check_license(self, options, path): + """ + Checks and Displays the license + """ + if self.rdmc.app.typepath.defs.isgen10: + if options.check_license[0] is not None: + result = self.get_license(options, path) + + if result.dict["ConfirmationRequest"]["EON"]["LicenseKey"] == options.check_license[0]: + self.rdmc.ui.printer("Matched. Provided key is installed on this server\n") + else: + self.rdmc.ui.printer("Provided key is not installed on this server\n") + else: + self.rdmc.ui.printer("Provide license key to check\n") + else: + self.rdmc.ui.printer("Feature supported only on gen 10 and above\n") + + def confirm_license(self, options, path): + """ + Confirm and Displays the license + """ + uri = path + "1" + "/Actions/HpeiLOLicense.ConfirmLicense" + contentsholder = {} + if options.issueto: + contentsholder["IssuedTo"] = options.issueto + try: + if self.rdmc.opts.verbose: + self.rdmc.ui.printer("HpeiLOLicense path and payload: %s, %s\n" % (uri, contentsholder)) + results = self.rdmc.app.post_handler(uri, contentsholder) + if results.status == 200: + self.rdmc.ui.printer("iLO License successfully confirmed.\n") + return True + else: + self.rdmc.ui.printer( + "iLO License confirmation failed. " + "Kindly check if the iLO is in factory mode or the license is advanced.\n" + ) + return False + except: + self.rdmc.ui.printer( + "iLO License confirmation failed. " + "Kindly check if the iLO is in factory mode or the license is advanced.\n" + ) + return False + + def get_license(self, options, path): + """ + Gets the license + """ + if self.rdmc.app.typepath.defs.isgen10: + path = path + "1/" + else: + path = path + "/1" + results = self.rdmc.app.get_handler(path, silent=True) + if results.status == 200: + return results + + def addlicensevalidation(self, options): + """ilolicense validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--install", + dest="license_key", + action="append", + help="""Installs the given normal or premium license""", + default=None, + ) + customparser.add_argument( + "--uninstall", + help="""Deletes the installed license""", + action="store_true", + dest="uninstall_license", + ) + customparser.add_argument( + "--check", + dest="check_license", + help="""Lists the specified license""", + action="append", + default=None, + ) + customparser.add_argument( + "--check_confirm", + dest="check_state", + action="store_true", + help="Checks the confirmed state and displays it", + ) + customparser.add_argument( + "--confirm", + dest="confirm", + action="store_true", + help="Confirms the license", + ) + customparser.add_argument( + "--issueto", + dest="issueto", + help="Confirms the license for issuer", + default=None, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloResetCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloResetCommand.py new file mode 100644 index 0000000..63db9db --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/IloResetCommand.py @@ -0,0 +1,137 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""iLO Reset Command for rdmc""" + +from redfish.ris.rmc_helper import IloResponseError + +try: + from rdmc_helper import ( + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class IloResetCommand: + """Reset iLO on the server that is currently logged in""" + + def __init__(self): + self.ident = { + "name": "iloreset", + "usage": None, + "description": "Reset iLO on the current logged in" " server.\n\tExample: iloreset", + "summary": "Reset iLO on the current logged in server.", + "aliases": [], + "auxcommands": [], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main iLO reset worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.iloresetvalidation(options) + + self.rdmc.ui.warn( + "\nAfter iLO resets, the session will be terminated." + "\nPlease wait for iLO to initialize completely before logging " + "in again.\nThis process may take up to 3 minutes to complete.\n\n" + ) + + select = "/redfish/v1/Managers/1/" + results = self.rdmc.app.get_handler(select, silent=True, service=True).dict + try: + results = results + except: + pass + + if results: + post_path = results["Actions"]["#Manager.Reset"]["target"] + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + bodydict = results + + try: + for item in bodydict["Actions"]: + if "Reset" in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = "Reset" + + post_path = bodydict["Actions"][item]["target"] + break + except: + action = "Reset" + + body = {"Action": action} + + postres = self.rdmc.app.post_handler(post_path, body, silent=True, service=True) + if postres.status == 200: + self.rdmc.ui.printer("A management processor reset is in progress.\n") + else: + self.rdmc.ui.error("An error occured during iLO reset.\n") + raise IloResponseError("") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def iloresetvalidation(self, options): + """reboot method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/LogconfigCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/LogconfigCommand.py new file mode 100644 index 0000000..4d7689d --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/LogconfigCommand.py @@ -0,0 +1,809 @@ +### +# Copyright 2025 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Command for configuring logging options""" +import json +import logging +import logging.config +import os + +try: + # Try top-level imports first (VMware packaging, flattened installs) + from rdmc_helper import LOGGER, InvalidCommandLineError, ReturnCodes + from logging_config_path import get_logging_config_path + from debug_utils import ( + set_file_handlers_level, + set_named_handler_level_if_exists, + update_config_handlers_levels, + update_config_loggers_levels, + set_runtime_logger_levels, + update_config_file_rotation_settings, + get_config_file_rotation_settings, + validate_rotation_parameters, + ) +except ImportError: + # Fallback to package-qualified imports (standard pip/development installs) + from ilorest.rdmc_helper import LOGGER, InvalidCommandLineError, ReturnCodes + from ilorest.logging_config_path import get_logging_config_path + from ilorest.debug_utils import ( + set_file_handlers_level, + set_named_handler_level_if_exists, + update_config_handlers_levels, + update_config_loggers_levels, + set_runtime_logger_levels, + update_config_file_rotation_settings, + get_config_file_rotation_settings, + validate_rotation_parameters, + ) + + +class LogconfigCommand: + """Command to configure logging options""" + + def __init__(self): + self.ident = { + "name": "logconfig", + "usage": None, + "description": ( + "Configure logging options for iLOREST\n\n\t" + "Example:\n\tilorest logconfig --enable-debug --logdir=/custom/path\n\t" + 'ilorest logconfig --logdir="C:\\path with spaces"\n\t' + "ilorest logconfig --enable-debug\n\t" + "ilorest logconfig --disable-debug\n\t" + "ilorest logconfig --log_file_size=5MB --log_retention_count=5\n\t" + "ilorest logconfig --log_file_size=10MB --log_retention_count=3\n\t" + "ilorest logconfig --show" + ), + "summary": "Configure logging options such as debug mode, log directory, and file rotation.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self.logging_config_path = get_logging_config_path() + + def parse_file_size(self, size_str): + """Parse file size string in MB format only + + :param size_str: size string (e.g., "5MB", "10mb", "1.5MB") + :type size_str: str + :returns: size in bytes + :rtype: int + :raises InvalidCommandLineError: if the size format is invalid + """ + if not isinstance(size_str, str): + size_str = str(size_str) + size_str = size_str.strip().upper() + + # Check if it ends with MB + if size_str.endswith("MB"): + try: + mb_value = float(size_str[:-2]) + if mb_value <= 0: + raise InvalidCommandLineError("File size must be a positive number") + return int(mb_value * 1024 * 1024) # Convert MB to bytes + except ValueError: + raise InvalidCommandLineError("Invalid MB format. Use format like '5MB' or '1.5MB'") + else: + # Only MB format is supported + raise InvalidCommandLineError("Invalid file size format. Use MB format only (e.g., 5MB, 10MB, 1.5MB)") + + def load_logging_config(self): + """Load logging configuration from JSON file + + :returns: dict containing logging configuration + """ + try: + if os.path.exists(self.logging_config_path): + with open(self.logging_config_path, "r") as config_file: + return json.load(config_file) + else: + # Return default configuration if file doesn't exist + return {"debug": False, "logdir": None} + except (json.JSONDecodeError, IOError) as ex: + LOGGER.warning(f"Failed to load logging config: {str(ex)}. Using defaults.") + return {"debug": False, "logdir": None} + + def save_logging_config(self, config_data): + """Save logging configuration to JSON file + + :param config_data: dictionary containing configuration to save + :type config_data: dict + """ + try: + # Ensure directory exists + config_dir = os.path.dirname(self.logging_config_path) + if not os.path.exists(config_dir): + os.makedirs(config_dir) + + with open(self.logging_config_path, "w") as config_file: + json.dump(config_data, config_file, indent=2) + + LOGGER.info(f"Logging configuration saved to: {self.logging_config_path}") + except IOError as ex: + raise InvalidCommandLineError(f"Failed to save logging config: {str(ex)}") + + def update_logging_config_value(self, key, value): + """Update a specific value in the logging configuration file + + :param key: configuration key to update + :type key: str + :param value: new value for the configuration key + :type value: any + :raises InvalidCommandLineError: if the key is not present in logging configuration + """ + # Load current configuration + logging_config = self.load_logging_config() + + # Check if the key exists in the configuration + if key not in logging_config: + raise InvalidCommandLineError( + f"Configuration parameter '{key}' not found in logging configuration file. " + f"Please ensure the parameter is defined in {self.logging_config_path}" + ) + + # Update the specific key + logging_config[key] = value + + # Save updated configuration + self.save_logging_config(logging_config) + + LOGGER.info(f"Updated logging config: {key} = {value}") + + def update_logging_config_logdir(self, logdir_path): + """Update log directory in the logging configuration file and apply to handlers + + :param logdir_path: new log directory path + :type logdir_path: str + """ + # Use the load/save helpers so behavior is consistent with other updates + try: + config_data = self.load_logging_config() + + # Ensure config_data is a dict + if not isinstance(config_data, dict): + raise InvalidCommandLineError("Loaded logging config is not a JSON object") + + # Store the logdir exactly as provided by the user + config_data["logdir"] = logdir_path + + # Update file handler paths if they exist. + # Preserve template-style filenames that use '%(logdir)s' so runtime + # substitution still works. For handlers with hardcoded absolute + # paths, move only the basename into the new directory. + handlers = config_data.get("handlers", {}) + for handler_name, handler_config in handlers.items(): + if not isinstance(handler_config, dict): + continue + + # Only consider handlers that have a 'filename' key or are FileHandlers + original_filename = handler_config.get("filename") + if original_filename: + try: + if "%(logdir)s" in original_filename: + # Leave template intact; runtime will substitute using config['logdir'] + pass + else: + # Replace directory part, keep basename + filename_only = os.path.basename(original_filename) + handler_config["filename"] = os.path.join(logdir_path, filename_only) + except Exception: + # skip modifying the handler if something goes wrong + continue + + # Persist using the command's save helper (handles directories and errors) + self.save_logging_config(config_data) + + # Try to apply to current session (best-effort) + # First ensure the log directory exists and resolve template variables + try: + # Create the directory if it doesn't exist (for immediate session application) + resolved_logdir = os.path.abspath(logdir_path) if not os.path.isabs(logdir_path) else logdir_path + if not os.path.exists(resolved_logdir): + os.makedirs(resolved_logdir, exist_ok=True) + # Create a copy of config for immediate application with resolved template variables + session_config = json.loads(json.dumps(config_data)) # Deep copy + # Replace %(logdir)s template with actual path in all handlers + for handler_name, handler_config in session_config.get("handlers", {}).items(): + if isinstance(handler_config, dict) and "filename" in handler_config: + filename = handler_config["filename"] + if "%(logdir)s" in filename: + # Replace template with actual resolved path + handler_config["filename"] = filename.replace("%(logdir)s", resolved_logdir) + logging.config.dictConfig(session_config) + LOGGER.info(f"Applied new log directory configuration: {logdir_path}") + except Exception as ex: + LOGGER.warning(f"Failed to apply logging config to current session: {str(ex)}") + + LOGGER.info(f"Updated log directory in configuration: {logdir_path}") + + except InvalidCommandLineError: + # Re-raise known command errors + raise + except Exception as ex: + raise InvalidCommandLineError(f"Failed to update log directory in config: {str(ex)}") + + def update_logging_config_levels(self, debug_enabled): + """Update logging levels in the logging configuration file + + :param debug_enabled: whether debug logging should be enabled + :type debug_enabled: bool + """ + try: + log_level = "DEBUG" if debug_enabled else "INFO" + + # Update persistent configuration for handlers and loggers. + # Track update success and return False to caller if any write/update failed. + success = True + + try: + if debug_enabled: + # Set file handler to DEBUG to capture both debug and info logs + success = success and bool( + update_config_handlers_levels(self.logging_config_path, "DEBUG", handler_names=["file"]) + ) + # Set stdout to WARNING to keep console clean + success = success and bool( + update_config_handlers_levels(self.logging_config_path, "CRITICAL", handler_names=["stdout"]) + ) + success = success and bool(update_config_loggers_levels(self.logging_config_path, "DEBUG")) + + if not success: + # caller displays warning + return False + + # Apply runtime levels: root logger at DEBUG but ensure stdout handler remains WARNING + root_logger = logging.getLogger() + # Set module/root loggers to DEBUG + set_runtime_logger_levels(root_logger, logging.DEBUG, set_module_logger=LOGGER) + # Ensure stdout handler (if present) is left at CRITICAL + set_named_handler_level_if_exists(root_logger, "logconfig_stdout", logging.CRITICAL) + # Ensure file handlers accept DEBUG + set_file_handlers_level(root_logger, logging.DEBUG) + else: + # Turning debug off: set file handler to INFO, stdout to WARNING + success = success and bool( + update_config_handlers_levels(self.logging_config_path, "INFO", handler_names=["file"]) + ) + success = success and bool( + update_config_handlers_levels(self.logging_config_path, "CRITICAL", handler_names=["stdout"]) + ) + success = success and bool( + update_config_handlers_levels(self.logging_config_path, "ERROR", handler_names=["stderr"]) + ) + success = success and bool(update_config_loggers_levels(self.logging_config_path, "INFO")) + + if not success: + # caller displays warning + return False + + root_logger = logging.getLogger() + set_runtime_logger_levels(root_logger, logging.INFO, set_module_logger=LOGGER) + # Ensure stdout handler stays at CRITICAL + set_named_handler_level_if_exists(root_logger, "logconfig_stdout", logging.CRITICAL) + # Set file handlers to INFO + set_file_handlers_level(root_logger, logging.INFO) + + LOGGER.info(f"Updated logging configuration: levels set to {log_level}") + except Exception: + # Runtime application failed, but config file updates may have succeeded. + LOGGER.debug("Failed to update runtime handler levels after saving config") + # We consider runtime application failures non-fatal for persistence; return True + return True + + return True + except (json.JSONDecodeError, IOError) as ex: + # JSON IO exceptions are considered as failures to persist configuration + LOGGER.debug(f"Failed to update logging config due to JSON/IO error: {str(ex)}") + return False + + def update_logging_config_file_rotation(self, max_bytes=None, backup_count=None): + """Update file rotation settings in the logging configuration file + + :param max_bytes: maximum size of log file in bytes before rotation + :type max_bytes: int + :param backup_count: number of log files to retain during rotation (log retention count) + :type backup_count: int + """ + try: + # Use the utility function to update rotation settings + success = update_config_file_rotation_settings(self.logging_config_path, max_bytes, backup_count) + + if not success: + warning_msg = ( + "Failed to update file rotation settings." + "No changes were applied." + ) + print(warning_msg) + return False + + # Log the changes + if max_bytes is not None: + LOGGER.info(f"Updated maxBytes: {max_bytes}") + + if backup_count is not None: + LOGGER.info(f"Updated log retention count: {backup_count}") + + # Try to apply to current session (best-effort) + try: + config_data = self.load_logging_config() + + # Create a copy of config for immediate application + session_config = json.loads(json.dumps(config_data)) # Deep copy + + # Resolve %(logdir)s template if present + logdir = config_data.get("logdir", "./ilorest_logs") + resolved_logdir = os.path.abspath(logdir) if not os.path.isabs(logdir) else logdir + + for handler_name, handler_config in session_config.get("handlers", {}).items(): + if isinstance(handler_config, dict) and "filename" in handler_config: + filename = handler_config["filename"] + if "%(logdir)s" in filename: + handler_config["filename"] = filename.replace("%(logdir)s", resolved_logdir) + + logging.config.dictConfig(session_config) + LOGGER.info("Applied new file rotation configuration to current session") + except Exception as ex: + LOGGER.warning(f"Failed to apply logging config to current session: {str(ex)}") + + except InvalidCommandLineError: + # Re-raise known command errors + raise + except Exception as ex: + raise InvalidCommandLineError(f"Failed to update file rotation settings in config: {str(ex)}") + + def apply_global_logging_config(self): + """Apply logging configuration from logging config file to current session + + This method reads the logging configuration and applies the current + log levels to the session loggers. + """ + try: + with open(self.logging_config_path, "r") as config_file: + config_data = json.load(config_file) + + # Determine the desired runtime level from persistent config and apply via helper + log_level = config_data.get("loggers", {}).get("", {}).get("level", "INFO") + runtime_level = logging.DEBUG if log_level == "DEBUG" else logging.INFO + root_logger = logging.getLogger() + + # Apply runtime logger and handler levels consistently + try: + set_runtime_logger_levels(root_logger, runtime_level, set_module_logger=LOGGER) + except Exception: + # Fallback in the unlikely event helpers fail + LOGGER.setLevel(runtime_level) + root_logger.setLevel(runtime_level) + + except (json.JSONDecodeError, IOError, FileNotFoundError) as ex: + LOGGER.warning(f"Failed to apply logging config: {str(ex)}") + + def run(self, line, help_disp=False): + """Main logconfig worker function + + :param line: command line string + :type line: str. + """ + if help_disp: + print("logconfig: Configure persistent logging options.") + print("Usage: logconfig [--enable-debug | --disable-debug] [--logdir=PATH]") + print(" [--log_file_size=SIZE] [--log_retention_count=COUNT] [--show]") + print(" --enable-debug Enable debug logging globally for all commands") + print(" --disable-debug Disable debug logging globally for all commands") + print(" --logdir=PATH Set the directory for log files (use quotes for path)") + print(" --log_file_size=SIZE Set the maximum size of log files before rotation") + print(" Specify in MB format only (e.g., 1MB, 5MB, 10MB)") + print(" --log_retention_count=COUNT Set the number of log files to retain") + print(" --show Show current logging configuration") + print("") + print("Note: Use --enable-debug to enable or --disable-debug to disable persistent debug mode.") + print(" Without any flags, shows current status.") + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + + # Check for old debug syntax patterns + line_str = " ".join(line) if isinstance(line, list) else str(line) + if "--debug" in line_str.lower(): + if len(args) > 0 and args[0].lower() in ["true", "false"]: + raise InvalidCommandLineError( + f"Invalid argument '{args[0]}'. The --debug true/false syntax is no longer supported. " + "Use --enable-debug to enable or --disable-debug to disable debug logging." + ) + else: + raise InvalidCommandLineError( + "The --debug flag is no longer supported. " + "Use --enable-debug to enable or --disable-debug to disable debug logging." + ) + + # Check for unexpected positional arguments that might be from old syntax + if len(args) > 0 and args[0].lower() in ["true", "false"]: + raise InvalidCommandLineError( + f"Invalid argument '{args[0]}'. " + "Use --enable-debug to enable or --disable-debug to disable debug logging." + ) + + # Handle global --logdir flag + if hasattr(self.rdmc, "opts") and hasattr(self.rdmc.opts, "logdir") and self.rdmc.opts.logdir: + options.logdir = self.rdmc.opts.logdir + self.rdmc.opts.logdir = None + + # Handle equals format for logdir: --logdir=path + if "=" in line_str: + parts = line_str.split() + for part in parts: + if part.startswith("--logdir="): + options.logdir = part.split("=", 1)[1] + elif part.startswith("--log_file_size="): + try: + size_str = part.split("=", 1)[1] + options.log_file_size = self.parse_file_size(size_str) + except (ValueError, InvalidCommandLineError) as e: + raise InvalidCommandLineError(f"log_file_size error: {str(e)}") + elif part.startswith("--log_retention_count="): + try: + options.backup_count = int(part.split("=", 1)[1]) + except ValueError: + raise InvalidCommandLineError("log retention count must be a valid integer") + + # Workaround for logdir: if logdir is None but we have args that look like paths + if options.logdir is None and len(args) > 0: + for arg in args: + if os.path.isabs(arg) or "/" in arg or "\\" in arg: + options.logdir = arg + break + + except Exception as ex: + LOGGER.error(f"Failed to parse command arguments: {str(ex)}") + return ReturnCodes.GENERAL_ERROR + + logOutput = [] + + # Check if show flag was provided - only short-circuit when no action flags are given. + # If --show is combined with other flags (enable/disable/logdir/etc.) + # we should process the actions first, then display the configuration + if hasattr(options, "show") and options.show and not any( + [ + hasattr(options, "enable_debug") and options.enable_debug, + hasattr(options, "disable_debug") and options.disable_debug, + hasattr(options, "logdir") and options.logdir, + hasattr(options, "log_file_size") and options.log_file_size, + hasattr(options, "backup_count") and options.backup_count, + ] + ): + try: + with open(self.logging_config_path, "r") as config_file: + config_data = json.load(config_file) + + # Show key logging information + if "loggers" in config_data and "" in config_data["loggers"]: + log_level = config_data["loggers"][""]["level"] + debug_enabled = log_level == "DEBUG" + + if "logdir" in config_data: + print(f"Log Directory: {config_data['logdir']}") + + # Show file rotation settings for RotatingFileHandler + rotation_settings = get_config_file_rotation_settings(self.logging_config_path) + if rotation_settings: + for handler_name, settings in rotation_settings.items(): + max_bytes = settings.get("maxBytes", "Not set") + backup_count = settings.get("backupCount", "Not set") + print(f"Log File Rotation ({handler_name}):") + + # Display file size in MB format + if max_bytes != "Not set" and isinstance(max_bytes, (int, float)): + mb_size = max_bytes / (1024 * 1024) + if mb_size == int(mb_size): + size_display = f"{int(mb_size)} MB" + else: + size_display = f"{mb_size:.1f} MB" + print(f" Maximum file size: {size_display}") + else: + print(f" Maximum file size: {max_bytes}") + + print(f" Log retention count: {backup_count}") + + # Show handlers information + if "handlers" in config_data: + print("Active Levels:") + for handler_name, handler_config in config_data["handlers"].items(): + handler_level = handler_config.get("level", "INFO") + print(f" - {handler_name}: {handler_level}") + + except Exception as ex: + # ex prints cwd/logging_config.json if configuration file not found + LOGGER.debug(f"Error reading configuration: {str(ex)}") + print("JSON configuration file not found") + return ReturnCodes.SUCCESS + + # Check if enable-debug flag was provided + if hasattr(options, "enable_debug") and options.enable_debug: + debug_enabled = True + + # Update current session logging + LOGGER.setLevel(logging.DEBUG) + + # Also set the root logger to DEBUG to ensure all debug messages are captured + root_logger = logging.getLogger() + root_logger.setLevel(logging.DEBUG) + + LOGGER.debug("Debug logging is enabled for current session") + + # Update logging configuration file with new logging levels + try: + success = self.update_logging_config_levels(debug_enabled) + if success: + LOGGER.info("Debug logging has been enabled globally.") + LOGGER.info("Debug mode will persist for all future iLORest sessions.") + LOGGER.info("To disable debug mode, use: logconfig --disable-debug") + logOutput.append("Debug logging has been enabled globally.") + logOutput.append("Debug mode will persist for all future iLORest sessions.") + logOutput.append("To disable debug mode, use: logconfig --disable-debug") + else: + warning_msg = ( + "Failed to enable debug mode." + "No changes were applied." + ) + print(warning_msg) + except Exception as ex: + LOGGER.error(f"Failed to update logging config: {str(ex)}") + + # Check if disable-debug flag was provided + elif hasattr(options, "disable_debug") and options.disable_debug: + debug_enabled = False + + # Set loggers back to INFO level when debug is disabled + LOGGER.setLevel(logging.INFO) + + # Reset root logger to INFO + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + + LOGGER.info("Debug logging is disabled for current session") + + # Update logging configuration file with new logging levels + try: + success = self.update_logging_config_levels(debug_enabled) + if success: + LOGGER.info("Debug logging has been disabled globally.") + LOGGER.info("Debug mode is now turned off for all future iLORest sessions.") + LOGGER.info("To enable debug mode, use: logconfig --enable-debug") + logOutput.append("Debug logging has been disabled globally.") + logOutput.append("Debug mode is now turned off for all future iLORest sessions.") + logOutput.append("To enable debug mode, use: logconfig --enable-debug") + else: + warning_msg = ( + "Failed to disable debug mode." + "No changes were applied." + ) + print(warning_msg) + except Exception as ex: + LOGGER.error(f"Failed to update logging config: {str(ex)}") + return ReturnCodes.GENERAL_ERROR + + # If no specific action flags were provided and --show was not used, show current status + if not any( + [ + hasattr(options, "enable_debug") and options.enable_debug, + hasattr(options, "disable_debug") and options.disable_debug, + hasattr(options, "logdir") and options.logdir, + hasattr(options, "log_file_size") and options.log_file_size, + hasattr(options, "backup_count") and options.backup_count, + hasattr(options, "show") and options.show, + ] + ): + try: + with open(self.logging_config_path, "r") as config_file: + config_data = json.load(config_file) + + debug_enabled = False + if "loggers" in config_data and "" in config_data["loggers"]: + log_level = config_data["loggers"][""]["level"] + debug_enabled = log_level == "DEBUG" + + if debug_enabled: + logOutput.append("Debug logging currently enabled globally.") + logOutput.append("To disable persistent debug mode, use: logconfig --disable-debug") + else: + logOutput.append("Debug logging currently disabled globally.") + logOutput.append("To enable persistent debug mode, use: logconfig --enable-debug") + except Exception as ex: + LOGGER.warning(f"Failed to read logging config: {str(ex)}") + logOutput.append("Debug logging status unknown.") + logOutput.append("To enable persistent debug mode, use: logconfig --enable-debug") + + # Configure log directory + if hasattr(options, "logdir") and options.logdir: + # For directory creation and writability checks, resolve to absolute path + resolved_path = os.path.abspath(options.logdir) if not os.path.isabs(options.logdir) else options.logdir + + if not os.path.exists(resolved_path): + try: + os.makedirs(resolved_path, exist_ok=True) + LOGGER.info(f"Created log directory: {resolved_path}") + except OSError as ex: + raise InvalidCommandLineError(f"Failed to create log directory '{resolved_path}': {str(ex)}") + + # Verify directory is writable + if not os.access(resolved_path, os.W_OK): + raise InvalidCommandLineError(f"Log directory '{resolved_path}' is not writable") + + # Update current session logging configuration + if hasattr(self.rdmc, "config"): + self.rdmc.config.logdir = options.logdir + + # Update logging configuration file and apply to handlers + try: + self.update_logging_config_logdir(options.logdir) + logOutput.append(f"Log directory updated to: {options.logdir}") + logOutput.append("New log files will be written to the specified directory.") + except Exception as ex: + LOGGER.error(f"Failed to update log directory configuration: {str(ex)}") + return ReturnCodes.GENERAL_ERROR + + # Configure log file rotation settings + if (hasattr(options, "log_file_size") and options.log_file_size is not None) or ( + hasattr(options, "backup_count") and options.backup_count is not None + ): + + max_bytes = getattr(options, "log_file_size", None) + backup_count = getattr(options, "backup_count", None) + + # Parse file size if it's a string (from argparse) + if max_bytes is not None and isinstance(max_bytes, str): + try: + max_bytes = self.parse_file_size(max_bytes) + except InvalidCommandLineError: + raise + + # Validate input values using utility function + is_valid, error_message = validate_rotation_parameters(max_bytes, backup_count) + if not is_valid: + raise InvalidCommandLineError(error_message) + + try: + success = self.update_logging_config_file_rotation(max_bytes, backup_count) + + if success: + if max_bytes is not None: + # Display the size in MB format + mb_size = max_bytes / (1024 * 1024) + if mb_size == int(mb_size): + size_display = f"{int(mb_size)} MB" + else: + size_display = f"{mb_size:.1f} MB" + logOutput.append(f"Log file maximum size updated to: {size_display}") + + if backup_count is not None: + logOutput.append(f"Log retention count updated to: {backup_count}") + + logOutput.append("Log rotation settings have been updated.") + except Exception as ex: + LOGGER.error(f"Failed to update log rotation configuration: {str(ex)}") + return ReturnCodes.GENERAL_ERROR + + # Print summary + for msg in logOutput: + print(msg) + + # If the user requested --show along with action flags, display the + # current configuration now that we've processed the actions. + if hasattr(options, "show") and options.show: + try: + with open(self.logging_config_path, "r") as config_file: + config_data = json.load(config_file) + + # Show key logging information + if "loggers" in config_data and "" in config_data["loggers"]: + log_level = config_data["loggers"][""].get("level", "INFO") + debug_enabled = log_level == "DEBUG" + + if "logdir" in config_data: + print(f"Log Directory: {config_data['logdir']}") + + # Show file rotation settings for RotatingFileHandler + rotation_settings = get_config_file_rotation_settings(self.logging_config_path) + if rotation_settings: + for handler_name, settings in rotation_settings.items(): + max_bytes = settings.get("maxBytes", "Not set") + backup_count = settings.get("backupCount", "Not set") + print(f"Log File Rotation ({handler_name}):") + + if max_bytes != "Not set" and isinstance(max_bytes, (int, float)): + mb_size = max_bytes / (1024 * 1024) + if mb_size == int(mb_size): + size_display = f"{int(mb_size)} MB" + else: + size_display = f"{mb_size:.1f} MB" + print(f" Maximum file size: {size_display}") + else: + print(f" Maximum file size: {max_bytes}") + + print(f" Log retention count: {backup_count}") + + # Show handlers information + if "handlers" in config_data: + print("Active Levels:") + for handler_name, handler_config in config_data["handlers"].items(): + handler_level = handler_config.get("level", "INFO") + print(f" - {handler_name}: {handler_level}") + + except Exception as ex: + LOGGER.debug(f"Error reading configuration for --show after actions: {str(ex)}") + print("JSON configuration file not found") + return ReturnCodes.SUCCESS + + def definearguments(self, customparser): + """arguments for the logconfig command""" + if not customparser: + return + + # Debug control arguments + customparser.add_argument( + "--enable-debug", + dest="enable_debug", + action="store_true", + help=( + "Enable persistent debug logging for all commands. " + "If not provided, debug logging state remains unchanged." + ), + ) + + customparser.add_argument( + "--disable-debug", + dest="disable_debug", + action="store_true", + help=("Disable persistent debug logging for all commands."), + ) + + # Log directory argument + customparser.add_argument( + "--logdir", + dest="logdir", + help="Set the directory for log files and update logging configuration.", + metavar="PATH", + ) + + # Log file rotation argument + customparser.add_argument( + "--log_file_size", + dest="log_file_size", + type=str, + help="Set the maximum size of log files before rotation. " + "Specify in MB format only (e.g., 1MB, 5MB, 10MB).", + metavar="SIZE", + ) + + # Log retention count argument + customparser.add_argument( + "--log_retention_count", + dest="backup_count", + type=int, + help="Set the number of log files to retain during rotation.", + metavar="COUNT", + ) + + # Show current configuration + customparser.add_argument( + "--show", + dest="show", + action="store_true", + help="Show current logging configuration.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/OneButtonEraseCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/OneButtonEraseCommand.py new file mode 100644 index 0000000..cfa5059 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/OneButtonEraseCommand.py @@ -0,0 +1,298 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Factory Defaults Command for rdmc""" +import time +from collections import OrderedDict + +import colorama +from six.moves import input + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + +CURSOR_UP_ONE = "\x1b[1A" +ERASE_LINE = "\x1b[2K" + + +class OneButtonEraseCommand: + """Backup and restore server using iLO's .bak file""" + + def __init__(self): + self.ident = { + "name": "onebuttonerase", + "usage": None, + "description": "Erase all iLO settings, Bios settings, User Data, and iLO Repository data." + "\n\tExample: onebuttonerase\n\n\tSkip the confirmation before" + " erasing system data.\n\texample: onebuttonerase --confirm\n\nWARNING: This " + "command will erase user data! Use with extreme caution! Complete erase can take" + " up to 24 hours to complete.", + "summary": "Performs One Button Erase on a system.", + "aliases": [], + "auxcommands": ["RebootCommand"], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main onebuttonerase function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("onebuttonerase command takes no arguments.") + + self.onebuttonerasevalidation(options) + + select = "ComputerSystem." + results = self.rdmc.app.select(selector=select) + + if self.rdmc.app.getiloversion() < 5.140: + raise IncompatibleiLOVersionError("One Button Erase is only available on iLO 5 1.40 " "and greater.") + try: + results = results[0].dict + except: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + if ( + results["Oem"]["Hpe"]["SystemROMAndiLOEraseStatus"] == "Idle" + and results["Oem"]["Hpe"]["UserDataEraseStatus"] == "Idle" + ): + post_path = None + body_dict = {"SystemROMAndiLOErase": True, "UserDataErase": True} + for item in results["Oem"]["Hpe"]["Actions"]: + if "SecureSystemErase" in item: + post_path = results["Oem"]["Hpe"]["Actions"][item]["target"] + break + + if options.confirm: + userresp = "erase" + else: + userresp = input( + 'Please type "erase" to begin erase process. Any other input will' + " cancel the operation. If you wish to skip this prompt add the --confirm flag: " + ) + + if userresp == "erase": + if post_path and body_dict: + self.rdmc.app.post_handler(post_path, body_dict) + self.rdmc.app.post_handler( + results["Actions"]["#ComputerSystem.Reset"]["target"], + {"ResetType": "ForceRestart"}, + ) + if not options.nomonitor: + self.monitor_erase(results["@odata.id"]) + return ReturnCodes.SUCCESS + else: + NoContentsFoundForOperationError("Unable to start One Button Erase.") + else: + self.rdmc.ui.printer("Canceling One Button Erase.\n") + return ReturnCodes.SUCCESS + else: + self.rdmc.ui.warn("System is already undergoing a One Button Erase process...\n") + if not options.nomonitor: + self.monitor_erase(results["@odata.id"]) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def monitor_erase(self, path): + """Monitor the One Button Erase progress + + :param path: Path to the one button monitor path + :type path: str. + """ + print_dict = { + "BIOSSettingsEraseStatus": "Bios Settings Erase:", + "iLOSettingsEraseStatus": "iLO Settings Erase:", + "ElapsedEraseTimeInMinutes": "Elapsed Time in Minutes:", + "EstimatedEraseTimeInMinutes": "Estimated Remaining Time in Minutes:", + "NVDIMMEraseStatus": "NVDIMM Erase:", + "NVMeDrivesEraseStatus": "NVMe Drive Erase:", + "SATADrivesEraseStatus": "SATA Drive Erase:", + "TPMEraseStatus": "TPM Erase:", + "SmartStorageEraseStatus": "Smart Storage Erase:", + "SystemROMAndiLOEraseStatus": "Bios and iLO Erase:", + "UserDataEraseStatus": "User Data Erase:", + } + colorama.init() + + self.rdmc.ui.printer("\tOne Button Erase Status\n") + self.rdmc.ui.printer("==========================================================\n") + results = self.rdmc.app.get_handler(path, service=True, silent=True) + counter = 0 + eraselines = 0 + while True: + if not (counter + 1) % 8: + results = self.rdmc.app.get_handler(path, service=True, silent=True) + print_data = self.gather_data(results.dict["Oem"]["Hpe"]) + + self.reset_output(eraselines) + + for key in list(print_data.keys()): + self.print_line(print_dict[key], print_data[key], counter) + if counter == 7: + counter = 0 + else: + counter += 1 + if all( + [ + print_data[key].lower() in ["completedwithsuccess", "completedwitherrors", "failed"] + for key in list(print_data.keys()) + if not key.lower() in ["elapsederasetimeinminutes", "estimatederasetimeinminutes"] + ] + ): + break + eraselines = len(list(print_data.keys())) + time.sleep(0.5) + colorama.deinit() + options = {} + self.cmdbase.logout_routine(self, options) + + def gather_data(self, resdict): + """Gather information on current progress from response + + :param resdict: response dictionary to parse + :type resdict: dict. + """ + retdata = OrderedDict() + data = [ + ("ElapsedEraseTimeInMinutes", None), + ("EstimatedEraseTimeInMinutes", None), + ( + "SystemROMAndiLOEraseComponentStatus", + ["BIOSSettingsEraseStatus", "iLOSettingsEraseStatus"], + ), + ( + "UserDataEraseComponentStatus", + [ + "NVDIMMEraseStatus", + "NVMeDrivesEraseStatus", + "SATADrivesEraseStatus", + "SmartStorageEraseStatus", + "TPMEraseStatus", + ], + ), + ] + for key, val in data: + if val: + if key == "SystemROMAndiLOEraseComponentStatus": + try: + resdict[key] + except KeyError: + retdata["SystemROMAndiLOEraseStatus"] = resdict["SystemROMAndiLOEraseStatus"] + elif key == "UserDataEraseComponentStatus": + try: + if not resdict[key]: + raise KeyError() + except KeyError: + retdata["UserDataEraseStatus"] = resdict["UserDataEraseStatus"] + for item in val: + try: + retdata[item] = resdict[key][item] + except KeyError: + pass + else: + try: + retdata[key] = resdict[key] + except KeyError: + pass + + return retdata + + def reset_output(self, numlines=0): + """reset the output for the next print""" + for _ in range(numlines): + self.rdmc.ui.printer(CURSOR_UP_ONE) + self.rdmc.ui.printer(ERASE_LINE) + + def print_line(self, pstring, value, ctr): + """print the line from system monitoring""" + pline = "%s %s" % (pstring, value) + + spinner = ["|", "/", "-", "\\"] + if str(value).lower() in ["initiated", "inprogress"]: + pline += "\t%s" % spinner[ctr % 4] + pline += "\n" + + self.rdmc.ui.printer(pline) + + def onebuttonerasevalidation(self, options): + """one button erase validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--nomonitor", + dest="nomonitor", + help="Optionally include this flag to skip monitoring of the one button erase process " + "and simply trigger the operation.", + action="store_true", + default=False, + ) + customparser.add_argument( + "--confirm", + dest="confirm", + help="Optionally include this flag to skip the confirmation prompt before starting One" + " Button Erase and begin the operation.", + action="store_true", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/RebootCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/RebootCommand.py new file mode 100644 index 0000000..662268d --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/RebootCommand.py @@ -0,0 +1,271 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Reboot Command for rdmc""" + +import time + +from six.moves import input + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class RebootCommand: + """Reboot server that is currently logged in""" + + def __init__(self): + self.ident = { + "name": "reboot", + "usage": None, + "description": "Remotely control system power state commands such as, " + "\n\t1. Turning the system on.\n\t2. Turning the system off.\n\t3. Power " + "cycling/rebooting.\n\t4. Issuing a Non-Maskable Interrupt (NMI).\n\t5. Any " + "number of pre-defined operations through virtual power-button presses." + "\n\n\tNote: By default a force " + "restart will occur, if the system is in an applicable power state.\n\texample: " + "reboot On\n\n\tOPTIONAL PARAMETERS AND DESCRIPTIONS:" + "\n\tOn \t\t(Turns the system on.)\n\tForceOff " + "\t(Performs an immediate non-graceful shutdown.)" + "\n\tForceRestart \t(DEFAULT) (Performs" + " an immediate non-graceful shutdown,\n\t\t\t" + " followed by a restart of the system.)\n\tNmi " + "\t\t(Generates a Non-Maskable Interrupt to cause" + " an\n\t\t\t immediate system halt.)\n\tPushPowerButton " + "(Simulates the pressing of the physical power " + "button\n\t\t\t on this system.)\n\n\tOEM PARAMETERS AND" + " DESCRIPTIONS:\n\tPress\t\t(Simulates the pressing of the" + " physical power button\n\t\t\t on this system.)\n\t" + "PressAndHold\t(Simulates pressing and holding of the power" + " button\n\t\t\t on this systems.)\n\tColdBoot\t(Immidiately" + " Removes power from the server,\n\t\t\tfollowed by a restart" + " of the system)", + "summary": "Reboot operations for the current logged in server.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main reboot worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if len(args) < 2: + self.rebootvalidation(options) + else: + raise InvalidCommandLineError("Invalid number of parameters." " Reboot takes a maximum of 1 parameter.") + + if not args: + self.rdmc.ui.warn( + "\nAfter the server is rebooted the session will be terminated." + "\nPlease wait for the server to boot completely before logging in " + "again.\nRebooting server in 3 seconds...\n" + ) + time.sleep(3) + else: + self.printreboothelp(args[0]) + time.sleep(3) + + select = "ComputerSystem." + results = self.rdmc.app.select(selector=select) + oemlist = ["press", "pressandhold", "coldboot"] + + try: + results = results[0] + except: + pass + + if results: + put_path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("Unable to find %s" % select) + + if args and args[0].lower() in oemlist: + bodydict = results.resp.dict["Oem"][self.rdmc.app.typepath.defs.oemhp] + + if args[0].lower() == "coldboot": + resettype = "SystemReset" + else: + resettype = "PowerButton" + else: + bodydict = results.resp.dict + resettype = "Reset" + + try: + for item in bodydict["Actions"]: + if resettype in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = resettype + + put_path = bodydict["Actions"][item]["target"] + break + except: + action = resettype + + if args and not args[0].lower() == "forcerestart": + if args[0].lower() == "on": + body = {"Action": action, "ResetType": "On"} + elif args[0].lower() == "forceoff": + body = {"Action": action, "ResetType": "ForceOff"} + elif args[0].lower() == "nmi": + body = {"Action": action, "ResetType": "Nmi"} + elif args[0].lower() == "pushpowerbutton": + body = {"Action": action, "ResetType": "PushPowerButton"} + elif args[0].lower() == "press": + body = {"Action": action, "PushType": "Press"} + elif args[0].lower() == "pressandhold": + body = {"Action": action, "PushType": "PressAndHold"} + elif args[0].lower() == "coldboot": + body = {"Action": action, "ResetType": "ColdBoot"} + else: + body = {"Action": action, "ResetType": "ForceRestart"} + + if options.confirm is True: + count = 0 + while True: + count = count + 1 + confirmation = input("Rebooting system, type yes to confirm or no to abort:") + if confirmation.lower() in ("no", "n") or count > 3: + self.rdmc.ui.printer("Aborting reboot.\n") + return ReturnCodes.SUCCESS + elif confirmation.lower() in ("yes", "y"): + break + + self.rdmc.app.post_handler(put_path, body) + + if not options.nologout: + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def printreboothelp(self, flag): + """helper print function for reboot function + + :param flag: command line option + :type flag: str + """ + if flag.upper() == "ON": + self.rdmc.ui.warn( + "\nThe server is powering on. Note, the current session will be " + "terminated.\nPlease wait for the server to boot completely before logging in " + "again.\nTurning on the server in 3 seconds...\n" + ) + elif flag.upper() == "FORCEOFF": + self.rdmc.ui.warn( + "\nThe server is powering off. Note, the current session will be " + "terminated.\nPlease wait for the server to power off completely before logging " + "in again.\nPowering off the server in 3 seconds...\n" + ) + elif flag.upper() == "FORCERESTART": + self.rdmc.ui.warn( + "\nForcing a server restart. Note, the current session will be " + "terminated.\nPlease wait for the server to boot completely before logging in " + "again.\nRebooting the server in 3 seconds...\n" + ) + elif flag.upper() == "NMI": + self.rdmc.ui.warn( + "\nA non-maskable interrupt will be issued to this server. Note, the " + "current session will be terminated.\nIssuing interrupt in 3 seconds...\n" + ) + elif flag.upper() == "PUSHPOWERBUTTON" or flag.upper() == "PRESS": + self.rdmc.ui.warn( + "\nThe server power button will be virtually pushed; the reaction " + "will be dependent on the current system power and boot state. Note the current " + "session will be terminated.\nVirtual push in 3 seconds...\n" + ) + elif flag.upper() == "COLDBOOT": + self.rdmc.ui.warn( + "\nThe server will be cold boot, power cycled. Note, the current " + "session will be terminated.\nPlease wait for the server to boot completely " + "before logging in again.\nCold Booting server in 3 seconds...\n" + ) + elif flag.upper() == "PRESSANDHOLD": + self.rdmc.ui.warn( + "\nThe server will be forcefully powered off. Note, the current " + "session will be terminated.\nPlease wait for the server to power off " + "completely before logging in again.\nPressing and holding the power button in " + "3 seconds...\n" + ) + else: + raise InvalidCommandLineError("Invalid parameter: '%s'. Please run" " 'help reboot' for parameters." % flag) + + def rebootvalidation(self, options): + """reboot method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--confirm", + dest="confirm", + action="store_true", + help="Optionally include to request user confirmation for reboot.", + default=False, + ) + customparser.add_argument( + "--nologout", + dest="nologout", + action="store_true", + help="Optionally include to not to logout of iLO connection.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SendTestCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SendTestCommand.py new file mode 100644 index 0000000..caef9a1 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SendTestCommand.py @@ -0,0 +1,155 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""SendTest Command for rdmc""" + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class SendTestCommand: + """Send syslog test to the logged in server""" + + def __init__(self): + self.ident = { + "name": "sendtest", + "usage": None, + "description": "Send syslog test to the " + "current logged in server.\n\tExample: sendtest syslog\n\n" + "\tSend alert mail test to the current logged in server.\n\t" + "sendtest alertmail\n\n\tSend SNMP test alert " + "to the current logged in server.\n\texample: sendtest snmpalert", + "summary": "Command for sending various tests to iLO.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main SentTestCommand function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if not len(args) == 1: + raise InvalidCommandLineError("sendtest command takes only one argument.") + + body = None + path = None + actionitem = None + + self.sendtestvalidation(options) + + if args[0].lower() == "snmpalert": + select = self.rdmc.app.typepath.defs.snmpservice + actionitem = "SendSNMPTestAlert" + elif args[0].lower() == "alertmail": + select = self.rdmc.app.typepath.defs.managernetworkservicetype + actionitem = "SendTestAlertMail" + elif args[0].lower() == "syslog": + select = self.rdmc.app.typepath.defs.managernetworkservicetype + actionitem = "SendTestSyslog" + else: + raise InvalidCommandLineError("sendtest command does not have " "parameter %s." % args[0]) + + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("%s not found.It may not " "be available on this system." % select) + + bodydict = results.resp.dict + + try: + if "Actions" in bodydict: + for item in bodydict["Actions"]: + if actionitem in item: + if self.rdmc.app.typepath.defs.isgen10: + actionitem = item.split("#")[-1] + + path = bodydict["Actions"][item]["target"] + break + else: + for item in bodydict["Oem"][self.rdmc.app.typepath.defs.oemhp]["Actions"]: + if actionitem in item: + if self.rdmc.app.typepath.defs.isgen10: + actionitem = item.split("#")[-1] + + path = bodydict["Oem"][self.rdmc.app.typepath.defs.oemhp]["Actions"][item]["target"] + break + + body = {"Action": actionitem} + except: + body = {"Action": actionitem, "Target": "/Oem/Hp"} + + self.rdmc.app.post_handler(path, body) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def sendtestvalidation(self, options): + """sendtestvalidation method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerCloneCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerCloneCommand.py new file mode 100644 index 0000000..bd2bf61 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerCloneCommand.py @@ -0,0 +1,2696 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Server Clone Command for rdmc""" + +import getpass +import json +import os +import os.path +import re +import sys +import time +import traceback +from argparse import RawDescriptionHelpFormatter +from collections import OrderedDict + +import jsonpath_rw +from six.moves import input + +import redfish.ris +from redfish.ris.rmc_helper import IdTokenError, IloResponseError, InstanceNotFoundError +from redfish.ris.utils import iterateandclear, json_traversal_delete_empty +from redfish.ris import SessionExpired + +try: + from rdmc_helper import ( + LOGGER, + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + InvalidKeyError, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + NoDifferencesFoundError, + ResourceExists, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + LOGGER, + Encryption, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + InvalidKeyError, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + NoDifferencesFoundError, + ResourceExists, + ReturnCodes, + ) + +# default file name +__DEFAULT__ = "

" +__MINENCRYPTIONLEN__ = 16 +__clone_file__ = "ilorest_clone.json" +__tmp_clone_file__ = "_ilorest_clone_tmp" +__tmp_sel_file__ = "_ilorest_sel_tmp" +__error_log_file__ = "clone_error_logfile.log" +__changelog_file__ = "changelog.log" +__tempfoldername__ = "serverclone_data" + + +def log_decor(func): + """ + Log Decorator function + :param func: function to be decorated + :type func: class method + """ + + def func_wrap(*args, **kwargs): + """ + Log Decorator function wrapper + :param args: function to be decorated + :type args: * + :param kwargs: keyword arguments + :type kwargs: * + """ + try: + return func(*args, **kwargs) + except IdTokenError as excp: + sys.stderr.write( + "You have logged into iLO with an account which has insufficient " + " user access privileges to modify properties in this type:\n %s\n" % (excp) + ) + if args[0].rdmc.opts.debug: + logging( + func.func_name if hasattr(func, "func_name") else str(func), + traceback.format_exc(), + excp, + args, + ) + except NoDifferencesFoundError as excp: + sys.stderr.write("No differences identified from current configuration.\n") + if args[0].rdmc.opts.debug: + logging( + func.func_name if hasattr(func, "func_name") else str(func), + traceback.format_exc(), + excp, + args, + ) + except ExitHandler: + sys.stderr.write("Exiting Serverclone command...no further changes have been implemented.\n") + raise NoChangesFoundOrMadeError("Exiting Serverclone command...no further changes have been implemented.\n") + + except Exception as excp: + sys.stderr.write("Unhandled exception(s) occurred: %s\n" % str(excp)) + if not args[0].rdmc.opts.debug: + args[0].rdmc.ui.error( + "Check the ServerClone Error logfile for further info: %s\n" % __error_log_file__, + excp, + ) + logging( + func.func_name if hasattr(func, "func_name") else str(func), + traceback.format_exc(), + excp, + args, + ) + + return func_wrap + + +def logging(command, _trace, error, _args): + """ + Handler for error logging + :param command: command in error + :type command: method identifier + :param _trace: traceback data + :type _trace: object + :param error: error logged (simplified version) + :type error: string + :param _agrs: array of methods arguments + :type _args: array + """ + + sys.stderr.write( + "An error occurred: %s. Check the ServerClone Error logfile " + "for further info: %s\n" % (error, __error_log_file__) + ) + sys.stderr.write("Logging error to '%s'.\n" % __error_log_file__) + with open(__error_log_file__, "a+") as efh: + efh.write(command + ":\n") + efh.write("Simplified Error: " + str(error) + "\n") + efh.write("Traceback:\n") + efh.write(str(_trace) + "\n") + efh.write("Args State in: '%s'.\n" % command) + i = 0 + for _arg in _args: + efh.write("Arg %s: %s\n" % (str(i), _arg)) + i += 1 + efh.write("\n") + + +class ExitHandler(Exception): + pass + + +class ServerCloneCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "serverclone", + "usage": None, + "description": "Clone from a server or restore to a server a JSON formatted file " + "containing the configuration settings of a system's iLO and Bios configuration.\n" + "SSA controller settings and logical configurations can be optionally be included for " + "save.\nTo view help on specific sub-commands run: serverclone -h\n\n" + "Example: serverclone

": + user_counter = user_counter + 1 + else: + self.c_log_write( + "Kindly modify the password from

to valid pass in the clone file for the user :" + + user["UserName"] + + "\n" + ) + if fed_data: + for fedkey in fed_data.items(): + fedacct = fedkey[1] + if not fedacct["FederationKey"] == "

": + fed_counter = fed_counter + 1 + else: + self.c_log_write( + "Kindly modify the password from

to valid pass in the clone file for the user :" + + fedacct["FederationName"] + + "\n" + ) + else: + self.rdmc.ui.printer("No federation accounts available\n") + if user_counter == len(data.items()) or fed_counter == len(fed_data.items()): + self.loadfunction(options) + self.load_storageclone(options) + else: + self.rdmc.ui.error( + "Please modify the default password

to a valid password " + "in the clone json file and rerun the load command" + ) + + elif not options.iLOSSA: + for useracct in data.items(): + user = useracct[1] + if not user["Password"] == "

": + user_counter = user_counter + 1 + else: + self.c_log_write( + "Kindly modify the password from

to valid pass in the clone file for the user :" + + user["UserName"] + + "\n" + ) + if fed_data: + for fedkey in fed_data.items(): + fedacct = fedkey[1] + if not fedacct["FederationKey"] == "

": + fed_counter = fed_counter + 1 + else: + self.c_log_write( + "Kindly modify the password from

to valid pass in the clone file for the user :" + + fedacct["FederationName"] + + "\n" + ) + else: + self.rdmc.ui.printer("No federation accounts available\n") + if user_counter == len(data.items()) or fed_counter == len(fed_data.items()): + self.loadfunction(options) + else: + self.rdmc.ui.error( + "Please modify the default password

to a valid password " + "in the clone json file and rerun the load command" + ) + else: + self.load_storageclone(options) + + self.cleanup() + # self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + @log_decor + def file_handler(self, filename, operation, options, data=None, sk=None): + """ + Wrapper function to read or write data to a respective file + :param data: data to be written to output file + :type data: container (list of dictionaries, dictionary, etc.) + :param file: filename to be written + :type file: string (generally this should be self.clone_file or tmp_clone_file + :param operation: file operation to be performed + :type operation: string ('w+', 'a+', 'r+') + :param sk: sort keys flag + :type sk: boolean + :param options: command line options + :type options: attribute + :returns: json file data + """ + writeable_ops = ["w", "w+", "a", "a+"] + + fdata = None + + try: + if operation in writeable_ops: + if options.encryption: + with open(filename, operation + "b") as outfile: + outfile.write( + Encryption().encrypt_file( + json.dumps( + data, + indent=2, + cls=redfish.ris.JSONEncoder, + sort_keys=sk, + ), + options.encryption, + ) + ) + else: + with open(filename, operation) as outfile: + outfile.write( + json.dumps( + data, + indent=2, + cls=redfish.ris.JSONEncoder, + sort_keys=sk, + ) + ) + else: + if options.encryption: + with open(filename, operation + "b") as file_handle: + fdata = json.loads(Encryption().decrypt_file(file_handle.read(), options.encryption)) + else: + with open(filename, operation) as file_handle: + fdata = json.loads(file_handle.read()) + except Exception as excp: + self.cleanup() + raise InvalidFileInputError( + "Unable to open file: %s.\nVerify the file location " "and the file has a valid JSON format.\n" % excp + ) + else: + return fdata + + @log_decor + def controller_id(self, options): + """ + Get iLO types from server and save storageclone URL get Controller + :parm options: command line options + :type options: attribute + :returns: returns list + """ + self.auxcommands["select"].selectfunction("StorageControllerCollection.") + ctr_content = self.rdmc.app.getprops() + ctrl_data = [] + all_ctrl = dict() + + for ct_controller in ctr_content: + path = ct_controller["Members"] + for i in path: + res = i["@odata.id"] + if self.rdmc.opts.verbose and not self.load: + sys.stdout.write("Saving properties of type %s \t\n" % res) + ctrl_data.append(res) + ctrl_id_url = res + "?$expand=." + get_ctr = self.rdmc.app.get_handler(ctrl_id_url, silent=True, service=True).dict + ctrl_id = get_ctr["@odata.id"].split("/") + ctrl_id = ctrl_id[8] + get_ctr = self.rdmc.app.removereadonlyprops(get_ctr, False, True) + all_ctrl[get_ctr["Name"]] = get_ctr + return all_ctrl + + @log_decor + def get_volume(self, options): + """ + Get iLO types from server and save storageclone URL get volumes + :parm options: command line options + :type options: attribute + :returns: returns list + """ + self.auxcommands["select"].selectfunction("VolumeCollection.") + vol_content = self.rdmc.app.getprops() + vol_data = [] + all_vol = dict() + for st_volume in vol_content: + path = st_volume["Members"] + for i in path: + res = i["@odata.id"] + if self.rdmc.opts.verbose: + sys.stdout.write("Saving properties of type %s \t\n" % res) + vol_data.append(res) + vol_id_url = res + "?$expand=." + get_vol = self.rdmc.app.get_handler(vol_id_url, silent=True, service=True).dict + # print("Assigned drive", get_vol["Links"]["Drives"]) + get_vol = self.rdmc.app.removereadonlyprops(get_vol, False, True) + all_vol[get_vol["Name"]] = get_vol + return all_vol + + @log_decor + def save_storageclone(self, options): + """ + Get iLO types from server and save storageclone URL + :parm options: command line options + :type options: attribute + :returns: returns dict and save json format + """ + + self.auxcommands["select"].selectfunction("StorageCollection.") + st_content = self.rdmc.app.getprops() + st_flag = False + all_stgcntrl = {} + de_url = [] + if options.storageclonefilename: + outfilename = options.storageclonefilename[0] + else: + outfilename = "ilorest_storage_clone.json" + out_file = open(outfilename, "w") + self.rdmc.ui.printer("Saving of storage clone file to '%s'...... \n" % out_file.name) + for st_controller in st_content: + path = st_controller["Members"] + for i in path: + res = i["@odata.id"] + if "DE" in res: + de_url.append(res) + if self.rdmc.opts.verbose: + sys.stdout.write("Saving properties of type %s \t\n" % res) + st_flag = True + storage_id_url = res + "?$expand=." + get_storage = self.rdmc.app.get_handler(storage_id_url, silent=True, service=True).dict + vol = self.get_volume(options) + ctr = self.controller_id(options) + get_storage = self.rdmc.app.removereadonlyprops(get_storage, False, True) + # print("controller id", ctr["SATA Storage Controller"]["Id"]) + # Controller and volume update abve method called + get_storage["Controllers"]["Members"].append(ctr) + del get_storage["Controllers"]["Members"][0] + get_storage["Volumes"]["Members"].append(vol) + del get_storage["Volumes"]["Members"][0] + all_stgcntrl[get_storage["Id"]] = get_storage + else: + continue + if st_flag: + json.dump(all_stgcntrl, out_file, indent=6) + out_file.close() + self.rdmc.ui.printer("Saving of storage clone file to '%s' is complete.\n" % out_file.name) + else: + sys.stdout.write("\nNo Storage controllers found which is redfish enabled \n") + + @log_decor + def get_drives_capacityt(self, options): + self.auxcommands["select"].selectfunction("Drive.") + drive_content = self.rdmc.app.getprops() + drive_list = [] + loc_list = [] + for st_drive in drive_content: + drive_cap = st_drive["CapacityBytes"] + drive_list.append(drive_cap) + drive_loc = st_drive["PhysicalLocation"]["PartLocation"]["ServiceLabel"] + loc = drive_loc.split(":") + drv_loc = str(loc[1].split("=")[1] + ":" + loc[2].split("=")[1] + ":" + loc[3].split("=")[1]) + loc_list.append(drv_loc) + return drive_list, loc_list + + @log_decor + def controller_get_id(self, options): + self.auxcommands["select"].selectfunction("StorageController.") + ctr_content = self.rdmc.app.getprops() + for ct_data in ctr_content: + ct_id = ct_data["Id"] + return ct_id + + @log_decor + def get_storage_de_id(self, options): + self.auxcommands["select"].selectfunction("StorageCollection.") + st_content = self.rdmc.app.getprops() + try: + for st_controller in st_content: + path = st_controller["Members"] + for i in path: + res = i["@odata.id"].split("/") + res = res[6] + if "DE" in res: + res + return res + except: + self.rdmc.ui.printer("Storage id not available") + + @log_decor + def load_storageclone(self, options): + """ + Load iLO Server storage URL. + :parm ilovoldata: iLO Server Volume(logical drive) payload to be loaded + :type ilovoldata: dict of values + """ + if options.storageclonefilename: + filename = options.storageclonefilename[0] + else: + filename = "ilorest_storage_clone.json" + while True: + ans = input( + "A configuration file %s containing configuration changes will be " + "applied to this iLO server resulting in system setting changes for " + "Storage urls like controllers, volume, deletion and " + "rearrangement of logical disks...etc. Please confirm you acknowledge " + "and would like to perform this operation now? (y/n)\n" % filename + ) + if ans.lower() == "y": + self.rdmc.ui.printer("Proceeding with Storage Clone Load Operation...\n") + break + elif ans.lower() == "n": + self.rdmc.ui.warn("Aborting load operation. No changes made to the server.\n") + return ReturnCodes.NO_CHANGES_MADE_OR_FOUND + else: + self.rdmc.ui.warn("Invalid input...\n") + + ctr_id = self.controller_get_id(options) + print("Controller id", ctr_id) + st_id_de = self.get_storage_de_id(options) + print("Storage ID only DE", st_id_de) + with open(filename, "r+b") as file_handle: + if options.encryption: + data = json.load(Encryption().decrypt_file(file_handle.read(), options.encryption)) + else: + data = json.load(file_handle) + for k, v in data.items(): + while True: + ans = input("Do you want to delete current %s controller? (y/n)\n" % k) + if ans.lower() == "y": + self.rdmc.ui.printer("Proceeding with Deletion DE storage...\n") + break + elif ans.lower() == "n": + self.rdmc.ui.warn("Aborting load operation. No changes made to the server.\n") + return ReturnCodes.NO_CHANGES_MADE_OR_FOUND + else: + self.rdmc.ui.warn("Invalid input...\n") + self.auxcommands["factoryresetcontroller"].run("--reset_type resetall --storageid " + k) + if self.rdmc.opts.verbose: + self.rdmc.ui.printer("Deleted Volume using factoryresetcontroller %s command.\n" % k) + v_data = v["Volumes"]["Members"][0] + for i, j in v_data.items(): + # print("Key", i) + writecachepolicy = j["WriteCachePolicy"] + readcachepolicy = j["ReadCachePolicy"] + displayname = j["DisplayName"] + raidtype = j["RAIDType"] + capacitybytes = j["CapacityBytes"] + dr, loc = self.get_drives_capacityt(options) + # print("------", dr, loc) + i = 0 + for d in dr: + print("drives list", d) + if int(d) > int(capacitybytes): + create_val = ( + raidtype + + " " + + str(loc[i]) + + " " + + "DisplayName " + + displayname + + " --iOPerfModeEnabled False" + + " --ReadCachePolicy " + + readcachepolicy + + " --WriteCachePolicy " + + writecachepolicy + + " --controller=" + + ctr_id + + " --capacitybytes " + + str(capacitybytes) + ) + print("createvolume properties", create_val) + self.auxcommands["createvolume"].run("volume " + create_val) + break + elif int(d) < int(capacitybytes): + print("Implementation logic", d) + break + else: + sys.stdout.write("Drive capacity is lesser than present drives") + i = i + 1 + + @log_decor + def getilotypes(self, options): + """ + Get iLO types from server and return a list of types + :parm options: command line options + :type options: attribute + :returns: returns list of types + """ + supported_types_dict = { + "ManagerAccount": ["4", "5", "6", "7"], + "AccountService": ["4", "5", "6", "7"], + "Bios": ["4", "5", "6", "7"], + "Manager": ["4", "5", "6", "7"], + "SNMP": ["4", "5", "6", "7"], + "iLOLicense": ["4", "5", "6", "7"], + "ManagerNetworkService": ["4", "5", "6", "7"], + "EthernetNetworkInterface": ["4", "5", "6", "7"], + "iLODateTime": ["4", "5", "6", "7"], + "iLOFederationGroup": ["4", "5", "6", "7"], + "iLOSSO": ["4", "5", "6", "7"], + "ESKM": ["4", "5", "6", "7"], + "ComputerSystem": ["4", "5", "6", "7"], + # "EthernetInterface": ["4", "5", "6"], + "ServerBootSettings": ["4", "5", "6", "7"], + "SecureBoot": ["4", "5", "6", "7"], + "SmartStorageConfig": ["5"], + "HpSmartStorage": ["4"], + "HpeSmartStorage": ["5"], + } + types_accepted = set() + if self.save: + if options.noBIOS: + self.rdmc.ui.warn("Bios configuration will be excluded.\n") + del supported_types_dict["Bios"] + if options.all: + self.rdmc.ui.printer("Note: Smart storage configuration and Storage included all.\n") + self.save_storageclone(options) + if not options.iLOSSA and not options.all: + self.rdmc.ui.printer("Note: Smart storage configuration will not be included.\n") + self.rdmc.ui.warn("Smart storage configuration will not be included.") + del supported_types_dict["SmartStorageConfig"] + del supported_types_dict["HpSmartStorage"] + del supported_types_dict["HpeSmartStorage"] + unsupported_types_list = [ + "Collection", + "PowerMeter", + "BiosMapping", + "Controller", + ] + + # supported types comparison + # types_accepted = set() + for _type in sorted(set(self.rdmc.app.types("--fulltypes"))): + if _type[:1].split(".")[0] == "#": + _type_mod = _type[1:].split(".")[0] + else: + _type_mod = _type.split(".")[0] + for stype in supported_types_dict: + if stype.lower() in _type_mod.lower(): + found = False + for ustype in unsupported_types_list: + if ustype.lower() in _type_mod.lower(): + found = True + break + if not found: + if self.curr_iloversion in supported_types_dict[stype]: + types_accepted.add(_type) + + return sorted(types_accepted) + + def loadfunction(self, options): + """ + Main load function. Handles SSO and SSL/TLS Certificates, kickoff load + helper, load of patch file, get server status and issues system reboot + and iLO reset after completion of all post and patch commands. + :param options: command line options + :type options: attribute + """ + + reset_confirm = True + + if not options.autocopy: + while True: + ans = input( + "A configuration file '%s' containing configuration changes will be " + "applied to this iLO server resulting in system setting changes for " + "BIOS, ethernet controllers, disk controllers, deletion and " + "rearrangement of logical disks...etc. Please confirm you acknowledge " + "and would like to perform this operation now? (y/n)\n" % self.clone_file + ) + if ans.lower() == "y": + self.rdmc.ui.printer("Proceeding with ServerClone Load Operation...\n") + break + elif ans.lower() == "n": + self.rdmc.ui.warn("Aborting load operation. No changes made to the server.\n") + return ReturnCodes.NO_CHANGES_MADE_OR_FOUND + else: + self.rdmc.ui.warn("Invalid input...\n") + + self._fdata = self.file_handler(self.clone_file, operation="r+", options=options) + self.loadhelper(options) + if self.rdmc.app.getiloversion() < 7: + self.load_idleconnectiontime(options) + # data = self._fdata["#ManagerAccount.v1_3_0.ManagerAccount"] + # if "#HpeiLOFederationGroup.v2_0_0.HpeiLOFederationGroup" in self._fdata: + # fed_data = self._fdata["#HpeiLOFederationGroup.v2_0_0.HpeiLOFederationGroup"] + # for fedkey in fed_data.items(): + # self.load_federation( + # fedkey[1], + # "#HpeiLOFederationGroup.v2_0_0.HpeiLOFederationGroup", + # "/redfish/v1/Managers/1/FederationGroups", + # options, + # ) + # else: + # self.rdmc.ui.printer("No federation accounts to load\n") + # pass + fed_data = dict() + federation_key = None + data = dict() + manager_key = None + for key in self._fdata: + if "ManagerAccount" in key: + data = self._fdata[key] + manager_key = key + if "HpeiLOFederationGroup" in key: + fed_data = self._fdata[key] + federation_key = key + if fed_data: + for fedkey in fed_data.items(): + self.load_federation( + fedkey[1], + federation_key, + "/redfish/v1/Managers/1/FederationGroups", + options, + ) + else: + self.rdmc.ui.printer("No federation accounts to load\n") + pass + for useracct in data.items(): + self.load_accounts(useracct[1], manager_key, "/redfish/v1/AccountService/Accounts", options) + + self.loadpatch(options) + self.getsystemstatus(options) + + if not options.autocopy: + while True: + ans = input("The system is ready to be reset. Perform a reset now? (y/n)\n") + if ans.lower() == "n": + reset_confirm = False + self.rdmc.ui.printer("Aborting Server Reboot and iLO reset...\n") + break + elif ans.lower() == "y": + break + else: + self.rdmc.ui.warn("Invalid input...\n") + else: + if options.noautorestart: + reset_confirm = False + + if reset_confirm: + if self.rdmc.app.current_client.base_url: # reset process in remote mode + self.rdmc.ui.printer("Resetting the server...\n") + sys_url = "/redfish/v1/Systems/1/" + sysresults = self.rdmc.app.get_handler(sys_url, service=True, silent=True).dict + power_state = sysresults["PowerState"] + if power_state == "On": + self.auxcommands["reboot"].run("ColdBoot") # force restart, cold boot + elif power_state == "Off": + self.rdmc.ui.printer("System is on power off state, powering ON\n") + self.auxcommands["reboot"].run("On") + self.rdmc.ui.printer("Waiting 3 minutes for reboot to complete...\n") + time.sleep(180) + self.rdmc.ui.printer("Resetting iLO...\n") + self.auxcommands["iloreset"].run("") + self.rdmc.ui.printer("You will need to re-login to access this system...\n") + else: # reset process in local mode + self.rdmc.ui.printer("Resetting local iLO...\n") + self.auxcommands["iloreset"].run("") + self.rdmc.ui.printer("Your system may require a reboot...use at your discretion\n") + + else: + self.rdmc.ui.printer("Your system may require a reboot...use at your discretion\n") + + self.rdmc.ui.printer( + "Loading of clonefile '%s' to server is complete. Review the " + "changelog file '%s'.\n" % (self.clone_file, self.change_log_file) + ) + + def loadhelper(self, options): + """ + Helper function for loading which calls additional helper functions for + Server BIOS and Firmware compatibility, type compatibility, patch or + postability (special functions). Data deemed for exclusive patching + (through load) is written into a temporary file, which is deleted unless + archived for later use. + :param options: command line options + :type options: attribute + """ + data = list() + + server_avail_types = self.getilotypes(options) + if not server_avail_types: + raise NoContentsFoundForOperationError("Unable to Obtain iLO Types from server.") + + if "Comments" in list(self._fdata.keys()): + self.system_compatibility_check(self._fdata["Comments"], options) + del self._fdata["Comments"] + else: + raise InvalidFileInputError("Clone File '%s' does not include a valid 'Comments' " "dictionary.") + if options.ssocert: + self.load_ssocertificate() # check and load sso certificates + if options.tlscert: + self.load_tlscertificate() # check and load tls certificates + + typelist = [] + for _x in self._fdata: + for _y in server_avail_types: + _x1 = re.split("#|.", _x) + _y1 = re.split("#|.", _y) + if _x1[0] == "": + _x1.pop(0) + if _y1[0] == "": + _y1.pop(0) + if _x1[0] == _y1[0]: + _comp_tuple = self.type_compare(_x, _y) + if _comp_tuple[0] and _comp_tuple[1]: + self.rdmc.ui.printer("Type '%s' is compatible with this system.\n" % _x) + typelist.append(_x) + else: + self.rdmc.ui.warn( + "The type: '%s' isn't compatible with the type: '%s'" + "found on this system. Associated properties can not " + "be applied...Skipping\n" % (_x, _y) + ) + + for _type in typelist: + # Skipping cloning of Datetime temporarily. This is an iLO issue. + if _type == "#HpeiLODateTime.v2_0_0.HpeiLODateTime": + continue + singlet = True + thispath = next(iter(self._fdata[_type].keys())) + try: + root_path_comps = self.get_rootpath(thispath) + + multi_sel = self.rdmc.app.select( + _type.split(".")[0] + ".", + (self.rdmc.app.typepath.defs.hrefstring, root_path_comps[0] + "*"), + path_refresh=True, + ) + + curr_sel = self.rdmc.app.select( + _type.split(".")[0] + ".", + (self.rdmc.app.typepath.defs.hrefstring, thispath), + path_refresh=True, + ) + + except InstanceNotFoundError: + if "iLOFederationGroup" in _type: + pass + else: + curr_sel = self.rdmc.app.select(_type.split(".")[0] + ".") + + except Exception as excp: + self.rdmc.ui.error( + "Unable to find the correct path based on system " "type and clone file type: %s\n" % _type, + excp, + ) + continue + finally: + try: + if "multi_sel" in locals() and "curr_sel" in locals(): + if ( + len(multi_sel) > 1 + and len(curr_sel) == 1 + and ( + root_path_comps[1].isdigit() + or "iLOFederationGroup" in _type + or "ManagerAccount" in _type + or "Manager" in _type + ) + ): + singlet = False + curr_sel = multi_sel + except (ValueError, KeyError): + pass + + scanned_dict = dict() + for thing in curr_sel: + scanned_dict[thing.path] = { + "Origin": "Server", + "Scanned": False, + "Data": thing.dict, + } + for thing in self._fdata[_type]: + # if we only have a single path, the base path is in the path and only a single + # instance was retrieved from the server + if "ManagerAccount" in _type or "iLOFederationGroup" in _type: + scanned_dict[thing] = { + "Origin": "File", + "Scanned": False, + "Data": self._fdata[_type][thing], + } + elif singlet and root_path_comps[0] in thing and len(scanned_dict) == 1: + scanned_dict[next(iter(scanned_dict))] = { + "Origin": "File", + "Scanned": False, + "Data": self._fdata[_type][thing], + } + else: + scanned_dict[thing] = { + "Origin": "File", + "Scanned": False, + "Data": self._fdata[_type][thing], + } + for path in scanned_dict.keys(): + try: + if scanned_dict[path]["Origin"] == "Server": + raise KeyError(path) + else: + sys.stdout.write( + "---Check special loading for entry---\ntype: %s\npath: " "%s\n" % (_type, path) + ) + tmp = self.subhelper(scanned_dict[path]["Data"], _type, path, options) + if tmp: + sys.stdout.write("Special entry not applicable...reserving for " "patch loading stage.\n") + data.append(tmp) + else: + sys.stdout.write("---Special loading complete for entry---.\n") + except KeyError as excp: + if path in str(excp) and self._fdata.get(_type) and "iLOFederationGroup" not in _type: + if self.delete( + scanned_dict[path]["Data"], + _type, + path, + self._fdata[_type], + options, + ): + # ok so this thing does not have a valid path, is not considered a + # deletable item so....idk what to do with you. You go to load. + # Goodluck + tmp = self.altsubhelper(scanned_dict[path]["Data"], _type, path) + if tmp: + data.append(tmp) + else: + # if the instance item was not replaced with an entry in the clone file then + # it will be deleted + self.rdmc.ui.warn("Entry at '%s' removed from this server.\n" % path) + + except Exception as excp: + self.rdmc.ui.error("An error occurred: '%s'" % excp) + continue + finally: + scanned_dict[path]["Scanned"] = True + + self.file_handler(self.tmp_clone_file, "w+", options, data, True) + + def subhelper(self, data, _type, path, options): + """ + Reusable code section for load helper + :param data: dict data (from file or server) + :type: dictionary + :param _type: cross compatible iLO type + :type: string + :param path: iLO schema path determined from system query + :type: string + :param prev + :parm options: command line options + :type options: attribute + """ + + # due to EthernetInterfaces OEM/HPE/DHCPv4 also having a key with 'Name' + # this is required, removing readonly types after POST commands have + # completed. Would be great if that was resolved... + prop_list = [ + "Modified", + "Type", + "Description", + "Status", + "Name", + "AttributeRegistry", + "links", + "SettingsResult", + "@odata.context", + "@odata.type", + "@odata.id", + "@odata.etag", + "Links", + "Actions", + "AvailableActions", + "MACAddress", + "BiosVersion", + ] + + tmp = dict() + tmp[_type] = {path: data} + tmp[_type][path] = self.rdmc.app.removereadonlyprops(tmp[_type][path], False, True, prop_list) + val_emp = tmp["#Bios.v1_0_4.Bios"]["/redfish/v1/systems/1/bios/settings/"]["Attributes"] + if val_emp["AdminName"] == "" and val_emp["AdminPhone"] == "" and val_emp["AdminEmail"] == "": + pass + else: + json_traversal_delete_empty(tmp, None, None) + # if not self.ilo_special_functions(tmp, _type, path, options): + return tmp + + @log_decor + def altsubhelper(self, file_data, _type, curr_path): + """ + Just for manipulating the file_data in the clone file and handing it off to load. + :param file_data: clone file data + :type: dictionary + :param _type: cross compatible iLO type + :type: string + :param curr_path: iLO schema path determined from system query + :type: string + :param file_path: iLO schema path as observed in the clone file + :type: string + :returns: a dictionary containing the data which will be passed off to load. + """ + + tmp = dict() + try: + tmp[_type] = {curr_path: file_data[_type][next(iter(file_data[_type]))]} + except KeyError: + tmp[_type] = {curr_path: file_data} + json_traversal_delete_empty(tmp, None, None) + return tmp + + def loadpatch(self, options): + """ + Load temporary patch file to server + :parm options: command line options + :type options: attribute + """ + self.rdmc.ui.printer("Patching remaining data.\n") + fdata = self.file_handler(self.tmp_clone_file, operation="r+", options=options) + for _sect in fdata: + _tmp_sel = {} + _key = next(iter(_sect)) + _tmp_sel[_key.split(".")[0] + "."] = _sect[_key] + self.file_handler(self.tmp_sel_file, "w", options, [_tmp_sel], True) + self.loadpatch_helper(_key, _sect, options) + + @log_decor + def loadpatch_helper(self, type, dict, options): + """ + Load temporary patch file to server + :parm options: command line options + :type options: attribute + """ + options_str = "" + if options.encryption: + options_str += " --encryption " + options.encryption + + if options.uniqueoverride: + options_str += " --uniqueoverride" + + self.rdmc.ui.printer("Patching '%s'.\n" % type) + try: + self.auxcommands["load"].run("-f " + self.tmp_sel_file + options_str) + except: + # placeholder for any exception + pass + + def gatherandsavefunction(self, typelist, options): + """ + Write parsed JSON save data to file + :param typelist: list of available types on iLO + :type typelist: list + :param options: command line options + :type options: attribute + """ + if options.iLOSSA: + self.save_storageclone(options) + else: + data = OrderedDict() + data.update(self.rdmc.app.create_save_header()) + self.rdmc.ui.printer("Saving of clone file to '%s'.......\n" % self.clone_file) + for _type in typelist: + self.gatherandsavehelper(_type, data, options) + self.file_handler(self.clone_file, "w+", options, data, False) + self.rdmc.ui.printer("Saving of clone file to '%s' is complete.\n" % self.clone_file) + + def gatherandsavehelper(self, _type, data, options): + """ + Collect data on types and parse properties (delete unnecessary/readonly/ + empty properties. + :param type: type for subsequent select and save + :type type: string + :param data: JSON data (to be written to file) + :type data: JSON + :param options: command line options + :type options: attribute + """ + _typep = _type.split(".")[0] + _spec_list = ["SmartStorageConfig", "iLOLicense", "Bios"] + + try: + if "EthernetInterface" in _type: + instances = self.rdmc.app.select( + _typep + ".", + ( + self.rdmc.app.typepath.defs.hrefstring, + self.rdmc.app.typepath.defs.managerpath + "*", + ), + path_refresh=True, + ) + elif "EthernetNetworkInterface" in _type: + instances = self.rdmc.app.select( + _typep + ".", + ( + "links/self/" + self.rdmc.app.typepath.defs.hrefstring, + self.rdmc.app.typepath.defs.managerpath + "*", + ), + path_refresh=True, + ) + else: + instances = self.rdmc.app.select(_typep + ".", path_refresh=True) + + for j, instance in enumerate(self.rdmc.app.getprops(insts=instances)): + if "#" in _typep: + _typep = _typep.split("#")[1] + if self.rdmc.app.typepath.defs.flagforrest: + try: + path = instance["links"]["self"][self.rdmc.app.typepath.defs.hrefstring] + except: + path = instance["links"][next(iter(instance["links"]))][self.rdmc.app.typepath.defs.hrefstring] + else: + path = instance[self.rdmc.app.typepath.defs.hrefstring] + + instance = self.ilo_special_functions(instance, _type, path, options) + + _itc_pass = True + for _itm in _spec_list: + if _itm.lower() in _type.lower(): + templist = [ + "Modified", + "Type", + "Description", + "Status", + "links", + "SettingsResult", + "@odata.context", + "@odata.type", + "@odata.id", + "@odata.etag", + "Links", + "Actions", + "AvailableActions", + "BiosVersion", + ] + instance = iterateandclear(instance, templist) + _itc_pass = False + break + if _itc_pass: + instance = self.rdmc.app.removereadonlyprops(instance, False, True) + + if instance and not options.iLOSSA: + if _typep != "SmartStorageConfig": + if self.rdmc.opts.verbose: + self.rdmc.ui.printer("Saving properties of type: %s, path: %s\n" % (_typep, path)) + elif _typep in options.iLOSSA: + sys.stdout.write("ILO SSA calling") + if _type not in data: + data[_type] = OrderedDict(sorted({path: instance}.items())) + else: + data[_type][path] = instance + else: + self.rdmc.ui.warn( + "Type: %s, path: %s does not contain any modifiable " + "properties on this system." % (_typep, path) + ) + + except Exception as excp: + self.rdmc.ui.printer('An error occurred saving type: %s\nError: "%s"' % (_typep, excp)) + + @log_decor + def getsystemstatus(self, options): + """ + Retrieve system status information and save to a changelog file. This + file will be added to an archive if the archive selection is made. + :parm options: command line options + :type options: attribute + """ + status_list = [] + + for item in self.rdmc.app.status(): + status_list.append(item) + + if status_list: + self.file_handler(self.change_log_file, "w", options, status_list, True) + with open(self.change_log_file, "rb") as myfile: + data = myfile.read() + if options.encryption: + data = Encryption().decrypt_file(data, options.encryption) + with open(self.change_log_file, "wb") as outfile: + outfile.write(data) + else: + self.rdmc.ui.printer("No changes pending.\n") + + def ilo_special_functions(self, data, _type, path, options): + """ + Function used by both load and save for Restful commands requing a + POST or PUT. + :param data: JSON payload for saved or loaded properties + :type data: json + :param _type: selected type + :type _type: string + :param path: path of the selected type + :type path: string + :parm options: command line options + :type options: attribute + :returns: returns boolean indicating if the type/path was found. + """ + identified = False + _typep = _type.split(".")[0] + if "#" in _typep: + _typep = _typep.split("#")[1] + + if "EthernetInterface" in _typep or "EthernetNetworkInterface" in _typep: + identified = True + # save function not needed + if self.load: + self.load_ethernet(data[_type][path], _type, path) + + elif "DateTime" in _typep: + # do not use identified=True. Kind of a hack to have additional items patched. + # save function not needed + if self.load: + self.load_datetime(data[_type][path], path) + + elif "LicenseService" in path and "License" in _typep: + identified = True + if self.save: + data = self.save_license(data, _type, options) + elif self.load: + self.load_license(data[_type][path]) + + elif "AccountService/Accounts" in path and "AccountService" not in _typep: + identified = True + if self.save: + data = self.save_accounts(data, _type, options) + elif self.load: + self.load_accounts(data[_type][path], _type, path, options) + + elif "FederationGroup" in _typep: + identified = True + if self.save: + data = self.save_federation(data, _type, options) + elif self.load: + self.load_federation(data[_type][path], _type, path, options) + + elif "StorageCollection" in _typep: + identified = True + # if self.save: + # data = self.save_smartstorage(data, _type, options) + # elif self.load: + # self.load_smartstorage(data[_type][path], _type, path) + + if self.save: + return data + + return identified + + def c_log_write(self, c_str): + with open(self.change_log_file, "a") as c_log: + c_log.write(c_str + "\n") + + @log_decor + def delete(self, data, _type, path, fdata, options): + """ + Delete operations to remove things on server + :param data: Data to be deleted from the server + :type data: dictionary + :parm _type: iLO type to be queried + :type _type: string + :param path: iLO schema path + :type path: string + :returns: boolean indicating if the delete option occurred. + """ + user_confirm = False + + if not options.autocopy and not options.iLOSSA and not options.all: + while True: + ans = input( + "\n%s\nAre you sure you would like to delete the entry?:\n" + % json.dumps(data, indent=1, sort_keys=True) + ) + if ans.lower() == "y": + self.rdmc.ui.printer("Proceeding with Deletion...\n") + user_confirm = True + break + elif ans.lower() == "n": + self.rdmc.ui.warn("Aborting deletion. No changes have been made made to " "the server.\n") + return False + else: + self.rdmc.ui.warn("Invalid input...\n") + + # if "StorageCollection" in _type "Storage" in _type or : + # sys.stdout.write("Storage related delete lun on particular controller") + + if "ManagerAccount" in _type: + user_name = data["UserName"] + + # obtaining account information on the current server as a check to verify the user + # provided a decent path to use. This can be re-factored. + try: + for curr_sel in self.rdmc.app.select(_type.split(".")[0] + "."): + try: + if "UserName" in list(curr_sel.dict.keys()): + _ = curr_sel.dict["UserName"] + # check file to make sure this is not to be added later? + for fpath in fdata: + try: + if fdata[fpath]["UserName"] == data["UserName"]: + self.rdmc.ui.warn( + "Account '%s' exists in '%s', not " + "deleting.\n" % (data["UserName"], self.clone_file) + ) + return False + except: + continue + + if data["UserName"] != "Administrator": + self.rdmc.ui.warn( + "Manager Account, '%s' was not found in the " + "clone file. Deleting entry from server.\n" % data["UserName"] + ) + if not options.autocopy and not options.iLOSSA and not options.all: + ans = user_confirm + else: + ans = True + if ans: + self.auxcommands["iloaccounts"].run("delete " + data["UserName"]) + self.c_log_write("[CHANGE]: Deleting user " + data["UserName"]) + time.sleep(2) + del fdata[fpath] + return False + else: + self.rdmc.ui.error( + "Deletion of the Default System Administrator " "account is not allowed.\n" + ) + except (KeyError, NameError): + self.rdmc.ui.error( + "Unable to obtain the account information " "for: '%s''s' account.\n" % user_name + ) + continue + except InstanceNotFoundError: + return True + return False + + if "FederationGroup" in _type: + fed_identifier = None + if "FederationName" in data: + fed_identifier = "FederationName" + elif "Name" in data: + fed_identifier = "Name" + else: + raise InvalidKeyError("An invalid key was provided for the Federation Group Name.") + if data[fed_identifier] != "DEFAULT": + self.rdmc.ui.warn( + "Federation Account, '%s' was not found in the clone file." + " Deleting entry from server.\n" % data[fed_identifier] + ) + for fpath in fdata: + if fdata[next(iter(fdata))].get("FederationName") == data[fed_identifier]: + self.rdmc.ui.warn("Account '%s' exists in file, not deleting." "\n" % data[fed_identifier]) + return False + if not options.autocopy and not options.iLOSSA and not options.all: + ans = user_confirm + else: + ans = True + if ans: + self.auxcommands["ilofederation"].run("delete " + data[fed_identifier]) + self.c_log_write("[CHANGE]: Deleting ilo federation user " + data[fed_identifier]) + else: + self.rdmc.ui.warn("Deletion of the Default iLO Federation Group is not allowed.\n") + return False + return True + + @log_decor + def load_ssocertificate(self): + """ + Load the SSO Certificate specified in the user defined options. + """ + self.rdmc.ui.printer("Uploading SSO Certificate...\n") + self.auxcommands["singlesignon"].run("importcert " + self.sso_cert_file) + + @log_decor + def load_tlscertificate(self): + """ + Load the SSO Certificate specified in the user defined options. + """ + self.rdmc.ui.printer("Uploading TLS Certificate...\n") + self.auxcommands["certificate"].run("tls " + self.https_cert_file) + + @log_decor + def load_ethernet(self, ethernet_data, _type, path): + """ + Load iLO Ethernet Adapters settings Payload. + :parm datetime_data: iLO Ethernet Adapters payload to be loaded + :type datetime_data: dict + :param _type: iLO schema type + :type _type: string + :param path: iLO schema path + :type path: string + """ + self.auxcommands["ethernet"].load_ethernet_aux(_type, path, ethernet_data) + + @log_decor + def load_idleconnectiontime(self, options): + """ + Load iLO IdleConnectionTimeoutMinutes. + :parm iloidlecondata: IdleConnectionTimeoutMinutes payload to be loaded + :type iloidlecondata: dict + :param path: iLO schema path + """ + filename = self.clone_file + idle_val = [] + cli_val = [] + + with open(filename, "r+b") as file_handle: + if options.encryption: + data = json.loads(Encryption().decrypt_file(file_handle.read(), options.encryption)) + else: + data = json.load(file_handle) + # data = json.load(file_handle, encoding='cp1252') + # idle_con = data["#Manager.v1_5_1.Manager"]["/redfish/v1/Managers/1/"]["Oem"]["Hpe"][ + # "IdleConnectionTimeoutMinutes" + # ] + # serialclispeed = data["#Manager.v1_5_1.Manager"] + # ["/redfish/v1/Managers/1/"]["Oem"]["Hpe"]["SerialCLISpeed"] + idle_con = None + serialclispeed = None + for key in data: + if "Manager.v" in key: + idle_con = data[key]["/redfish/v1/Managers/1/"]["Oem"]["Hpe"]["IdleConnectionTimeoutMinutes"] + serialclispeed = data[key]["/redfish/v1/Managers/1/"]["Oem"]["Hpe"]["SerialCLISpeed"] + idle_val.append(idle_con) + cli_val.append(serialclispeed) + for _t in self._fdata: + try: + if "Manager" in _t: + _t_path = next(iter(list(self._fdata.get(_t).keys()))) + pass_dict = {"Oem": {self.rdmc.app.typepath.defs.oemhp: {"IdleConnectionTimeoutMinutes": idle_con}}} + cli_dict = {"Oem": {self.rdmc.app.typepath.defs.oemhp: {"SerialCLISpeed": serialclispeed}}} + sys.stdout.write("IdleConnectionTimeoutMinutes data %s\n" % pass_dict) + sys.stdout.write("SerialCLISpeed data %s\n" % cli_dict) + sys.stdout.write("Manager data %s\n" % _t_path) + self.rdmc.app.patch_handler(_t_path, pass_dict) + self.c_log_write("[CHANGE]: " + _t_path + ":" + str(pass_dict)) + self.rdmc.app.patch_handler(_t_path, cli_dict) + self.c_log_write("[CHANGE]: " + _t_path + ":" + str(cli_dict)) + break + except KeyError: + pass + + @log_decor + def load_datetime(self, datetime_data, path): + """ + Load iLO NTP Servers, DateTime Locale Payload. + :parm datetime_data: iLO NTP Server and Datetime payload to be loaded + :type datetime_data: dict + :param path: iLO schema path + :type path: string + """ + errors = [] + + if "StaticNTPServers" in datetime_data: + self.rdmc.ui.printer( + "Attempting to modify 'UseNTPServers' in each iLO Management " + "Network Interface regarding the StaticNTPServers list in " + "section 'iLODateTime (DateTime)'\n" + ) + oem_str = self.rdmc.app.typepath.defs.oempath + prop_str = (oem_str + "/DHCPv4/UseNTPServers")[1:] + path_str = self.rdmc.app.typepath.defs.managerpath + "*" + _instances = self.rdmc.app.select("EthernetInterface", (self.rdmc.app.typepath.defs.hrefstring, path_str)) + _content = self.rdmc.app.getprops("EthernetInterface", [prop_str], None, True, True, _instances) + + for item in _content: + try: + if next(iter(jsonpath_rw.parse("$..UseNTPServers").find(item))).value: + self.rdmc.app.patch_handler( + path, + {oem_str: {"DHCPv4": {"UseNTPServers": True}}}, + ) + self.c_log_write( + "[CHANGE]: " + path + ":" + str({oem_str: {"DHCPv4": {"UseNTPServers": True}}}) + ) + else: + self.rdmc.app.patch_handler( + path, + {oem_str: {"DHCPv4": {"UseNTPServers": False}}}, + ) + self.c_log_write( + "[CHANGE]: " + path + ":" + str({oem_str: {"DHCPv4": {"UseNTPServers": False}}}) + ) + except IloResponseError as excp: + errors.append("iLO Responded with the following error: %s.\n" % excp) + + if errors: + self.rdmc.ui.error( + "iLO responded with an error while attempting to set values " + "for 'UseNTPServers'. An attempt to patch DateTime " + "properties will be performed, but may be unsuccessful.\n" + ) + raise IloResponseError("The following errors in, 'DateTime' were found " "collectively: %s" % errors) + + @log_decor + def save_license(self, license_data, _type, options): + """ + Save iLO Server License. + :parm license_data: iLO Server License payload to be saved + :type license_data: dict + :param _type: iLO schema type + :type _type: string + :param options: command line options + :type options: attribute + """ + key_found = False + valid_key = False + license_keys = [] + try: + if "LicenseKey" in list(license_data["ConfirmationRequest"]["EON"].keys()): + license_keys.append(license_data["ConfirmationRequest"]["EON"]["LicenseKey"]) + except: + pass + finally: + license_keys.append(license_data.get("LicenseKey")) + for lic in reversed(license_keys): + if lic != "" and lic is not None: + license_key = lic + key_found = True + if self.rdmc.opts.verbose: + self.rdmc.ui.printer("License Key Found ending in: %s\n" % license_key.split("-")[-1]) + segpass = [] + for seg in lic.split("-"): + if "XXXXX" in seg.upper(): + segpass.append(True) + + if True not in segpass: + valid_key = True + break + + if not key_found: + self.rdmc.ui.printer("A License Key was not found on this system.\n") + license_key = "XXXXX-XXXXX-XXXXX-XXXXX-XXXXX" + + if not options.autocopy and not valid_key and not options.iLOSSA and not options.all: + while True: + segpass = [] + license_key = input("Provide your license key: (press enter to skip)\n") + + if license_key.count("X") == 25 or license_key.count("-") == 0: + break + + for seg in license_key[0].split("-"): + if len(seg) == 5: + segpass.append(True) + + if len(segpass) == 5: + break + else: + segpass = False + self.rdmc.ui.warn("An Invalid License Key was Provided: %s" % license_key) + else: + self.rdmc.ui.warn("Remember to verify your License Key...") + + # clear everything, we do not need and just keep license key + license_data = {"LicenseKey": license_key.upper()} + return license_data + + @log_decor + def load_license(self, ilolicdata): + """ + Load iLO Server License. + :parm ilolicdata: iLO Server License payload to be loaded + :type ilolicdata: dict + """ + license_error_list = "InvalidLicenseKey" + license_str = "" + try: + license_str = ilolicdata["LicenseKey"] + segpass = [] + for seg in license_str.split("-"): + if len(seg) == 5: + segpass.append(True) + + if len(segpass) == 5: + self.rdmc.ui.printer("Attempting to load a license key to the server.") + self.auxcommands["ilolicense"].run("" + license_str) + else: + raise ValueError + except IloResponseError as excp: + if str(excp) in license_error_list: + self.rdmc.ui.error("iLO is not accepting your license key ending in '%s'." % license_str.split("-")[-1]) + except ValueError: + self.rdmc.ui.error("An Invalid License Key ending in '%s' was provided." % license_str.split("-")[-1]) + + @log_decor + def save_accounts(self, accounts, _type, options): + """ + Load iLO User Account Data. + :parm accounts: iLO User Account payload to be saved + :type accounts: dict + :param _type: iLO schema type + :type _type: string + :param options: command line options + :type options: attribute + """ + try: + account_type = next(iter(jsonpath_rw.parse("$..Name").find(accounts))).value + except StopIteration: + account_type = None + + try: + account_un = next(iter(jsonpath_rw.parse("$..UserName").find(accounts))).value + except StopIteration: + account_un = None + + try: + account_ln = next(iter(jsonpath_rw.parse("$..LoginName").find(accounts))).value + except StopIteration: + account_ln = None + + try: + privileges = next(iter(jsonpath_rw.parse("$..Privileges").find(accounts))).value + except StopIteration: + privileges = None + + try: + role_id = next(iter(jsonpath_rw.parse("$..RoleId").find(accounts))).value + except StopIteration: + role_id = None + + password = [__DEFAULT__, __DEFAULT__] + if not options.autocopy and not options.iLOSSA and not options.all: + while True: + for i in range(2): + if i < 1: + self.rdmc.ui.printer("Please input the desired password for user: %s\n" % account_un) + else: + self.rdmc.ui.printer("Please re-enter the desired password for user: %s\n" % account_un) + + password[i] = getpass.getpass() + try: + [password[i], _] = password[i].split("\r") + except ValueError: + pass + + if password[0] == password[1] and (password[0] is not None or password[0] != ""): + break + else: + ans = input("You have entered two different passwords...Retry?(y/n)\n") + if ans.lower() != "y": + self.rdmc.ui.printer("Skipping Account Migration for: %s\n" % account_un) + return None + else: + if self.rdmc.opts.verbose: + self.rdmc.ui.printer( + "Remember to edit password for user: '%s', login name: '%s'" "." % (account_un, account_ln) + ) + + if not password[0]: + password[0] = __DEFAULT__ + self.rdmc.ui.printer("Using a placeholder password of '%s' in %s file.\n" % (password[0], self.clone_file)) + accounts = { + "AccountType": account_type, + "UserName": account_un, + "LoginName": account_ln, + "Password": password[0], + "RoleId": role_id, + "Privileges": privileges, + } + + return accounts + + def getsesprivs(self, availableprivsopts=False): + """Finds and returns the curent session's privileges + + :param availableprivsopts: return available privileges + :type availableprivsopts: boolean. + """ + if self.rdmc.app.current_client: + sespath = self.rdmc.app.current_client.session_location + sespath = ( + self.rdmc.app.current_client.default_prefix + + sespath.split(self.rdmc.app.current_client.default_prefix)[-1] + ) + + ses = self.rdmc.app.get_handler(sespath, service=False, silent=True) + + if not ses: + raise SessionExpired("Invalid session. Please logout and " "log back in or include credentials.") + + sesprivs = { + "HostBIOSConfigPriv": True, + "HostNICConfigPriv": True, + "HostStorageConfigPriv": True, + "LoginPriv": True, + "RemoteConsolePriv": True, + "SystemRecoveryConfigPriv": True, + "UserConfigPriv": True, + "VirtualMediaPriv": True, + "VirtualPowerAndResetPriv": True, + "iLOConfigPriv": True, + } + if "Oem" in ses.dict: + sesoemhp = ses.dict["Oem"][self.rdmc.app.typepath.defs.oemhp] + if "Privileges" in list(sesoemhp.keys()): + sesprivs = sesoemhp["Privileges"] + availableprivs = list(sesprivs.keys()) + updated_privs = dict() + for priv, val in sesprivs.items(): + if val: + updated_privs[priv] = sesprivs[priv] + sesprivs = updated_privs + del updated_privs + else: + sesprivs = None + if availableprivsopts: + return availableprivs + else: + return sesprivs + + @log_decor + def load_accounts(self, user_accounts, _type, path, options): + """ + Load iLO User Account Data. + :parm user_accounts: iLO User Account payload to be loaded + :type user_accounts: dict + :param _type: iLO schema type + :type _type: string + :param path: iLO schema path + :type path: string + """ + found_user = False + if "UserName" in user_accounts: + user_name = user_accounts["UserName"] + else: + user_name = user_accounts["User_Name"] + if "LoginName" in user_accounts: + login_name = user_accounts["LoginName"] + else: + login_name = user_accounts["Login_Name"] + + # set minimum password length: + for _t in self._fdata: + try: + if "AccountService" in _t: + _t_path = next(iter(list(self._fdata.get(_t).keys()))) + pass_dict = {"Oem": {self.rdmc.app.typepath.defs.oemhp: {}}} + pass_dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["MinPasswordLength"] = self._fdata[_t][_t_path][ + "Oem" + ][self.rdmc.app.typepath.defs.oemhp]["MinPasswordLength"] + del self._fdata[_t][_t_path]["Oem"][self.rdmc.app.typepath.defs.oemhp]["MinPasswordLength"] + self.rdmc.app.patch_handler(_t_path, pass_dict) + self.c_log_write("[CHANGE]: " + _t_path + ":" + str(pass_dict)) + break + except KeyError: + pass + except Exception as excp: + self.rdmc.ui.error( + "Unable to set minimum password length for manager accounts.\n", + excp, + ) + + # set the current privileges to those in the clone file + curr_privs = user_accounts["Privileges"] + + # set the current role to that in the clone file + curr_role_id = role_id = None + role_id = user_accounts.get("RoleId") + + if self.rdmc.app.typepath.defs.flagforrest: + _ = "links/self/" + self.rdmc.app.typepath.defs.hrefstring + else: + _ = self.rdmc.app.typepath.defs.hrefstring + + # obtaining account information on the current server as a check to verify the user + # provided a decent path to use. This can be re-factored. + try: + for curr_sel in self.rdmc.app.select(_type.split(".")[0] + "."): + try: + curr_privs = curr_sel.dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["Privileges"] + curr_role_id = curr_sel.dict.get("RoleId") + if "UserName" in list(curr_sel.dict.keys()): + curr_un = curr_sel.dict["UserName"] + else: + curr_un = curr_sel.dict["Oem"][self.rdmc.app.typepath.defs.oemhp]["LoginName"] + if curr_un != user_name: + continue + else: + found_user = True + break + except (KeyError, NameError): + self.rdmc.ui.error("Unable to obtain the account information for: '%s''s'" "account.\n" % user_name) + continue + except InstanceNotFoundError: + pass + + if not found_user: + self.rdmc.ui.printer("Account '%s' was not found on this system.\n" % user_name) + + user_pass = user_accounts["Password"] + + (add_privs_str, remove_privs_str) = self.priv_helper(user_accounts, curr_privs) + + if curr_role_id == role_id: + role_id = None + + # Don't think we need to rely on ResourceExists. Should be able to easily tell which + # operation should be performed before this point. + if user_pass: + if user_pass == __DEFAULT__: + self.rdmc.ui.warn("The default password will be attempted.") + try: + if found_user: + raise ResourceExists("") + + # issue here then we just let it happen and perform a modify on the account. + elif role_id: + self.auxcommands["iloaccounts"].run( + "add " + user_name + " " + login_name + " " + user_pass + " " + " --role " + role_id + ) + self.c_log_write("[CHANGE]: Added " + user_name + " with role id " + role_id) + time.sleep(2) + elif add_privs_str: + self.auxcommands["iloaccounts"].run( + "add " + user_name + " " + login_name + " " + user_pass + " " + " --addprivs " + add_privs_str + ) + self.c_log_write("[CHANGE]: Added " + user_name + " with privs string " + str(add_privs_str)) + time.sleep(2) + else: + self.auxcommands["iloaccounts"].run("add " + user_name + " " + login_name + " " + user_pass) + self.c_log_write("[CHANGE]: Added " + user_name) + time.sleep(2) + except ResourceExists: + self.rdmc.ui.warn( + "The account name '%s' exists on this system. " "Checking for account modifications.\n" % user_name + ) + self.rdmc.ui.printer("Changing account password for '%s'.\n" % user_name) + self.auxcommands["iloaccounts"].run("changepass " + user_name + " " + user_pass) + self.c_log_write("[CHANGE]: Changing password for " + user_name) + time.sleep(2) + # if the user includes both role_id and privileges then privileges are applied + # first skipping role, if they exist. Extra steps, yes, in certain cases + # but not necessarily. + if role_id: + self.rdmc.ui.printer("Changing roles for user: '%s'.\n" % user_name) + self.auxcommands["iloaccounts"].run("modify " + user_name + " --role " + role_id) + self.c_log_write("[CHANGE]: Modify role for " + user_name) + time.sleep(2) + else: + if "blobstore" in self.rdmc.app.current_client.base_url: + url = "/redfish/v1/Managers/1/SecurityService/" + get_security = self.rdmc.app.get_handler(url, silent=True, service=True).dict + security_mode = get_security["SecurityState"] + + if ( + "10" in add_privs_str + and "Production" in security_mode + and (options.user is None and options.password is None) + ): + self.c_log_write( + "Warning: For the users In local mode privileges might not get " + "updated in Production mode , Kindly rerun the serverclone load " + "with passing credentials which has recovery privilege" + ) + self.rdmc.ui.error( + "Warning: For the users In local mode privileges might not get " + "updated in Production mode , Kindly rerun the serverclone load " + "with passing credentials which has recovery privilege" + ) + elif not (options.user is None and options.password is None): + LOGGER.info("Logging out of the session without user and password") + self.rdmc.app.current_client.logout() + LOGGER.info("Logging in with user and password for deleting system recovery set") + self.rdmc.app.current_client._user_pass = (options.user, options.password) + self.rdmc.app.current_client.login(self.rdmc.app.current_client.auth_type) + if add_privs_str: + self.rdmc.ui.printer("Adding privileges for user: '%s'.\n" % user_name) + self.auxcommands["iloaccounts"].run( + "modify " + user_name + " --addprivs " + add_privs_str + ) + self.c_log_write("[CHANGE]: Adding privs for " + user_name) + time.sleep(2) + if remove_privs_str: + self.rdmc.ui.printer("Removing privileges for user: '%s'.\n" % user_name) + self.auxcommands["iloaccounts"].run( + "modify " + user_name + " --removeprivs " + remove_privs_str + ) + self.c_log_write("[CHANGE]: Removing privs for " + user_name) + time.sleep(2) + elif role_id: + self.auxcommands["iloaccounts"].run("modify " + user_name + " --role " + role_id) + self.c_log_write("[CHANGE]: Modify role id for " + user_name) + time.sleep(2) + else: + if add_privs_str: + self.rdmc.ui.printer("Adding privileges for user: '%s'.\n" % user_name) + self.auxcommands["iloaccounts"].run("modify " + user_name + " --addprivs " + add_privs_str) + self.c_log_write("[CHANGE]: Adding privs for " + user_name) + time.sleep(2) + if remove_privs_str: + self.rdmc.ui.printer("Removing privileges for user: '%s'.\n" % user_name) + self.auxcommands["iloaccounts"].run( + "modify " + user_name + " --removeprivs " + remove_privs_str + ) + self.c_log_write("[CHANGE]: Removing privs for " + user_name) + time.sleep(2) + elif role_id: + self.auxcommands["iloaccounts"].run("modify " + user_name + " --role " + role_id) + self.c_log_write("[CHANGE]: Modify role id for " + user_name) + time.sleep(2) + else: + raise Exception( + "A password was not provided for account: '%s', path: '%s'. " + "iLO accounts will not be altered without a valid password.\n" % (user_name, path) + ) + + @log_decor + def save_federation(self, fedaccts, _type, options): + """ + Save of Federation Account Data. + :parm fedaccts: Federation account payload to be saved + :type fedaccts: dict + :param _type: iLO schema type + :type _type: string + :param options: command line options + :type options: attribute + """ + + try: + fed_name = next(iter(jsonpath_rw.parse("$..Name").find(fedaccts))).value + except StopIteration: + privileges = None + + try: + fed_id = next(iter(jsonpath_rw.parse("$..Id").find(fedaccts))).value + except StopIteration: + privileges = None + + try: + privileges = next(iter(jsonpath_rw.parse("$..Privileges").find(fedaccts))).value + except StopIteration: + privileges = None + + fedkey = [__DEFAULT__, __DEFAULT__] + # if options.iLOSSA: + # sys.stdout.write("Smart Storage Array functionality") + + if not options.autocopy and not options.iLOSSA and not options.all: + while True: + for i in range(2): + if i < 1: + self.rdmc.ui.printer("Please input the federation key for Federation " "user: %s\n" % fed_name) + else: + self.rdmc.ui.printer( + "Please re-enter the federation key for Federation " "user: %s\n" % fed_name + ) + + fedkey[i] = getpass.getpass() + try: + [fedkey[i], _] = fedkey[i].split("\r") + except ValueError: + pass + + if fedkey[0] == fedkey[1] and (fedkey[0] is not None or fedkey[0] != ""): + break + else: + ans = input("You have entered two different federation keys...Retry?(y/n)\n") + if ans.lower() != "y": + self.rdmc.ui.printer("Skipping Federation Account Migration for: " "%s\n" % fed_name) + return None + else: + self.rdmc.ui.warn("Remember to edit the Federation key for acct: '%s'." % fed_name) + + if not fedkey[0]: + fedkey[0] = __DEFAULT__ + self.rdmc.ui.warn("Using a placeholder federation key '%s' in %s file.\n" % (fedkey[0], self.clone_file)) + fedaccts = { + "AccountID": fed_id, + "FederationName": fed_name, + "FederationKey": fedkey[0], + "Privileges": privileges, + } + return fedaccts + + @log_decor + def load_federation(self, fed_accounts, _type, path, options): + """ + Load of Federation Account Data. + :parm fed_accounts: Federation account payload to be loaded + :type fed_accounts: dict + """ + + found_user = False + fed_name = fed_accounts["FederationName"] + fed_key = fed_accounts["FederationKey"] + + # set the current privileges to those in the clone file + curr_privs = fed_accounts["Privileges"] + + # obtaining account information on the current server as a check to verify the user + # provided a decent path to use. This can be re-factored. + try: + for curr_sel in self.rdmc.app.select(_type.split(".")[0] + "."): + try: + curr_privs = curr_sel.dict.get("Privileges") + curr_fed = curr_sel.dict.get("Name") + if curr_fed != fed_name: + continue + else: + found_user = True + break + except (KeyError, NameError): + self.rdmc.ui.error("Unable to obtain the account information for: '%s''s'" "account.\n" % fed_name) + continue + except InstanceNotFoundError: + pass + + if not found_user: + self.rdmc.ui.warn("Fed Account '%s' was not found on this system.\n" % fed_name) + + if fed_key: + if fed_key == __DEFAULT__: + self.rdmc.ui.warn("The default federation key will be attempted.") + (add_privs_str, remove_privs_str) = self.priv_helper(fed_accounts, curr_privs) + try: + if found_user: + raise ResourceExists("") + else: + self.rdmc.ui.printer("Adding '%s' to iLO Federation.\n" % fed_name) + self.auxcommands["ilofederation"].run("add " + fed_name + " " + fed_key + " " + add_privs_str) + time.sleep(2) + except ResourceExists: + self.rdmc.ui.warn("This account already exists on this system: '%s'\n" % fed_name) + self.rdmc.ui.printer("Changing Federation account: '%s's key\n" % fed_name) + self.auxcommands["ilofederation"].run("changekey " + fed_name + " " + fed_key) + except ValueError: + self.rdmc.ui.error("Some other error occured while attempting to create this " "account: %s" % fed_name) + finally: + if "blobstore" in self.rdmc.app.current_client.base_url: + url = "/redfish/v1/Managers/1/SecurityService/" + get_security = self.rdmc.app.get_handler(url, silent=True, service=True).dict + security_mode = get_security["SecurityState"] + + if ( + "10" in add_privs_str + and (options.user is None and options.password is None) + and "Production" in security_mode + ): + self.c_log_write( + "Warning: For the users In local mode privileges might not get updated in " + "Production mode , Kindly rerun the serverclone load with " + "passing credentials which has recovery privilege" + ) + self.rdmc.ui.error( + "Warning: For the users In local mode privileges might not get updated in " + "Production mode , Kindly rerun the serverclone load with " + "passing credentials which has recovery privilege" + ) + elif not (options.user is None and options.password is None): + LOGGER.info("Logging out of the session without user and password") + self.rdmc.app.current_client.logout() + LOGGER.info("Logging in with user and password for deleting system recovery set") + self.rdmc.app.current_client._user_pass = (options.user, options.password) + self.rdmc.app.current_client.login(self.rdmc.app.current_client.auth_type) + + if add_privs_str: + self.rdmc.ui.printer("Adding privs to Federation account: '%s'\n" % fed_name) + self.auxcommands["ilofederation"].run( + "modify " + + fed_name + + " " + + fed_key + + " --addprivs " + + add_privs_str + + " -u " + + options.user + + " -p " + + options.password + ) + time.sleep(2) + if remove_privs_str: + self.rdmc.ui.printer("Removing privs from Federation account: '%s'\n" % fed_name) + self.auxcommands["ilofederation"].run( + "modify " + + fed_name + + " " + + fed_key + + " --removeprivs " + + remove_privs_str + + " -u " + + options.user + + " -p " + + options.password + ) + time.sleep(2) + + else: + if add_privs_str: + self.rdmc.ui.printer("Adding privs to Federation account: '%s'\n" % fed_name) + self.auxcommands["ilofederation"].run( + "modify " + fed_name + " " + fed_key + " --addprivs " + add_privs_str + ) + time.sleep(2) + if remove_privs_str: + self.rdmc.ui.printer("Removing privs from Federation account: '%s'\n" % fed_name) + self.auxcommands["ilofederation"].run( + "modify " + fed_name + " " + fed_key + " --removeprivs " + remove_privs_str + ) + time.sleep(2) + + else: + self.rdmc.ui.warn( + "A valid Federation key was not provided...skipping account " + "creation or modification for Fed. Acct '%s'" % fed_name + ) + + @log_decor + def save_smartstorage(self, drive_data, _type): + """ + Smart Storage Disk and Array Controller Configuration save. + :parm drive_data: Smart Storage Configuration payload to be saved + :type drive_data: dict + :param _type: iLO schema type + :type _type: string + """ + + @log_decor + def load_smartstorage(self, controller_data, _type, path): + """ + Smart Storage Disk and Array Controller Configuration load. + :parm controller_data: Smart Storage Configuration payload to be loaded + :type controller_data: dict + :param _type: iLO schema type + :type _type: string + :param path: iLO schema path + :type path: string + """ + self.smartarrayobj.load(controller_data) + + # Helper Functions + @log_decor + def system_compatibility_check(self, sys_info, options): + """ + Check if files needed for serverclone are available + :param sys_info: dictionary of comments for iLO firmware and BIOS ROM + versions + :type sys_info: dict + :param options: command line options + :type options: attribute + """ + + checks = [] + try: + curr_sys_info = self.rdmc.app.create_save_header()["Comments"] + curr_ilorev = format(float(self.curr_ilorev[0] + "." + self.curr_ilorev[1:]), ".2f") + if self.rdmc.app.getiloversion() >= 7: + file_ilorev, file_iloversion, _, _ = sys_info["iLOVersion"].split(" ") + file_iloversion = self.curr_iloversion + else: + _, file_iloversion, file_ilorev = sys_info["iLOVersion"].split(" ") + file_ilorev = file_ilorev.split("v")[-1] + self.rdmc.ui.printer("This system has iLO Version %s. \n" % curr_sys_info["iLOVersion"]) + self.rdmc.ui.printer("This system has BIOS Version %s.\n" % curr_sys_info["BIOSFamily"]) + if curr_sys_info["BIOSFamily"] == sys_info["BIOSFamily"]: + self.rdmc.ui.printer("BIOS Versions are compatible.\n") + checks.append(True) + else: + self.rdmc.ui.warn( + "BIOS Versions are different. Suggest to have" + " '%s' in place before upgrading.\n" % sys_info["BIOSFamily"] + ) + checks.append(False) + # Commenting out this line as same inforation is being printed twice. + # self.rdmc.ui.printer( + # "This system has iLO %s with firmware revision %s.\n" + # % (self.curr_iloversion, curr_ilorev) + # ) + if self.curr_iloversion == file_iloversion and curr_ilorev == file_ilorev: + self.rdmc.ui.printer("iLO Versions are fully compatible.\n") + checks.append(True) + elif self.curr_iloversion == file_iloversion and curr_ilorev != file_ilorev: + self.rdmc.ui.warn( + "The iLO Versions are compatible; however, the revisions " + "differ (system version: iLO %s %s, file version: iLO %s %s). Some " + "differences in properties, schemas and incompatible dependencies may " + "exist. Proceed with caution.\n" % (self.curr_iloversion, curr_ilorev, file_iloversion, file_ilorev) + ) + checks.append(False) + else: + self.rdmc.ui.warn( + "The iLO Versions are different. Compatibility issues may exist " + "attempting to commit changes to this system.\n(System version: iLO %s %s, " + "file version: iLO %s %s)\n" % (self.curr_iloversion, curr_ilorev, file_iloversion, file_ilorev) + ) + checks.append(False) + except KeyError as exp: + if "iLOVersion" in str(exp): + self.rdmc.ui.warn("iLOVersion not found in clone file 'Comments' dictionary.\n") + elif "BIOSFamily" in str(exp): + self.rdmc.ui.warn("BIOS Family not found in clone file 'Comments' dictionary.\n") + else: + raise Exception("%s" % exp) + + if (len(checks) == 0 or False in checks) and not options.autocopy and not options.iLOSSA and not options.all: + while True: + ans = input( + "Would you like to continue with migration of iLO configuration from " + "'%s' to '%s'? (y/n)\n" % (sys_info["Model"], curr_sys_info["Model"]) + ) + if ans.lower() == "n": + raise ExitHandler("Aborting load operation. No changes made to the server.") + elif ans.lower() == "y": + break + else: + self.rdmc.ui.warn("Invalid input...\n") + + self.rdmc.ui.printer( + "Attempting system clone from a '%s' to a '%s'.\n" % (sys_info["Model"], curr_sys_info["Model"]) + ) + + def priv_helper(self, desired_priv, curr_privs): + """ + Privilege helper. Assigns privileges to a string for addition or removal when loading + iLO management account or iLO Federation data + :param desired_priv: dictionary of desired privileges + :type desired_priv: dict + :param curr_priv: dictionary of current system privileges + :type curr_priv: dict + """ + + add_privs_str = "" + remove_privs_str = "" + + if desired_priv.get("Privileges").get("HostBIOSConfigPriv"): + add_privs_str += "8," + else: + remove_privs_str += "8," + + if desired_priv.get("Privileges").get("HostNICConfigPriv"): + add_privs_str += "7," + else: + remove_privs_str += "7," + + if desired_priv.get("Privileges").get("HostStorageConfigPriv"): + add_privs_str += "9," + else: + remove_privs_str += "9," + if desired_priv.get("Privileges").get("LoginPriv"): + add_privs_str += "1," + else: + remove_privs_str += "1," + + if desired_priv.get("Privileges").get("RemoteConsolePriv"): + add_privs_str += "2," + else: + remove_privs_str += "2," + + if desired_priv.get("Privileges").get("SystemRecoveryConfigPriv"): + add_privs_str += "10," + else: + remove_privs_str += "10," + + if desired_priv.get("Privileges").get("UserConfigPriv"): + add_privs_str += "3," + else: + remove_privs_str += "3," + if desired_priv.get("Privileges").get("VirtualMediaPriv"): + add_privs_str += "5," + else: + remove_privs_str += "5," + + if desired_priv.get("Privileges").get("VirtualPowerAndResetPriv"): + add_privs_str += "6," + else: + remove_privs_str += "6," + + if desired_priv.get("Privileges").get("iLOConfigPriv"): + add_privs_str += "4," + else: + remove_privs_str += "4," + + return (add_privs_str[:-1], remove_privs_str[:-1]) + + def get_rootpath(self, path): + """ + Obtain the root path of the current path (multiple instances within a path) + :param path: current type path + :returns: a tuple including either the root_path and the ending or the original path and + ending + """ + + root_path = "" + + if path[-1] == "/": + ending = path.split("/")[-2] + else: + ending = path.split("/")[-1] + + entries_list = [(pos.start(), pos.end()) for pos in list(re.finditer(ending, path))] + root_path, ident_ending = ( + path[: entries_list[-1][0]], + path[entries_list[-1][0] :], + ) + + # check to verify the root path + ending match the original path. + _ = "" + if len(root_path + ident_ending) == len(path): + return (root_path, _.join(ident_ending.split("/"))) + return (path, ident_ending) + + def get_filenames(self): + """ + Obtain a dictionary of filenames for clonefile, and cert files + :returns: returns dictionary of filenames + """ + return { + "clone_file": self.clone_file, + "https_cert_file": self.https_cert_file, + "sso_cert_file": self.sso_cert_file, + } + + def check_files(self, options): + """ + Check if files needed for serverclone are available + :param options: command line options + :type options: attribute + """ + if options.encryption: + if self.save: + self.rdmc.ui.printer("Serverclone JSON, '%s' will be encrypted.\n" % self.clone_file) + if self.load: + self.rdmc.ui.printer("Loading the encrypted JSON clone file: %s.\n" % self.clone_file) + self.rdmc.ui.printer("Note: Make sure %s is encrypted.\n" % self.clone_file) + + # delete anything in the change log file + with open(self.change_log_file, "w+") as clf: + clf.write("") + # delete anything in the error log file + with open(self.error_log_file, "w+") as elf: + elf.write("") + + # check the clone file exists (otherwise create) + try: + if options.encryption: + file_handle = open(self.clone_file, "r+b") + else: + file_handle = open(self.clone_file, "r+") + file_handle.close() + except: + if self.save: + if options.encryption: + file_handle = open(self.clone_file, "w+b") + else: + file_handle = open(self.clone_file, "w+") + file_handle.close() + else: + self.rdmc.ui.error("The clone file '%s', selected for loading," " was not found.\n" % self.clone_file) + raise IOError + + @log_decor + def type_compare(self, type1, type2): + """ + iLO schema type compatibility verification + :param type1 + :type string + :param type2 + :type string + :returns: return tuple with booleans of comparison checks + """ + _type1 = type1 + _type2 = type2 + checklist = ["Major"] # , 'Minor'] #No minor checking for now + + found_type = False + compatible = list() + + _type1 = self.type_break(_type1) + _type2 = self.type_break(_type2) + + if _type1[type1]["Type"].lower() == _type2[type2]["Type"].lower(): + found_type = True + + for item in checklist: + if _type1[type1]["Version"][item] == _type2[type2]["Version"][item]: + compatible.append("True") + else: + compatible.append("False") + + if "False" in compatible: + return (found_type, False) + return (found_type, True) + + @log_decor + def type_break(self, _type): + """ + Breakdown of each iLO schema type for version comparison + :param _type: iLO schema type + :type _type: string + """ + + _type2 = dict() + _type_breakdown = _type.split("#")[-1].split(".") + _type2[_type] = dict([("Type", _type_breakdown[0]), ("Version", {})]) + versioning = list() + if len(_type_breakdown) == 3 and "_" in _type_breakdown[1]: + rev = _type_breakdown[1].split("_") + _type2[_type]["Version"] = { + "Major": int(rev[0][-1]), + "Minor": int(rev[1]), + "Errata": int(rev[2]), + } + elif len(_type_breakdown) > 3 and "_" not in _type: + for value in _type_breakdown: + if value.isdigit(): + versioning.append(int(value)) + _type2[_type]["Version"] = { + "Major": versioning[0], + "Minor": versioning[1], + "Errata": versioning[2], + } + + return _type2 + + def serverclonevalidation(self, options): + """ + Serverclone validation function. Validates command line options and + initiates a login to the iLO Server. + :param options: command line options + :type options: list. + """ + + self._cache_dir = os.path.join(self.rdmc.app.cachedir, __tempfoldername__) + if not os.path.exists(self._cache_dir): + os.makedirs(self._cache_dir) + self.tmp_clone_file = os.path.join(self._cache_dir, __tmp_clone_file__) + self.tmp_sel_file = os.path.join(self._cache_dir, __tmp_sel_file__) + # self.change_log_file = os.path.join(self._cache_dir, __changelog_file__) + # self.error_log_file = os.path.join(self._cache_dir, __error_log_file__) + + self.cmdbase.login_select_validation(self, options) + + if options.clonefilename: + if len(options.clonefilename) < 2: + self.clone_file = options.clonefilename[0] + else: + raise InvalidCommandLineError("Only a single clone file may be specified.") + else: + self.clone_file = __clone_file__ + + if options.encryption: + if len(options.encryption.encode("utf8")) not in [16, 24, 32]: + raise InvalidKeyError( + "An invalid encryption key has been used with a length of: " + "%s chars....ensure the encryption key length is 16, 24 or " + "32 characters long." % len((options.encryption).encode("utf8")) + ) + # filenames + if self.load: + if options.ssocert: + if len(options.ssocert) < 2 and self.load: + self.sso_cert_file = options.ssocert[0] + else: + raise InvalidCommandLineError("Ensure you are loading a single SSO certificate" ".\n") + if options.tlscert: + if len(options.tlscert) < 2 and self.load: + self.https_cert_file = options.tlscert[0] + else: + raise InvalidCommandLineError("Ensure you are loading a single TLS certificate" ".\n") + if self.rdmc.opts.debug: + self.rdmc.ui.warn( + "Debug selected...all exceptions will be handled in an external log " + "file (check error log for automatic testing).\n" + ) + with open(self.error_log_file, "w+") as efh: + efh.write("") + + @staticmethod + def options_argument_group(parser): + """Define option arguments group + :param parser: The parser to add the login option group to + :type parser: ArgumentParser/OptionParser + """ + + parser.add_argument( + "--encryption", + dest="encryption", + help="Optionally include this flag to encrypt/decrypt a file" " using the key provided.", + default=None, + ) + parser.add_argument( + "-f", + "--clonefile", + dest="clonefilename", + help="Optionally rename the default clone file 'ilorest_clone.json'", + action="append", + default=None, + ) + parser.add_argument( + "-sf", + "--storageclonefile", + dest="storageclonefilename", + help="Optionally rename the default clone file 'ilorest_storage_clone.json'", + action="append", + default=None, + ) + parser.add_argument( + "--uniqueoverride", + dest="uniqueoverride", + action="store_true", + help="Override the measures stopping the tool from writing." "over items that are system unique.", + default=None, + ) + parser.add_argument( + "--auto", + dest="autocopy", + help="Optionally include this flag to ignore user prompts for save or load processes.", + action="store_true", + default=None, + ) + + def definearguments(self, customparser): + """Wrapper function for new command main function + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + # self.options_argument_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + subcommand_parser.required = True + save_help = "Save an iLO, Bios and SSA config." + # save sub-parser + save_parser = subcommand_parser.add_parser( + "save", + help=save_help, + description=save_help + "\n\texample: serverclone save" + "\n\n\tSave iLO config omitting BIOS attributes to a non-default file name.\n\t" + "example: serverclone save -f serv_clone.json --nobios" + "\n\n\tSave an encrypted iLO configuration file (to the default file name)\n\t" + "example: serverclone save --encryption ", + formatter_class=RawDescriptionHelpFormatter, + ) + save_parser.add_argument( + "--ilossa", + dest="iLOSSA", + help="Optionally include this flag to include configuration of" " iLO Smart Array Devices during save.", + action="store_true", + default=None, + ) + + save_parser.add_argument( + "--all", + dest="all", + help="Optionally include this flag to include all" " iLO Smart Array Devices and All during save.", + action="store_true", + default=None, + ) + + save_parser.add_argument( + "--nobios", + dest="noBIOS", + help="Optionally include this flag to omit save of Bios configuration.", + action="store_true", + default=None, + ) + self.cmdbase.add_login_arguments_group(save_parser) + self.options_argument_group(save_parser) + + load_help = "Load an iLO, Bios and/or SSA config." + # load sub-parser + load_parser = subcommand_parser.add_parser( + "load", + help=load_help, + description=load_help + "SSO and TLS certificates may be" + "added on load.\n\n\tLoad a clone file from a non-default file name.\n\t" + "example: serverclone load -f serv_clone.json" + "\n\n\tLoad a clone file with SSO and TLS certificates.\n\t" + "example: serverclone load -ssocert sso.txt --tlscert tls.txt" + "\n\n\tLoad a clone file which has been encrypted.\n\t" + "example: serverclone load --encryption abc12abc12abc123\n\n\t", + formatter_class=RawDescriptionHelpFormatter, + ) + load_parser.add_argument( + "--ssocert", + dest="ssocert", + help="Use this flag during 'load' to include an SSO certificate." + " This should be properly formatted in a simple text file.", + action="append", + default=None, + ) + load_parser.add_argument( + "--tlscert", + dest="tlscert", + help="Use this flag during 'load' to include a TLS certificate." + " This should be properly formatted in a simple text file.", + action="append", + default=None, + ) + load_parser.add_argument( + "--all", + dest="all", + help="Optionally include this flag to include all" " iLO Smart Array Devices and All during save.", + action="store_true", + default=None, + ) + load_parser.add_argument( + "--ilossa", + dest="iLOSSA", + help="Optionally include this flag to include all" " iLO Smart Array Devices and All during loadn.", + action="store_true", + default=None, + ) + load_parser.add_argument( + "--noautorestart", + dest="noautorestart", + help="Optionally noautorestart after loading", + action="store_true", + default=False, + ) + self.cmdbase.add_login_arguments_group(load_parser) + self.options_argument_group(load_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerConfigurationLockCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerConfigurationLockCommand.py new file mode 100644 index 0000000..b3b40d9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerConfigurationLockCommand.py @@ -0,0 +1,279 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### +# -*- coding: utf-8 -*- +"""BiosDefaultsCommand for rdmc""" + +from argparse import RawDescriptionHelpFormatter + +try: + from rdmc_helper import ( + LOGGER, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidPasswordLengthError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + LOGGER, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidPasswordLengthError, + ReturnCodes, + ) + + +class ServerConfigurationLockCommand: + """Set BIOS settings back to default for the server that is currently + logged in""" + + def __init__(self): + self.ident = { + "name": "serverconfiglock", + "usage": None, + "description": "To to perform validation checks on raw JSON data " + "server.\n\texample: serverconfiglock\n\t" + "example: serverconfiglock enable --serverconfiglockpassword=AWS@123456789 " + "--serverconfiglockexcludefwrevs=True\t serverconfiglockexcludefwrevs set True or False\n\t" + "example: serverconfiglock disable --serverconfiglockpassword=AWS@1234 " + "--serverconfiglockdisable=True\t serverconfiglockdisable set True or False\n\t" + "example: serverconfiglock display", + "summary": "The BIOS feature “Server Configuration Lock” supports certain parameters," + "including a password. This password has a 16 to 31 character limit. " + "“Server Configuration Lock” is not one of the special commands in iLO REST.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main BIOS defaults worker function""" + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + bios_path = self.rdmc.app.typepath.defs.biospath + resp = self.rdmc.app.get_handler(bios_path, silent=True, service=True).dict + match_url = resp["Oem"]["Hpe"]["Links"]["ServerConfigLock"]["@odata.id"] + if "oem" in match_url: + scs_path = bios_path + "oem/hpe/serverconfiglock/settings/" + else: + scs_path = bios_path + "serverconfiglock/settings/" + self.serverconfiglockvalidation(options) + if options.command: + if options.command.lower() == "enable": + self.enable_scs(options, scs_path) + elif options.command.lower() == "disable": + self.disable_scs(options, scs_path) + elif options.command == "display": + self.display_scs(scs_path) + else: + raise InvalidCommandLineError("%s is not a valid option for this " "command." % str(options.command)) + else: + raise InvalidCommandLineError( + "Please provide either enable, disable and display subcommand. " + "For help or usage related information, use -h or --help" + ) + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def enable_scs(self, options, scs_path): + """ + Enable/Disable the SCL config function + """ + if options.serverconfiglockpassword: + ser_cf_password = options.serverconfiglockpassword + self.serverconfiglockpassword_validation(options) + if options.serverconfiglockexcludefwrevs.lower() == "true": + serverconfrevs = "True" + elif options.serverconfiglockexcludefwrevs.lower() == "false": + serverconfrevs = "False" + else: + raise InvalidCommandLineError("ServerConfigLockExcludeFwRevs value invalid set only true or false") + body = {"ServerConfigLockPassword": ser_cf_password, "ServerConfigLockExcludeFwRevs": serverconfrevs} + try: + self.rdmc.ui.printer("payload: %s \n" % body) + LOGGER.info("payload", body) + resp = self.rdmc.app.patch_handler(scs_path, body) + if resp.status == 200: + self.rdmc.ui.printer("SCS enabled successfully\n") + LOGGER.info("SCS enabled successfully") + self.rdmc.ui.printer("Help: System Reboot is required after changing the serverconfiglock settings \n") + return ReturnCodes.SUCCESS + except IncompatibleiLOVersionError: + self.rdmc.ui.error("iLO FW version on this server doesnt support SCS") + LOGGER.error("iLO FW version on this server doesnt support SCS") + return ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + + def disable_scs(self, options, scs_path): + """ + Disable the SCL config function + """ + if options.serverconfiglockpassword: + ser_cf_password = options.serverconfiglockpassword + self.serverconfiglockpassword_validation(options) + if options.serverconfiglockdisable.lower() == "true": + sclockdisable = "True" + elif options.serverconfiglockdisable.lower() == "false": + sclockdisable = "False" + else: + raise InvalidCommandLineError("ServerConfigLockDisable value invalid set only true or false") + body = {"ServerConfigLockPassword": ser_cf_password, "ServerConfigLockDisable": sclockdisable} + try: + self.rdmc.ui.printer("payload: %s \n" % body) + LOGGER.info("payload", body) + resp = self.rdmc.app.patch_handler(scs_path, body) + if resp.status == 200: + self.rdmc.ui.printer("SCS Disabled successfully\n") + LOGGER.info("SCS Disabled successfully") + self.rdmc.ui.printer("Help: System Reboot is required after changing the serverconfiglock settings \n") + return ReturnCodes.SUCCESS + except IncompatibleiLOVersionError: + self.rdmc.ui.error("iLO FW version on this server doesnt support SCS") + LOGGER.error("iLO FW version on this server doesnt support SCS") + return ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + + def display_scs(self, scs_path): + """ + Display SCS details + """ + ilo_ver = self.rdmc.app.getiloversion() + if ilo_ver >= 5.290 or ilo_ver < 6.000: + results = self.rdmc.app.get_handler(scs_path, silent=True) + if results.status == 200: + self.rdmc.ui.printer("Server Configuration Lock setting details ...\n") + LOGGER.info("Server Configuration Lock setting details ...") + results = results.dict + self.print_scs_info(results) + else: + self.rdmc.ui.printer("For iLO6, this command is not yet supported.\n") + LOGGER.error("For iLO6, this command is not yet supported.") + + def print_scs_info(self, results): + """ + Prints the SCL: info + """ + for key, value in results.items(): + if "@odata" not in key: + if type(value) is dict: + self.print_cert_info(value) + else: + self.rdmc.ui.printer(key + ":" + str(value) + "\n") + + def serverconfiglockpassword_validation(self, options): + """ + Password validation for ServerConfigLockPassword method + """ + import re + + if options.serverconfiglockpassword: + password = options.serverconfiglockpassword + if len(password) < 16 or len(password) > 31: + LOGGER.info("Length of password requires min of 16 and max of 31 character " "excluding '/' character.\n") + raise InvalidPasswordLengthError( + "Length of password requires min of 16 and max of 31 character " "excluding '/' character.\n" + ) + elif re.search("[0-9]", password) is None: + print("Make sure your password has a number in it\n") + elif re.search("[A-Z]", password) is None: + print("Make sure your password has a capital letter in it\n") + + def serverconfiglockvalidation(self, options): + """new command method validation function""" + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + subcommand_parser = customparser.add_subparsers(dest="command") + enable_help = "To enable ServerConfigurationLock \n" + + enable_parser = subcommand_parser.add_parser( + "enable", + help=enable_help, + description=enable_help + "\n\tExample:\n\tserverconfiglock enable ", + formatter_class=RawDescriptionHelpFormatter, + ) + enable_parser.add_argument( + "--serverconfiglockpassword", + dest="serverconfiglockpassword", + help="Set a serverconfiglockpassword", + required=True, + type=str, + default=None, + ) + enable_parser.add_argument( + "--serverconfiglockexcludefwrevs", + dest="serverconfiglockexcludefwrevs", + help="to set/enable serverconfiglockexcludefwrevs", + default=False, + required=True, + ) + self.cmdbase.add_login_arguments_group(enable_parser) + + status_help = "To check the ServerConfigurationLock display\n" + status_parser = subcommand_parser.add_parser( + "display", + help=status_help, + description=status_help + "\n\tExample:\n\tserverconfiglock display or " "\n\tserverconfiglock status -j", + formatter_class=RawDescriptionHelpFormatter, + ) + + self.cmdbase.add_login_arguments_group(status_parser) + + disable_help = "To disable the ServerConfigurationLock\n" + disable_parser = subcommand_parser.add_parser( + "disable", + help=disable_help, + description=disable_help + "\n\tExample:\n\tserverconfiglock disable", + formatter_class=RawDescriptionHelpFormatter, + ) + disable_parser.add_argument( + "--serverconfiglockpassword", + dest="serverconfiglockpassword", + help="Server Configuration Lock password and to digitally fingerprint the system to enable Server " + "Configuration Lock.", + required=True, + type=str, + default=None, + ) + disable_parser.add_argument( + "--serverconfiglockdisable", + dest="serverconfiglockdisable", + help="Select this option to disable Server Configuration Lock.", + required=True, + default=False, + ) + self.cmdbase.add_login_arguments_group(disable_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerInfoCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerInfoCommand.py new file mode 100644 index 0000000..8f810c0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerInfoCommand.py @@ -0,0 +1,1158 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### +# -*- coding: utf-8 -*- +"""Server Info Command for rdmc""" +import re +import sys +from collections import OrderedDict + +import jsonpath_rw + +try: + from rdmc_helper import ( + UI, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + UI, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class ServerInfoCommand: + """Show details of a server""" + + def __init__(self): + self.ident = { + "name": "serverinfo", + "usage": None, + "description": "Shows all information.\n\tExample: serverinfo\n\t\t" + "serverinfo --all\n\n\t" + "Show enabled fan, processor, and thermal information.\n\texample: " + "serverinfo --fans --processors --thermals --proxy\n\n\tShow all memory " + "and fan information, including absent locations in json format.\n\t" + "example: serverinfo --proxy --firmware --software --memory --fans --showabsent -j\n", + "summary": "Shows aggregate health status and details of the currently logged in server.", + "aliases": ["health", "serverstatus", "systeminfo"], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main serverinfo function. + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("serverinfo command takes no " "arguments.") + + self.serverinfovalidation(options) + + self.optionsvalidation(options) + + info = self.gatherinfo(options) + + if not info: + raise InvalidCommandLineError("Please verify the commands entered " "and try again.") + if "proxy" in info and info["proxy"]: + if "WebProxyConfiguration" in info["proxy"]["Oem"]["Hpe"]: + info["proxy"] = info["proxy"]["Oem"]["Hpe"]["WebProxyConfiguration"] + + if options.json: + # if options.processors and options.json and self.rdmc.app.typepath.defs.isgen9: + # pass + # elif options.memory and self.rdmc.app.typepath.defs.isgen9: + # pass + # else: + self.build_json_out(info, options.showabsent) + else: + self.prettyprintinfo(info, options.showabsent) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def gatherinfo(self, options): + """Gather info to printout based on options + + :param options: command line options + :type options: list. + """ + info = {} + path = self.rdmc.app.typepath.defs.systempath + if path == "/rest/v1/Systems/1" or "/redfish/v1/Systems/1/": + path = "/redfish/v1/Systems/" + else: + pass + if options.system: + info["system"] = OrderedDict() + csysresults = self.rdmc.app.get_handler(path, service=True, silent=True).dict + # csysresults = self.rdmc.app.select(selector="ComputerSystemCollection") + # csysresults = csysresults[0].dict + members = csysresults["Members"] + for id in members: + for mem_id in id.values(): + if path in mem_id: + csysresults = self.rdmc.app.get_handler(mem_id, service=True, silent=True).dict + try: + csysresults = csysresults[0].dict + except: + pass + if csysresults: + text = "%s %s" % ( + csysresults["Manufacturer"], + csysresults["Model"], + ) + info["system"]["Model"] = re.sub(r"^(HPE)\s+\1\s+", r"\1 ", text) + info["system"]["Bios Version"] = csysresults["BiosVersion"] + + biosresults = self.rdmc.app.select(selector=self.rdmc.app.typepath.defs.biostype) + + try: + biosresults = biosresults[0].dict + except: + pass + + if biosresults: + try: + info["system"]["Serial Number"] = biosresults["Attributes"]["SerialNumber"] + except: + if "SerialNumber" in biosresults: + info["system"]["Serial Number"] = biosresults["SerialNumber"] + ethresults = None + getloc = self.rdmc.app.getidbytype("EthernetInterfaceCollection.") + if getloc: + for loc in getloc: + if "/systems/1/" in loc.lower(): + ethresults = self.rdmc.app.getcollectionmembers(loc) + break + if ethresults: + niccount = 0 + info["system"]["ethernet"] = OrderedDict() + for eth in ethresults: + niccount += 1 + if eth["Name"] == "": + if "MACAddress" in eth: + info["system"]["ethernet"][niccount] = eth["MACAddress"] + else: + info["system"]["ethernet"][niccount] = eth["PermanentMACAddress"] + elif eth["Name"] != "": + if "MACAddress" in eth: + info["system"]["ethernet"][eth["Name"]] = eth["MACAddress"] + else: + info["system"]["ethernet"][eth["Name"]] = eth["PermanentMACAddress"] + info["system"]["NICCount"] = niccount + if options.thermals or options.fans: + data = None + if not self.rdmc.app.typepath.defs.isgen9: + getloc = self.rdmc.app.getidbytype("Thermal.") + else: + getloc = self.rdmc.app.getidbytype("ThermalMetrics.") + if getloc: + data = self.rdmc.app.get_handler(getloc[0], silent=True, service=True) + if options.thermals: + if not getloc: + info["thermals"] = None + else: + info["thermals"] = data.dict["Temperatures"] + if options.fans: + if not getloc: + info["fans"] = None + else: + info["fans"] = data.dict["Fans"] + if options.memory: + data = None + if self.rdmc.app.typepath.defs.isgen9: + mem_path = "/redfish/v1/Systems/1/Memory/" + "?$expand=." + getloc = self.rdmc.app.get_handler(mem_path, silent=True, service=True).dict + collectiondata = getloc["Oem"][self.rdmc.app.typepath.defs.oemhp] + if collectiondata and not options.json: + sys.stdout.write("---------------------------------\n") + sys.stdout.write("Memory/DIMM Board Information:\n") + sys.stdout.write("---------------------------------\n") + memory_status = "Advanced Memory Protection Status: %s \n" % collectiondata["AmpModeStatus"] + sys.stdout.write(memory_status) + sys.stdout.write("AmpModeActive: %s \n" % collectiondata["AmpModeActive"]) + sys.stdout.write("AmpModeSupported: %s \n" % collectiondata["AmpModeSupported"]) + sys.stdout.write("Type: %s \n" % collectiondata["Type"]) + if collectiondata and options.json: + tmp = dict() + tmp["Memory"] = dict() + tmp["Memory"]["Advanced Memory Protection Status"] = collectiondata["AmpModeStatus"] + tmp["Memory"]["AmpModeActive"] = collectiondata["AmpModeActive"] + tmp["Memory"]["AmpModeSupported"] = collectiondata["AmpModeSupported"] + tmp["Memory"]["Type"] = collectiondata["Type"] + UI().print_out_json(tmp) + getloc = self.rdmc.app.getidbytype("MemoryCollection.") + if getloc: + data = self.rdmc.app.getcollectionmembers(getloc[0], fullresp=True)[0] + info["memory"] = data + else: + info["memory"] = None + if options.proxy: + data = None + getloc = self.rdmc.app.getidbytype("NetworkProtocol.") + if getloc: + data = self.rdmc.app.getcollectionmembers(getloc[0], fullresp=True)[0] + info["proxy"] = data + else: + info["proxy"] = None + if options.processors: + data = None + if not self.rdmc.app.typepath.defs.isgen9: + getloc = self.rdmc.app.getidbytype("ProcessorCollection.") + else: + # getloc = self.rdmc.app.getidbytype("ProcessorCollection.") + getloc = self.rdmc.app.getidbytype("Processor.") + tot_proc = [] + if getloc == "/rest/v1/Systems/1/Processors/1" or "/rest/v1/Systems/1/Processors/2": + getloc = "/redfish/v1/Systems/1/Processors/" + get_p = self.rdmc.app.get_handler(getloc, service=True, silent=True).dict["Members"] + output = "" + if get_p and not options.json: + sys.stdout.write("------------------------------------------------\n") + sys.stdout.write("Processor:\n") + sys.stdout.write("------------------------------------------------\n") + for pp in get_p: + dd = pp["@odata.id"] + data = self.rdmc.app.get_handler(dd, service=True, silent=True).dict + output += "Processor %s:\n" % data["Id"] + output += "\tModel: %s\n" % data["Model"] + output += "\tStep: %s\n" % data.get("ProcessorId", {}).get("Step", "NA") + output += "\tSocket: %s\n" % data["Socket"] + output += "\tMax Speed: %s MHz\n" % data["MaxSpeedMHz"] + try: + output += ( + "\tSpeed: %s MHz\n" + % data["Oem"][self.rdmc.app.typepath.defs.oemhp]["RatedSpeedMHz"] + ) + except KeyError: + pass + output += "\tCores: %s\n" % data["TotalCores"] + output += "\tThreads: %s\n" % data["TotalThreads"] + try: + for cache in data["Oem"][self.rdmc.app.typepath.defs.oemhp]["Cache"]: + output += "\t%s: %s KB\n" % ( + cache["Name"], + cache["InstalledSizeKB"], + ) + except KeyError: + pass + try: + output += "\tHealth: %s\n" % data["Status"]["Health"] + except KeyError: + pass + self.rdmc.ui.printer(output, verbose_override=True) + if get_p and options.json: + for pp in get_p: + dd = pp["@odata.id"] + data = self.rdmc.app.get_handler(dd, service=True, silent=True).dict + process = "Processor %s" % data["Id"] + tmp = dict() + tmp[process] = dict() + tmp[process]["Processor"] = data["Id"] + tmp[process]["Model"] = data["Model"] + tmp[process]["Step"] = data.get("ProcessorId", {}).get("Step", "NA") + tmp[process]["Socket"] = data["Socket"] + tmp[process]["Max Speed"] = data["MaxSpeedMHz"] + try: + tmp[process].update( + { + "Speed": "%s MHz" + % data["Oem"][self.rdmc.app.typepath.defs.oemhp]["RatedSpeedMHz"] + } + ) + except KeyError: + pass + tmp[process].update({"Cores": data["TotalCores"]}) + tmp[process].update({"Threads": data["TotalThreads"]}) + UI().print_out_json(tmp) + else: + pass + if getloc and not self.rdmc.app.typepath.defs.isgen9: + data = self.rdmc.app.getcollectionmembers(getloc[0]) + + info["processor"] = data + else: + info["processor"] = None + if options.power: + data = None + if not self.rdmc.app.typepath.defs.isgen9: + getloc = self.rdmc.app.getidbytype("Power.") + else: + getloc = self.rdmc.app.getidbytype("PowerMetrics.") + if getloc: + data = self.rdmc.app.get_handler(getloc[0], silent=True, service=True) + info["power"] = data.dict + else: + info["power"] = None + if options.firmware: + data = None + if not self.rdmc.app.typepath.defs.isgen10: + getloc = self.rdmc.app.getidbytype("FwSwVersionInventory") + else: + getloc = self.rdmc.app.getidbytype("SoftwareInventoryCollection.") + + for gloc in getloc: + if "FirmwareInventory" in gloc: + data = self.rdmc.app.getcollectionmembers(gloc) + # if not self.rdmc.app.typepath.defs.isgen10: + # data1 = list(data) + info["firmware"] = data + if options.software: + data = None + if not self.rdmc.app.typepath.defs.isgen10: + getloc = self.rdmc.app.getidbytype("SoftwareInventory") + else: + getloc = self.rdmc.app.getidbytype("SoftwareInventoryCollection.") + data_list = list() + for gloc in getloc: + if "SoftwareInventory" in gloc: + if not self.rdmc.app.typepath.defs.isgen10: + data = self.rdmc.app.getcollectionmembers(gloc) + data_list.append(data.dict) + else: + data = self.rdmc.app.getcollectionmembers(gloc) + if not self.rdmc.app.typepath.defs.isgen10: + info["software"] = data_list + else: + info["software"] = data + if not options.showabsent: + jsonpath_expr = jsonpath_rw.parse("$..State") + matches = jsonpath_expr.find(info) + matches.reverse() + + for match in matches: + if match.value.lower() == "absent": + arr = None + statepath = "/" + str(match.full_path).replace(".", "/") + arr = re.split(r"[\[\]]", statepath) + if arr: + removedict = None + start = arr[0].split("/") + for key in start: + if key: + if not removedict: + removedict = info[key] + else: + removedict = removedict[key] + del removedict[int(arr[1])] + return info + + def build_json_out(self, info, absent): + headers = list(info.keys()) + content = dict() + if "power" in headers and info["power"]: + data = info["power"] + if data is not None: + for control in data["PowerControl"]: + if "PowerCapacityWatts" in control: + power_cap = {"Total Power Capacity": "%s W" % control["PowerCapacityWatts"]} + if "PowerConsumedWatts" in control: + power_cap.update({"Total Power Consumed": "%s W" % control["PowerConsumedWatts"]}) + if "PowerMetrics" in control: + if "AverageConsumedWatts" in control["PowerMetrics"]: + power_mertic = {"Average Power": "%s W" % control["PowerMetrics"]["AverageConsumedWatts"]} + if "MaxConsumedWatts" in control["PowerMetrics"]: + power_mertic.update( + {"Max Consumed Power": "%s W" % control["PowerMetrics"]["MaxConsumedWatts"]} + ) + if "MinConsumedWatts" in control["PowerMetrics"]: + power_mertic.update( + {"Minimum Consumed Power": "%s W" % control["PowerMetrics"]["MinConsumedWatts"]} + ) + if "IntervalInMin" in control["PowerMetrics"]: + powercontent = ( + "Power Metrics on %s min. Intervals" % control["PowerMetrics"]["IntervalInMin"] + ) + test = {powercontent: power_mertic} + content = {"power": power_cap} + content["power"].update(test) + try: + powersuplies = [] + for supply in data["PowerSupplies"]: + power_supply = "Power Supply %s" % supply["Oem"][self.rdmc.app.typepath.defs.oemhp]["BayNumber"] + powersupply = {} + if "PowerCapacityWatts" in supply: + powersupply = {"Power Capacity": "%s W" % supply["PowerCapacityWatts"]} + if "LastPowerOutputWatts" in supply: + powersupply.update({"Last Power Output": "%s W" % supply["LastPowerOutputWatts"]}) + if "LineInputVoltage" in supply: + powersupply.update({"Input Voltage": "%s V" % supply["LineInputVoltage"]}) + if "LineInputVoltageType" in supply: + powersupply.update({"Input Voltage Type": supply["LineInputVoltageType"]}) + if "HotplugCapable" in supply["Oem"][self.rdmc.app.typepath.defs.oemhp]: + powersupply.update( + {"Hotplug Capable": supply["Oem"][self.rdmc.app.typepath.defs.oemhp]["HotplugCapable"]} + ) + if "iPDUCapable" in supply["Oem"][self.rdmc.app.typepath.defs.oemhp]: + powersupply.update( + {"iPDU Capable": supply["Oem"][self.rdmc.app.typepath.defs.oemhp]["iPDUCapable"]} + ) + try: + if "Health" in supply["Status"]: + powersupply.update({"Health": supply["Status"]["Health"]}) + except KeyError: + pass + # if absent: + try: + if "State" in supply["Status"]: + powersupply.update({"State": supply["Status"]["State"]}) + except KeyError: + pass + # powerdetails = {power_supply: powersupply} + powersuplies.append(powersupply) + content["power"].update({"PowerSupplies": powersuplies}) + for redundancy in data["Redundancy"]: + redund_name = redundancy["Name"] + redundancy_dict = {"Redundancy Mode": redundancy["Mode"]} + try: + redundancy_dict.update({"Redundancy Health": redundancy["Status"]["Health"]}) + redundancy_dict.update({"Redundancy State": redundancy["Status"]["State"]}) + content.update({redund_name: redundancy_dict}) + except KeyError: + pass + except KeyError: + pass + + if "firmware" in headers and info["firmware"]: + firmware_info = [] + data = info["firmware"] + if data is not None: + if not self.rdmc.app.typepath.defs.isgen10: + for key, fw in data.items(): + for fw_gen9 in fw: + fw_str = fw_gen9["Name"] + ": " + fw_gen9["VersionString"] + firmware_info.append(fw_str) + else: + for fw in data: + fw_str = fw["Name"] + ": " + fw["Version"] + firmware_info.append(fw_str) + content.update({"firmware": firmware_info}) + + if "software" in headers and info["software"]: + output = "" + software_info = {} + data = info["software"] + if data is not None: + if isinstance(data, dict) or isinstance(data, list): + for sw in data: + software_info.update({sw["Name"]: sw.get("Version", "NA")}) + else: + # if not options.json: + software_info = "No information available for the server\n" + self.rdmc.ui.printer(software_info, verbose_override=True) + content.update({"software": software_info}) + + if "memory" in headers and info["memory"]: + data = info["memory"] + count = 1 + if data is not None: + collectiondata = data["Oem"][self.rdmc.app.typepath.defs.oemhp] + output = "Memory/DIMM Board Information" + + memory_status = "Advanced Memory Protection Status: %s" % collectiondata["AmpModeStatus"] + memorylist = {} + for board in collectiondata["MemoryList"]: + board_detail = "Board CPU: %s" % board["BoardCpuNumber"] + memory_info = {"Total Memory Size": "%s MiB" % board["BoardTotalMemorySize"]} + memory_info.update({"Board Memory Frequency": "%s MHz" % board["BoardOperationalFrequency"]}) + memory_info.update({"Board Memory Voltage": "%s MiB" % board["BoardOperationalVoltage"]}) + memorylist.update({board_detail: memory_info}) + memorystatus = {memory_status: memorylist} + memoryinfo = {output: memorystatus} + + for dimm in data[self.rdmc.app.typepath.defs.collectionstring]: + memoryconfig = {"Location": dimm["DeviceLocator"]} + try: + memoryconfig.update( + { + "Memory Type": "%s %s" + % ( + dimm["MemoryType"], + dimm["MemoryDeviceType"], + ) + } + ) + except KeyError: + memoryconfig.update({"Memory Type": dimm["MemoryType"]}) + memoryconfig.update({"Capacity": "%s MiB" % dimm["CapacityMiB"]}) + try: + memoryconfig.update({"Speed": "%s MHz" % dimm["OperatingSpeedMhz"]}) + memoryconfig.update({"Status": dimm["Oem"][self.rdmc.app.typepath.defs.oemhp]["DIMMStatus"]}) + memoryconfig.update({"Health": dimm["Status"]["Health"]}) + except KeyError: + pass + + if absent: + try: + memoryconfig.update({"State": dimm["Status"]["State"]}) + except KeyError: + pass + + memory_config = "Memory/DIMM Configuration" + " " + str(count) + memoryinfo.update({memory_config: memoryconfig}) + content.update({"memory": memoryinfo}) + count = count + 1 + + if "fans" in headers and info["fans"]: + fan_output = {} + fan_details = {} + if info["fans"] is not None: + for fan in info["fans"]: + fan_name = "" + if not self.rdmc.app.typepath.defs.isgen9: + fan_name = "%s" % fan["Name"] + else: + fan_name = "%s" % fan["FanName"] + fan_output = {} + fan_output.update({"Location": fan["Oem"][self.rdmc.app.typepath.defs.oemhp]["Location"]}) + if "Reading" in fan: + fan_output.update({"Reading": "%s%%" % fan["Reading"]}) + fan_output.update({"Redundant": fan["Oem"][self.rdmc.app.typepath.defs.oemhp]["Redundant"]}) + fan_output.update( + {"Hot Pluggable": fan["Oem"][self.rdmc.app.typepath.defs.oemhp]["HotPluggable"]} + ) + try: + if "Health" in fan["Status"]: + fan_output.update({"Health": fan["Status"]["Health"]}) + except KeyError: + pass + + # if absent: + try: + fan_output.update({"State": fan["Status"]["State"]}) + except KeyError: + pass + fan_details.update({fan_name: fan_output}) + content.update({"fans": fan_details}) + + if "thermals" in headers and info["thermals"]: + if info["thermals"] is not None: + sensor = "" + thermal_detail = {} + for temp in info["thermals"]: + if "SensorNumber" in temp: + sensor = "Sensor #%s:" % temp["SensorNumber"] + if "PhysicalContext" in temp: + thermal_info = {"Location": temp["PhysicalContext"]} + thermal_info.update({"Current Temp": "%s C" % temp["ReadingCelsius"]}) + if "UpperThresholdCritical" in temp: + thermal_info.update({"Critical Threshold": "%s C" % temp["UpperThresholdCritical"]}) + else: + thermal_info.update({"Critical Threshold": "-"}) + if "UpperThresholdFatal" in temp: + thermal_info.update({"Fatal Threshold": "%s C" % temp["UpperThresholdFatal"]}) + else: + thermal_info.update({"Fatal Threshold": "-"}) + try: + if "Health" in temp["Status"]: + thermal_info.update({"Health": temp["Status"]["Health"]}) + except KeyError: + pass + if absent: + try: + thermal_info.update({"State": temp["Status"]["State"]}) + except KeyError: + pass + thermal_detail.update({sensor: thermal_info}) + content.update({"thermals": thermal_detail}) + + if "processor" in headers and info["processor"]: + data = info["processor"] + processor_info = {} + if data is not None: + if not self.rdmc.app.typepath.defs.isgen9: + for processor in data: + process = "Processor %s" % processor["Id"] + processor_date = {"Model": processor["Model"]} + processor_date.update({"Step": processor.get("ProcessorId", {}).get("Step", "NA")}) + processor_date.update({"Socket": processor["Socket"]}) + processor_date.update({"Max Speed": "%s MHz" % processor["MaxSpeedMHz"]}) + try: + processor_date.update( + { + "Speed": "%s MHz" + % processor["Oem"][self.rdmc.app.typepath.defs.oemhp]["RatedSpeedMHz"] + } + ) + except KeyError: + pass + processor_date.update({"Cores": processor["TotalCores"]}) + processor_date.update({"Threads": processor["TotalThreads"]}) + try: + for cache in processor["Oem"][self.rdmc.app.typepath.defs.oemhp]["Cache"]: + processor_date.update({cache["Name"]: "%s KB" % cache["InstalledSizeKB"]}) + except KeyError: + pass + try: + processor_date.update({"Health": processor["Status"]["Health"]}) + except KeyError: + pass + if absent: + try: + processor_date.update({"State": processor["Status"]["State"]}) + except KeyError: + pass + processor_info.update({process: processor_date}) + else: + data = data.dict + # for processor in data: + process = "Processor %s" % data["Id"] + processor_date = {"Model": data["Model"]} + processor_date.update({"Step": data.get("ProcessorId", {}).get("Step", "NA")}) + processor_date.update({"Socket": data["Socket"]}) + processor_date.update({"Max Speed": "%s MHz" % data["MaxSpeedMHz"]}) + try: + processor_date.update( + {"Speed": "%s MHz" % data["Oem"][self.rdmc.app.typepath.defs.oemhp]["RatedSpeedMHz"]} + ) + except KeyError: + pass + processor_date.update({"Cores": data["TotalCores"]}) + processor_date.update({"Threads": data["TotalThreads"]}) + try: + for cache in data["Oem"][self.rdmc.app.typepath.defs.oemhp]["Cache"]: + processor_date.update({cache["Name"]: "%s KB" % cache["InstalledSizeKB"]}) + except KeyError: + pass + try: + processor_date.update({"Health": data["Status"]["Health"]}) + except KeyError: + pass + if absent: + try: + processor_date.update({"State": data["Status"]["State"]}) + except KeyError: + pass + processor_info.update({process: processor_date}) + content.update({"processor": processor_info}) + + if "proxy" in headers and info["proxy"]: + proxy_info = {} + data = info["proxy"] + try: + if data is not None: + for k, v in data.items(): + proxy_info.update({k: v}) + except KeyError: + pass + content.update({"proxy": proxy_info}) + + if "system" in headers: + data = info["system"] + system = {} + if data is not None: + for key, val in list(data.items()): + if key == "ethernet": + embedded_nic = {"Embedded NIC Count": data["NICCount"]} + system.update(embedded_nic) + for name in sorted(data["ethernet"]): + mac_name = str(name) + " " + "MAC" + mac = {mac_name: data["ethernet"][name]} + system.update(mac) + elif not key == "NICCount": + nic = {key: val} + system.update(nic) + content.update({"system": system}) + + UI().print_out_json(content) + + def prettyprintinfo(self, info, absent): + """Print info in human readable form from json + + :param info: info data collected + :type info: dict. + :param absent: flag to show or hide absent components + :type absent: bool. + """ + output = "" + headers = list(info.keys()) + if "system" in headers: + data = info["system"] + output = "------------------------------------------------\n" + output += "System:\n" + output += "------------------------------------------------\n" + if data is not None: + for key, val in list(data.items()): + if key == "ethernet": + output += "Embedded NIC Count: %s\n" % data["NICCount"] + for name in sorted(data["ethernet"]): + output += "%s MAC: %s\n" % (name, data["ethernet"][name]) + elif not key == "NICCount": + output += "%s: %s\n" % (key, val) + self.rdmc.ui.printer(output, verbose_override=True) + + if "firmware" in headers and info["firmware"]: + data = info["firmware"] + output = "------------------------------------------------\n" + output += "Firmware: \n" + output += "------------------------------------------------\n" + if data is not None: + if not self.rdmc.app.typepath.defs.isgen10: + for key, fw in data.items(): + for fw_gen9 in fw: + output += "%s : %s\n" % ( + fw_gen9["Name"], + fw_gen9["VersionString"], + ) + else: + for fw in data: + output += "%s : %s\n" % (fw["Name"], fw["Version"]) + self.rdmc.ui.printer(output, verbose_override=True) + + if "software" in headers and info["software"]: + output = "" + data = info["software"] + if not isinstance(data, dict) and not isinstance(data, list): + data = data.dict + output = "------------------------------------------------\n" + output += "Software: \n" + output += "------------------------------------------------\n" + if data is not None: + if isinstance(data, dict) or isinstance(data, list): + for sw in data: + output += "%s : %s\n" % (sw["Name"], sw.get("Version", "NA")) + else: + output = "No information available for the server\n" + self.rdmc.ui.printer(output, verbose_override=True) + + if "proxy" in headers and info["proxy"]: + output = "" + data = info["proxy"] + output = "------------------------------------------------\n" + output += "Proxy: \n" + output += "------------------------------------------------\n" + try: + if data is not None: + for k, v in data.items(): + output += "%s : %s\n" % (k, v) + except KeyError: + pass + self.rdmc.ui.printer(output, verbose_override=True) + + if "processor" in headers and info["processor"]: + + output = "" + data = info["processor"] + output = "------------------------------------------------\n" + output += "Processor:\n" + output += "------------------------------------------------\n" + if data is not None: + if not self.rdmc.app.typepath.defs.isgen9: + for processor in data: + output += "Processor %s:\n" % processor["Id"] + output += "\tModel: %s\n" % processor["Model"] + output += "\tStep: %s\n" % processor.get("ProcessorId", {}).get("Step", "NA") + output += "\tSocket: %s\n" % processor.get("Socket", "NA") + output += "\tMax Speed: %s MHz\n" % processor.get("MaxSpeedMHz", "NA") + try: + output += ( + "\tSpeed: %s MHz\n" + % processor["Oem"][self.rdmc.app.typepath.defs.oemhp]["RatedSpeedMHz"] + ) + except KeyError: + pass + output += "\tCores: %s\n" % processor.get("TotalCores", "NA") + output += "\tThreads: %s\n" % processor.get("TotalThreads", "NA") + try: + for cache in processor["Oem"][self.rdmc.app.typepath.defs.oemhp]["Cache"]: + output += "\t%s: %s KB\n" % ( + cache["Name"], + cache["InstalledSizeKB"], + ) + except KeyError: + pass + try: + output += "\tHealth: %s\n" % processor["Status"]["Health"] + except KeyError: + pass + if absent: + try: + output += "\tState: %s\n" % processor["Status"]["State"] + except KeyError: + pass + self.rdmc.ui.printer(output, verbose_override=True) + else: + data = data.dict + output += "Processor %s:\n" % data["Id"] + output += "\tModel: %s\n" % data["Model"] + output += "\tStep: %s\n" % data.get("ProcessorId", {}).get("Step", "NA") + output += "\tSocket: %s\n" % data["Socket"] + output += "\tMax Speed: %s MHz\n" % data["MaxSpeedMHz"] + try: + output += "\tSpeed: %s MHz\n" % data["Oem"][self.rdmc.app.typepath.defs.oemhp]["RatedSpeedMHz"] + except KeyError: + pass + output += "\tCores: %s\n" % data.get("TotalCores", "NA") + output += "\tThreads: %s\n" % data.get("TotalThreads", "NA") + try: + for cache in data["Oem"][self.rdmc.app.typepath.defs.oemhp]["Cache"]: + output += "\t%s: %s KB\n" % ( + cache["Name"], + cache["InstalledSizeKB"], + ) + except KeyError: + pass + try: + output += "\tHealth: %s\n" % data["Status"]["Health"] + except KeyError: + pass + if absent: + try: + output += "\tState: %s\n" % data["Status"]["State"] + except KeyError: + pass + self.rdmc.ui.printer(output, verbose_override=True) + + if "memory" in headers and info["memory"]: + data = info["memory"] + if data is not None: + collectiondata = data["Oem"][self.rdmc.app.typepath.defs.oemhp] + output = "------------------------------------------------\n" + output += "Memory/DIMM Board Information:\n" + output += "------------------------------------------------\n" + output += "Advanced Memory Protection Status: %s\n" % collectiondata["AmpModeStatus"] + for board in collectiondata["MemoryList"]: + output += "Board CPU: %s \n" % board["BoardCpuNumber"] + output += "\tTotal Memory Size: %s MiB\n" % board["BoardTotalMemorySize"] + output += "\tBoard Memory Frequency: %s MHz\n" % board["BoardOperationalFrequency"] + output += "\tBoard Memory Voltage: %s MiB\n" % board["BoardOperationalVoltage"] + output += "------------------------------------------------\n" + output += "Memory/DIMM Configuration:\n" + output += "------------------------------------------------\n" + for dimm in data[self.rdmc.app.typepath.defs.collectionstring]: + output += "Location: %s\n" % dimm["DeviceLocator"] + try: + output += "Memory Type: %s %s\n" % ( + dimm["MemoryType"], + dimm["MemoryDeviceType"], + ) + except KeyError: + output += "Memory Type: %s\n" % dimm["MemoryType"] + output += "Capacity: %s MiB\n" % dimm["CapacityMiB"] + try: + output += "Speed: %s MHz\n" % dimm["OperatingSpeedMhz"] + + output += "Status: %s\n" % dimm["Oem"][self.rdmc.app.typepath.defs.oemhp]["DIMMStatus"] + output += "Health: %s\n" % dimm["Status"]["Health"] + except KeyError: + pass + if absent: + try: + output += "State: %s\n" % dimm["Status"]["State"] + except KeyError: + pass + output += "\n" + self.rdmc.ui.printer(output, verbose_override=True) + + if "power" in headers and info["power"]: + data = info["power"] + output = "------------------------------------------------\n" + output += "Power:\n" + output += "------------------------------------------------\n" + if data is not None: + for control in data["PowerControl"]: + output += "Total Power Capacity: %s W\n" % control["PowerCapacityWatts"] + output += "Total Power Consumed: %s W\n" % control["PowerConsumedWatts"] + output += "\n" + output += "Power Metrics on %s min. Intervals:\n" % control["PowerMetrics"]["IntervalInMin"] + output += "\tAverage Power: %s W\n" % control["PowerMetrics"]["AverageConsumedWatts"] + output += "\tMax Consumed Power: %s W\n" % control["PowerMetrics"]["MaxConsumedWatts"] + output += "\tMinimum Consumed Power: %s W\n" % control["PowerMetrics"]["MinConsumedWatts"] + try: + for supply in data["PowerSupplies"]: + output += "------------------------------------------------\n" + output += "Power Supply %s:\n" % supply["Oem"][self.rdmc.app.typepath.defs.oemhp]["BayNumber"] + output += "------------------------------------------------\n" + if "PowerCapacityWatts" in supply: + output += "Power Capacity: %s W\n" % supply["PowerCapacityWatts"] + if "LastPowerOutputWatts" in supply: + output += "Last Power Output: %s W\n" % supply["LastPowerOutputWatts"] + if "LineInputVoltage" in supply: + output += "Input Voltage: %s V\n" % supply["LineInputVoltage"] + if "LineInputVoltageType" in supply: + output += "Input Voltage Type: %s\n" % supply["LineInputVoltageType"] + if "HotplugCapable" in supply["Oem"][self.rdmc.app.typepath.defs.oemhp]: + output += ( + "Hotplug Capable: %s\n" + % supply["Oem"][self.rdmc.app.typepath.defs.oemhp]["HotplugCapable"] + ) + if "iPDUCapable" in supply["Oem"][self.rdmc.app.typepath.defs.oemhp]: + output += ( + "iPDU Capable: %s\n" % supply["Oem"][self.rdmc.app.typepath.defs.oemhp]["iPDUCapable"] + ) + try: + if "Health" in supply["Status"]: + output += "Health: %s\n" % supply["Status"]["Health"] + except KeyError: + pass + + try: + if "State" in supply["Status"]: + output += "State: %s\n" % supply["Status"]["State"] + except KeyError: + pass + for redundancy in data["Redundancy"]: + output += "------------------------------------------------\n" + output += "%s\n" % redundancy["Name"] + output += "------------------------------------------------\n" + output += "Redundancy Mode: %s\n" % redundancy["Mode"] + try: + output += "Redundancy Health: %s\n" % redundancy["Status"]["Health"] + output += "Redundancy State: %s\n" % redundancy["Status"]["State"] + except KeyError: + pass + except KeyError: + pass + self.rdmc.ui.printer(output, verbose_override=True) + + if "fans" in headers and info["fans"]: + output = "------------------------------------------------\n" + output += "Fan(s):\n" + output += "------------------------------------------------\n" + if info["fans"] is not None: + for fan in info["fans"]: + if not self.rdmc.app.typepath.defs.isgen9: + output += "%s:\n" % fan["Name"] + else: + output += "%s:\n" % fan["FanName"] + output += "\tLocation: %s\n" % fan["Oem"][self.rdmc.app.typepath.defs.oemhp]["Location"] + if "Reading" in fan: + output += "\tReading: %s%%\n" % fan["Reading"] + output += "\tRedundant: %s\n" % fan["Oem"][self.rdmc.app.typepath.defs.oemhp]["Redundant"] + output += ( + "\tHot Pluggable: %s\n" % fan["Oem"][self.rdmc.app.typepath.defs.oemhp]["HotPluggable"] + ) + try: + if "Health" in fan["Status"]: + output += "\tHealth: %s\n" % fan["Status"]["Health"] + except KeyError: + pass + + try: + output += "\tState: %s\n" % fan["Status"]["State"] + except KeyError: + pass + self.rdmc.ui.printer(output, verbose_override=True) + + if "thermals" in headers and info["thermals"]: + output = "------------------------------------------------\n" + output += "Thermal:\n" + output += "------------------------------------------------\n" + if info["thermals"] is not None: + for temp in info["thermals"]: + if "SensorNumber" in temp: + output += "Sensor #%s:\n" % temp["SensorNumber"] + if "PhysicalContext" in temp: + output += "\tLocation: %s\n" % temp["PhysicalContext"] + output += "\tCurrent Temp: %s C\n" % temp["ReadingCelsius"] + if "UpperThresholdCritical" in temp: + output += "\tCritical Threshold: %s C\n" % temp["UpperThresholdCritical"] + else: + output += "\tCritical Threshold: -\n" + if "UpperThresholdFatal" in temp: + output += "\tFatal Threshold: %s C\n" % temp["UpperThresholdFatal"] + else: + output += "\tFatal Threshold: -\n" + try: + if "Health" in temp["Status"]: + output += "\tHealth: %s\n" % temp["Status"]["Health"] + except KeyError: + pass + if absent: + try: + output += "\tState: %s\n" % temp["Status"]["State"] + except KeyError: + pass + self.rdmc.ui.printer(output, verbose_override=True) + + def serverinfovalidation(self, options): + """serverinfo method validation function. + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def optionsvalidation(self, options): + """Checks/updates options. + :param options: command line options + :type options: list + """ + optlist = [ + options.proxy, + options.firmware, + options.software, + options.memory, + options.thermals, + options.fans, + options.power, + options.processors, + options.system, + ] + + if not any(optlist): + self.setalloptionstrue(options) + if options.all: + self.setalloptionstrue(options) + return options + + def setalloptionstrue(self, options): + """Updates all selector options values to be True. + :param options: command line options + :type options: list + """ + options.memory = True + options.thermals = True + options.firmware = True + options.software = True + options.fans = True + options.processors = True + options.power = True + options.system = True + options.proxy = True + options.showabsent = True + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-all", + "--all", + dest="all", + action="store_true", + help="Add information for all types.", + default=False, + ) + customparser.add_argument( + "-fw", + "--firmware", + dest="firmware", + action="store_true", + help="Add firmware information to the output.", + default=False, + ) + customparser.add_argument( + "-sw", + "--software", + dest="software", + action="store_true", + help="Add software information to the output.", + default=False, + ) + customparser.add_argument( + "-memory", + "--memory", + dest="memory", + action="store_true", + help="Add memory DIMM information to the output.", + default=False, + ) + customparser.add_argument( + "-fans", + "--fans", + dest="fans", + action="store_true", + help="Add fans information to the output.", + default=False, + ) + customparser.add_argument( + "-processors", + "--processors", + dest="processors", + action="store_true", + help="Add processor(s) information to the output.", + default=False, + ) + customparser.add_argument( + "-thermals", + "--thermals", + dest="thermals", + action="store_true", + help="Add thermal information to the output.", + default=False, + ) + customparser.add_argument( + "-power", + "--power", + dest="power", + action="store_true", + help="Add power information to the output.", + default=False, + ) + customparser.add_argument( + "-system", + "--system", + dest="system", + action="store_true", + help="Add basic system information to the output.", + default=False, + ) + customparser.add_argument( + "--showabsent", + dest="showabsent", + action="store_true", + help="Include information on absent components in the output.", + default=False, + ) + customparser.add_argument( + "-proxy", + "--proxy", + dest="proxy", + action="store_true", + help="Add proxy information to the output", + default=False, + ) + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerStateCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerStateCommand.py new file mode 100644 index 0000000..ea40b7a --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerStateCommand.py @@ -0,0 +1,106 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Server State Command for rdmc""" + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class ServerStateCommand: + """Returns the current state of the server that is currently logged in""" + + def __init__(self): + self.ident = { + "name": "serverstate", + "usage": None, + "description": "Returns the current state of the" " server\n\n\tExample: serverstate", + "summary": "Returns the current state of the server.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main serverstate function + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError( + "Invalid number of parameters, " "serverstate command does not take any parameters." + ) + + self.serverstatevalidation(options) + + path = self.rdmc.app.typepath.defs.systempath + results = self.rdmc.app.get_handler(path, silent=True, uncache=True).dict + + if results: + results = results["Oem"][self.rdmc.app.typepath.defs.oemhp]["PostState"] + self.rdmc.ui.printer("The server is currently in state: " + results + "\n") + else: + raise NoContentsFoundForOperationError("Unable to retrieve server state") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def serverstatevalidation(self, options): + """Server state method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerlogsCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerlogsCommand.py new file mode 100644 index 0000000..0b10d42 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/ServerlogsCommand.py @@ -0,0 +1,1480 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Log Operations Command for rdmc""" + +import ctypes +import datetime +import itertools +import json +import os +import platform +import shlex +import string +import subprocess +import sys +import tempfile +import time + +from six.moves import queue + +import redfish.hpilo.risblobstore2 as risblobstore2 +from redfish.rest.connections import SecurityStateError +from redfish.ris.utils import filter_output + +try: + from rdmc_helper import ( + LOGGER, + UI, + IncompatibleiLOVersionError, + InvalidCListFileError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + InvalidKeyError, + InvalidMSCfileInputError, + MultipleServerConfigError, + NoContentsFoundForOperationError, + PartitionMoutingError, + ReturnCodes, + UnabletoFindDriveError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + LOGGER, + UI, + IncompatibleiLOVersionError, + InvalidCListFileError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + InvalidKeyError, + InvalidMSCfileInputError, + MultipleServerConfigError, + NoContentsFoundForOperationError, + PartitionMoutingError, + ReturnCodes, + UnabletoFindDriveError, + ) + +if os.name == "nt": + import win32api +elif sys.platform != "darwin" and "VMkernel" not in platform.uname(): + import pyudev + + +class ServerlogsCommand: + """Download logs from the server that is currently logged in""" + + def __init__(self): + self.ident = { + "name": "serverlogs", + "usage": None, + "description": "Download the AHS" + " logs from the logged in server.\n\texample: serverlogs " + "--selectlog=AHS \n\n\tClear the AHS logs " + "from the logged in server.\n\texample: serverlogs " + "--selectlog=AHS --clearlog\n\n\tDownload the IEL" + " logs from the logged in server.\n\texample: serverlogs " + "--selectlog=IEL -f IELlog.txt\n\n\tClear the IEL logs " + "from the logged in server.\n\texample: serverlogs " + "--selectlog=IEL --clearlog\n\n\tDownload the IML" + " logs from the logged in server.\n\texample: serverlogs " + "--selectlog=IML -f IMLlog.txt\n\n\tClear the IML logs " + "from the logged in server.\n\texample: serverlogs " + "--selectlog=IML --clearlog\n\n\t(IML LOGS ONLY FEATURE)" + "\n\tInsert entry in the IML logs from the logged in " + 'server.\n\texample: serverlogs --selectlog=IML -m "Text' + ' message for maintenance"\n\n\tDownload the iLO Security' + " logs from the logged in server.\n\texample: serverlogs " + "--selectlog=SL -f SLlog.txt\n\n\tClear the iLO Security logs " + "from the logged in server.\n\texample: serverlogs " + "--selectlog=SL --clearlog\n\n\t" + "Download logs from multiple servers\n\t" + "example: serverlogs --mpfile mpfilename.txt -o output" + "directorypath --mplog=IEL,IML\n\t" + "Note: multiple server file(mpfilename.txt) " + "format (1 server per new line)\n\t" + "--url -u admin -p password\n\t" + "--url -u admin -p password\n\t" + "--url -u admin -p password\n\n\t" + "Insert customised string " + "if required for AHS\n\texample: serverlogs --selectlog=" + 'AHS --customiseAHS "from=2014-03-01&&to=2014' + '-03-30"\n\n\t(AHS LOGS ONLY FEATURE)\n\tInsert the location/' + "path of directory where AHS log needs to be saved." + " \n\texample: serverlogs --selectlog=AHS " + "--directorypath=C:\\Python38\\DataFiles\n\n\tRepair IML log." + "\n\texample: serverlogs --selectlog=IML --repair IMLlogID", + "summary": "Download and perform log operations.", + "aliases": ["logservices"], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self.dontunmount = None + self.queue = queue.Queue() + self.abspath = None + self.lib = None + + def run(self, line, help_disp=False): + """Main serverlogs function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if not getattr(options, "sessionid", False): + self.serverlogsvalidation(options) + + if options.mpfilename: + self.rdmc.ui.printer("Downloading logs for multiple servers...\n") + return self.gotompfunc(options) + + self.serverlogsworkerfunction(options) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def serverlogsworkerfunction(self, options): + """ "Main worker function outlining the process + + :param options: command line options + :type options: list. + """ + ilover = self.rdmc.app.getiloversion() + if not options.service: + raise InvalidCommandLineError("Please select a log type using the --selectlog option.") + + if options.service.lower() == "iml": + path = self.returnimlpath(options=options) + elif options.service.lower() == "iel": + path = self.returnielpath(options=options) + elif options.service.lower() == "sl": + path = self.returnslpath(options=options) + elif options.service.lower() == "ahs" and options.filter: + raise InvalidCommandLineError("Cannot filter AHS logs.") + elif ( + options.service.lower() == "ahs" + and (not self.rdmc.app.typepath.url or self.rdmc.app.typepath.url.startswith("blobstore")) + and not options.clearlog + and ilover < 7 + ): + if options.customiseAHS is not None: + current_date = str(datetime.datetime.now()).split()[0] + date_check = options.customiseAHS + from_date = date_check.split("&")[0].split("=")[1] + to_date = date_check.split("to")[-1].split("=")[-1] + if to_date < from_date or from_date > current_date: + raise InvalidCommandLineError("Please provide valid date to customiseAHS") + self.downloadahslocally(options=options) + return + elif options.service.lower() == "ahs": + if ilover >= 7 and self.rdmc.app.typepath.url.startswith("blobstore"): + raise InvalidCommandLineError( + "Download of AHS is not supported via chif in iLO7.\n" "Kindly re-login with VNIC and re execute.\n" + ) + else: + path = self.returnahspath(options) + else: + raise InvalidCommandLineError("Log opted does not exist!") + + data = None + + if options.clearlog: + self.clearlog(path) + elif options.mainmes: + self.addmaintenancelogentry(options, path=path) + elif options.repiml: + self.repairlogentry(options, path=path) + else: + data = self.downloaddata(path=path, options=options) + + self.savedata(options=options, data=data) + + def gotompfunc(self, options): + """ "Function to download logs from multiple servers concurrently + + :param options: command line options + :type options: list. + """ + if options.mpfilename: + mfile = options.mpfilename + outputdir = None + + if options.outdirectory: + outputdir = options.outdirectory + + if self.runmpfunc(mpfile=mfile, outputdir=outputdir, options=options): + return ReturnCodes.SUCCESS + else: + raise MultipleServerConfigError("One or more servers failed to download logs.") + + def runmpfunc(self, mpfile=None, outputdir=None, options=None): + """Main worker function for multi file command + + :param mpfile: configuration file + :type mpfile: string. + :param outputdir: custom output directory + :type outputdir: string. + """ + # self.logoutobj.run("") + LOGGER.info("Validating input server collection file.") + data = self.validatempfile(mpfile=mpfile, options=options) + + if not data: + return False + + processes = [] + finalreturncode = True + outputform = "%Y-%m-%d-%H-%M-%S" + + if outputdir: + if outputdir.endswith(('"', "'")) and outputdir.startswith(('"', "'")): + outputdir = outputdir[1:-1] + + if not os.path.isdir(outputdir): + raise InvalidCommandLineError("The given output folder path does not exist.") + + dirpath = outputdir + else: + dirpath = os.getcwd() + + dirname = "%s_%s" % ( + datetime.datetime.now().strftime(outputform), + "MSClogs", + ) + createdir = os.path.join(dirpath, dirname) + os.mkdir(createdir) + + oofile = open(os.path.join(createdir, "CompleteOutputfile.txt"), "w+") + self.rdmc.ui.printer("Creating multiple processes to load configuration " "concurrently to all servers...\n") + + while True: + if not self.queue.empty(): + line = self.queue.get() + else: + break + + finput = "\n" + "Output for " + line[line.index("--url") + 1] + ": \n\n" + urlvar = line[line.index("--url") + 1] + urlfilename = urlvar.split("//")[-1] + line[line.index("-f") + 1] = str(line[line.index("-f") + 1]) + urlfilename + + if "python" in os.path.basename(sys.executable.lower()): + # If we are running from source + # we have to add the python file to the command + listargforsubprocess = [sys.executable, sys.argv[0]] + line + else: + listargforsubprocess = [sys.executable] + line + + if os.name != "nt": + listargforsubprocess = " ".join(listargforsubprocess) + + logfile = open(os.path.join(createdir, urlfilename + ".txt"), "w+") + pinput = subprocess.Popen( + listargforsubprocess, + shell=True, + stdout=logfile, + stderr=logfile, + ) + + processes.append((pinput, finput, urlvar, logfile)) + + for pinput, finput, urlvar, logfile in processes: + pinput.wait() + returncode = pinput.returncode + finalreturncode = finalreturncode and not returncode + + logfile.close() + logfile = open(os.path.join(createdir, urlvar + ".txt"), "r+") + oofile.write(finput + str(logfile.read())) + oofile.write("-x+x-" * 16) + logfile.close() + + if returncode == 0: + self.rdmc.ui.printer("Loading Configuration for {} : SUCCESS\n".format(urlvar)) + else: + self.rdmc.ui.error("Loading Configuration for {} : FAILED\n".format(urlvar)) + self.rdmc.ui.error( + "ILOREST return code : {}.\n" + "For more details please check {}" + ".txt under {} directory.\n".format(returncode, urlvar, createdir) + ) + + oofile.close() + + if finalreturncode: + self.rdmc.ui.printer("All servers have been successfully configured.\n") + + return finalreturncode + + def validatempfile(self, mpfile=None, options=None): + """Validate temporary file + + :param mpfile: configuration file + :type mpfile: string. + :param lfile: custom file name + :type lfile: string. + """ + self.rdmc.ui.printer("Checking given server information...\n") + + if not mpfile: + return False + elif mpfile.startswith(('"', "'")) and mpfile[0] == mpfile[-1]: + mpfile = mpfile[1:-1] + + if not os.path.isfile(mpfile) or not options.mplog: + raise InvalidFileInputError( + "File '%s' doesn't exist, please " "create file by running save command." % mpfile + ) + + logs = self.checkmplog(options) + + try: + with open(mpfile, "r") as myfile: + data = list() + cmdtorun = ["serverlogs"] + globalargs = ["-v", "--nocache"] + + while True: + line = myfile.readline() + if not line: + break + + for logval in logs: + cmdargs = [ + "--selectlog=" + str(logval), + "-f", + str(logval), + ] + if line.endswith(os.linesep): + line.rstrip(os.linesep) + + args = shlex.split(line, posix=False) + + if len(args) < 6: + self.rdmc.ui.printer( + "\nInput file format should be " + "1 server per new line \n" + "For Example: " + "--url -u -p \n\n" + ) + self.rdmc.ui.error("Incomplete data in input file: {}".format(line)) + raise InvalidMSCfileInputError("Please verify the " "contents of the %s file" % mpfile) + else: + linelist = globalargs + cmdtorun + args + cmdargs + line = str(line).replace("\n", "") + self.queue.put(linelist) + data.append(linelist) + except Exception as excp: + LOGGER.info("%s", str(excp)) + raise excp + + if data: + return data + + return False + + def checkmplog(self, options): + """Function to validate mplogs options + + :param options: command line options + :type options: list. + """ + if options.mplog: + logs = str(options.mplog) + if "," in logs: + logs = logs.split(",") + return logs + if logs in ("all", "IEL", "IML", "AHS"): + if logs == "all": + logs = ["IEL", "IML", "AHS"] + else: + logs = [logs] + return logs + raise InvalidCommandLineError("Error in mplogs options") + + def addmaintenancelogentry(self, options, path=None): + """Worker function to add maintenance log + + :param options: command line options + :type options: list. + :param path: path to post maintenance log + :type path: str + """ + LOGGER.info("Adding maintenance logs") + if options.mainmes is None: + raise InvalidCommandLineError("") + + if options.service != "IML": + raise InvalidCommandLineError("Log opted cannot make maintenance entries!") + + message = options.mainmes + + if message.endswith(('"', "'")) and message.startswith(('"', "'")): + message = message[1:-1] + + if path: + bodydict = dict() + bodydict["path"] = path + bodydict["body"] = {"EntryCode": "Maintenance", "Message": message} + + LOGGER.info( + "Writing maintenance post to %s with %s", + str(path), + str(bodydict["body"]), + ) + + self.rdmc.app.post_handler(path, bodydict["body"]) + + def repairlogentry(self, options, path=None): + """Worker function to repair maintenance log + + :param options: command line options + :type options: list. + :param path: path of IML log Entries + :type path: str + """ + if options.repiml is None or (options.clearlog and options.repiml): + raise InvalidCommandLineError("") + + if options.service != "IML": + raise InvalidCommandLineError("Log opted cannot repair maintenance entries.") + + imlid = options.repiml + + if imlid.endswith(('"', "'")) and imlid.startswith(('"', "'")): + imlid = imlid[1:-1] + + if path: + imlidpath = [path if path[-1] == "/" else path + "/"][0] + str(imlid) + "/" + bodydict = dict() + bodydict["path"] = imlidpath + if self.rdmc.app.typepath.defs.isgen9: + bodydict["body"] = {"Oem": {"Hp": {"Repaired": True}}} + else: + bodydict["body"] = {"Oem": {"Hpe": {"Repaired": True}}} + LOGGER.info( + "Repairing maintenance log at %s with %s", + str(imlidpath), + str(bodydict["body"]), + ) + + self.rdmc.app.patch_handler(imlidpath, bodydict["body"]) + + def clearlog(self, path): + """Worker function to clear logs. + + :param path: path to post clear log action + :type path: str + """ + LOGGER.info("Clearing logs.") + if path and self.rdmc.app.typepath.defs.isgen9: + if path.endswith("/Entries"): + path = path[: -len("/Entries")] + elif path.endswith("Entries/"): + path = path[: -len("Entries/")] + + bodydict = dict() + bodydict["path"] = path + bodydict["body"] = {"Action": "ClearLog"} + self.rdmc.app.post_handler(path, bodydict["body"]) + elif path: + action = path.split("/")[-2] + bodydict = dict() + bodydict["path"] = path + bodydict["body"] = {"Action": action} + self.rdmc.app.post_handler(path, bodydict["body"]) + + def downloaddata(self, path=None, options=None): + """Worker function to download the log files + + :param options: command line options + :type options: list. + :param path: path to download logs + :type path: str + """ + if path: + LOGGER.info("Getting data from %s", str(path)) + if options.service == "AHS": + data = self.rdmc.app.get_handler(path, silent=True, uncache=True) + if data: + return data.ori + else: + raise NoContentsFoundForOperationError("Unable to retrieve AHS logs.") + else: + data = self.rdmc.app.get_handler(path, silent=True) + + datadict = data.dict + + try: + completedatadictlist = datadict["Items"] if "Items" in datadict else datadict["Members"] + except Exception: + self.rdmc.ui.error("No data available within log.\n") + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + + if self.rdmc.app.typepath.defs.flagforrest: + morepages = True + + while morepages: + if "links" in datadict and "NextPage" in datadict["links"]: + next_link_uri = path + "?page=" + str(datadict["links"]["NextPage"]["page"]) + + href = "%s" % next_link_uri + data = self.rdmc.app.get_handler(href, silent=True) + datadict = data.dict + + try: + completedatadictlist = completedatadictlist + datadict["Items"] + except Exception: + self.rdmc.ui.error("No data available within log.\n") + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + else: + morepages = False + else: + datadict = list() + + for members in completedatadictlist: + if len(list(members.keys())) == 1: + memberpath = members[self.rdmc.app.typepath.defs.hrefstring] + data = self.rdmc.app.get_handler(memberpath, silent=True) + datadict = datadict + [data.dict] + else: + datadict = datadict + [members] + completedatadictlist = datadict + + if completedatadictlist: + try: + return completedatadictlist + except Exception: + self.rdmc.ui.error("Could not get the data from server.\n") + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + else: + self.rdmc.ui.error("No log data present.\n") + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + else: + self.rdmc.ui.error("Path not found for input log.\n") + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + + def returnimlpath(self, options=None): + """Return the requested path of the IML logs + + :param options: command line options + :type options: list. + """ + LOGGER.info("Obtaining IML path for download.") + path = "" + val = "/redfish/v1/Systems/1/LogServices/IML/" + filtereddatainstance = self.rdmc.app.get_handler(val, silent=True).dict + + try: + filtereddictslists = filtereddatainstance + if not filtereddictslists: + raise NoContentsFoundForOperationError("Unable to retrieve instance.") + # for filtereddict in filtereddictslists: + if filtereddictslists["Name"] == "Integrated Management Log": + if options.clearlog: + if self.rdmc.app.typepath.defs.flagforrest: + linkpath = filtereddictslists["links"] + selfpath = linkpath["self"] + path = selfpath["href"] + elif self.rdmc.app.typepath.defs.isgen9: + path = filtereddictslists[self.rdmc.app.typepath.defs.hrefstring] + else: + actiondict = filtereddictslists["Actions"] + clearkey = [x for x in actiondict if x.endswith("ClearLog")] + path = actiondict[clearkey[0]]["target"] + else: + linkpath = filtereddictslists["links"] if "links" in filtereddictslists else filtereddictslists + dictpath = linkpath["Entries"] + dictpath = dictpath[0] if isinstance(dictpath, list) else dictpath + path = dictpath[self.rdmc.app.typepath.defs.hrefstring] + if not path: + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + except NoContentsFoundForOperationError as excp: + raise excp + except Exception: + self.rdmc.ui.error("No path found for the entry.\n") + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + + if self.rdmc.opts.verbose: + self.rdmc.ui.printer(str(path) + "\n") + + return path + + def returnielpath(self, options=None): + """Return the requested path of the IEL logs + + :param options: command line options + :type options: list. + """ + LOGGER.info("Obtaining IEL path for download.") + path = "" + val = "/redfish/v1/Managers/1/LogServices/IEL/" + filtereddatainstance = self.rdmc.app.get_handler(val, silent=True).dict + + try: + filtereddictslists = filtereddatainstance + if not filtereddictslists: + raise NoContentsFoundForOperationError("Unable to retrieve instance.") + # for filtereddict in filtereddictslists: + if filtereddictslists["Name"] == "iLO Event Log": + if options.clearlog: + if self.rdmc.app.typepath.defs.flagforrest: + linkpath = filtereddictslists["links"] + selfpath = linkpath["self"] + path = selfpath["href"] + elif self.rdmc.app.typepath.defs.isgen9: + path = filtereddictslists[self.rdmc.app.typepath.defs.hrefstring] + else: + actiondict = filtereddictslists["Actions"] + clearkey = [x for x in actiondict if x.endswith("ClearLog")] + path = actiondict[clearkey[0]]["target"] + else: + linkpath = filtereddictslists["links"] if "links" in filtereddictslists else filtereddictslists + dictpath = linkpath["Entries"] + dictpath = dictpath[0] if isinstance(dictpath, list) else dictpath + path = dictpath[self.rdmc.app.typepath.defs.hrefstring] + + if not path: + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + except NoContentsFoundForOperationError as excp: + raise excp + except Exception: + self.rdmc.ui.error("No path found for the entry.\n") + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + + if self.rdmc.opts.verbose: + self.rdmc.ui.printer(str(path) + "\n") + + return path + + def returnslpath(self, options=None): + """Return the requested path of the SL logs + + :param options: command line options + :type options: list. + """ + + if not self.rdmc.app.typepath.defs.isgen10 or not self.rdmc.app.getiloversion() >= 5.210: + raise IncompatibleiLOVersionError("Security logs are only available on iLO 5 2.10 or" " greater.") + LOGGER.info("Obtaining SL path for download.") + path = "" + val = "/redfish/v1/Systems/1/LogServices/SL/" + filtereddatainstance = self.rdmc.app.get_handler(val, silent=True).dict + + try: + filtereddictslists = filtereddatainstance + if not filtereddictslists: + raise NoContentsFoundForOperationError("Unable to retrieve instance.") + # for filtereddict in filtereddictslists: + if filtereddictslists["Id"] == "SL": + if options.clearlog: + actiondict = filtereddictslists["Actions"] + clearkey = [x for x in actiondict if x.endswith("ClearLog")] + path = actiondict[clearkey[0]]["target"] + else: + dictpath = filtereddictslists["Entries"] + dictpath = dictpath[0] if isinstance(dictpath, list) else dictpath + path = dictpath[self.rdmc.app.typepath.defs.hrefstring] + + if not path: + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + except NoContentsFoundForOperationError as excp: + raise excp + except Exception: + self.rdmc.ui.error("No path found for the entry.\n") + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + + if self.rdmc.opts.verbose: + self.rdmc.ui.printer(str(path) + "\n") + + return path + + def returnahspath(self, options): + """Return the requested path of the AHS logs + + :param options: command line options + :type options: list. + """ + LOGGER.info("Obtaining AHS path for download.") + path = "" + + if options.filename: + raise InvalidCommandLineError( + "AHS logs must be downloaded with " "default name. Please re-run command without filename option." + ) + + val = "/redfish/v1/Managers/1/ActiveHealthSystem/" + filtereddatainstance = self.rdmc.app.get_handler(val, silent=True).dict + + try: + filtereddictslists = filtereddatainstance + + if not filtereddictslists: + raise NoContentsFoundForOperationError("Unable to retrieve log instance.") + + # for filtereddict in filtereddictslists: + if options.clearlog: + if self.rdmc.app.typepath.defs.flagforrest: + linkpath = filtereddictslists["links"] + selfpath = linkpath["self"] + path = selfpath["href"] + elif self.rdmc.app.typepath.defs.isgen9: + path = filtereddictslists[self.rdmc.app.typepath.defs.hrefstring] + else: + actiondict = filtereddictslists["Actions"] + clearkey = [x for x in actiondict if x.endswith("ClearLog")] + path = actiondict[clearkey[0]]["target"] + else: + linkpath = filtereddictslists["links"] if "links" in filtereddictslists else filtereddictslists["Links"] + + ahslocpath = linkpath["AHSLocation"] + path = ahslocpath["extref"] + weekpath = None + + if options.downloadallahs: + path = path + elif options.customiseAHS: + custr = options.customiseAHS + if custr.startswith(("'", '"')) and custr.endswith(("'", '"')): + custr = custr[1:-1] + + if custr.startswith("from="): + path = path.split("downloadAll=1")[0] + + path = path + custr + else: + if "RecentWeek" in list(linkpath.keys()): + weekpath = linkpath["RecentWeek"]["extref"] + elif "AHSFileStart" in list(filtereddictslists.keys()): + enddate = filtereddictslists["AHSFileEnd"].split("T")[0] + startdate = filtereddictslists["AHSFileStart"].split("T")[0] + + enddat = list(map(int, enddate.split("-"))) + startdat = list(map(int, startdate.split("-"))) + + weekago = datetime.datetime.now() - datetime.timedelta(days=6) + weekagostr = list(map(int, (str(weekago).split()[0]).split("-"))) + + strdate = min( + max( + datetime.date(weekagostr[0], weekagostr[1], weekagostr[2]), + datetime.date(startdat[0], startdat[1], startdat[2]), + ), + datetime.date(enddat[0], enddat[1], enddat[2]), + ) + + aweekstr = "from=" + str(strdate) + "&&to=" + enddate + else: + week_ago = datetime.datetime.now() - datetime.timedelta(days=6) + aweekstr = ( + "from=" + str(week_ago).split()[0] + "&&to=" + str(datetime.datetime.now()).split()[0] + ) + + path = path.split("downloadAll=1")[0] + path = weekpath if weekpath else path + aweekstr + + if not path: + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + except NoContentsFoundForOperationError as excp: + raise excp + except Exception: + self.rdmc.ui.error("No path found for the entry.\n") + raise NoContentsFoundForOperationError("Unable to retrieve logs.") + + if self.rdmc.opts.verbose: + self.rdmc.ui.printer(str(path) + "\n") + + return path + + def savedata(self, options=None, data=None): + """Save logs into the specified filename + + :param options: command line options + :type options: list. + :param data: log data + :type data: dict + """ + LOGGER.info("Saving/Writing data...") + if data: + data = self.filterdata(data=data, tofilter=options.filter) + if options.service == "AHS": + filename = self.getahsfilename(options) + + with open(filename, "wb") as foutput: + foutput.write(data) + elif options.filename: + with open(options.filename[0], "w") as foutput: + if options.json: + foutput.write(str(json.dumps(data, indent=2, sort_keys=True))) + else: + foutput.write(str(json.dumps(data))) + else: + if options.json: + UI().print_out_json(data) + else: + UI().print_out_human_readable(data) + + def downloadahslocally(self, options=None): + """Download AHS logs locally + + :param options: command line options + :type options: list. + """ + try: + self.downloadahslocalworker(options) + except Exception: + self.unmountbb() + raise + return + + def downloadahslocalworker(self, options): + """Worker function to download AHS logs locally + + :param options: command line options + :type options: list. + """ + LOGGER.info("Entering AHS local download functions...") + self.dontunmount = True + + if self.rdmc.app.typepath.ilogen and self.rdmc.app.typepath.ilogen < 4: + raise IncompatibleiLOVersionError("Need at least iLO 4 for this program to run!\n") + + if sys.platform == "darwin": + raise InvalidCommandLineError("AHS loacal download is not supported on MacOS") + elif "VMkernel" in platform.uname(): + raise InvalidCommandLineError("AHS loacal download is not supported on VMWare") + + if options.filename: + raise InvalidCommandLineError( + "AHS logs must be downloaded with default name! " "Re-run command without filename!" + ) + + secstate = risblobstore2.BlobStore2().get_security_state() + # self.rdmc.ui.printer('Security State is {}...\n'.format(secstate)) + + if isinstance(secstate, bytes): + secstate = secstate.decode("utf-8") + if isinstance(secstate, str): + secstate = secstate.replace("\x00", "").strip() + if secstate == "": + secstate = 0 + self.rdmc.ui.printer("Security State is {}...\n".format(secstate)) + if int(secstate) > 3: + raise SecurityStateError("AHS logs cannot be downloaded locally " "in high security state.\n") + + self.lib = risblobstore2.BlobStore2.gethprestchifhandle() + + self.rdmc.ui.printer("Mounting AHS partition...\n") + + try: + (manual_ovr, abspath) = self.getbbabspath() + except (PartitionMoutingError, IOError): + self.mountbb() + (manual_ovr, abspath) = self.getbbabspath() + self.dontunmount = False + + LOGGER.info("Blackbox folder path:%s", ",".join(next(os.walk(abspath))[2])) + if "data" not in abspath: + self.abspath = os.path.join(abspath, "data") + LOGGER.info("Blackbox data files path:%s", self.abspath) + + if self.rdmc.app.typepath.ilogen: + self.updateiloversion() + try: + cfilelist = self.getclistfilelisting() + allfiles = self.getfilenames(options=options, cfilelist=cfilelist) + self.getdatfilelisting(cfilelist=cfilelist, allfile=allfiles) + self.createahsfile(ahsfile=self.getahsfilename(options)) + except Exception as excp: + raise PartitionMoutingError( + "An exception occurred obtaining Blackbox data files. " "The directory may be empty: %s.\n" % excp + ) + + if not manual_ovr: + self.unmountbb() + else: + self.unmountbb() + self.manualunmountbb(abspath) + + def updateiloversion(self): + """Update iloversion to create appropriate headers.""" + LOGGER.info("Updating iloversion to format data appropriately") + self.lib.updateiloversion.argtypes = [ctypes.c_float] + self.lib.updateiloversion(float("2." + str(self.rdmc.app.typepath.ilogen))) + + def createahsfile(self, ahsfile=None): + """Create the AHS file + + :param ahsfile: ahsfilename + :type ahsfile: str + """ + LOGGER.info("Creating AHS file from the formatted data.") + self.clearahsfile(ahsfile=ahsfile) + self.lib.setAHSFilepath.argtypes = [ctypes.c_char_p] + self.lib.setAHSFilepath(os.path.abspath(ahsfile).encode("utf-8", "ignore")) + self.lib.setBBdatapath.argtypes = [ctypes.c_char_p] + self.lib.setBBdatapath(self.abspath.encode("utf-8", "ignore")) + self.lib.createAHSLogFile_G9() + + def clearahsfile(self, ahsfile=None): + """Clear the ahslog file if already present in filesystem + + :param ahsfile: ahsfilename + :type ahsfile: str + """ + LOGGER.info("Clear redundant AHS file in current folder.") + try: + os.remove(ahsfile) + except Exception: + pass + + def getdatfilelisting(self, cfilelist=None, allfile=None): + """Create headers based on the AHS log files within blackbox + + :param cfilelist: configuration files in blackbox + :type cfilelist: list of strings + :param allfile: all files within blackbox + :type allfile: list + """ + LOGGER.info("Reading Blackbox to determine data files.") + allfile = list(set(allfile) | set(cfilelist)) + LOGGER.info("Final filelist %s", str(allfile)) + for files in allfile: + if files.startswith((".", "..")): + continue + + bisrequiredfile = False + + if files.split(".")[0] in [x.split(".")[0] for x in cfilelist]: + if files.endswith("bb"): + bisrequiredfile = True + self.lib.updatenfileoptions() + + self.lib.gendatlisting.argtypes = [ + ctypes.c_char_p, + ctypes.c_bool, + ctypes.c_uint, + ] + + filesize = os.stat(os.path.join(self.abspath, files)).st_size + self.lib.gendatlisting(files.encode("ascii", "ignore"), bisrequiredfile, filesize) + + def getfilenames(self, options=None, cfilelist=None): + """Get all file names from the blackbox directory.""" + LOGGER.info("Obtaining all relevant file names from Blackbox.") + datelist = list() + allfiles = list() + + filenames = next(os.walk(self.abspath))[2] + timenow = (str(datetime.datetime.now()).split()[0]).split("-") + strdate = enddate = datetime.date(int(timenow[0]), int(timenow[1]), int(timenow[2])) + + if not options.downloadallahs and not options.customiseAHS: + weekago = datetime.datetime.now() - datetime.timedelta(days=6) + weekagostr = (str(weekago).split()[0]).split("-") + strdate = datetime.date(int(weekagostr[0]), int(weekagostr[1]), int(weekagostr[2])) + + if options.customiseAHS: + instring = options.customiseAHS + if instring.startswith(("'", '"')) and instring.endswith(("'", '"')): + instring = instring[1:-1] + try: + (strdatestr, enddatestr) = [e.split("-") for e in instring.split("from=")[-1].split("&&to=")] + strdate = datetime.date(int(strdatestr[0]), int(strdatestr[1]), int(strdatestr[2])) + enddate = datetime.date(int(enddatestr[0]), int(enddatestr[1]), int(enddatestr[2])) + except Exception as excp: + LOGGER.warning(excp) + raise InvalidCommandLineError("Cannot parse customised AHSinput.") + + atleastonefile = False + for files in list(filenames): + if not files.endswith("bb"): + # Check the logic for the non bb files + allfiles.append(files) + continue + + if options.downloadallahs: + atleastonefile = True + if files in ("ilo_boot_support.zbb", "sys_boot_support.zbb"): + allfiles.append(files) + filenames.append(files) + LOGGER.info("%s, number of files%s", files, len(filenames)) + continue + filenoext = files.rsplit(".", 1)[0] + filesplit = filenoext.split("-") + + try: + newdate = datetime.date(int(filesplit[1]), int(filesplit[2]), int(filesplit[3])) + datelist.append(newdate) + + if (strdate <= newdate) and (newdate <= enddate) and not options.downloadallahs: + allfiles.append(files) + atleastonefile = True + except Exception: + pass + + _ = [cfilelist.remove(fil) for fil in list(cfilelist) if fil not in filenames] + + if options.customiseAHS: + for files in reversed(cfilelist): + try: + filenoext = files.rsplit(".", 1)[0] + filesplit = filenoext.split("-") + newdate = datetime.date(int(filesplit[1]), int(filesplit[2]), int(filesplit[3])) + if not ((strdate <= newdate) and (newdate <= enddate)): + cfilelist.remove(files) + except Exception: + pass + + if options.downloadallahs: + strdate = min(datelist) if datelist else strdate + enddate = max(datelist) if datelist else enddate + else: + strdate = max(min(datelist), strdate) if datelist else strdate + enddate = min(max(datelist), enddate) if datelist else enddate + + LOGGER.info( + "All filenames: %s; Download files: %s", + str(filenames), + str(allfiles), + ) + + if atleastonefile: + self.updateminmaxdate(strdate=strdate, enddate=enddate) + return filenames if options.downloadallahs else allfiles + else: + raise NoContentsFoundForOperationError("No AHS log files found.") + + def updateminmaxdate(self, strdate=None, enddate=None): + """Get the minimum and maximum date of files into header + + :param strdate: starting date of ahs logs + :type strdate: dateime obj + :param enddate: ending date of ahs logs + :type enddate: datetime obj + """ + LOGGER.info("Updating min and max dates for download.") + self.lib.updateMinDate.argtypes = [ + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ] + self.lib.updateMinDate(strdate.year, strdate.month, strdate.day) + self.lib.updateMaxDate.argtypes = [ + ctypes.c_int, + ctypes.c_int, + ctypes.c_int, + ] + self.lib.updateMaxDate(enddate.year, enddate.month, enddate.day) + + def getclistfilelisting(self): + """Get files present within clist.pkg .""" + LOGGER.info("Getting all config files that are required.") + sclistpath = os.path.join(self.abspath, "clist.pkg") + cfilelist = [] + if os.path.isfile(sclistpath): + cfile = open(sclistpath, "rb") + data = cfile.read() + + if data == "": + raise InvalidCListFileError("Could not read Cfile\n") + + sizeofcfile = len(str(data)) + sizeofrecord = self.lib.sizeofchifbbfilecfgrecord() + count = sizeofcfile / sizeofrecord + revcount = 0 + + while count >= 1: + dat = data[revcount * sizeofrecord : (revcount + 1) * sizeofrecord] + dat = ctypes.create_string_buffer(dat) + self.lib.getbbfilecfgrecordname.argtypes = [ctypes.c_char_p] + self.lib.getbbfilecfgrecordname.restype = ctypes.c_char_p + ptrname = self.lib.getbbfilecfgrecordname(dat) + name = str(bytearray(ptrname[:32][:])) + + if name not in cfilelist: + cfilelist.append(name) + + count = count - 1 + revcount = revcount + 1 + else: + cfilelist = [f for f in os.listdir(self.abspath) if f.endswith(".zbb")] + + LOGGER.info("CLIST files %s", str(cfilelist)) + return cfilelist + + def getbbabspath(self): + """Get blackbox folder path.""" + LOGGER.info("Obtaining the absolute path of blackbox.") + count = 0 + + while count < 20: + if os.name == "nt": + drives = self.get_available_drives() + + for i in drives: + try: + label = win32api.GetVolumeInformation(i + ":")[0] + + if label == "BLACKBOX": + abspathbb = i + ":\\data\\" + self.abspath = abspathbb + cfilelist = self.getclistfilelisting() + if not cfilelist: + self.unmountbb() + self.manualmountbb() + else: + return False, abspathbb + except Exception: + pass + else: + with open("/proc/mounts", "r") as fmount: + while True: + lin = fmount.readline() + + if len(lin.strip()) == 0: + break + + if r"/BLACKBOX" in lin: + abspathbb = lin.split()[1] + self.abspath = abspathbb + cfilelist = self.getclistfilelisting() + if not cfilelist: + self.unmountbb() + self.manualmountbb() + else: + return False, abspathbb + + if count > 5: + found, path = self.manualmountbb() + if found: + return True, path + + count = count + 1 + time.sleep(5) + + raise PartitionMoutingError("iLO not responding to request " "for mounting AHS partition") + + def manualmountbb(self): + """Manually mount blackbox when after fixed time.""" + LOGGER.info("Manually mounting the blackbox.") + + try: + context = pyudev.Context() + for device in context.list_devices(subsystem="block"): + if device.get("ID_FS_LABEL") == "BLACKBOX": + dirpath = os.path.join(tempfile.gettempdir(), "BLACKBOX") + + if not os.path.exists(dirpath): + try: + os.makedirs(dirpath) + except Exception as excp: + raise excp + + pmount = subprocess.Popen( + ["mount", device.device_node, dirpath], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + _, _ = pmount.communicate() + + return True, dirpath + except UnicodeDecodeError as excp: + self.unmountbb() + raise UnabletoFindDriveError(excp) + + return False, None + + def manualunmountbb(self, dirpath): + """Manually unmount blackbox when after fixed time + + :param dirpath: mounted directory path + :type dirpath: str + """ + LOGGER.info("Manually unmounting the blackbox.") + pmount = subprocess.Popen(["umount", dirpath], stdout=subprocess.PIPE, stderr=subprocess.PIPE) + _, _ = pmount.communicate() + + def mountbb(self): + """Mount blackbox.""" + LOGGER.info("Mounting blackbox...") + bs2 = risblobstore2.BlobStore2() + bs2.mount_blackbox() + bs2.channel.close() + + def unmountbb(self): + """Unmount blacbox.""" + if not self.dontunmount: + bs2 = risblobstore2.BlobStore2() + bs2.bb_media_unmount() + bs2.channel.close() + + def get_available_drives(self): + """Obtain all drives""" + LOGGER.info("Unmounting blackbox...") + if "Windows" not in platform.system(): + return [] + + drive_bitmask = ctypes.cdll.kernel32.GetLogicalDrives() + + return list( + itertools.compress( + string.ascii_uppercase, + [ord(drive) - ord("0") for drive in bin(drive_bitmask)[:1:-1]], + ) + ) + + def filterdata(self, data=None, tofilter=None): + """Filter the logs + + :param data: log data + :type data: dict + :param tofilter: command line filter option + :type tofilter: str + """ + LOGGER.info("Filtering logs based on requsted options.") + if tofilter and data: + try: + if (str(tofilter)[0] == str(tofilter)[-1]) and str(tofilter).startswith(("'", '"')): + tofilter = tofilter[1:-1] + + (sel, val) = tofilter.split("=") + sel = sel.strip() + val = val.strip() + + if val.lower() == "true" or val.lower() == "false": + val = val.lower() in ("yes", "true", "t", "1") + except Exception: + raise InvalidCommandLineError("Invalid filter" " parameter format [filter_attribute]=[filter_value]") + + # Severity inside Oem/Hpe should be filtered + if sel == "Severity": + if not self.rdmc.app.typepath.defs.isgen9: + sel = "Oem/Hpe/" + sel + else: + sel = "/" + sel + data = filter_output(data, sel, val) + if not data: + raise NoContentsFoundForOperationError("Filter returned no matches.") + + return data + + def getahsfilename(self, options): + """Create a default name if no ahsfilename is passed + + :param options: command line options + :type options: list. + """ + LOGGER.info("Obtaining Serialnumber from iLO for AHS filename.") + + if not options.sessionid: + val = "ComputerSystem." + filtereddatainstance = self.rdmc.app.select(selector=val) + snum = None + + try: + filtereddictslists = [x.resp.dict for x in filtereddatainstance] + + if not filtereddictslists: + raise NoContentsFoundForOperationError("") + except Exception: + try: + resp = self.rdmc.app.get_handler( + self.rdmc.app.typepath.defs.systempath, + silent=True, + service=True, + uncache=True, + ) + snum = resp.dict["SerialNumber"] if resp else snum + except KeyError: + raise InvalidKeyError( + "Unable to find key SerialNumber, please check path %s" % self.rdmc.app.typepath.defs.systempath + ) + except Exception: + raise NoContentsFoundForOperationError("Unable to retrieve log instance.") + + snum = filtereddictslists[0]["SerialNumber"] if not snum else snum + else: + resp = self.rdmc.app.get_handler( + "/redfish/v1/systems/1", + sessionid=options.sessionid, + silent=True, + service=True, + uncache=True, + ) + if "SerialNumber" in resp.dict: + snum = resp.dict["SerialNumber"] if resp else snum + snum = "UNKNOWN" if snum.isspace() else snum + timenow = (str(datetime.datetime.now()).split()[0]).split("-") + todaysdate = "".join(timenow) + + resp = self.rdmc.app.get_handler( + "/redfish/v1", + sessionid=options.sessionid, + silent=True, + service=True, + uncache=True, + ) + ahsdefaultfilename = "HPE_" + snum + "_" + todaysdate + ".ahs" + if "Vendor" in resp.dict: + ahsdefaultfilename = ahsdefaultfilename.replace("HPE", resp.dict["Vendor"]) + + if options.directorypath: + dir_name = options.directorypath + dir_name = dir_name.encode("utf-8").decode("utf-8") + if not os.path.exists(dir_name): + os.makedirs(dir_name) + ahsdefaultfilename = os.path.join(dir_name, ahsdefaultfilename) + + return ahsdefaultfilename + + def serverlogsvalidation(self, options): + """Serverlogs method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag if you wish to use a different" + " filename than the default one. The default filename is" + " ilorest.json.", + action="append", + default=None, + ) + customparser.add_argument( + "--filter", + dest="filter", + help="Optionally set a filter value for a filter attribute." + " This uses the provided filter for the currently selected" + " type. Note: Use this flag to narrow down your results. For" + " example, selecting a common type might return multiple" + " objects that are all of that type. If you want to modify" + " the properties of only one of those objects, use the filter" + " flag to narrow down results based on properties." + "\t\t\t\t\t Usage: --filter [ATTRIBUTE]=[VALUE]", + default=None, + ) + customparser.add_argument( + "--selectlog", + dest="service", + help="""Read log from the given log service. Options: IML, """ """IEL or AHS.""", + default=None, + ) + customparser.add_argument( + "--clearlog", + "-c", + dest="clearlog", + action="store_true", + help="""Clears the logs for a the selected option.""", + default=None, + ) + customparser.add_argument( + "--maintenancemessage", + "-m", + dest="mainmes", + help="""Maintenance message to be inserted into the log. """ """(IML LOGS ONLY FEATURE)""", + default=None, + ) + customparser.add_argument( + "--customiseAHS", + dest="customiseAHS", + help="""Allows customised AHS log data to be downloaded.""", + default=None, + ) + customparser.add_argument( + "--downloadallahs", + dest="downloadallahs", + action="store_true", + help="""Allows complete AHS log data to be downloaded.""", + default=None, + ) + customparser.add_argument( + "--directorypath", + dest="directorypath", + help="""Directory path for the ahs file.""", + default=None, + ) + customparser.add_argument( + "--mpfile", + dest="mpfilename", + help="""use the provided filename to obtain server information.""", + default=None, + ) + customparser.add_argument( + "--outputdirectory", + dest="outdirectory", + help="""use the provided directory to """ """output data for multiple server downloads.""", + default=None, + ) + customparser.add_argument( + "--mplog", + dest="mplog", + help="""used to indicate the logs to be downloaded """ + """on multiple servers. Allowable values: IEL, IML,""" + """ AHS, all or combination of any two.""", + default=None, + ) + customparser.add_argument( + "--repair", + "-r", + dest="repiml", + help="""Repair the IML logs with the given ID.""", + default=None, + ) + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SetTwoFactorAuthenticationCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SetTwoFactorAuthenticationCommand.py new file mode 100644 index 0000000..d4e6aec --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SetTwoFactorAuthenticationCommand.py @@ -0,0 +1,378 @@ +### +# Copyright 2021-2023 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### +# -*- coding: utf-8 -*- +"""TwoFactorAuthentication Command for rdmc""" + +from argparse import RawDescriptionHelpFormatter + +from redfish.ris import IdTokenError +from redfish.ris.rmc_helper import IloResponseError + +try: + from rdmc_helper import ( + IloLicenseError, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoCurrentSessionEstablished, + ReturnCodes, + TfaEnablePreRequisiteError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IloLicenseError, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoCurrentSessionEstablished, + ReturnCodes, + TfaEnablePreRequisiteError, + ) + +from redfish.ris.ris import SessionExpired + + +class SetTwoFactorAuthenticationCommand: + """Main new command template class""" + + def __init__(self): + self.ident = { + "name": "settwofactorauthentication", + "usage": "settwofactorauthentication\n\n", + "description": "Run to enable ,disable ,get status or setup smtp settings for Two factor " + "authentication\n\t" + "Example:\n\tsettwofactorauthentication enable or \n\t" + "settwofactorauthentication disable \n\t" + "settwofactorauthentication status or \n\t" + "settwofactorauthentication status -j\n" + "settwofactorauthentication smtp --smtpfortfaenabled true --alertmailsenderdomain 'test@hpe.com' " + "--alertmailsmtpserver 'smtp3.hpe.com' --alertmailsmtpport 587\n", + "summary": "Enables the server to use Two factor authentication, monitored", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def twofactorauth(self, options): + """two authentication function + + :param enable or disable + """ + + # pre requisite checks + + path = self.rdmc.app.typepath.defs.managerpath + "NetworkProtocol" + resp = self.rdmc.app.get_handler(path, service=False, silent=True) + + status = resp.dict["Oem"]["Hpe"]["SMTPForTFAEnabled"] + if not status: + self.rdmc.ui.error("SMTP for TFA is not enabled\n") + raise TfaEnablePreRequisiteError( + "\nTo be able to enable or disable TFA\nKindly enable and set the SMTP details prior to enabling TFA " + "for the server . You can use our command as given below for enabling smtp for tfa \n\n" + "SetTwoFactorAuthentication smtp --smtpfortfaenabled true --alertmailsenderdomain 'testuser@test.com' " + "--alertmailsmtpserver 'smtp.server.com' \n" + ) + + results = self.rdmc.app.select(selector="AccountService.", path_refresh=True)[0].dict + path = results[self.rdmc.app.typepath.defs.hrefstring] + + # check whether default schema is selected authentication tyep + resp = self.rdmc.app.get_handler(path, service=False, silent=True) + if not resp.dict["LDAP"]["ServiceEnabled"]: + self.rdmc.ui.error("Ldap service is not enabled\n") + raise TfaEnablePreRequisiteError( + "\nTo be able to enable or disable TFA\nKindly enable LDAP Directory Authentication to use Directory " + "Default Schema. Same can be set using below command\n\ndirectory ldap enable " + "--authentication=DefaultSchema\n" + ) + + body = dict() + if options.command.lower() == "enable": + serviceEnabled = "Enabled" + else: + serviceEnabled = "Disabled" + body.update({"Oem": {"Hpe": {"TwoFactorAuth": serviceEnabled}}}) + + try: + self.rdmc.app.patch_handler(path, body) + return ReturnCodes.SUCCESS + except IloLicenseError: + self.rdmc.ui.error("Error Occured while enable or disable operation for TFA") + return ReturnCodes.ILO_LICENSE_ERROR + except IdTokenError: + self.rdmc.ui.error("Insufficient Privilege to enable or disable TFA ") + return ReturnCodes.RIS_MISSING_ID_TOKEN + except IloResponseError: + self.rdmc.ui.error("Provided values are invalid , iLO threw error") + return ReturnCodes.RIS_ILO_RESPONSE_ERROR + + def twofactorauth_smtp(self, options): + """two authentication smtp setup function + + :param smtpfortfaenabled : true\false + :type smtpfortfaenabled: str. + """ + path = self.rdmc.app.typepath.defs.managerpath + "NetworkProtocol" + resp_get = self.rdmc.app.get_handler(path, service=False, silent=True) + body = dict() + range1 = range(1, 65536, 1) + if options.SMTPPort is not None: + if options.SMTPPort in range1: + SMTPPort = options.SMTPPort + else: + raise InvalidCommandLineError("Enter correct server port value in the range 1 to 65535") + else: + SMTPPort = 25 + + if options.SMTPForTFA[0].lower() == "true": + SMTPForTFAEnabled = True + else: + SMTPForTFAEnabled = False + + if options.SenderDomain is None: + if resp_get.dict["Oem"]["Hpe"]["AlertMailSenderDomain"] is not None: + SenderDomain = resp_get.dict["Oem"]["Hpe"]["AlertMailSenderDomain"] + else: + SenderDomain = "" + else: + SenderDomain = options.SenderDomain[0] + + if options.SMTPServer is None: + if resp_get.dict["Oem"]["Hpe"]["AlertMailSMTPServer"] is not None: + SMTPServer = resp_get.dict["Oem"]["Hpe"]["AlertMailSMTPServer"] + else: + SMTPServer = "" + else: + SMTPServer = options.SMTPServer[0] + + body.update( + { + "Oem": { + "Hpe": { + "SMTPForTFAEnabled": SMTPForTFAEnabled, + "AlertMailSenderDomain": SenderDomain, + "AlertMailSMTPServer": SMTPServer, + "AlertMailSMTPPort": SMTPPort, + } + } + } + ) + try: + self.rdmc.app.patch_handler(path, body) + return ReturnCodes.SUCCESS + + except IloResponseError: + self.rdmc.ui.error("Provided values are invalid , iLO threw error") + return ReturnCodes.RIS_ILO_RESPONSE_ERROR + except IloLicenseError: + self.rdmc.ui.error("Error Occured while setting smtp settings of TFA ") + return ReturnCodes.ILO_LICENSE_ERROR + except IdTokenError: + self.rdmc.ui.error("Insufficient Privilege to setting smtp settings of TFA ") + return ReturnCodes.RIS_MISSING_ID_TOKEN + + def twofactorauth_status(self, json=False): + """two authentication status function + + :param json: json + :type json: bool + """ + results = self.rdmc.app.select(selector="AccountService.", path_refresh=True)[0].dict + path = results[self.rdmc.app.typepath.defs.hrefstring] + resp = self.rdmc.app.get_handler(path, service=False, silent=True) + if resp.status != 200: + self.rdmc.ui.error("session seems to be expired\n") + raise SessionExpired("Invalid session. Please logout and log back in or include credentials.") + tfa_info = resp.dict["Oem"]["Hpe"] + output = "------------------------------------------------\n" + output += "TFA Status : %s\n" % (tfa_info["TwoFactorAuth"]) + if not json: + self.rdmc.ui.printer(output, verbose_override=True) + else: + self.rdmc.ui.printer("\n--- TFA status ---\n") + self.rdmc.ui.print_out_json(tfa_info) + + path = self.rdmc.app.typepath.defs.managerpath + "NetworkProtocol" + resp = self.rdmc.app.get_handler(path, service=False, silent=True) + if resp.status != 200: + raise SessionExpired("Invalid session. Please logout and log back in or include credentials.") + smtp_info = resp.dict["Oem"]["Hpe"] + output = "\n------------------------------------------------\n" + output += "SMTP for TFA Status : %s\n" % (smtp_info["SMTPForTFAEnabled"]) + if not json: + self.rdmc.ui.printer(output, verbose_override=True) + else: + self.rdmc.ui.printer("\n--- SMTP for TFA status ---\n") + self.rdmc.ui.print_out_json(smtp_info) + + return ReturnCodes.SUCCESS + + def run(self, line, help_disp=False): + """Wrapper function for two factor authentication main function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.cmdbase.login_select_validation(self, options) + if not self.rdmc.app.redfishinst: + raise NoCurrentSessionEstablished("Please login to iLO and retry the command") + + ilo_ver = self.rdmc.app.getiloversion() + if ilo_ver < 5.290 or (ilo_ver < 6.150 and ilo_ver > 5.999): + raise IncompatibleiLOVersionError( + "TFA Feature is only available with iLO 5 version 2.90 or higher and iLO 6 version 1.50 or higher.\n" + ) + + # validation checks + self.twofactorauthenticationvalidation(options) + if options.command: + if options.command.lower() == "enable": + returncode = self.twofactorauth(options) + elif options.command.lower() == "disable": + returncode = self.twofactorauth(options) + elif options.command.lower() == "smtp": + returncode = self.twofactorauth_smtp(options) + elif options.command.lower() == "status": + if options.json: + returncode = self.twofactorauth_status(json=True) + else: + returncode = self.twofactorauth_status() + else: + raise InvalidCommandLineError("%s is not a valid option for this " "command." % str(options.command)) + else: + raise InvalidCommandLineError( + "Please provide either enable, disable , status or smtp as additional subcommand. " + "For help or usage related information, use -h or --help" + ) + # logout routine + self.cmdbase.logout_routine(self, options) + # Return code + return returncode + + def twofactorauthenticationvalidation(self, options): + """new command method validation function""" + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + enable_help = "To enable two factor authentication\n" + # connect sub-parser + enable_parser = subcommand_parser.add_parser( + "enable", + help=enable_help, + description=enable_help + "\n\tExample:\n\tsettwofactorauthentication enable ", + formatter_class=RawDescriptionHelpFormatter, + ) + + self.cmdbase.add_login_arguments_group(enable_parser) + + status_help = "To check the settwofactorauthentication status\n" + status_parser = subcommand_parser.add_parser( + "status", + help=status_help, + description=status_help + "\n\tExample:\n\tsettwofactorauthentication status or " + "\n\tsettwofactorauthentication status -j", + formatter_class=RawDescriptionHelpFormatter, + ) + status_parser.add_argument( + "-j", + "--json", + dest="json", + help="to print in json format", + action="store_true", + default=False, + ) + self.cmdbase.add_login_arguments_group(status_parser) + + disable_help = "To disable the two factor authentication\n" + disable_parser = subcommand_parser.add_parser( + "disable", + help=disable_help, + description=disable_help + "\n\tExample:\n\tsettwofactorauthentication disable", + formatter_class=RawDescriptionHelpFormatter, + ) + self.cmdbase.add_login_arguments_group(disable_parser) + + emailconfig_help = "To set the smtp setting for settwofactorauthentication\n" + emailconfig_parser = subcommand_parser.add_parser( + "smtp", + help=emailconfig_help, + description=emailconfig_help + + "\n\tExample:\n\tsettwofactorauthentication smtp --smtpfortfaenabled true --alertmailsenderdomain " + "'test@hpe.com' --alertmailsmtpserver 'smtp3.hpe.com' --alertmailsmtpport 587 ", + formatter_class=RawDescriptionHelpFormatter, + ) + emailconfig_parser.add_argument( + "--smtpfortfaenabled", + dest="SMTPForTFA", + help="smtp service enable , supply either true or false as input. " + "If true then remaining inputs cant be null or empty", + action="append", + default=None, + ) + emailconfig_parser.add_argument( + "--alertmailsenderdomain", + dest="SenderDomain", + action="append", + help="smtp user name , supply a valid aduser for this field", + default=None, + ) + emailconfig_parser.add_argument( + "--alertmailsmtpserver", + dest="SMTPServer", + action="append", + help="smtp server , supply a valid smtp server for this parameter", + default=None, + ) + + emailconfig_parser.add_argument( + "--alertmailsmtpport", + dest="SMTPPort", + type=int, + help="smtp port , supply a valid smtp port for this parameter, default is 25", + default=25, + ) + + self.cmdbase.add_login_arguments_group(emailconfig_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SigRecomputeCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SigRecomputeCommand.py new file mode 100644 index 0000000..e7b5e0d --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SigRecomputeCommand.py @@ -0,0 +1,107 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""SigRecompute Command for rdmc""" + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class SigRecomputeCommand: + """Recalculate the signature of the servers configuration""" + + def __init__(self): + self.ident = { + "name": "sigrecompute", + "usage": None, + "description": "Recalculate the signature on " + "the computers configuration.\n\texample: sigrecompute\n\n" + "\tNote: sigrecompute command is not available on Redfish systems.", + "summary": "Command to recalculate the signature of the computer's " "configuration.", + "aliases": [], + "auxcommands": [], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main sigrecompute function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if args: + raise InvalidCommandLineError("Sigrecompute command takes no arguments.\n") + + self.sigrecomputevalidation(options) + path = self.rdmc.app.typepath.defs.systempath + + if self.rdmc.app.typepath.defs.flagforrest: + body = {"Action": "ServerSigRecompute", "Target": "/Oem/Hp"} + self.rdmc.app.post_handler(path, body) + else: + raise IncompatibleiLOVersionError("Sigrecompute action not available on redfish.\n") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def sigrecomputevalidation(self, options): + """sigrecomputevalidation method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SingleSignOnCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SingleSignOnCommand.py new file mode 100644 index 0000000..9ffa932 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/SingleSignOnCommand.py @@ -0,0 +1,210 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Single Sign On Command for rdmc""" +from argparse import RawDescriptionHelpFormatter + +try: + from rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + + +class SingleSignOnCommand: + """Commands Single Sign On actions to the server""" + + def __init__(self): + self.ident = { + "name": "singlesignon", + "usage": None, + "description": "Add or remove single sign on (SSO) records.\nTo view help on specific " + "sub-commands run: singlesignon -h\n\nExample: singlesignon " + "importcert -h\n\n", + "summary": "Command for all single sign on available actions. ", + "aliases": ["sso"], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main SingleSignOnCommand function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.singlesignonvalidation(options) + + actionitem = None + select = self.rdmc.app.typepath.defs.hpilossotype + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + path = results.resp.request.path + else: + raise NoContentsFoundForOperationError("%s not found." % select) + + bodydict = results.resp.dict + + if not options.command: + return ReturnCodes.SUCCESS + + if options.command.lower() == "importdns": + actionitem = "ImportDNSName" + body = {"Action": actionitem, "DNSName": options.dnsname} + elif options.command.lower() == "importcert": + cert = None + certtype = None + actionitem = "ImportCertificate" + + try: + cert = open(options.cert, "r") + + if cert: + certtext = cert.read() + cert.close() + + if certtext: + certtype = "DirectImportCert" + except: + pass + + if not certtype: + certtype = "ImportCertUri" + certtext = options.cert + + body = {"Action": actionitem, "CertType": certtype, "CertInput": certtext} + + elif options.command.lower() == "deleterecord": + if options.record.lower() == "all": + actionitem = "DeleteAllSSORecords" + body = {"Action": actionitem} + else: + actionitem = "DeleteSSORecordbyNumber" + + try: + body = {"Action": actionitem, "RecordNumber": int(options.record)} + except: + raise InvalidCommandLineError("Record to delete must be a number") + + try: + for item in bodydict["Actions"]: + if actionitem in item: + if self.rdmc.app.typepath.defs.isgen10: + actionitem = item.split("#")[-1] + body["Action"] = actionitem + + path = bodydict["Actions"][item]["target"] + break + except: + pass + + self.rdmc.app.post_handler(path, body) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def singlesignonvalidation(self, options): + """single sign on validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + subcommand_parser = customparser.add_subparsers(dest="command") + save_import_dns_help = "Import a DNS name." + # importdns sub-parser + import_dns_parser = subcommand_parser.add_parser( + "importdns", + help=save_import_dns_help, + description=save_import_dns_help + "\n\texample singlesignon importdns dnsname", + formatter_class=RawDescriptionHelpFormatter, + ) + import_dns_parser.add_argument("dnsname", help="DNS Name to be imported", metavar="DNSNAME") + save_import_cert_help = "Import certificate from URI or file." + # importcert sub-parser + import_cert_parser = subcommand_parser.add_parser( + "importcert", + help=save_import_cert_help, + description=save_import_cert_help + "\n\texample singlesignon importcert cert.txt", + formatter_class=RawDescriptionHelpFormatter, + ) + import_cert_parser.add_argument( + "cert", + help="Certificate URI or Certificate File to be imported.", + metavar="CERTIFICATE", + ) + # delete sub-parser + delete_sso_help = "Delete a single or all SSO records." + delete_sso_parser = subcommand_parser.add_parser( + "deleterecord", + help=delete_sso_help, + description=delete_sso_help + "\nDelete a single record:\nexample deleterecord 2\n" + "Delete all records:\nexample singlesignon deleterecord all", + formatter_class=RawDescriptionHelpFormatter, + ) + delete_sso_parser.add_argument( + "record", + help="Record to be deleted (or use keyword 'all' to delete all records)", + metavar="RECORD", + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/VirtualMediaCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/VirtualMediaCommand.py new file mode 100644 index 0000000..ea95fac --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/VirtualMediaCommand.py @@ -0,0 +1,450 @@ +### +# Copyright 2016-2023 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Virtual Media Command for rdmc""" + +try: + from rdmc_helper import ( + UI, + IloLicenseError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + ReturnCodes, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + UI, + IloLicenseError, + ) + +from redfish.ris import rmc_helper + + +class VirtualMediaCommand: + """ Changes the iscsi configuration for the server that is currently """ """ logged in """ + + def __init__(self): + self.ident = { + "name": "virtualmedia", + "usage": None, + "description": "Run without" + " arguments to view the available virtual media sources." + "\n\tExample: virtualmedia\n\n\tInsert virtual media and " + "set to boot on next restart.\n\texample: virtualmedia 2 " + "http://xx.xx.xx.xx/vm.iso --bootnextreset\n\n\tRemove " + "current inserted media.\n\texample: virtualmedia 2 --remove", + "summary": "Command for inserting and removing virtual media.", + "aliases": [], + "auxcommands": [ + "GetCommand", + "SetCommand", + "SelectCommand", + "RebootCommand", + ], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main iscsi configuration worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + if len(args) > 2: + raise InvalidCommandLineError( + "Invalid number of parameters. " "virtualmedia command takes a maximum of 2 parameters." + ) + else: + self.virtualmediavalidation(options) + + resp = self.rdmc.app.get_handler("/rest/v1/Managers/1/VirtualMedia/1", silent=True) + + if not resp.status == 200: + raise IloLicenseError("") + + self.auxcommands["select"].run("VirtualMedia.") + ilover = self.rdmc.app.getiloversion() + + if self.rdmc.app.monolith.is_redfish: + isredfish = True + paths = self.auxcommands["get"].getworkerfunction("@odata.id", options, results=True, uselist=False) + ids = self.auxcommands["get"].getworkerfunction("Id", options, results=True, uselist=False) + paths = {ind: path for ind, path in enumerate(paths)} + ids = {ind: id for ind, id in enumerate(ids)} + for path in paths: + paths[path] = paths[path]["@odata.id"] + # paths = self.uniquevalmaker(paths) + else: + isredfish = False + paths = self.auxcommands["get"].getworkerfunction("links/self/href", options, results=True, uselist=False) + ids = self.auxcommands["get"].getworkerfunction("Id", options, results=True, uselist=False) + paths = {ind: path for ind, path in enumerate(paths)} + ids = {ind: id for ind, id in enumerate(ids)} + for path in paths: + paths[path] = paths[path]["links"]["self"]["href"] + # To keep indexes consistent between versions + if not list(ids.keys())[0] == list(list(ids.values())[0].values())[0]: + finalpaths = {} + for path in paths: + finalpaths.update({int(list(ids[path].values())[0]): paths[path]}) + paths = finalpaths + if options.removevm: + self.vmremovehelper(args, options, paths, isredfish, ilover) + elif len(args) == 2: + self.vminserthelper(args, options, paths, isredfish, ilover) + elif options.bootnextreset: + self.vmbootnextreset(args, paths) + elif not args: + self.vmdefaulthelper(options, paths) + else: + raise InvalidCommandLineError("Invalid parameter(s). Please run" " 'help virtualmedia' for parameters.") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def uniquevalmaker(self, paths): + unique_values = set(paths.values()) + + res = {} + i = 0 + for val in unique_values: + if val.endswith("/"): + val = val[:-1] + res[i] = val + i = i + 1 + else: + res[i] = val + i = i + 1 + i = 0 + res1 = dict() + for val in set(res.values()): + res1[i] = val + i = i + 1 + return res1 + + def vmremovehelper(self, args, options, paths, isredfish, ilover): + """Worker function to remove virtual media + + :param args: arguments passed from command line + :type args: list + :param paths: virtual media paths + :type paths: list + :param isredfish: redfish flag + :type isredfish: bool + :param ilover: iloversion + :type ilover: int + :param options: command line options + :type options: list. + """ + path = None + + if isredfish: + path, body = self.vmredfishhelper("remove", args[0]) + else: + if ilover <= 4.230: + body = {"Image": None} + else: + body = {"Action": "EjectVirtualMedia", "Target": "/Oem/Hp"} + + try: + path = paths[int(args[0])] if not path else path + except: + raise InvalidCommandLineError( + "Invalid input value for virtual " + "media please run the command with no " + "arguments for possible values." + ) + + if ilover <= 4.230: + self.rdmc.app.patch_handler(path, body) + else: + self.rdmc.app.post_handler(path, body) + + if options.reboot: + self.auxcommands["reboot"].run(options.reboot) + + def vminserthelper(self, args, options, paths, isredfish, ilover): + """Worker function to insert virtual media + + :param args: arguments passed from command line + :type args: list + :param paths: virtual media paths + :type paths: list + :param isredfish: redfish flag + :type isredfish: bool + :param ilover: iloversion + :type ilover: int + :param options: command line options + :type options: list. + """ + path = None + if not args[1].startswith("http://") and not args[1].startswith("https://"): + raise InvalidCommandLineError("Virtual media path must be a URL.") + if args[0] in "1": + if not args[1].endswith(".img"): + raise InvalidCommandLineError("Only .img files are allowed") + if args[0] in "2": + if not args[1].endswith(".iso"): + raise InvalidCommandLineError("Only .iso files are allowed") + if isredfish: + path, body = self.vmredfishhelper("insert", args[0], args[1]) + else: + if ilover <= 4.230: + body = {"Image": args[1]} + else: + body = { + "Action": "InsertVirtualMedia", + "Target": "/Oem/Hp", + "Image": args[1], + } + + try: + path = paths[int(args[0])] if not path else path + except: + raise InvalidCommandLineError( + "Invalid input value for virtual " + "media please run the command with " + "no arguments for possible values." + ) + + if ilover <= 4.230: + self.rdmc.app.patch_handler(path, body) + else: + try: + results = self.rdmc.app.post_handler(path, body) + if results.status == 200: + if options.bootnextreset: + self.vmbootnextreset(args, paths) + if options.reboot: + self.auxcommands["reboot"].run(options.reboot) + return ReturnCodes.SUCCESS + except rmc_helper.IloLicenseError: + raise IloLicenseError("Error:License Key Required\n") + # except Exception: + # self.rdmc.ui.printer("Please unmount/Eject virtual media and try it again\n") + + def vmdefaulthelper(self, options, paths): + """Worker function to reset virtual media config to default + + :param paths: virtual media paths + :type paths: list + :param options: command line options + :type options: list. + """ + images = {} + count = 0 + mediatypes = self.auxcommands["get"].getworkerfunction("MediaTypes", options, results=True, uselist=False) + ids = self.auxcommands["get"].getworkerfunction("Id", options, results=True, uselist=False) + ids = {ind: id for ind, id in enumerate(ids)} + mediatypes = {ind: med for ind, med in enumerate(mediatypes)} + # To keep indexes consistent between versions + if not list(ids.keys())[0] == list(list(ids.values())[0].values())[0]: + finalmet = {} + for mount in mediatypes: + finalmet.update({int(list(ids[mount].values())[0]): mediatypes[mount]}) + mediatypes = finalmet + + for path in paths: + count += 1 + image = self.rdmc.app.get_handler(paths[path], service=True, silent=True) + image = image.dict["Image"] + images.update({path: image}) + + self.rdmc.ui.printer("Available Virtual Media Options:\n") + if getattr(options, "json", False): + json_str = dict() + json_str["MediaTypes"] = dict() + # else: + # self.rdmc.ui.printer("Available Virtual Media Options:\n") + + for image in images: + media = "" + + if images[image]: + imagestr = images[image] + else: + imagestr = "None" + + for medtypes in mediatypes[image]["MediaTypes"]: + media += medtypes + " " + + if getattr(options, "json", False): + json_str["MediaTypes"][str(media)] = imagestr + else: + self.rdmc.ui.printer( + "[%s] Media Types Available: %s Image Inserted:" " %s\n" % (str(image), str(media), imagestr) + ) + if getattr(options, "json", False): + UI().print_out_json(json_str) + + def vmbootnextreset(self, args, paths): + """Worker function to boot virtual media on next serverreset + + :param args: arguments passed from command line + :type args: list + :param paths: all virtual media paths + :type paths: list + """ + try: + path = paths[int(args[0])] + except: + raise InvalidCommandLineError( + "Invalid input value for virtual media" + " please run the command with no " + "arguments for possible values." + ) + + self.rdmc.app.patch_handler( + path, + {"Oem": {self.rdmc.app.typepath.defs.oemhp: {"BootOnNextServerReset": True}}}, + service=True, + silent=True, + ) + + def vmredfishhelper(self, action, number, image=None): + """Redfish version of the worker function + + :param action: action item + :type action: str + :param number: virtual media ID + :type number: int + """ + + results = self.rdmc.app.select(selector="VirtualMedia.") + bodydict = None + + try: + for result in results: + if result.resp.dict["Id"] == number: + bodydict = result.resp.dict + break + except: + pass + + if not bodydict: + raise InvalidCommandLineError( + "Invalid input value for virtual media" + " please run the command with no " + "arguments for possible values." + ) + if action == "remove" and not bodydict["Inserted"]: + raise InvalidCommandLineError( + "Invalid input value for virtual media." + " No media present in this drive to unmount. Please recheck " + "arguments for possible values." + ) + + if action == "insert" and image: + for item in bodydict["Oem"][self.rdmc.app.typepath.defs.oemhp]["Actions"]: + if "InsertVirtualMedia" in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = "InsertVirtualMedia" + + path = bodydict["Oem"][self.rdmc.app.typepath.defs.oemhp]["Actions"][item]["target"] + body = {"Action": action, "Image": image} + break + elif action == "remove": + for item in bodydict["Oem"][self.rdmc.app.typepath.defs.oemhp]["Actions"]: + if "EjectVirtualMedia" in item: + if self.rdmc.app.typepath.defs.isgen10: + action = item.split("#")[-1] + else: + action = "EjectVirtualMedia" + + path = bodydict["Oem"][self.rdmc.app.typepath.defs.oemhp]["Actions"][item]["target"] + body = {"Action": action} + break + else: + return None, None + + return path, body + + def virtualmediavalidation(self, options): + """sigrecomputevalidation method validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "--reboot", + dest="reboot", + help="Use this flag to perform a reboot command function after" + " completion of operations. For help with parameters and" + " descriptions regarding the reboot flag, run help reboot.", + default=None, + ) + customparser.add_argument( + "--remove", + dest="removevm", + action="store_true", + help="Use this flag to remove the media from the selection.", + default=False, + ) + customparser.add_argument( + "--bootnextreset", + dest="bootnextreset", + action="store_true", + help="Use this flag if you wish to boot from the image on " + "next server reboot. NOTE: The image will be ejected " + "automatically on the second server reboot so that the server " + "does not boot to this image twice.", + default=False, + ) + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/AppAccountCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/AppAccountCommand.cpython-310.pyc new file mode 100644 index 0000000..75001ba Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/AppAccountCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/CertificateCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/CertificateCommand.cpython-310.pyc new file mode 100644 index 0000000..2ddb67a Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/CertificateCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ChannelInterfaceCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ChannelInterfaceCommand.cpython-310.pyc new file mode 100644 index 0000000..fd37f80 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ChannelInterfaceCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ClearRestApiStateCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ClearRestApiStateCommand.cpython-310.pyc new file mode 100644 index 0000000..a7507ca Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ClearRestApiStateCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ComputeOpsManagementCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ComputeOpsManagementCommand.cpython-310.pyc new file mode 100644 index 0000000..1099f33 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ComputeOpsManagementCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/DirectoryCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/DirectoryCommand.cpython-310.pyc new file mode 100644 index 0000000..2e5fb95 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/DirectoryCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/DisableIloFunctionalityCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/DisableIloFunctionalityCommand.cpython-310.pyc new file mode 100644 index 0000000..13c39c7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/DisableIloFunctionalityCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ESKMCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ESKMCommand.cpython-310.pyc new file mode 100644 index 0000000..057bdc1 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ESKMCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/EthernetCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/EthernetCommand.cpython-310.pyc new file mode 100644 index 0000000..ec7e3f6 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/EthernetCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/FactoryDefaultsCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/FactoryDefaultsCommand.cpython-310.pyc new file mode 100644 index 0000000..a47c072 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/FactoryDefaultsCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/FirmwareIntegrityCheckCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/FirmwareIntegrityCheckCommand.cpython-310.pyc new file mode 100644 index 0000000..f8ffaa0 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/FirmwareIntegrityCheckCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/FirmwareUpdateCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/FirmwareUpdateCommand.cpython-310.pyc new file mode 100644 index 0000000..320f869 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/FirmwareUpdateCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IPProfilesCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IPProfilesCommand.cpython-310.pyc new file mode 100644 index 0000000..a43d4ea Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IPProfilesCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloAccountsCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloAccountsCommand.cpython-310.pyc new file mode 100644 index 0000000..a296c23 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloAccountsCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloBackupRestoreCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloBackupRestoreCommand.cpython-310.pyc new file mode 100644 index 0000000..0343344 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloBackupRestoreCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloFederationCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloFederationCommand.cpython-310.pyc new file mode 100644 index 0000000..ca2987e Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloFederationCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloLicenseCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloLicenseCommand.cpython-310.pyc new file mode 100644 index 0000000..387fd97 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloLicenseCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloResetCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloResetCommand.cpython-310.pyc new file mode 100644 index 0000000..cdf3cfb Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/IloResetCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/LogconfigCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/LogconfigCommand.cpython-310.pyc new file mode 100644 index 0000000..bdc37e8 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/LogconfigCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/OneButtonEraseCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/OneButtonEraseCommand.cpython-310.pyc new file mode 100644 index 0000000..e711f74 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/OneButtonEraseCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/RebootCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/RebootCommand.cpython-310.pyc new file mode 100644 index 0000000..95b6ed7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/RebootCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SendTestCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SendTestCommand.cpython-310.pyc new file mode 100644 index 0000000..5db79da Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SendTestCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerCloneCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerCloneCommand.cpython-310.pyc new file mode 100644 index 0000000..863e963 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerCloneCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerConfigurationLockCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerConfigurationLockCommand.cpython-310.pyc new file mode 100644 index 0000000..e9a9bd8 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerConfigurationLockCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerInfoCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerInfoCommand.cpython-310.pyc new file mode 100644 index 0000000..58727ff Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerInfoCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerStateCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerStateCommand.cpython-310.pyc new file mode 100644 index 0000000..16b3d15 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerStateCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerlogsCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerlogsCommand.cpython-310.pyc new file mode 100644 index 0000000..886cf3a Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/ServerlogsCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SetTwoFactorAuthenticationCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SetTwoFactorAuthenticationCommand.cpython-310.pyc new file mode 100644 index 0000000..41ab836 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SetTwoFactorAuthenticationCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SigRecomputeCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SigRecomputeCommand.cpython-310.pyc new file mode 100644 index 0000000..37a09a4 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SigRecomputeCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SingleSignOnCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SingleSignOnCommand.cpython-310.pyc new file mode 100644 index 0000000..002102e Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/SingleSignOnCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/VirtualMediaCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/VirtualMediaCommand.cpython-310.pyc new file mode 100644 index 0000000..88771a5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/VirtualMediaCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..c5bcb00 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__init__.py new file mode 100644 index 0000000..a546cc3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__init__.py @@ -0,0 +1,16 @@ +### +# Copyright 2021-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +### +"""Helper modules for ComputeOpsManagement command""" + +from .config_resolver import ConfigResolver +from .network_configurator import NetworkConfigurator +from .ilo_connector import IloConnector +from .progress_tracker import ProgressTracker + +__all__ = [ + "ConfigResolver", + "NetworkConfigurator", + "IloConnector", + "ProgressTracker", +] diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..ac51289 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/config_resolver.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/config_resolver.cpython-310.pyc new file mode 100644 index 0000000..e47883c Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/config_resolver.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/ilo_connector.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/ilo_connector.cpython-310.pyc new file mode 100644 index 0000000..f75af95 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/ilo_connector.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/network_configurator.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/network_configurator.cpython-310.pyc new file mode 100644 index 0000000..8c2a47d Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/network_configurator.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/progress_tracker.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/progress_tracker.cpython-310.pyc new file mode 100644 index 0000000..abebfd5 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/__pycache__/progress_tracker.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/config_resolver.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/config_resolver.py new file mode 100644 index 0000000..6e5d71b --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/config_resolver.py @@ -0,0 +1,142 @@ +### +# Copyright 2021-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +### +# -*- coding: utf-8 -*- +"""Configuration resolver for ComputeOpsManagement""" + +import json +from ipaddress import ip_address +from typing import Dict, Any, List + + +class ConfigResolver: + """Handles configuration file parsing and IP resolution""" + + @staticmethod + def resolve_ilo_config(config_json_path: str) -> Dict[str, Dict[str, Any]]: + """Resolve and merge configuration for all iLOs from config file + + :param config_json_path: Path to JSON configuration file + :type config_json_path: str + :returns: Dictionary mapping IP addresses to their merged configurations + :rtype: dict + """ + with open(config_json_path, "r") as f: + config_json = json.load(f) + + resolved = {} + common_settings = config_json.get("commonSettings", {}) + + # Extract common settings with type checking + common_ilo_auth = ( + common_settings.get("iloAuthentication", {}) + if isinstance(common_settings.get("iloAuthentication"), dict) + else {} + ) + common_com = ( + common_settings.get("computeOpsManagement", {}) + if isinstance(common_settings.get("computeOpsManagement"), dict) + else {} + ) + common_network = common_settings.get("network", {}) if isinstance(common_settings.get("network"), dict) else {} + + common_dns = common_network.get("dns", []) + common_ntp = common_network.get("ntp", []) + common_proxy = common_network.get("proxy", {}) + + def build_config(ip_str, entry_settings, dns, ntp, skip_dns, skip_ntp, skip_proxy): + """Build merged configuration for a single IP""" + entry_ilo_auth = entry_settings.get("iloAuthentication", {}) + entry_com = entry_settings.get("computeOpsManagement", {}) + entry_proxy = entry_settings.get("network", {}).get("proxy", {}) + + return { + "ip": ip_str, + "username": entry_ilo_auth.get("username") or common_ilo_auth.get("username"), + "password": entry_ilo_auth.get("password") or common_ilo_auth.get("password"), + "pcid": entry_com.get("pcid") or common_com.get("pcid"), + "activationKey": entry_com.get("activationKey") or common_com.get("activationKey"), + "dns": [] if skip_dns else dns, + "ntp": [] if skip_ntp else ntp, + "skipDns": skip_dns, + "skipNtp": skip_ntp, + "skipProxy": skip_proxy, + "proxy": entry_proxy if entry_proxy else common_proxy, + "auth": entry_ilo_auth if entry_ilo_auth else common_ilo_auth, + } + + # Process individual entries + individual_entries = config_json.get("targets", {}).get("ilos", {}).get("individual", []) + for entry in individual_entries: + ip = entry["ip"] + skip_dns = entry.get("skipDns", False) + skip_ntp = entry.get("skipNtp", False) + skip_proxy = entry.get("skipProxy", False) + config = build_config(ip, entry, common_dns, common_ntp, skip_dns, skip_ntp, skip_proxy) + resolved[ip] = config + + # Process range entries with optimized conversion + range_entries = config_json.get("targets", {}).get("ilos", {}).get("ranges", []) + for range_entry in range_entries: + start_ip = ip_address(range_entry["start"]) + end_ip = ip_address(range_entry["end"]) + skip_dns = range_entry.get("skipDns", False) + skip_ntp = range_entry.get("skipNtp", False) + skip_proxy = range_entry.get("skipProxy", False) + network = range_entry.get("network", {}) + range_dns = [] if skip_dns else network.get("dns", common_dns) + range_ntp = [] if skip_ntp else network.get("ntp", common_ntp) + + # Optimize: Convert to int once, iterate directly + start_int = int(start_ip) + end_int = int(end_ip) + for ip_int in range(start_int, end_int + 1): + ip_str = str(ip_address(ip_int)) + config = build_config(ip_str, {}, range_dns, range_ntp, skip_dns, skip_ntp, skip_proxy) + resolved[ip_str] = config + + return resolved + + @staticmethod + def extract_target_ips(targets: dict) -> List[Dict[str, Any]]: + """Extract all target IPs from configuration with validation + + :param targets: targets section from config + :type targets: dict + :returns: list of IP information dictionaries + :rtype: list + """ + import ipaddress + + target_ips = [] + ilos = targets.get("ilos", {}) + + # Process individual IPs + individual_ilos = ilos.get("individual", []) + for ilo in individual_ilos: + ip_info = {"ip": ilo["ip"], "settings": ilo} + target_ips.append(ip_info) + + # Process IP ranges with size validation + ranges = ilos.get("ranges", []) + for range_config in ranges: + start_ip = ipaddress.IPv4Address(range_config["start"]) + end_ip = ipaddress.IPv4Address(range_config["end"]) + + # Validate range size to prevent memory issues + range_size = int(end_ip) - int(start_ip) + 1 + if range_size > 1000: + from ilorest.rdmc_helper import InvalidCommandLineError + + raise InvalidCommandLineError( + f"IP range {range_config['start']} to {range_config['end']} " + f"contains {range_size} IPs. Maximum allowed is 1000 IPs." + ) + + current_ip = start_ip + while current_ip <= end_ip: + ip_info = {"ip": str(current_ip), "settings": range_config} + target_ips.append(ip_info) + current_ip += 1 + + return target_ips diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/ilo_connector.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/ilo_connector.py new file mode 100644 index 0000000..1bbca36 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/ilo_connector.py @@ -0,0 +1,210 @@ +### +# Copyright 2021-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +### +# -*- coding: utf-8 -*- +"""iLO connector module for ComputeOpsManagement""" + +import requests +from requests.auth import HTTPBasicAuth +from requests.exceptions import ConnectionError, Timeout, RequestException +import time +from typing import Tuple, Optional + + +class IloConnector: + """Handles iLO connection and COM onboarding operations""" + + @staticmethod + def connect_to_com( + ip: str, username: str, password: str, credential: str, logger, max_retries: int = 3 + ) -> Tuple[bool, Optional[str]]: + """Enable COM connection on iLO + + :param ip: iLO IP address + :param username: iLO username + :param password: iLO password + :param credential: PCID or activation key + :param logger: logger instance + :param max_retries: maximum retry attempts + :returns: tuple of (success, fail_reason) + """ + base_url = f"https://{ip}" + session = requests.Session() + session.auth = HTTPBasicAuth(username, password) + session.verify = False + + # Prepare COM enable payload + payload = {"Oem": {"Hpe": {"CloudConnect": {"CloudConnectEnabled": True, "ActivationKey": credential}}}} + + url = f"{base_url}/redfish/v1/Managers/1/" + retry_count = 0 + success = False + + while retry_count <= max_retries and not success: + try: + logger.info(f"Sending COM enable request to {ip} (attempt {retry_count + 1})") + response = session.patch(url, json=payload, timeout=30) + + if response.status_code in [200, 202]: + success = True + logger.info("COM enable request successful") + else: + logger.warning(f"COM enable failed with status {response.status_code}") + if retry_count < max_retries: + retry_count += 1 + logger.info(f"Retrying COM enable ({retry_count}/{max_retries})") + time.sleep(5) + else: + return False, "COM enable request failed" + + except (ConnectionError, Timeout, RequestException) as conn_err: + logger.warning(f"Connection issue during COM enable: {str(conn_err)}") + if retry_count < max_retries: + retry_count += 1 + logger.info(f"Retrying after connection error ({retry_count}/{max_retries})") + time.sleep(15) + else: + logger.error(f"COM enable failed after {max_retries} retries") + return False, "Connection failed after retries" + + if not success: + return False, "COM enable request failed" + + # Wait for connection to complete + max_wait = 180 + wait_interval = 12 + waited = 0 + + logger.info("Waiting for COM connection to complete...") + + while waited < max_wait: + time.sleep(wait_interval) + waited += wait_interval + + try: + response = session.get(f"{base_url}/redfish/v1/Managers/1/", timeout=30) + if response.status_code == 200: + manager_data = response.json() + cloud_connect = manager_data.get("Oem", {}).get("Hpe", {}).get("CloudConnect", {}) + status = cloud_connect.get("CloudConnectStatus", "") + fail_reason = cloud_connect.get("FailReason", "") + + if status == "Connected": + logger.info("COM connection successful") + return True, None + elif status in ["ConnectionFailed", "NotConnected"]: + logger.warning(f"COM connection failed: {status}, reason: {fail_reason}") + return False, fail_reason if fail_reason else "Connection failed" + elif status == "NotEnabled": + logger.warning(f"COM not enabled: {fail_reason}") + return False, fail_reason if fail_reason else "COM not enabled" + else: + logger.info(f"COM connection status: {status}, waiting...") + + except (ConnectionError, Timeout, RequestException) as check_err: + logger.warning(f"Connection issue during status check: {str(check_err)}") + except Exception as check_err: + logger.warning(f"Error during status check: {str(check_err)}") + + logger.warning(f"COM connection timed out after {max_wait} seconds") + return False, "Connection timeout" + + @staticmethod + def wait_for_ilo_after_reset(ip: str, username: str, password: str, logger, timeout_minutes: int = 3) -> bool: + """Wait for iLO to come back online after reset + + :param ip: iLO IP address + :param username: iLO username + :param password: iLO password + :param logger: logger instance + :param timeout_minutes: timeout in minutes + :returns: True if iLO came back online, False otherwise + """ + base_url = f"https://{ip}" + timeout_seconds = timeout_minutes * 60 + check_interval = 10 + max_attempts = int(timeout_seconds / check_interval) + start_time = time.time() + + logger.info(f"Waiting up to {timeout_minutes} minutes for iLO {ip} to come back online...") + + for attempt in range(max_attempts): + try: + response = requests.get( + f"{base_url}/redfish/v1/", auth=HTTPBasicAuth(username, password), verify=False, timeout=30 + ) + + if response.status_code == 200: + elapsed_time = time.time() - start_time + logger.info( + f"iLO {ip} is back online after {elapsed_time:.1f} seconds " + f"(attempt {attempt + 1}/{max_attempts})" + ) + return True + + except (ConnectionError, Timeout): + pass # Expected during reset + except Exception as e: + logger.warning(f"Unexpected error while waiting for iLO {ip}: {str(e)}") + + if attempt < max_attempts - 1: + logger.info( + f"iLO {ip} still rebooting. Checking again in {check_interval} " + f"seconds... (attempt {attempt + 1}/{max_attempts})" + ) + time.sleep(check_interval) + + elapsed_time = time.time() - start_time + logger.error( + f"Timeout: iLO {ip} did not come back online within {timeout_minutes} " + f"minutes ({elapsed_time:.1f} seconds elapsed)" + ) + return False + + @staticmethod + def get_ilo_version(ip: str, username: str, password: str) -> Tuple[float, int]: + """Get iLO version information + + :param ip: iLO IP address + :param username: iLO username + :param password: iLO password + :returns: tuple of (ilo_version, major_version) + """ + try: + base_url = f"https://{ip}" + response = requests.get( + f"{base_url}/redfish/v1/Managers/1/", auth=HTTPBasicAuth(username, password), verify=False, timeout=30 + ) + + if response.status_code == 200: + data = response.json() + fw_version = data.get("FirmwareVersion", "") + + # Parse version string like "iLO 5 v2.47" + parts = fw_version.split() + if len(parts) >= 3 and parts[0].lower() == "ilo": + major = int(parts[1]) + version_str = parts[2].lstrip("v").replace(".", "") + version = float(f"{major}.{version_str}") + return version, major + + # Return default compatible version + return 5.247, 5 + + except Exception: + return 5.247, 5 + + @staticmethod + def is_pin_onboarding_supported(ilo_version: float, major_version: int) -> bool: + """Check if PIN-based onboarding is supported + + :param ilo_version: iLO version as float + :param major_version: iLO major version + :returns: True if PIN onboarding supported + """ + # PIN onboarding supported on iLO 6 and iLO 5 >= 2.70 + if major_version >= 6: + return True + elif major_version == 5 and ilo_version >= 5.270: + return True + return False diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/network_configurator.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/network_configurator.py new file mode 100644 index 0000000..c07fbb9 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/network_configurator.py @@ -0,0 +1,218 @@ +### +# Copyright 2021-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +### +# -*- coding: utf-8 -*- +"""Network configuration module for ComputeOpsManagement""" + +from requests.exceptions import ConnectionError, Timeout, RequestException +from typing import Optional, Tuple + + +class NetworkConfigurator: + """Handles DNS, NTP, and Proxy configuration for iLOs""" + + @staticmethod + def configure_dns(session, base_url: str, dns_servers: list, logger) -> None: + """Configure DNS servers on iLO + + :param session: requests Session object + :param base_url: base URL for iLO + :param dns_servers: list of DNS servers + :param logger: logger instance + """ + try: + # Check if already configured + current_dns = NetworkConfigurator.get_current_dns_servers(session, base_url) + + if current_dns is not None: + desired_dns = sorted([srv.strip() for srv in dns_servers if srv and srv.strip()]) + current_dns_sorted = sorted([srv.strip() for srv in current_dns if srv and srv.strip()]) + + if desired_dns == current_dns_sorted: + logger.info(f"DNS servers already configured as desired: {desired_dns}") + return + + # Separate IPv4 and IPv6 DNS servers + ipv4_dns = [] + ipv6_dns = [] + + for dns_server in dns_servers: + if dns_server: + dns_clean = dns_server.strip() + if dns_clean: + if NetworkConfigurator._is_ipv6_address(dns_clean): + ipv6_dns.append(dns_clean) + else: + ipv4_dns.append(dns_clean) + + # Configure via EthernetInterfaces endpoint + dns_config_url = f"{base_url}/redfish/v1/Managers/1/EthernetInterfaces/1" + + payload = { + "DHCPv4": {"UseDNSServers": False}, + "DHCPv6": {"UseDNSServers": False}, + "Oem": { + "Hpe": { + "DHCPv4": {"UseDNSServers": False}, + "DHCPv6": {"UseDNSServers": False}, + "IPv4": {"DNSServers": ipv4_dns}, + "IPv6": {"DNSServers": ipv6_dns}, + } + }, + } + + response = session.patch(dns_config_url, json=payload, timeout=30) + if response.status_code in [200, 204]: + logger.info(f"Successfully configured DNS servers: {dns_servers}") + else: + raise Exception(f"Failed to configure DNS. HTTP {response.status_code}") + + except Exception as e: + logger.error(f"Error configuring DNS: {str(e)}") + raise + + @staticmethod + def configure_ntp(session, base_url: str, ntp_servers: list, allow_ilo_reset: bool, logger) -> Tuple[bool, bool]: + """Configure NTP servers on iLO + + :param session: requests Session object + :param base_url: base URL for iLO + :param ntp_servers: list of NTP servers + :param allow_ilo_reset: allow iLO reset if needed + :param logger: logger instance + :returns: tuple of (ilo_was_reset, needs_reset) + """ + ilo_was_reset = False + needs_reset = False + dhcp_disabled = False + + try: + # Check if already configured + current_ntp = NetworkConfigurator.get_current_ntp_servers(session, base_url) + + if current_ntp is not None: + desired_ntp = sorted([srv.strip() for srv in ntp_servers if srv and srv.strip()]) + current_ntp_sorted = sorted([srv.strip() for srv in current_ntp if srv and srv.strip()]) + + if desired_ntp == current_ntp_sorted: + logger.info(f"NTP servers already configured as desired: {desired_ntp}") + return ilo_was_reset, needs_reset + + # Try to disable DHCP NTP if needed + eth_url = f"{base_url}/redfish/v1/Managers/1/EthernetInterfaces" + + try: + eth_response = session.get(eth_url, timeout=30) + if eth_response.status_code == 200: + members = eth_response.json().get("Members", []) + + for member in members: + interface_url = base_url + member.get("@odata.id", "") + try: + iface_resp = session.get(interface_url, timeout=30) + if iface_resp.status_code == 200: + iface_data = iface_resp.json() + if iface_data.get("Status", {}).get("State") == "Enabled": + # Check if DHCP NTP already disabled + dhcpv4_ntp = iface_data.get("DHCPv4", {}).get("UseNTPServers", False) + dhcpv6_ntp = iface_data.get("DHCPv6", {}).get("UseNTPServers", False) + + if not dhcpv4_ntp and not dhcpv6_ntp: + logger.info("DHCP NTP already disabled") + dhcp_disabled = True + break + + # Disable DHCP NTP + dhcp_payload = { + "DHCPv4": {"UseNTPServers": False}, + "DHCPv6": {"UseNTPServers": False}, + "Oem": { + "Hpe": { + "DHCPv4": {"UseNTPServers": False}, + "DHCPv6": {"UseNTPServers": False}, + } + }, + } + + dhcp_resp = session.patch(interface_url, json=dhcp_payload, timeout=30) + if dhcp_resp.status_code in [200, 204]: + logger.info("Successfully disabled DHCP NTP") + dhcp_disabled = True + break + except (ConnectionError, Timeout, RequestException): + logger.warning("Connection error accessing interface - may already be disabled") + dhcp_disabled = True # Assume already disabled + break + except (ConnectionError, Timeout, RequestException): + logger.warning("Connection error accessing interfaces - proceeding anyway") + dhcp_disabled = True # Assume already disabled + + # Configure static NTP servers + ntp_url = f"{base_url}/redfish/v1/Managers/1/DateTime" + ntp_list = list(ntp_servers) + if len(ntp_list) == 1: + ntp_list.append("") + + payload = {"StaticNTPServers": ntp_list} + response = session.patch(ntp_url, json=payload, timeout=30) + + if response.status_code not in [200, 204]: + raise Exception(f"Failed to configure NTP. HTTP {response.status_code}") + + logger.info(f"Successfully configured NTP servers: {ntp_servers}") + + # Reset if allowed + if allow_ilo_reset: + reset_url = f"{base_url}/redfish/v1/Managers/1/Actions/Manager.Reset" + reset_payload = {"ResetType": "GracefulRestart"} + reset_resp = session.post(reset_url, json=reset_payload, timeout=30) + if reset_resp.status_code in [200, 202, 204]: + logger.info("iLO reset initiated for NTP configuration") + ilo_was_reset = True + else: + needs_reset = True + + except Exception as e: + logger.error(f"Error configuring NTP: {str(e)}") + raise + + return ilo_was_reset, needs_reset + + @staticmethod + def get_current_dns_servers(session, base_url: str) -> Optional[list]: + """Get current DNS configuration from iLO""" + try: + dns_url = f"{base_url}/redfish/v1/Managers/1/EthernetInterfaces/1" + response = session.get(dns_url, timeout=30) + + if response.status_code == 200: + data = response.json() + oem_hpe = data.get("Oem", {}).get("Hpe", {}) + ipv4_dns = oem_hpe.get("IPv4", {}).get("DNSServers", []) + ipv6_dns = oem_hpe.get("IPv6", {}).get("DNSServers", []) + + # Use list comprehension for efficiency + return [srv for srv in ipv4_dns + ipv6_dns if srv] + return None + except Exception: + return None + + @staticmethod + def get_current_ntp_servers(session, base_url: str) -> Optional[list]: + """Get current NTP configuration from iLO""" + try: + ntp_url = f"{base_url}/redfish/v1/Managers/1/DateTime" + response = session.get(ntp_url, timeout=30) + + if response.status_code == 200: + data = response.json() + ntp_servers = data.get("StaticNTPServers", []) + return [srv for srv in ntp_servers if srv] + return None + except Exception: + return None + + @staticmethod + def _is_ipv6_address(address: str) -> bool: + """Check if address is IPv6""" + return ":" in address diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/progress_tracker.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/progress_tracker.py new file mode 100644 index 0000000..9555d9d --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/com_helpers/progress_tracker.py @@ -0,0 +1,112 @@ +### +# Copyright 2021-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +### +# -*- coding: utf-8 -*- +"""Progress tracking module for ComputeOpsManagement bulk operations""" + + +class ProgressTracker: + """Handles progress bar display for bulk operations""" + + def __init__(self, total: int, desc: str = "", unit: str = ""): + """Initialize progress tracker + + :param total: Total number of items + :param desc: Description for progress bar + :param unit: Unit of measurement + """ + self.total = total + self.desc = desc + self.unit = unit + self.current = 0 + self.status_info = "" + + # Display initial progress + self._display() + + def update(self, n: int = 1, status: str = ""): + """Update progress by n items + + :param n: Number of items completed + :param status: Optional status message + """ + self.current += n + self.status_info = f" - {status}" if status else "" + self._display() + + def set_description(self, desc: str): + """Update description + + :param desc: New description + """ + self.desc = desc + self._display() + + def close(self): + """Close progress bar with newline""" + print() # Move to next line + + def _display(self): + """Display progress bar""" + progress_bar = self._get_progress_bar() + print(f"\r{self.desc}: {self.current}/{self.total} " f"{progress_bar}{self.status_info}", end="", flush=True) + + def _get_progress_bar(self, width: int = 40) -> str: + """Generate visual progress bar + + :param width: Width of progress bar + :returns: Progress bar string + """ + progress_pct = (self.current / self.total * 100) if self.total > 0 else 0 + filled_length = int(width * self.current // self.total) if self.total > 0 else 0 + bar = "#" * filled_length + "-" * (width - filled_length) + return f"[{bar}] {progress_pct:.1f}%" + + +class TqdmProgressTracker: + """Wrapper for tqdm progress bar when available""" + + def __init__(self, tqdm_instance): + """Initialize with tqdm instance + + :param tqdm_instance: tqdm progress bar object + """ + self.pbar = tqdm_instance + + def update(self, n: int = 1, status: str = ""): + """Update progress + + :param n: Number of items completed + :param status: Optional status message + """ + if status: + self.pbar.set_postfix_str(status) + self.pbar.update(n) + + def set_description(self, desc: str): + """Update description + + :param desc: New description + """ + self.pbar.set_description(desc) + + def close(self): + """Close progress bar""" + self.pbar.close() + + +def create_progress_tracker(total: int, desc: str = "", unit: str = ""): + """Factory function to create appropriate progress tracker + + :param total: Total number of items + :param desc: Description + :param unit: Unit of measurement + :returns: Progress tracker instance + """ + try: + from tqdm import tqdm + + pbar = tqdm(total=total, desc=desc, unit=unit) + return TqdmProgressTracker(pbar) + except ImportError: + return ProgressTracker(total, desc, unit) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/InputTemplate.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/InputTemplate.py new file mode 100644 index 0000000..d8ed91e --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/InputTemplate.py @@ -0,0 +1,31 @@ +template = { + "commonSettings": { + "computeOpsManagement": {"activationKey": "ACTIVATION-KEY-XXXX-YYYY-ZZZZ", "workspace_id": "WORKSPACE-ID-PLACEHOLDER-12345"}, + "iloAuthentication": {"iloUser": "GENERIC_ADMIN_USER", "iloPassword": "GENERIC_SECURE_PASSWORD"}, + "network": {"dns": ["203.0.113.10", "203.0.113.11"], "ntp": ["198.51.100.50", "198.51.100.51"]}, + "proxy": { + "server": "proxy.example.net", + "port": 8080, + "credentials": {"username": "PROXY_USER", "password": "PROXY_PASSWORD"}, + }, + }, + "targets": { + "ilos": { + "individual": [ + {"ip": "192.0.2.10"}, + {"ip": "192.0.2.11", "network": {"dns": ["203.0.113.20", "203.0.113.21"]}}, + {"ip": "192.0.2.12", "network": {"ntp": ["198.51.100.60", "198.51.100.61"]}, "skipDns": True}, + {"ip": "192.0.2.13", "network": {"dns": ["203.0.113.200"]}, "skipNtp": True}, + {"ip": "192.0.2.14", "skipDns": True, "skipNtp": True}, + {"ip": "192.0.2.15", "skipProxy": True}, + {"ip": "192.0.2.16", "skipDns": True, "skipProxy": True}, + ], + "ranges": [ + {"start": "192.0.2.100", "end": "192.0.2.110"}, + {"start": "192.0.2.120", "end": "192.0.2.125", "skipNtp": True}, + {"start": "192.0.2.130", "end": "192.0.2.132", "network": {"ntp": ["198.51.100.99"]}}, + {"start": "192.0.2.140", "end": "192.0.2.142", "skipProxy": True}, + ], + } + }, +} diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/__init__.py new file mode 100644 index 0000000..e87aa37 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/__init__.py @@ -0,0 +1,2 @@ +"""Data module for iLO Commands""" + diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/__pycache__/InputTemplate.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/__pycache__/InputTemplate.cpython-310.pyc new file mode 100644 index 0000000..064d37e Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/__pycache__/InputTemplate.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..0af0852 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_COMMANDS/data/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/DeleteComponentCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/DeleteComponentCommand.py new file mode 100644 index 0000000..e070e8e --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/DeleteComponentCommand.py @@ -0,0 +1,180 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Delete Component Command for rdmc""" + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class DeleteComponentCommand: + """Main download command class""" + + def __init__(self): + self.ident = { + "name": "deletecomp", + "usage": None, + "description": "Run to delete component(s) of the currently logged in system.\n\n\t" + "Delete a single component by name.\n\texample: deletecomp " + "CP327.zip\n\n\tDelete multiple components by " + "id.\n\tExample: deletecomp 377fg6c4 327cf4c7\n\n\tDelete " + "multiple components by filename.\n\texample: deletecomp " + "CP327.exe CP99.exe", + "summary": "Deletes components/binaries from the iLO Repository.", + "aliases": [], + "auxcommands": [], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main deletecomp worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.deletecomponentvalidation(options) + + if self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("iLO Repository commands are " "only available on iLO 5.") + + comps = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/ComponentRepository/") + + if not comps: + self.rdmc.ui.printer("No components found to delete\n") + + elif options.deleteall: + delopts = [] + + for comp in comps: + try: + if comp["Locked"]: + self.rdmc.ui.warn( + "Unable to delete %s. It is in use by " + "an install set or update task.\n" % comp["Filename"] + ) + continue + except KeyError: + pass + delopts.append(comp["@odata.id"]) + + self.deletecomponents(comps, delopts) + elif args: + self.deletecomponents(comps, args) + else: + InvalidCommandLineError("Please include the component(s) you wish to delete") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def deletecomponents(self, comps, delopts): + """component validation function + + :param comps: component list + :type comps: list. + :param delopts: delete items list + :type delopts: list. + """ + if "," in delopts: + delopts = delopts.split(",") + elif not isinstance(delopts, list): + delopts = [delopts] + + for opt in delopts: + deleted = False + + if "/" in opt: + self.rdmc.app.delete_handler(opt) + deleted = True + else: + for comp in comps: + if opt == comp["Id"] or opt == comp["Filename"]: + try: + if comp["Locked"]: + self.rdmc.ui.error( + "Unable to delete %s. It is in use by " + "an install set or update task.\n" % comp["Filename"] + ) + continue + except KeyError: + pass + + self.rdmc.app.delete_handler(comp["@odata.id"]) + deleted = True + + if deleted: + self.rdmc.ui.printer("Deleted %s\n" % opt) + self.rdmc.ui.printer("Component " + opt + " deleted successfully.\n") + # self.rdmc.ui.printer("[200] The operation completed successfully.\n") + else: + raise InvalidCommandLineError("Cannot find or unable to delete component %s" % opt) + + def deletecomponentvalidation(self, options): + """component validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-a", + "--all", + dest="deleteall", + action="store_true", + help="""Delete all components.""", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/DownloadComponentCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/DownloadComponentCommand.py new file mode 100644 index 0000000..9c38bb0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/DownloadComponentCommand.py @@ -0,0 +1,229 @@ +### +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ## + +# -*- coding: utf-8 -*- +"""Download Component Command for rdmc""" + +import ctypes +import os +import time +from ctypes import c_char_p, c_int + +try: + from rdmc_helper import ( + DownloadError, + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + DownloadError, + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + ReturnCodes, + ) + + +def human_readable_time(seconds): + """Returns human readable time + + :param seconds: Amount of seconds to parse. + :type seconds: string. + """ + seconds = int(seconds) + hours = seconds / 3600 + seconds = seconds % 3600 + minutes = seconds / 60 + seconds = seconds % 60 + + return "{:02.0f} hour(s) {:02.0f} minute(s) {:02.0f} second(s) ".format(hours, minutes, seconds) + + +class DownloadComponentCommand: + """Main download component command class""" + + def __init__(self): + self.ident = { + "name": "downloadcomp", + "usage": None, + "description": "Run to " + "download the file from path\n\texample: downloadcomp " + "/fwrepo/filename.exe --outdir " + "download the file by name\n\texample: downloadcomp " + "filename.exe --outdir ", + "summary": "Downloads components/binaries from the iLO Repository.", + "aliases": [], + "auxcommands": [], + } + + def run(self, line, help_disp=False): + """Wrapper function for download command main function + + :param line: command line input + :type line: string. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.downloadcomponentvalidation(options) + + if self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("iLO Repository commands are " "only available on iLO 5.") + + start_time = time.time() + ret = ReturnCodes.FAILED_TO_DOWNLOAD_COMPONENT + + self.rdmc.ui.printer("Downloading component, this may take a while...\n") + + if "blobstore" in self.rdmc.app.current_client.base_url: + ret = self.downloadlocally(options) + else: + ret = self.downloadfunction(options) + + self.rdmc.ui.printer("%s\n" % human_readable_time(time.time() - start_time)) + + self.cmdbase.logout_routine(self, options) + # Return code + return ret + + def downloadfunction(self, options): + """Main download command worker function + + :param options: command options (argparse) + :type options: options. + """ + filename = options.component.rsplit("/", 1)[-1] + + if not options.outdir: + destination = os.path.join(os.getcwd(), filename) + else: + destination = os.path.join(options.outdir, filename) + + if not os.path.exists(os.path.join(os.path.split(destination)[0])): + raise InvalidFileInputError("Invalid output file location.") + if not os.access(os.path.join(os.path.split(destination)[0]), os.W_OK): + raise InvalidFileInputError("File location is not writable.") + if os.access(destination, os.F_OK) and not os.access(destination, os.W_OK): + raise InvalidFileInputError("Existing File cannot be overwritten.") + + if options.component[0] != "/": + options.component = "/" + options.component + + if "fwrepo" not in options.component: + options.component = "/fwrepo/" + options.component + + results = self.rdmc.app.get_handler(options.component, uncache=True) + + if results.status == 404: + raise DownloadError( + "Downloading of component %s failed, please check the component name and check if the " + "component exists in the repository\n" % options.component + ) + + with open(destination, "wb") as local_file: + local_file.write(results.ori) + + self.rdmc.ui.printer("Download complete\n") + + return ReturnCodes.SUCCESS + + def downloadlocally(self, options=None): + """Used to download a component from the iLO Repo locally + + :param options: command options (argparse) + :type options: options. + """ + try: + dll = self.rdmc.app.current_client.connection._conn.channel.dll + dll.downloadComponent.argtypes = [c_char_p, c_char_p] + dll.downloadComponent.restype = c_int + + filename = options.component.rsplit("/", 1)[-1] + if not options.outdir: + destination = os.path.join(os.getcwd(), filename) + else: + destination = os.path.join(options.outdir, filename) + + if not os.path.exists(os.path.join(os.path.split(destination)[0])): + raise InvalidFileInputError("Invalid output file location.") + if not os.access(os.path.join(os.path.split(destination)[0]), os.W_OK): + raise InvalidFileInputError("File location is not writable.") + if os.access(destination, os.F_OK) and not os.access(destination, os.W_OK): + raise InvalidFileInputError("Existing File cannot be overwritten.") + + ret = dll.downloadComponent( + ctypes.create_string_buffer(filename.encode("utf-8")), + ctypes.create_string_buffer(destination.encode("utf-8")), + ) + + if ret != 0: + self.rdmc.ui.error( + "Component " + filename + " download failed, please check the " + "component name and check if the component exists in the respository.\n" + ) + return ReturnCodes.FAILED_TO_DOWNLOAD_COMPONENT + else: + self.rdmc.ui.printer("Component " + filename + " downloaded successfully.\n") + self.rdmc.ui.printer("[200] The operation completed successfully.\n") + + except Exception as excep: + raise DownloadError(str(excep)) + + return ReturnCodes.SUCCESS + + def downloadcomponentvalidation(self, options): + """Download command method validation function + + :param options: command options + :type options: options. + """ + self.rdmc.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for download command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "component", + help="""Component name (starting with path '/fwrepo/') of the target""" """ component.""", + metavar="[COMPONENT URI]", + ) + customparser.add_argument( + "--outdir", + dest="outdir", + help="""Output directory for saving the file.""", + default="", + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/FwpkgCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/FwpkgCommand.py new file mode 100644 index 0000000..f99f82e --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/FwpkgCommand.py @@ -0,0 +1,766 @@ +# ## +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ## + +# -*- coding: utf-8 -*- +"""Fwpkg Command for rdmc""" + +import os +import json +from random import randint +import shutil +import zipfile +import tempfile +from io import open + +import ctypes +from ctypes import c_char_p, c_int, c_bool + +from redfish.hpilo.risblobstore2 import BlobStore2 + +try: + from rdmc_helper import ( + LOGGER, + IncompatibleiLOVersionError, + ReturnCodes, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + UploadError, + TaskQueueError, + FirmwareUpdateError, + FlashUnsupportedByIloError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + LOGGER, + IncompatibleiLOVersionError, + ReturnCodes, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + UploadError, + TaskQueueError, + FirmwareUpdateError, + FlashUnsupportedByIloError, + ) + + +class FwpkgCommand: + """Fwpkg command class""" + + def __init__(self): + self.ident = { + "name": "flashfwpkg", + "usage": None, + "description": "Run to upload and flash " + "components from fwpkg files.\n\n\tUpload component and flashes it or sets a task" + "queue to flash.\n\texample: flashfwpkg component.fwpkg.\n\n\t" + "Skip extra checks before adding taskqueue. (Useful when adding " + "many flashfwpkg taskqueue items in sequence.)\n\texample: flashfwpkg " + "component.fwpkg --ignorechecks" + "\n\n\t Uploading component " + "flashfwpkg create taskqueue using target and flashes.\n\texample: flashfwpkg " + "component.fwpkg --targets ", + "summary": "Flashes fwpkg components using the iLO repository.", + "aliases": ["fwpkg"], + "auxcommands": [ + "UploadComponentCommand", + "UpdateTaskQueueCommand", + "FirmwareUpdateCommand", + "FwpkgCommand", + ], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main fwpkg worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + if "-" in line[0] and "thirdparty" in line[0]: + line.insert(0, "dummy_arg") + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.fwpkgvalidation(options) + + if self.rdmc.app.typepath.defs.isgen9: + LOGGER.error("iLO Repository commands are only available on iLO 5.") + raise IncompatibleiLOVersionError("iLO Repository commands are only available on iLO 5.") + + if self.rdmc.app.getiloversion() <= 5.120 and options.fwpkg.lower().startswith("iegen10"): + raise IncompatibleiLOVersionError( + "Please upgrade to iLO 5 1.20 or greater to ensure correct flash of this firmware." + ) + + updateservice_url = "/redfish/v1/UpdateService/" + # Get handler to check Accept3rdPartyFirmware enabled or disabled + get_content = self.rdmc.app.get_handler(updateservice_url, silent=True, service=True).dict + + accept3rdpartyfw = "" + if ( + get_content + and "Oem" in get_content + and "Hpe" in get_content["Oem"] + and "Accept3rdPartyFirmware" in get_content["Oem"]["Hpe"] + ): + accept3rdpartyfw = get_content["Oem"]["Hpe"]["Accept3rdPartyFirmware"] + + line_l = line[0].lower() + if ( + line + and ".fwpkg" not in line_l + and (".pup" in line_l or ".hpb" in line_l or ".fup" in line_l or ".bin" in line_l) + ): + if not accept3rdpartyfw and not options.enable_thirdparty_fw: + raise InvalidCommandLineErrorOPTS( + "Please use the option --enable_thirdparty_fw to flash Third " + "Party Firmware Packages.\nBy default only .fwpkg files are allowed.\n" + ) + + if options.enable_thirdparty_fw: + self.rdmc.ui.printer("Enabling Third party Firmware flashing.\n") + self.rdmc.app.patch_handler( + updateservice_url, + {"Oem": {"Hpe": {"Accept3rdPartyFirmware": True}}}, + silent=True, + service=True, + ) + if "dummy" in line[0]: + return ReturnCodes.SUCCESS + elif options.disable_thirdparty_fw: + self.rdmc.ui.printer("Disabling Third party Firmware flashing.\n") + self.rdmc.app.patch_handler( + updateservice_url, + {"Oem": {"Hpe": {"Accept3rdPartyFirmware": False}}}, + silent=True, + service=True, + ) + if "dummy" in line[0]: + return ReturnCodes.SUCCESS + + tempdir = "" + fwpkg_l = options.fwpkg.lower() + if ( + not fwpkg_l.endswith(".fwpkg") + and not fwpkg_l.endswith(".fup") + and not fwpkg_l.endswith(".hpb") + and not fwpkg_l.endswith(".bin") + and not fwpkg_l.endswith(".pup") + ): + LOGGER.error("Invalid file type. Please make sure the file provided is a valid .fwpkg file type.") + raise InvalidFileInputError( + "Invalid file type. Please make sure the file provided is a valid .fwpkg file type." + ) + + try: + components, tempdir, comptype, _ = self.preparefwpkg(self, options.fwpkg, command_name="flashfwpkg") + if comptype == "D": + LOGGER.error("Component Type D, Unable to flash this fwpkg file.") + raise InvalidFileInputError("Unable to flash this fwpkg file.") + + elif options.targets: + for component in components: + component_l = component.lower() + if ( + component_l.endswith(".fwpkg") + or component_l.endswith(".hpb") + or component_l.endswith(".HPb") + or component_l.endswith(".bin") + or component_l.endswith(".fup") + or component_l.endswith(".pup") + or component_l.endswith(".zip") + ): + uploadcommand = "--component %s" % component + else: + uploadcommand = "--component %s" % os.path.join(tempdir, component) + if options.forceupload: + uploadcommand += " --forceupload" + self.rdmc.ui.printer("Uploading firmware: %s\n" % os.path.basename(component)) + self.auxcommands["uploadcomp"].run(uploadcommand) + targets = [] + target_list = options.targets.split(",") + for target in target_list: + target_url = "/redfish/v1/UpdateService/FirmwareInventory/" + str(target) + "/" + targets.append(target_url) + verified = self.verify_targets(options.targets) + if not verified: + self.rdmc.ui.error("Provided target was not available, Please provide valid target id\n") + return ReturnCodes.INVALID_TARGET_ERROR + path = "/redfish/v1/UpdateService/UpdateTaskQueue/" + try: + self.taskqueuecheck() + except TaskQueueError as excp: + if options.ignore: + self.rdmc.ui.warn(str(excp) + "\n") + else: + raise excp + + comp = "%s" % os.path.basename(component) + try: + newtask = { + "Name": "Update-%s" % (str(randint(0, 1000000)),), + "Command": "ApplyUpdate", + "Filename": os.path.basename(comp), + "UpdatableBy": ["Bmc"], + "TPMOverride": options.tover, + "Targets": targets, + } + self.rdmc.ui.printer('payload: "%s"\n' % newtask) + except ValueError: + pass + self.rdmc.ui.printer('Creating task: "%s"\n' % newtask["Name"]) + self.rdmc.app.post_handler(path, newtask) + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + elif comptype in ["C", "BC"]: + try: + self.taskqueuecheck() + except TaskQueueError as excp: + if options.ignore: + self.rdmc.ui.warn(str(excp) + "\n") + else: + raise excp + self.applyfwpkg(options, tempdir, components, comptype) + + if comptype == "A": + message = "Firmware has successfully been flashed.\n" + if "ilo" in options.fwpkg.lower(): + message += "iLO will reboot to complete flashing. Session will be" " terminated.\n" + elif comptype in ["B", "BC"]: + message = ( + "Firmware has successfully been flashed and a reboot is required for " + "this firmware to take effect.\n" + ) + elif comptype in ["C", "BC"]: + message = "This firmware is set to flash on reboot.\n" + if ".bin" not in components[0]: + if "blobstore" not in self.rdmc.app.redfishinst.base_url: + if not self.auxcommands["uploadcomp"].wait_for_state_change(): + # Failed to upload the component. + raise FirmwareUpdateError("Error while processing the component.") + + self.rdmc.ui.printer(message) + + except (FirmwareUpdateError, UploadError) as excp: + raise excp + + finally: + if tempdir: + shutil.rmtree(tempdir) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def taskqueuecheck(self): + """Check taskqueue for potential issues before starting""" + + select = "ComputerSystem." + results = self.rdmc.app.select(selector=select, path_refresh=True) + + try: + results = results[0] + except: + pass + + powerstate = results.resp.dict["PowerState"] + tasks = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/UpdateTaskQueue/") + + for task in tasks: + if task["State"] == "Exception": + raise TaskQueueError( + "Exception found in taskqueue which will " + "prevent firmware from flashing. Please run " + "iLOrest command: taskqueue --cleanqueue to clear" + " any errors before continuing." + ) + if task["UpdatableBy"] == "Uefi" and not powerstate == "Off" or task["Command"] == "Wait": + raise TaskQueueError( + "Taskqueue item found that will " + "prevent firmware from flashing immediately. Please " + "run iLOrest command: taskqueue --resetqueue to " + "reset the queue if you wish to flash immediately " + "or include --ignorechecks to add this firmware " + "into the task queue anyway." + ) + if tasks: + self.rdmc.ui.warn( + "Items are in the taskqueue that may delay the flash until they " + "are finished processing. Use the taskqueue command to monitor updates.\n" + ) + + def get_comp_type(self, payload, command_name="flashfwpkg"): + """Gets the component type and returns it + + :param payload: json payload of .fwpkg file + :type payload: dict. + :returns: returns the type of component. Either A,B,C, or D. + :rtype: string + """ + ilo_ver_int = self.rdmc.app.getiloversion() + ctype = "" + if "Uefi" in payload["UpdatableBy"] and "RuntimeAgent" in payload["UpdatableBy"]: + ctype = "D" + elif "Uefi" in payload["UpdatableBy"] and "Bmc" in payload["UpdatableBy"]: + fw_url = "/redfish/v1/UpdateService/FirmwareInventory/" + "?$expand=." + data = self.rdmc.app.get_handler(fw_url, silent=True).dict["Members"] + da_flag = False + cc_flag = False + if data is not None: + type_set = None + for fw in data: + for device in payload["Devices"]["Device"]: + if ( + fw["Oem"]["Hpe"].get("Targets") is not None + and device["Target"] in fw["Oem"]["Hpe"]["Targets"] + ): + if fw["Oem"]["Hpe"].get("DeviceContext") is None: + LOGGER.error("DeviceContext is not found, please wait for a while & try again.") + raise UploadError("DeviceContext is not found, please wait for a while & try again.") + else: + # print device context in debugger + LOGGER.info("DeviceContext {}".format(fw["Oem"]["Hpe"]["DeviceContext"])) + if ( + "Slot=" in fw["Oem"]["Hpe"]["DeviceContext"] + and ":" in fw["Oem"]["Hpe"]["DeviceContext"] + ): + if ( + (ilo_ver_int >= 6.169 and ilo_ver_int < 7.000) or ilo_ver_int >= 7.113 + ) and command_name != "uploadcomp": + if fw["Updateable"]: + cc_flag = True + else: + raise FlashUnsupportedByIloError( + "The flashing of this component " "is not supported by iLO.\n" + ) + else: + cc_flag = True + else: + if fw["Updateable"]: + cc_flag = True + else: + da_flag = True + if cc_flag and da_flag: + ctype = "BC" + type_set = True + elif cc_flag and not da_flag: + ctype = "B" + type_set = True + elif not cc_flag and da_flag: + ctype = "C" + type_set = True + if type_set is None: + LOGGER.error("Component type is not identified, Please check if the particular H/W is present") + # ilo_ver_int = self.rdmc.app.getiloversion() + ilo_ver = str(ilo_ver_int) + error_msg = ( + "Cannot flash the component on this server, check whether the component is fwpkg-v2 " + "or check whether the server is iLO" + ) + if ilo_ver_int >= 6: + error_msg = ( + error_msg + ilo_ver[0] + ", FW is above 1.50 or the particular drive HW is present\n" + ) + else: + error_msg = ( + error_msg + ilo_ver[0] + ", FW is above 2.30 or the particular drive HW is present\n" + ) + raise IncompatibleiLOVersionError(error_msg) + else: + for device in payload["Devices"]["Device"]: + for image in device["FirmwareImages"]: + flash_key = next((key for key in image.keys() if key.lower() == "directflashok"), None) + + if flash_key is None: + raise InvalidFileInputError("Cannot flash this firmware.") + if flash_key and image[flash_key]: + ctype = "A" + if image["ResetRequired"]: + ctype = "B" + break + elif image.get("UefiFlashable", image.get("UEFIFlashable", False)): + ctype = "C" + break + else: + ctype = "D" + LOGGER.info("Component Type identified is {}".format(ctype)) + return ctype + + @staticmethod + def check_decoupled(self, pkgfile): + """check for decoupled json + + :param pkgfile: Location of the .fwpkg file + :type pkgfile: string. + :returns: returns the flag for flashing and decoupling + :rtype: bool, bool, string + """ + if os.path.dirname(pkgfile): # This means it's not just a file name + base_dir = os.path.dirname(pkgfile) + package_file = os.path.basename(pkgfile) + else: + base_dir = os.getcwd() + package_file = pkgfile + + package_name, _ = os.path.splitext(package_file) + + # Construct the expected path for the JSON file + json_file_name = f"{package_name}.json" + json_file_path = os.path.join(base_dir, json_file_name) + + # List all files in the base directory and check for a case-insensitive match + decoupled_flag = False + payload_file = None + + # List all files in the base directory + files_in_dir = os.listdir(base_dir) + + # Check if any file in the directory matches the JSON file name case-insensitively + for file in files_in_dir: + if file.lower() == json_file_name.lower(): + # File found, set flag and read the file + decoupled_flag = True + json_file_path = os.path.join(base_dir, file) # Update json_file_path to match the case of the file + with open(json_file_path, "r", encoding="utf-8") as json_file: + payload_file = json.load(json_file) + break # Exit once the file is found + + # If the file was not found , decoupled_flag remains False + return decoupled_flag, payload_file + + @staticmethod + def preparefwpkg(self, pkgfile, command_name="flashfwpkg"): + """Prepare fwpkg file for flashing + + :param pkgfile: Location of the .fwpkg file + :type pkgfile: string. + :returns: returns the files needed to flash, directory they are located + in, and type of file. + :rtype: string, string, string + """ + files = [] + imagefiles = [] + payloaddata = None + tempdir = tempfile.mkdtemp() + pldmflag = False + pkgfile_l = pkgfile.lower() + if ( + not pkgfile_l.endswith(".fup") + and not pkgfile_l.endswith(".hpb") + and not pkgfile_l.endswith(".bin") + and not pkgfile_l.endswith(".pup") + ): + decoupled_flag, payload_file = self.auxcommands["flashfwpkg"].check_decoupled(self, pkgfile) + if not decoupled_flag: + try: + zfile = zipfile.ZipFile(pkgfile) + zfile.extractall(tempdir) + zfile.close() + except Exception as excp: + raise InvalidFileInputError("Unable to unpack file. " + str(excp)) + + files = os.listdir(tempdir) + + if "payload.json" in files: + with open(os.path.join(tempdir, "payload.json"), encoding="utf-8") as pfile: + data = pfile.read() + payloaddata = json.loads(data) + else: + raise InvalidFileInputError( + "Invalid FWPKG component due to missing component metadata. \nPlease download the json file " + "along with FWPKG component or provide FWPKG component having payload.json file" + ) + else: + payloaddata = payload_file + if ( + not pkgfile_l.endswith(".fup") + and not pkgfile_l.endswith(".hpb") + and not pkgfile_l.endswith(".bin") + and not pkgfile_l.endswith(".pup") + ): + comptype = self.auxcommands["flashfwpkg"].get_comp_type(payloaddata, command_name) + else: + comptype = "A" + + results = self.rdmc.app.getprops(selector="UpdateService.", props=["Oem/Hpe/Capabilities"]) + if comptype in ["C", "BC"]: + imagefiles = [self.auxcommands["flashfwpkg"].type_c_change(tempdir, pkgfile)] + else: + if ( + not pkgfile_l.endswith("fup") + and not pkgfile_l.endswith(".hpb") + and not pkgfile_l.endswith(".bin") + and not pkgfile_l.endswith(".pup") + ): + for device in payloaddata["Devices"]["Device"]: + for firmwareimage in device["FirmwareImages"]: + if "PLDMImage" in firmwareimage and firmwareimage["PLDMImage"]: + pldmflag = True + if firmwareimage["FileName"] not in imagefiles: + imagefiles.append(firmwareimage["FileName"]) + + if ( + "blobstore" in self.rdmc.app.redfishinst.base_url + and comptype in ["A", "B", "BC"] + and results + and "UpdateFWPKG" in results[0]["Oem"]["Hpe"]["Capabilities"] + ): + dll = BlobStore2.gethprestchifhandle() + dll.isFwpkg20.argtypes = [c_char_p, c_int] + dll.isFwpkg20.restype = c_bool + + with open(pkgfile, "rb") as fwpkgfile: + fwpkgdata = fwpkgfile.read() + + fwpkg_buffer = ctypes.create_string_buffer(fwpkgdata) + if dll.isFwpkg20(fwpkg_buffer, 2048): + imagefiles = [pkgfile] + tempdir = "" + if ( + pkgfile_l.endswith(".hpb") + or pkgfile_l.endswith(".fup") + or pkgfile_l.endswith(".pup") + or pkgfile_l.endswith(".bin") + ): + imagefiles = [pkgfile] + + elif self.rdmc.app.getiloversion() > 5.230 and payloaddata.get("PackageFormat") == "FWPKG-v2": + imagefiles = [pkgfile] + return imagefiles, tempdir, comptype, pldmflag + + def type_c_change(self, tdir, pkgloc): + """Special changes for type C + + :param tempdir: path to temp directory + :type tempdir: string. + :param components: components to upload + :type components: list. + + :returns: The location of the type C file to upload + :rtype: string. + """ + + shutil.copy(pkgloc, tdir) + + fwpkgfile = os.path.split(pkgloc)[1] + zfile = fwpkgfile[:-6] + ".zip" + zipfileloc = os.path.join(tdir, zfile) + + os.rename(os.path.join(tdir, fwpkgfile), zipfileloc) + + return zipfileloc + + def applyfwpkg(self, options, tempdir, components, comptype): + """Apply the component to iLO + + :param options: command line options + :type options: list. + :param tempdir: path to temp directory + :type tempdir: string. + :param components: components to upload + :type components: list. + :param comptype: type of component. Either A,B,C, or D. + :type comptype: str. + """ + + for component in components: + component_l = component.lower() + if ( + component_l.endswith(".fwpkg") + or component_l.endswith(".hpb") + or component_l.endswith(".bin") + or component_l.endswith(".fup") + or component_l.endswith(".pup") + or component_l.endswith(".zip") + ): + uploadcommand = "--component %s" % component + else: + uploadcommand = "--component %s" % os.path.join(tempdir, component) + + if options.forceupload: + uploadcommand += " --forceupload" + if comptype in ["A", "B"]: + LOGGER.info("Setting --update_target --update_repository options as it is A or B") + uploadcommand += " --update_target --update_repository" + elif comptype in ["BC"]: + LOGGER.info("Setting --update_target --update_repository options and " "calling uploadcomp once.") + uploadcommand1 = uploadcommand + " --update_repository --update_target" + if options.tover: + LOGGER.info("Setting --tpmover if tpm enabled.") + uploadcommand1 += " --tpmover" + ret = self.auxcommands["uploadcomp"].run(uploadcommand1) + if ret != ReturnCodes.SUCCESS: + raise UploadError("Error uploading component.") + LOGGER.info( + "Continuing the flow with --component such that uploadcomp" + " is called again to upload component to the repository." + ) + if options.update_srs: + LOGGER.info("Setting --update_srs to store as recovery set.") + uploadcommand += " --update_srs" + if options.response: + LOGGER.info("Setting --response for extended UpdateService Status") + uploadcommand += " --response" + if options.tover: + LOGGER.info("Setting --tpmover if tpm enabled.") + uploadcommand += " --tpmover" + if "update_repository" not in uploadcommand: + self.rdmc.ui.printer("Uploading firmware: %s\n" % os.path.basename(component)) + if "update_target" in uploadcommand: + self.rdmc.ui.printer("Flashing firmware: %s\n" % os.path.basename(component)) + try: + ret = self.auxcommands["uploadcomp"].run(uploadcommand) + if ret != ReturnCodes.SUCCESS: + raise UploadError + except UploadError: + if comptype in ["A", "B", "BC"]: + select = self.rdmc.app.typepath.defs.hpilofirmwareupdatetype + results = self.rdmc.app.select(selector=select) + + try: + results = results[0] + except: + pass + + if results: + update_path = results.resp.request.path + error = self.rdmc.app.get_handler(update_path, silent=True) + self.auxcommands["firmwareupdate"].printerrmsg(error) + else: + raise FirmwareUpdateError("Error occurred while updating the firmware.") + else: + raise UploadError("Error uploading component.") + + if comptype in ["C", "BC"]: + self.rdmc.ui.warn("Setting a taskqueue item to flash UEFI flashable firmware.\n") + path = "/redfish/v1/updateservice/updatetaskqueue" + newtask = { + "Name": "Update-%s" % (str(randint(0, 1000000)),), + "Command": "ApplyUpdate", + "Filename": os.path.basename(component), + "UpdatableBy": ["Uefi"], + "TPMOverride": options.tover, + } + res = self.rdmc.app.post_handler(path, newtask) + + if res.status != 201: + raise TaskQueueError("Not able create UEFI task.\n") + else: + self.rdmc.ui.printer( + "Created UEFI Task for Component " + os.path.basename(component) + " successfully.\n" + ) + + def verify_targets(self, target): + target_list = target.split(",") + for target in target_list: + try: + target_url = "/redfish/v1/UpdateService/FirmwareInventory/" + target + dd = self.rdmc.app.get_handler(target_url, service=True, silent=True) + if dd.status == 404: + return False + except: + return False + return True + + def fwpkgvalidation(self, options): + """fwpkg validation function + + :param options: command line options + :type options: list. + """ + self.rdmc.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument("fwpkg", help="""fwpkg file path""", metavar="[FWPKG]", default=None) + customparser.add_argument( + "--forceupload", + dest="forceupload", + action="store_true", + help="Add this flag to force upload firmware with the same name already on the repository.", + default=False, + ) + customparser.add_argument( + "--ignorechecks", + dest="ignore", + action="store_true", + help="Add this flag to ignore all checks to the taskqueue before attempting to process the .fwpkg file.", + default=False, + ) + customparser.add_argument( + "--enable_thirdparty_fw", + dest="enable_thirdparty_fw", + action="store_true", + help="Add this flag to enable flashing third party firmware to server.", + default=False, + ) + customparser.add_argument( + "--disable_thirdparty_fw", + dest="disable_thirdparty_fw", + action="store_true", + help="Add this flag to disable flashing third party firmware to server.", + default=False, + ) + customparser.add_argument( + "--tpmover", + dest="tover", + action="store_true", + help="If set then the TPMOverrideFlag is passed in on the associated flash operations", + default=False, + ) + customparser.add_argument( + "--update_srs", + dest="update_srs", + action="store_true", + help="Add this flag to update the System Recovery Set with the uploaded firmware. " + "NOTE: This requires an account login with the system recovery set privilege.", + default=False, + ) + customparser.add_argument( + "--response", + dest="response", + action="store_true", + help="Use this flag to return the iLO response body.", + default=False, + ) + customparser.add_argument( + "--targets", + help="If targets value specify a comma separated\t" "firmwareinventory id only", + metavar="targets_indices", + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/InstallSetCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/InstallSetCommand.py new file mode 100644 index 0000000..be7140f --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/InstallSetCommand.py @@ -0,0 +1,576 @@ +# ## +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ## + +# -*- coding: utf-8 -*- +"""Install Set Command for rdmc""" + +import json +import re +from argparse import RawDescriptionHelpFormatter +from datetime import datetime + +try: + from rdmc_helper import ( + LOGGER, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + NoContentsFoundForOperationError, + ReturnCodes, + InstallsetError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + LOGGER, + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileInputError, + NoContentsFoundForOperationError, + ReturnCodes, + InstallsetError, + ) + + +class InstallSetCommand: + """Main download command class""" + + def __init__(self): + self.ident = { + "name": "installset", + "usage": None, + "description": "Run to perform operations on install sets.\nTo view help on specific " + "sub-commands run: installset -h\n\n\tExample: installset " + "add -h\nNote: iLO 5 required.", + "summary": "Manages install sets for iLO.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main listcomp worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + _subcommands = ["add", "delete", "invoke"] + found = False + for i, arg in enumerate(line): + if arg in _subcommands: + found = True + try: + if line[i + 1] not in list(self.parser._option_string_actions.keys()): + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + else: + raise IndexError + except (KeyError, IndexError): + (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True) + else: + continue + if not found: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.installsetvalidation(options) + + if self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("iLO Repository commands are " "only available on iLO 5.") + + if hasattr(options, "name"): + if options.name: + if options.name.startswith('"') and options.name.endswith('"'): + options.name = options.name[1:-1] + if hasattr(options, "removeall"): + if options.removeall: + self.removeinstallsets(options) + if options.command.lower() == "add": + if not options.json: + raise InvalidCommandLineError("add command requires an install set file.") + else: + self.addinstallset(options.json, options) + elif options.command.lower() == "delete" and (hasattr(options, "removeall") and not options.removeall): + self.deleteinstallset(options) + # elif options.command.lower() == "delete" and not hasattr(options, "removeall"): + # self.deleteinstallset(options.name) + elif options.command.lower() == "invoke": + self.invokeinstallset(options) + else: + self.printinstallsets(options) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def addinstallset(self, setfile, options): + """Adds an install set + :param setfile: filename of install set + :type setfile: str. + :param name: string of the install set name to add + :type name: str. + """ + path = "/redfish/v1/UpdateService/InstallSets/" + comps = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/ComponentRepository/") + + sets = self.rdmc.app.getcollectionmembers(path) + + if not options.name: + options.name = str(datetime.now()) + options.name = options.name.replace(":", "") + options.name = options.name.replace(" ", "T") + try: + inputfile = open(setfile, "r") + sequences = json.loads(inputfile.read()) + except Exception as excp: + raise InvalidFileInputError("%s" % excp) + + listtype = self.validatefile(sequences) + + if listtype: + body = {"Name": options.name, "Sequence": sequences} + else: + body = sequences + if "Sequence" in body: + sequences = body["Sequence"] + else: + raise InvalidFileInputError("Invalid install set file.") + sequences = body["Sequence"] + + filenamelist = [x["Filename"] for x in comps] + + for sequence in sequences: + if sequence["Command"] == "ApplyUpdate": + if "Filename" in list(sequence.keys()): + if not sequence["Filename"] in filenamelist: + raise NoContentsFoundForOperationError( + "Component" + " referenced in install set is not present on" + " iLO Drive: %s" % sequence["Filename"] + ) + elif sequence["Command"] == "WaitTimeSeconds": + sequence["WaitTimeSeconds"] = int(sequence["WaitTimeSeconds"]) + for setvar in sets: + if setvar["Name"] == body["Name"]: + raise InvalidCommandLineError("Install set name is already in use.") + + # self.rdmc.app.post_handler(path, body) + if "IsRecovery" in body and body["IsRecovery"]: + self.rdmc.ui.printer("This is a system recovery install set: %s\n" % body["Name"]) + self.rdmc.ui.warn("Adding system recovery set: %s\n" % body["Name"]) + if not (options.user and options.password): + raise InstallsetError( + "Adding system recovery set needs to be passed with " "--user and --password options, add failed\n" + ) + if "blobstore" in self.rdmc.app.current_client.base_url: + LOGGER.info("Logging out of the session without user and password") + self.rdmc.app.current_client.logout() + LOGGER.info("Logging in with user and password for deleting system recovery set") + self.rdmc.app.current_client._user_pass = (options.user, options.password) + self.rdmc.app.current_client.login(self.rdmc.app.current_client.auth_type) + self.rdmc.app.post_handler(path, body) + else: + self.rdmc.app.post_handler(path, body) + else: + self.rdmc.app.post_handler(path, body) + + def invokeinstallset(self, options): + """Invokes the named set + :param name: string of the install set name to invoke + :type name: str. + """ + path = None + name = options.name + sets = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/InstallSets/") + + for setvar in sets: + if setvar["Name"] == name: + path = setvar["Actions"]["#HpeComponentInstallSet.Invoke"]["target"] + + if not path: + raise NoContentsFoundForOperationError("No install set with the" " provided name could be found.") + + self.rdmc.ui.printer("Invoking install set:%s\n" % name) + + payload = self.checkpayloadoptions(options) + self.rdmc.app.post_handler(path, payload) + + def deleteinstallset(self, options): + """Deletes the named set + + :param name: string of the install set name to delete + :type name: str. + """ + self.rdmc.ui.printer("Deleting install set: %s...\n" % options.name) + sets = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/InstallSets/") + found = False + + for setvar in sets: + if setvar["Name"] == options.name: + found = True + path = setvar["@odata.id"] + if "IsRecovery" in setvar and setvar["IsRecovery"]: + self.rdmc.ui.printer("This is a system recovery install set: %s\n" % setvar["Name"]) + self.rdmc.ui.warn("Deleting system recovery set: %s\n" % setvar["Name"]) + if not (options.user and options.password): + raise InstallsetError( + "Deleting system recovery set needs to be passed with " + "--user and --password options, delete failed\n" + ) + if "blobstore" in self.rdmc.app.current_client.base_url: + LOGGER.info("Logging out of the session without user and password") + self.rdmc.app.current_client.logout() + LOGGER.info("Logging in with user and password for deleting system recovery set") + self.rdmc.app.current_client._user_pass = (options.user, options.password) + self.rdmc.app.current_client.login(self.rdmc.app.current_client.auth_type) + self.rdmc.app.delete_handler(path) + else: + self.rdmc.app.delete_handler(path) + else: + self.rdmc.app.delete_handler(path) + if not found: + raise NoContentsFoundForOperationError("Unable to find the specified install set.") + + def removeinstallsets(self, options): + """Removes all install sets""" + sets = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/InstallSets/") + + if not sets: + self.rdmc.ui.printer("No install sets found.\n") + + self.rdmc.ui.printer("Deleting all install sets...\n") + + for setvar in sets: + if setvar["IsRecovery"]: + self.rdmc.ui.warn("Attempting to delete recovery set: %s\n" % setvar["Name"]) + if not (options.user and options.password): + raise InstallsetError( + "Deleting system recovery set needs to be passed with " + "--user and --password options, delete failed\n" + ) + self.rdmc.ui.warn("Deleting system recovery set: %s\n" % setvar["Name"]) + LOGGER.debug( + "This is to inform that ,Recovery install set %s will also be " + "deleted as RemoveAll option is being used\n" % setvar["Name"] + ) + + if "blobstore" in self.rdmc.app.current_client.base_url: + LOGGER.info("Logging out of the session without user and password") + self.rdmc.app.current_client.logout() + LOGGER.info("Logging in with user and password for deleting system recovery set") + self.rdmc.app.current_client._user_pass = (options.user, options.password) + self.rdmc.app.current_client.login(self.rdmc.app.current_client.auth_type) + self.rdmc.app.delete_handler(setvar["@odata.id"]) + else: + self.rdmc.app.delete_handler(setvar["@odata.id"]) + else: + self.rdmc.ui.printer("Deleting install set: %s\n" % setvar["Name"]) + self.rdmc.app.delete_handler(setvar["@odata.id"]) + + def build_json_out(self, options): + sets = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/InstallSets/") + list_content = dict() + for setvar in sets: + content = dict() + content[setvar["Name"]] = dict() + content[setvar["Name"]].update({"IsRecovery": setvar["IsRecovery"]}) + list_content.update(content) + + if "Sequence" not in list(setvar.keys()): + content.update({"No Sequences in set."}) + continue + + for item in setvar["Sequence"]: + if "Filename" in list(item.keys()): + name = item["Name"] + filename_cmd = "%s %s" % (item["Command"], item["Filename"]) + content[setvar["Name"]].update({name: {filename_cmd}}) + elif "WaitTimeSeconds" in list(item.keys()): + wait_name = item["Name"] + wait_command = "%s %s seconds" % ( + item["Command"], + str(item["WaitTimeSeconds"]), + ) + content[setvar["Name"]].update({wait_name: {wait_command}}) + else: + content[setvar["Name"]].update({item["Name"]: {item["Command"]}}) + return list_content + + def printinstallsets(self, options): + """Prints install sets""" + sets = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/InstallSets/") + if not options.json: + self.rdmc.ui.printer("Install Sets:\n") + + if not sets: + self.rdmc.ui.printer("No install sets found.\n") + elif not options.json: + for setvar in sets: + if setvar["IsRecovery"]: + recovery = "[Recovery Set]" + else: + recovery = "" + self.rdmc.ui.printer("\n%s: %s\n" % (setvar["Name"], recovery)) + + if "Sequence" not in list(setvar.keys()): + self.rdmc.ui.warn("\tNo Sequences in set.\n") + continue + + for item in setvar["Sequence"]: + if "Filename" in list(item.keys()): + self.rdmc.ui.printer("\t%s: %s %s\n" % (item["Name"], item["Command"], item["Filename"])) + elif "WaitTimeSeconds" in list(item.keys()): + self.rdmc.ui.printer( + "\t%s: %s %s seconds\n" + % ( + item["Name"], + item["Command"], + str(item["WaitTimeSeconds"]), + ) + ) + else: + self.rdmc.ui.printer("\t%s: %s\n" % (item["Name"], item["Command"])) + elif options.json: + json_content = self.build_json_out(options) + self.rdmc.ui.print_out_json(json_content) + + def validatefile(self, installsetfile): + """validates json file + + :param file: json file to validate + :type file: string. + """ + listtype = True + keylist = ["Name", "UpdatableBy", "Command", "WaitTimeSeconds", "Filename"] + + if isinstance(installsetfile, list): + for item in installsetfile: + for key in item: + if key not in keylist: + raise InvalidFileInputError("Property %s is invalid " "for an install set." % key) + else: + listtype = False + + return listtype + + def checkpayloadoptions(self, options): + """checks for payload options and adds needed ones to payload + + :param options: command line options + :type options: list. + """ + + payload = {} + if options.exafter: + self.checktime(options.exafter) + payload["Expire"] = options.exafter + if options.safter: + self.checktime(options.safter) + payload["StartAfter"] = options.safter + if options.tover: + payload["TPMOverride"] = True + if options.uset: + payload["UpdateRecoverySet"] = True + if options.ctakeq: + payload["ClearTaskQueue"] = True + + return payload + + def checktime(self, timestr): + """check the time string for valid format + + :param timestr: time string to check + :type timestr: string. + """ + rfdtregex = "\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ?" + if not re.match(rfdtregex, timestr): + raise InvalidCommandLineError( + "Invalid redfish date-time format. " "Accepted formats: YYYY-MM-DDThh:mm:ss, YYYY-MM-DDThh:mm:ssZ" + ) + + def installsetvalidation(self, options): + """installset validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + # default sub-parser + default_parser = subcommand_parser.add_parser( + "default", + help="Running without any sub-command will return a list of all available install " + "sets on the currently logged in server.", + ) + default_parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + default_parser.add_argument( + "--removeall", + dest="removeall", + help="Optionally include this flag to remove all install sets on the currently logged " "in server.", + action="store_true", + default=False, + ) + self.cmdbase.add_login_arguments_group(default_parser) + + # add sub-parser + add_help = "Adds an install set on the currently logged in server." + add_parser = subcommand_parser.add_parser( + "add", + help=add_help, + description=add_help + "\n\texample: installset add installsetfile.json " + "--name=newinstallsetname.\n\n\tNote: iLO will provide a default " + "install set name if the flag '--name' is not provided.", + formatter_class=RawDescriptionHelpFormatter, + ) + add_parser.add_argument( + "json", + help="Json file containing the install set tasks and sequencing" + '\n\n\texample install set JSON file:\n\t[\n\t\t{\n\t\t\t"Name": ' + '"Wait",\n\t\t\t"UpdatableBy": ["RuntimeAgent"],\n\t\t\t' + '"Command": "Wait",\n\t\t\t"WaitTimeSeconds": 60\n\t\t},\n\t\t{' + '\n\t\t\t"Name": "uniqueName",\n\t\t\t"UpdatableBy": ' + '["RuntimeAgent"],\n\t\t\t"Command": "ApplyUpdate",\n\t\t\t"' + 'Filename": "filename.exe"\n\t\t},\n\t\t{\n\t\t\t"Name": ' + '"Reboot",\n\t\t\t"UpdatableBy": ["RuntimeAgent"],\n\t\t\t' + '"Command": "ResetServer"\n\t\t}\n\t]', + metavar="JSONFILE", + ) + add_parser.add_argument("-n", "--name", dest="name", help="Install set name.", default=None) + + self.cmdbase.add_login_arguments_group(add_parser) + + # invoke sub-parser + invoke_help = "Invoke execution of an install script on the currently logged in server." + invoke_parser = subcommand_parser.add_parser( + "invoke", + help=invoke_help, + description=invoke_help + "\n\nExamples:\n\tTo simply invoke an install set\n\t" + "installset invoke --name=installsetname", + formatter_class=RawDescriptionHelpFormatter, + ) + invoke_parser.add_argument( + "-n", + "--name", + help="Install set name.", + metavar="NAME", + nargs="?", + default="", + required=True, + ) + invoke_parser.add_argument( + "--cleartaskqueue", + dest="ctakeq", + action="store_true", + help="This option allows previous items in the task queue to" + " be cleared before the Install Set is invoked", + default=False, + ) + invoke_parser.add_argument( + "--expire", + dest="exafter", + help="Optionally include this flag if you wish to set the" + " time for installset to expire. ISO 8601 Redfish-style time " + "string to be written after which iLO will automatically change " + "state to Expired", + default=None, + ) + invoke_parser.add_argument( + "--startafter", + dest="safter", + help="Optionally include this flag if you wish to set the" + " earliest execution time for installset. ISO 8601 Redfish-style " + "time string to be used.", + default=None, + ) + invoke_parser.add_argument( + "--tpmover", + dest="tover", + action="store_true", + help="If set then the TPMOverrideFlag is passed in on the " "associated flash operations", + default=False, + ) + invoke_parser.add_argument( + "--updaterecoveryset", + dest="uset", + action="store_true", + help="If set then the components in the flash operations are used" + " to replace matching contents in the Recovery Set.", + default=False, + ) + self.cmdbase.add_login_arguments_group(invoke_parser) + + # delete sub-parser + delete_help = "Delete one or more install sets from the currently logged in server." + delete_parser = subcommand_parser.add_parser( + "delete", + help=delete_help, + description=delete_help + "\n\nExamples:\n\nTo delete a single install set:\n\t" + "installset delete --name=installsetname.\n\nTo delete all install sets\n\t" + "installset delete --removeall", + formatter_class=RawDescriptionHelpFormatter, + ) + delete_parser.add_argument( + "-n", + "--name", + help="Install set name", + metavar="NAME", + nargs="?", + default="", + ) + delete_parser.add_argument( + "--removeall", + dest="removeall", + help="Optionally include this flag to remove all install sets on the currently logged " "in server.", + action="store_true", + default=False, + ) + self.cmdbase.add_login_arguments_group(delete_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/ListComponentCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/ListComponentCommand.py new file mode 100644 index 0000000..a5b1a27 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/ListComponentCommand.py @@ -0,0 +1,140 @@ +# ## +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ## + +# -*- coding: utf-8 -*- +"""List Component Command for rdmc""" + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class ListComponentCommand: + """Main download command class""" + + def __init__(self): + self.ident = { + "name": "listcomp", + "usage": None, + "description": "Run to list the components of " "the currently logged in system.\n\texample: listcomp", + "summary": "Lists components/binaries from the iLO Repository.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main listcomp worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.listcomponentvalidation(options) + + if self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("iLO Repository commands are " "only available on iLO 5.") + + comps = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/ComponentRepository/") + + if comps: + self.printcomponents(comps, options) + else: + self.rdmc.ui.printer("No components found.\n") + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def printcomponents(self, comps, options): + """Print components function + + :param comps: list of components + :type comps: list. + """ + if options.json: + jsonout = dict() + for comp in comps: + jsonout[comp["Id"]] = comp + self.rdmc.ui.print_out_json(jsonout) + else: + for comp in comps: + self.rdmc.ui.printer( + "Id: %s\nName: %s\nVersion: %s\nLocked:%s\nComponent " + "Uri:%s\nFile Path: %s\nSizeBytes: %s\n\n" + % ( + comp["Id"], + comp["Name"], + comp["Version"], + "Yes" if comp["Locked"] else "No", + comp["ComponentUri"], + comp["Filepath"], + str(comp["SizeBytes"]), + ) + ) + + def listcomponentvalidation(self, options): + """listcomp validation function + + :param options: command line options + :type options: list. + """ + self.rdmc.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/MaintenanceWindowCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/MaintenanceWindowCommand.py new file mode 100644 index 0000000..9e6f16c --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/MaintenanceWindowCommand.py @@ -0,0 +1,326 @@ +# ## +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ## + +# -*- coding: utf-8 -*- +"""Update Task Queue Command for rdmc""" + +import re +from argparse import RawDescriptionHelpFormatter +from random import randint + +from redfish.ris.rmc_helper import ValidationError + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + ) + +__subparsers__ = ["add", "delete"] + + +class MaintenanceWindowCommand: + """Main maintenancewindow command class""" + + def __init__(self): + self.ident = { + "name": "maintenancewindow", + "usage": None, + "description": "Add or delete maintenance windows from the iLO repository.\nTo " + " view help on specific sub-commands run: maintenancewindow -h\n\n" + "NOTE: iLO 5 required.", + "summary": "Manages the maintenance windows for iLO.", + "aliases": [], + "auxcommands": [], + } + + def run(self, line, help_disp=False): + """Main update maintenance window worker function + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + ident_subparser = False + for cmnd in __subparsers__: + if cmnd in line: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + ident_subparser = True + break + if not ident_subparser: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.maintenancewindowvalidation(options) + + if self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("iLO Repository commands are only available on iLO 5.") + + windows = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/MaintenanceWindows/") + + if options.command.lower() == "add": + self.addmaintenancewindow(options, windows, options.time_window) + elif options.command.lower() == "delete": + self.deletemaintenancewindow(windows, options.identifier) + else: + self.listmainenancewindows(options, windows) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def addmaintenancewindow(self, options, windows, startafter): + """Add a maintenance window + + :param options: command line options + :type options: list. + :param windows: list of maintenance windows on the system + :type windows: list. + :param startafter: redfish date-time string to start the maintenance window + :type startafter: str. + """ + adddata = {"StartAfter": startafter} + + if options.name: + adddata["Name"] = options.name + else: + adddata["Name"] = "MW-%s" % str(randint(0, 1000000)) + + if options.description: + if options.description.startswith('"') and options.description.endswith('"'): + options.description = options.description[1:-1] + adddata["Description"] = options.description + + if options.expire: + adddata["Expire"] = options.expire + + errors = self.validatewindow(adddata, windows) + + if not errors: + path = "/redfish/v1/UpdateService/MaintenanceWindows/" + self.rdmc.app.post_handler(path, adddata) + else: + self.rdmc.ui.error("Invalid Maintenance Window:\n") + for error in errors: + self.rdmc.ui.error("\t" + error + "\n") + raise ValidationError("") + + def deletemaintenancewindow(self, windows, nameid): + """Delete a maintenance window by Id or Name + + :param windows: list of maintenance windows on the system + :type windows: list. + :param nameid: id or name string to remove + :type nameid: str. + """ + + deleted = False + for window in windows: + if window["Name"] == nameid or window["Id"] == nameid: + path = window["@odata.id"] + self.rdmc.ui.printer("Deleting %s\n" % nameid) + self.rdmc.app.delete_handler(path) + deleted = True + break + + if not deleted: + raise NoContentsFoundForOperationError("No maintenance window found with that Name/Id.") + + def listmainenancewindows(self, options, windows): + """Lists the maintenance windows + + :param options: command line options + :type options: list. + :param windows: list of maintenance windows on the system + :type windows: list. + """ + + outstring = "" + jsonwindows = [] + + if windows: + for window in windows: + if options.json: + jsonwindows.append(dict((key, val) for key, val in window.items() if "@odata." not in key)) + else: + outstring += "%s:" % window["Name"] + outstring += "%s:" % window["Id"] + if "Description" in list(window.keys()) and window["Description"]: + outstring += "\n\tDescription: %s" % window["Description"] + else: + outstring += "\n\tDescription: %s" % "No description." + outstring += "\n\tStart After: %s" % window["StartAfter"] + if "Expire" in list(window.keys()): + outstring += "\n\tExpires at: %s" % window["Expire"] + else: + outstring += "\n\tExpires at: %s" % "No expire time set." + outstring += "\n" + if jsonwindows: + self.rdmc.ui.print_out_json(jsonwindows) + else: + self.rdmc.ui.printer(outstring) + else: + self.rdmc.ui.warn("No maintenance windows found on system.\n") + + def validatewindow(self, cmw, windows): + """Validate the maintenance window before adding + + :param cmw: a maintenance window candidate + :type cmw: dict. + :param windows: list of maintenance windows on the system + :type windows: list. + :returns: returns a list of errors or a empty list if no errors + """ + errorlist = [] + rfdtregex = "\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\dZ?" + + for window in windows: + if cmw["Name"] == window["Name"]: + errorlist.append("Maintenance window with Name: %s already exists." % cmw["Name"]) + + if "Name" in list(cmw.keys()): + if len(cmw["Name"]) > 64: + errorlist.append("Name must be 64 characters or less.") + if "Description" in list(cmw.keys()): + if len(cmw["Description"]) > 64: + errorlist.append("Description must be 64 characters or less.") + if "Expire" in list(cmw.keys()): + if not re.match(rfdtregex, cmw["Expire"]): + errorlist.append( + "Invalid redfish date-time format in Expire. " + "Accepted formats: YYYY-MM-DDThh:mm:ss, YYYY-MM-DDThh:mm:ssZ" + ) + if not re.match(rfdtregex, cmw["StartAfter"]): + errorlist.append( + "Invalid redfish date-time format in StartAfter. " + "Accepted formats YYYY-MM-DDThh:mm:ss, YYYY-MM-DDThh:mm:ssZ" + ) + + return errorlist + + def maintenancewindowvalidation(self, options): + """maintenencewindow validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + subcommand_parser = customparser.add_subparsers(dest="command") + + # default sub-parser + default_parser = subcommand_parser.add_parser( + "default", + help="Running without any sub-command will return all maintenace windows on the " + "currently logged in server.", + ) + default_parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + self.cmdbase.add_login_arguments_group(default_parser) + + # add sub-parser + add_help = "Adds a maintenance window to iLO" + add_parser = subcommand_parser.add_parser( + "add", + help=add_help, + description=add_help + "\nexample: maintenancewindow add 1998-11-21T00:00:00 " + "--expire=1998-11-22T00:00:00 --name=MyMaintenanceWindow --description " + '"My maintenance window description.,"', + formatter_class=RawDescriptionHelpFormatter, + ) + add_parser.add_argument( + "time_window", + help="Specify the time window start period in DateTime format.\ni.e. YEAR-MONTH-DAY" + "THOUR:MINUTE:SECOND.\nexample: 1998-11-21T10:59:58", + metavar="TIMEWINDOW", + ) + add_parser.add_argument( + "--description", + dest="description", + help="Optionally include this flag if you would like to add a " + "description to the maintenance window you create", + default=None, + ) + add_parser.add_argument( + "--name", + dest="name", + help="Optionally include this flag if you would like to add a " + "name to the maintenance window you create. If you do not specify one" + " a unique name will be added.", + default=None, + ) + add_parser.add_argument( + "--expire", + dest="expire", + help="Optionally include this flag if you would like to add a " "time the maintenance window expires.", + default=None, + ) + self.cmdbase.add_login_arguments_group(add_parser) + + # delete sub-parser + delete_help = "Deletes the specified maintenance window on the currently logged in server." + delete_parser = subcommand_parser.add_parser( + "delete", + help=delete_help, + description=delete_help + "\nexample: maintenancewindow delete mymaintenancewindowname\n" + "Note: The maintenance window identifier can be referenced by Name or ID#." + "maintenancewindow delete name", + formatter_class=RawDescriptionHelpFormatter, + ) + delete_parser.add_argument( + "identifier", + help="The unique identifier provided by iLO or the identifier provided by '--name' " + "when the maintenance window was created.", + ) + self.cmdbase.add_login_arguments_group(delete_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/MakeInstallSetCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/MakeInstallSetCommand.py new file mode 100644 index 0000000..b934282 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/MakeInstallSetCommand.py @@ -0,0 +1,340 @@ +# ## +# Copyright 2016-2021 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ## + +# -*- coding: utf-8 -*- +"""Install Set Command for rdmc""" + +import json +import os + +from six.moves import input + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + ReturnCodes, + ) + + +class MakeInstallSetCommand: + """Command class to create installset payload""" + + def __init__(self): + self.ident = { + "name": "makeinstallset", + "usage": None, + "description": "Run to enter a guided shell for making " + "install sets. If not currently\n\tlogged into a server will perform " + "basic guidance on making an installset,\n\tif logged into a server " + "will provide guidance based on the current\n\tcomponents on the system. " + "If you wish to use this command on a logged in\n\tserver upload the " + "components before running for best results.", + "summary": "Creates install sets for iLO.", + "aliases": ["minstallset"], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self.defaultprops = { + "UpdatableBy": ["Bmc"], + "Command": "ApplyUpdate", + "WaitTimeSeconds": 0, + "Filename": "", + } + self.helptext = { + "Command": "Possible Commands: ApplyUpdate, ResetServer, " "ResetBmc, Wait", + "UpdatableBy": "Possible Update parameter(s)" + ":\nBmc: Updatable by iLO\nUefi: Updatable by Uefi\n" + "RuntimeAgent: Updatable by runtime agent such as SUM/SUT", + "WaitTimeSeconds": "Number of seconds to pause in Wait " "command.", + "Filename": "Unique filename of component on " "iLO repository", + } + self.loggedin = None + self.comps = None + + def run(self, line, help_disp=False): + """Main installset worker function + + :param line: string of arguments passed in + :type line: str. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.loggedin = None + self.comps = None + self.loggedin = self.minstallsetvalidation() + + if self.loggedin and self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("iLO Repository commands are " "only available on iLO 5.") + + self.rdmc.ui.warn("This command will run in interactive mode.\n") + if args: + raise InvalidCommandLineError("makeinstallset command takes no arguments.") + + self.minstallsetworker(options) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def minstallsetworker(self, options): + """Main installset creation worker + + :param options: command line options + :type options: list. + """ + comps = {} + totcomps = [] + count = -1 + totcount = 0 + self.rdmc.ui.warn("Entering new shell, type quit to leave!\n") + if self.loggedin: + self.rdmc.ui.printer("Running in logged in mode.") + self.comps = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/ComponentRepository/") + else: + self.rdmc.ui.printer("Running in basic guidance mode.") + while True: + comps = {} + count = -1 + reqdprops = ["Command"] + while True: + if len(reqdprops) <= count: + break + if count == -1: + line = input("\nEnter a name for this command: ") + else: + self.rdmc.ui.printer("\n" + self.helptext[reqdprops[count]] + "\n") + if self.loggedin and reqdprops[count].lower() == "filename": + filenm, updateby, target = self.checkfiles(options) + comps["Filename"] = filenm + comps["UpdatableBy"] = updateby + if options.targets: + comps["Targets"] = target + break + else: + line = input("Enter " + reqdprops[count] + " for " + comps["Name"] + ": ") + if line.endswith(os.linesep): + line.rstrip(os.linesep) + if line == "quit": + break + elif line == "" and not count == -1: + line = self.defaultprops[reqdprops[count]] + if count == -1: + comps["Name"] = line + else: + while True: + validated = self.validatepropvalue(reqdprops[count], line, reqdprops) + if not validated: + if line == "quit": + break + line = input( + "Input %s is not a valid property " "for %s. Try again: " % (line, reqdprops[count]) + ) + else: + comps[reqdprops[count]] = validated + break + count = count + 1 + if line == "quit": + break + else: + totcomps.append(comps) + totcount = totcount + 1 + + if not totcount: + self.rdmc.ui.warn("No sequences created. Exiting without creating an installset.\n") + else: + while True: + isrecovery = input("Is this a recovery installset? ") + isrecovery = True if str(isrecovery).lower() in ["true", "t", "yes", "y"] else isrecovery + isrecovery = False if str(isrecovery).lower() in ["false", "f", "no", "n"] else isrecovery + if not isinstance(isrecovery, bool): + self.rdmc.ui.warn("'Isrecovery' should be either true or false.\n") + continue + else: + break + + if isrecovery: + installsetname = "System Recovery Set" + else: + while True: + installsetname = input("Enter installset name: ") + if not installsetname: + self.rdmc.ui.warn("Install set must have a name.\n") + else: + break + + description = input("Enter description for the installset: ") + + body = { + "Name": installsetname, + "Description": description, + "IsRecovery": isrecovery, + "Sequence": totcomps, + } + + self.rdmc.ui.print_out_json(body) + with open(options.filename, "w") as outfile: + json.dump(body, outfile, indent=2, sort_keys=True) + + self.rdmc.ui.printer("installset saved to %s\n" % options.filename) + + def validatepropvalue(self, propvalue, givenvalue, reqdprops): + """Validates a string returning the correct type + + :param propvalue: property to validate + :type propvalue: string. + :param givenvalue: value to validate + :type givenvalue: string. + :param reqdprops: the required property list + :type reqdprops: list. + """ + validated_property = None + if propvalue == "WaitTimeSeconds": + validated_property = int(givenvalue) + elif propvalue == "UpdatableBy": + if isinstance(givenvalue, list): + validated_property = givenvalue + value = [x.strip() for x in givenvalue.split(",")] + for ind, item in enumerate(value): + if item.lower() == "runtimeagent": + value[ind] = "RuntimeAgent" + elif item.lower() == "uefi": + value[ind] = "Uefi" + elif item.lower() == "bmc": + value[ind] = "Bmc" + validated_property = value + elif propvalue == "Command": + if givenvalue.lower() == "applyupdate": + if self.loggedin and not self.comps: + self.rdmc.ui.printer("All components on the system are already " "added to the installset.\n") + else: + reqdprops.append("Filename") + reqdprops.append("UpdatableBy") + validated_property = "ApplyUpdate" + elif givenvalue.lower() == "resetserver": + validated_property = "ResetServer" + elif givenvalue.lower() == "resetbmc": + validated_property = "ResetBmc" + elif givenvalue.lower() == "wait": + reqdprops.append("WaitTimeSeconds") + validated_property = "Wait" + elif propvalue == "Filename": + if givenvalue: + validated_property = givenvalue + + return validated_property + + def get_target(self, target): + target_list = target.split(",") + for target in target_list: + try: + target_url = "/redfish/v1/UpdateService/FirmwareInventory/" + target + dd = self.rdmc.app.get_handler(target_url, service=True, silent=True) + if dd.status == 404: + return False + except: + return False + return True + + def checkfiles(self, options): + count = 0 + self.rdmc.ui.printer("Components currently in the repository that have not " "been added to the installset:\n") + for comp in self.comps: + count += 1 + self.rdmc.ui.printer("[%d] %s %s\n" % (count, comp["Name"], comp["Version"])) + while True: + userinput = input("Select the number of the component you want to add to " "the install set: ") + try: + userinput = int(userinput) + if userinput > count or userinput == 0: + raise + break + except: + self.rdmc.ui.warn("Input is not a valid number.\n") + filename = self.comps[userinput - 1]["Filename"] + updatableby = self.comps[userinput - 1]["UpdatableBy"] + if options.targets: + targets = [] + target_list = options.targets.split(",") + for target in target_list: + target_url = "/redfish/v1/UpdateService/FirmwareInventory/" + str(target) + "/" + targets.append(target_url) + verified = self.get_target(options.targets) + if not verified: + self.rdmc.ui.error("Provided target was not available, Please provide valid target id\n") + return ReturnCodes.INVALID_TARGET_ERROR + target = targets + return filename, updatableby, target + else: + target = None + del self.comps[userinput - 1] + return filename, updatableby, target + + def minstallsetvalidation(self): + """makeinstallset validation function""" + + try: + _ = self.rdmc.app.current_client + loggedin = True + except: + loggedin = False + + return loggedin + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-f", + "--filename", + dest="filename", + help="Use this flag if you wish to use a different" + " filename than the default one. The default filename is" + " myinstallset.json", + default="myinstallset.json", + ) + customparser.add_argument( + "--targets", + help="If targets value specify a comma separated" "firmwareinventory id only", + metavar="targets_indices", + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/UpdateTaskQueueCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/UpdateTaskQueueCommand.py new file mode 100644 index 0000000..5256a7f --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/UpdateTaskQueueCommand.py @@ -0,0 +1,412 @@ +# ## +# Copyright 2016-2023 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ## + +# -*- coding: utf-8 -*- +"""Update Task Queue Command for rdmc""" + +from argparse import RawDescriptionHelpFormatter +from random import randint + +from redfish.ris.rmc_helper import IdTokenError + +try: + from rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + TaskQueueError, + LOGGER, + ) +except ImportError: + from ilorest.rdmc_helper import ( + IncompatibleiLOVersionError, + InvalidCommandLineErrorOPTS, + NoContentsFoundForOperationError, + ReturnCodes, + TaskQueueError, + LOGGER, + ) + +__subparsers__ = ["create"] + + +class UpdateTaskQueueCommand: + """Main download command class""" + + def __init__(self): + self.ident = { + "name": "taskqueue", + "usage": None, + "description": "Run to add or remove tasks from the task queue. Added tasks are " + "appended to the end of the queue. Note: iLO 5 required.\n" + "Example:\n\ttaskqueue create 30\n\t" + "taskqueue create \n\t" + "taskqueue --cleanqueue\n\t" + "taskqueue --resetqueue\n\t" + "taskqueue\n", + "summary": "Manages the update task queue for iLO.", + "aliases": [], + "auxcommands": [], + } + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + + def run(self, line, help_disp=False): + """Main update task queue worker function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + line.append("-h") + try: + (_, _) = self.rdmc.rdmc_parse_arglist(self, line) + except: + return ReturnCodes.SUCCESS + return ReturnCodes.SUCCESS + try: + ident_subparser = False + for cmnd in __subparsers__: + if cmnd in line: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line) + ident_subparser = True + break + if not ident_subparser: + (options, args) = self.rdmc.rdmc_parse_arglist(self, line, default=True) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.updatetaskqueuevalidation(options) + + if self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("iLO Repository commands are only available on iLO 5.") + + if options.command.lower() == "create": + self.createtask(options.keywords, options) + else: + if options.resetqueue: + self.resetqueue(options) + elif options.cleanqueue: + self.cleanqueue() + self.printqueue(options) + + self.cmdbase.logout_routine(self, options) + # Return code + return ReturnCodes.SUCCESS + + def resetqueue(self, options): + """Deletes everything in the update task queue""" + tasks = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/UpdateTaskQueue/") + if not tasks: + self.rdmc.ui.printer("No tasks found.\n") + + self.rdmc.ui.printer("Deleting all update tasks...\n") + + for task in tasks: + self.rdmc.ui.printer("Deleting: %s\n" % task["Name"]) + if "RecoveryPrivilege" in task and task["RecoveryPrivilege"]: + self.rdmc.ui.printer("This task is associated with updating recovery set\n") + if not (options.user and options.password): + raise TaskQueueError("Deleting this task needs " "--user and --password options, delete failed\n") + if "blobstore" in self.rdmc.app.current_client.base_url: + LOGGER.info("Logging out of the session without user and password") + self.rdmc.app.current_client.logout() + LOGGER.info("Logging in with user and password for deleting system recovery set") + self.rdmc.app.current_client._user_pass = (options.user, options.password) + self.rdmc.app.current_client.login(self.rdmc.app.current_client.auth_type) + self.rdmc.app.delete_handler(task["@odata.id"]) + else: + self.rdmc.app.delete_handler(task["@odata.id"]) + else: + self.rdmc.app.delete_handler(task["@odata.id"]) + + def cleanqueue(self): + """Deletes all finished or errored tasks in the update task queue""" + tasks = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/UpdateTaskQueue/") + if not tasks: + self.rdmc.ui.printer("No tasks found.\n") + + self.rdmc.ui.printer("Cleaning update task queue...\n") + + for task in tasks: + if task["State"] == "Complete" or task["State"] == "Exception": + self.rdmc.ui.printer("Deleting %s...\n" % task["Name"]) + self.rdmc.app.delete_handler(task["@odata.id"]) + + def createtask(self, tasks, options): + """Creates a task in the update task queue + + :param tasks: arguments for creating tasks + :type tasks: list. + :param options: command line options + :type options: list. + """ + + tpmflag = None + + path = "/redfish/v1/UpdateService/UpdateTaskQueue/" + comps = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/" "ComponentRepository/") + curr_tasks = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/UpdateTaskQueue/") + for task in tasks: + usedcomp = None + newtask = None + if options.targets: + targets = [] + target_list = options.targets.split(",") + for target in target_list: + target_url = "/redfish/v1/UpdateService/FirmwareInventory/" + str(target) + "/" + targets.append(target_url) + verified = self.verify_targets(options.targets) + if not verified: + self.rdmc.ui.error("Provided target was not available, Please provide valid target id\n") + return ReturnCodes.INVALID_TARGET_ERROR + try: + usedcomp = int(task) + newtask = { + "Name": "Wait-%s %s seconds" % (str(randint(0, 1000000)), str(usedcomp)), + "Command": "Wait", + "WaitTimeSeconds": usedcomp, + "UpdatableBy": ["Bmc"], + } + if options.targets: + newtask["Targets"] = targets + except ValueError: + pass + + if task.lower() == "reboot": + newtask = { + "Name": "Reboot-%s" % str(randint(0, 1000000)), + "Command": "ResetServer", + "UpdatableBy": ["RuntimeAgent"], + } + elif not newtask: + if tpmflag is None: + if options.tover: + tpmflag = True + else: + tpmflag = False + # TODO: Update to monolith check + results = self.rdmc.app.get_handler(self.rdmc.app.typepath.defs.biospath, silent=True) + if results.status == 200: + contents = results.dict if self.rdmc.app.typepath.defs.isgen9 else results.dict["Attributes"] + tpmstate = contents["TpmState"] + if "Enabled" in tpmstate and not tpmflag: + raise IdTokenError("") + + for curr_task in curr_tasks: + if ( + "Filename" in curr_task + and curr_task["Filename"] == task + and curr_task["State"].lower() != "exception" + ): + raise TaskQueueError( + "This file already has a task queue for flashing " + "associated with it. Reset the taskqueue and " + "retry if you need to add this task again." + ) + for comp in comps: + if comp["Filename"] == task: + usedcomp = comp + break + + if not usedcomp: + raise NoContentsFoundForOperationError( + "Component " "referenced is not present on iLO Drive: %s" % task + ) + newtask = { + "Name": "Update-%s %s" + % ( + str(randint(0, 1000000)), + usedcomp["Name"].encode("ascii", "ignore"), + ), + "Command": "ApplyUpdate", + "Filename": usedcomp["Filename"], + "UpdatableBy": usedcomp["UpdatableBy"], + "TPMOverride": tpmflag, + } + if options.targets: + newtask["Targets"] = targets + + self.rdmc.ui.printer('Creating task: "%s"\n' % newtask["Name"]) + self.rdmc.ui.printer('payload: "%s"\n' % newtask) + self.rdmc.app.post_handler(path, newtask) + + def verify_targets(self, target): + target_list = target.split(",") + for target in target_list: + try: + target_url = "/redfish/v1/UpdateService/FirmwareInventory/" + target + dd = self.rdmc.app.get_handler(target_url, service=True, silent=True) + if dd.status == 404: + return False + except: + return False + return True + + def printqueue(self, options): + """Prints the update task queue + + :param options: command line options + :type options: list. + """ + tasks = self.rdmc.app.getcollectionmembers("/redfish/v1/UpdateService/UpdateTaskQueue/") + if not tasks: + self.rdmc.ui.printer("No tasks found.\n") + return + + if not options.json: + self.rdmc.ui.printer("\nCurrent Update Task Queue:\n\n") + + if not options.json: + for task in tasks: + self.rdmc.ui.printer("Task %s:\n" % task["Name"]) + if "Filename" in list(task.keys()): + self.rdmc.ui.printer( + "\tCommand: %s\n\tFilename: %s\n\t" + "State:%s\n" % (task["Command"], task["Filename"], task["State"]) + ) + elif "WaitTimeSeconds" in list(task.keys()): + self.rdmc.ui.printer( + "\tCommand: %s %s seconds\n\tState:%s\n" + % (task["Command"], str(task["WaitTimeSeconds"]), task["State"]) + ) + else: + self.rdmc.ui.printer("\tCommand:%s\n\tState: %s\n" % (task["Command"], task["State"])) + elif options.json: + outjson = dict() + for task in tasks: + outjson[task["Name"]] = dict() + outjson[task["Name"]]["Command"] = task["Command"] + if "Filename" in task: + outjson[task["Name"]]["Filename"] = task["Filename"] + if "WaitTimeSeconds" in task: + outjson[task["Name"]]["WaitTimeSeconds"] = task["WaitTimeSeconds"] + outjson[task["Name"]]["State"] = task["State"] + self.rdmc.ui.print_out_json(outjson) + + def updatetaskqueuevalidation(self, options): + """taskqueue validation function + + :param options: command line options + :type options: list. + """ + self.cmdbase.login_select_validation(self, options) + + @staticmethod + def options_argument_group(parser): + """Define optional arguments group + + :param parser: The parser to add the --addprivs option group to + :type parser: ArgumentParser/OptionParser + """ + group = parser.add_argument_group( + "GLOBAL OPTIONS", + "Options are available for all " "arguments within the scope of this command.", + ) + + group.add_argument( + "--tpmover", + dest="tover", + action="store_true", + help="If set then the TPMOverrideFlag is passed in on the " "associated flash operations", + default=False, + ) + + def definearguments(self, customparser): + """Wrapper function for new command main function + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + subcommand_parser = customparser.add_subparsers(dest="command") + + default_parser = subcommand_parser.add_parser( + "default", + help="Running without any sub-command will return the current task \n" + "queue information on the currently logged in server.", + ) + default_parser.add_argument( + "-r", + "--resetqueue", + action="store_true", + dest="resetqueue", + help="Remove all update tasks in the queue.\n\texample: taskqueue --resetqueue or taskqueue -r", + default=False, + ) + default_parser.add_argument( + "-c", + "--cleanqueue", + action="store_true", + dest="cleanqueue", + help="Clean up all finished or errored tasks left pending.\n\texample: taskqueue " + "--cleanqueue or taskqueue -c", + default=False, + ) + default_parser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + self.cmdbase.add_login_arguments_group(default_parser) + self.options_argument_group(default_parser) + + # create + create_help = "Create a new task queue task." + create_parser = subcommand_parser.add_parser( + "create", + help=create_help, + description=create_help + "\n\n\tCreate a new task for 30 secs:\n\t\ttaskqueue " + "create 30\n\n\tCreate a new reboot task.\n\t\ttaskqueue create reboot" + "\n\n\tCreate a new component task.\n\t\ttaskqueue create compname" + "\n\n\tCreate multiple tasks at once.\n\t\ttaskqueue create 30 " + '"compname compname2 reboot"' + "\n\n\tCreate a new task using targets: \n\t\t taskqueue create compname --targets \n\n\t", + formatter_class=RawDescriptionHelpFormatter, + ) + create_parser.add_argument( + "keywords", + help="Keyword for a task queue item. *Note*: Multiple tasks can be created by " + "using quotations wrapping all tasks, delimited by whitespace.", + metavar="KEYWORD", + type=str, + nargs="+", + default="", + ) + create_parser.add_argument( + "--targets", + help="If targets value specify a comma separated\t" "firmwareinventory id only", + metavar="targets_indices", + ) + + self.cmdbase.add_login_arguments_group(create_parser) + self.options_argument_group(create_parser) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/UploadComponentCommand.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/UploadComponentCommand.py new file mode 100644 index 0000000..3417ee0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/UploadComponentCommand.py @@ -0,0 +1,857 @@ +# ## +# Copyright 2016 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# ## + +# -*- coding: utf-8 -*- +"""Upload Component Command for rdmc""" + +import os +import json +import sys +import time +import shutil +from random import choice +from string import ascii_lowercase +import ctypes +from six.moves import input +from ctypes import ( + c_char_p, + c_void_p, + byref, + c_ubyte, + c_int, + c_uint32, + POINTER, + create_string_buffer, +) +from redfish.hpilo.rishpilo import ( + HpIloInitialError, + HpIloChifAccessDeniedError, + HpIloNoChifDriverError, +) +from redfish.hpilo.rishpilo import BlobReturnCodes +from redfish.hpilo.risblobstore2 import BlobStore2 + +try: + from rdmc_helper import ( + UI, + LOGGER, + ReturnCodes, + InvalidCommandLineErrorOPTS, + UploadError, + IncompatibleiLOVersionError, + TimeOutError, + InvalidFileInputError, + ) +except ImportError: + from ilorest.rdmc_helper import ( + UI, + LOGGER, + ReturnCodes, + InvalidCommandLineErrorOPTS, + UploadError, + IncompatibleiLOVersionError, + TimeOutError, + InvalidFileInputError, + ) + + +def human_readable_time(seconds): + """Returns human readable time + + :param seconds: Amount of seconds to parse. + :type seconds: string. + """ + seconds = int(seconds) + hours = seconds / 3600 + seconds = seconds % 3600 + minutes = seconds / 60 + seconds = seconds % 60 + + return "{:02.0f} hour(s) {:02.0f} minute(s) {:02.0f} second(s) ".format(hours, minutes, seconds) + + +class UploadComponentCommand: + """Constructor""" + + def __init__(self): + self.ident = { + "name": "uploadcomp", + "usage": None, + "description": "Run to upload the component on " + "to iLO Repository\n\n\tUpload component to the iLO " + "repository.\n\texample: uploadcomp --component " + "--compsig \n\n\tFlash the component " + "instead of add to the iLO repository.\n\texample: " + "uploadcomp --component --update_target " + "--update_repository", + "summary": "Upload components/binary to the iLO Repository.", + "aliases": [], + "auxcommands": ["FwpkgCommand"], + } + + self.cmdbase = None + self.rdmc = None + self.auxcommands = dict() + self.status_content = {} + + def run(self, line, help_disp=False): + """Wrapper function for upload command main function + + :param line: string of arguments passed in + :type line: str. + :param help_disp: display help flag + :type line: bool. + """ + if help_disp: + self.parser.print_help() + return ReturnCodes.SUCCESS + try: + (options, _) = self.rdmc.rdmc_parse_arglist(self, line) + except (InvalidCommandLineErrorOPTS, SystemExit): + if ("-h" in line) or ("--help" in line): + return ReturnCodes.SUCCESS + else: + raise InvalidCommandLineErrorOPTS("") + + self.uploadcommandvalidation(options) + fwpkg = False + if ( + options.component.endswith(".fwpkg") + or options.component.endswith(".fup") + or options.component.endswith(".hpb") + or options.component.endswith(".HPb") + or options.component.endswith(".pup") + ): + fwpkg = True + command_name = self.cmdbase.name + comp, loc, ctype, pldmfw = self.auxcommands["flashfwpkg"].preparefwpkg( + self, options.component, command_name + ) + # if pldm firmware + if pldmfw: + path = self.rdmc.app.typepath.defs.systempath + results = self.rdmc.app.get_handler(path, service=True, silent=True).dict + device_discovery_status = results["Oem"]["Hpe"]["DeviceDiscoveryComplete"]["DeviceDiscovery"] + LOGGER.info("Device Discovery Status is {}".format(device_discovery_status)) + # check for device discovery + # if ("DeviceDiscoveryComplete" not in device_discovery_status): + # raise DeviceDiscoveryInProgress( + # "Device Discovery in progress...Please retry flashing firmware after 10 minutes" + # ) + if ctype in ["C", "BC"]: + options.component = comp[0] + # else: + # options.component = os.path.join(loc, comp[0]) + + if self.rdmc.app.typepath.defs.isgen9: + raise IncompatibleiLOVersionError("iLO Repository commands are only available on iLO 5.") + + filestoupload = self._check_and_split_files(options) + validation = self.componentvalidation(options, filestoupload) + if validation: + start_time = time.time() + ret = ReturnCodes.FAILED_TO_UPLOAD_COMPONENT + + # ret = self.uploadfunction(filestoupload, options) + if "blobstore" in self.rdmc.app.current_client.base_url: + ret = self.uploadlocally(filestoupload, options) + else: + ret = self.uploadfunction(filestoupload, options) + + if ret == ReturnCodes.SUCCESS and not options.update_target: + + self.rdmc.ui.printer("Uploading took %s\n" % human_readable_time(time.time() - start_time)) + + if len(filestoupload) > 1: + path, _ = os.path.split((filestoupload[0])[1]) + shutil.rmtree(path) + elif fwpkg: + if os.path.exists(loc): + shutil.rmtree(loc) + elif not validation and options.forceupload: + self.rdmc.ui.printer( + "Component " + filestoupload[0][0] + " is already present in repository ,Hence skipping the re upload\n" + ) + ret = ReturnCodes.SUCCESS + else: + ret = ReturnCodes.FAILED_TO_UPLOAD_COMPONENT + + self.cmdbase.logout_routine(self, options) + # Return code + return ret + + def componentvalidation(self, options, filelist): + """Check for duplicate component in repository + + :param options: command line options + :type options: list. + :param filelist: list of files to be uploaded (multiple files will be + generated for items over 32K in size) + :type filelist: list of strings + """ + validation = True + prevfile = None + + path = "/redfish/v1/UpdateService/ComponentRepository/?$expand=." + results = self.rdmc.app.get_handler(path, service=True, silent=True) + + results = results.dict + + if "Members" in results and results["Members"]: + for comp in results["Members"]: + for filehndl in filelist: + if ( + comp["Filename"].upper() == str(filehndl[0]).upper() + and not options.forceupload + and prevfile != filehndl[0].upper() + ): + ans = input( + "A component with the same name (%s) has " + "been found. Would you like to upload and " + "overwrite this file? (y/n)" % comp["Filename"] + ) + + while ans.lower() != "n" and ans.lower() != "y": + ans = input("Please enter valid option (y/n)") + + if ans.lower() == "n": + self.rdmc.ui.printer( + "Error: Upload stopped by user due to filename conflict." + " If you would like to bypass this check include the" + ' "--forceupload" flag.\n' + ) + validation = False + break + elif ( + comp["Filename"].upper() == str(filehndl[0]).upper() + and options.forceupload + and prevfile != filehndl[0].upper() + ): + if comp["Locked"]: + options.update_repository = False + else: + options.update_repository = True + if not options.update_target and comp["Locked"]: + validation = False + break + if options.update_repository: + if ( + comp["Filename"].upper() == str(filehndl[0]).upper() + and prevfile != filehndl[0].upper() + and comp["Locked"] + ): + self.rdmc.ui.printer( + "Error: Component is currently locked by a taskqueue task or " + "installset. Remove any installsets or taskqueue tasks " + "containing the file and try again OR use taskqueue command " + "to put the component to installation queue\n" + ) + validation = False + break + elif ( + comp["Filename"].upper() == str(filehndl[0]).upper() + and prevfile != filehndl[0].upper() + and comp["Locked"] + and options.forceupload + ): + validation = False + break + prevfile = str(comp["Filename"].upper()) + return validation + + def _check_and_split_files(self, options): + """Check and split the file to upload on to iLO Repository + + :param options: command line options + :type options: list. + """ + + def check_file_rw(filename, rw): + try: + fd = open(filename, rw) + fd.close() + except IOError: + raise InvalidFileInputError("The file '%s' could not be opened for upload" % filename) + + maxcompsize = 32 * 1024 * 1024 + filelist = [] + + # Lets get the component filename + _, filename = os.path.split(options.component) + check_file_rw(os.path.normpath(options.component), "r") + self.rdmc.ui.printer("Successfully checked '%s'.\n" % filename) + size = os.path.getsize(options.component) + + # This is to upload the binary directly to flash scenario + if not options.componentsig: + if not self.findcompsig(filename): + return [(filename, options.component, options.componentsig, 0)] + + if size > maxcompsize: + self.rdmc.ui.printer("Component is more than 32MB in size.\n") + self.rdmc.ui.printer("Component size = %s\n" % str(size)) + section = 1 + + sigpath, _ = os.path.split(options.componentsig) + check_file_rw(os.path.normpath(options.componentsig), "r") + filebasename = filename[: filename.rfind(".")] + tempfoldername = "bmn" + "".join(choice(ascii_lowercase) for i in range(12)) + + if self.rdmc.app.cache: + tempdir = os.path.join(self.rdmc.app.cachedir, tempfoldername) + else: + tempdir = os.path.join(sys.executable, tempfoldername) + + self.rdmc.ui.printer("Spliting component. Temporary " "cache directory at %s\n" % tempdir) + + if not os.path.exists(tempdir): + os.makedirs(tempdir) + + with open(options.component, "rb") as component: + while True: + data = component.read(maxcompsize) + if len(data) != 0: + sectionfilename = filebasename + "_part" + str(section) + sectionfilepath = os.path.join(tempdir, sectionfilename) + + sectioncompsigpath = os.path.join(sigpath, sectionfilename + ".compsig") + sigfullpath = os.path.join(tempdir, sigpath) + if not os.path.exists(sigfullpath): + os.makedirs(sigfullpath) + writefile = open(sectionfilepath, "wb") + writefile.write(data) + writefile.close() + + item = ( + filename, + sectionfilepath, + sectioncompsigpath, + section - 1, + ) + + filelist.append(item) + section += 1 + + if len(data) != maxcompsize: + break + + return filelist + else: + return [(filename, options.component, options.componentsig, 0)] + + def uploadfunction(self, filelist, options=None): + """Main upload command worker function + + :param filelist: List of files to upload. + :type filelist: list. + :param options: command line options + :type options: list. + """ + + # returns a tuple with the state and the result dict + state, result = self.get_update_service_state() + + if state != "COMPLETED" and state != "COMPLETE" and state != "ERROR" and state != "IDLE": + self.rdmc.ui.error("iLO UpdateService is busy. Please try again.") + + return ReturnCodes.UPDATE_SERVICE_BUSY + + sessionkey = self.rdmc.app.current_client.session_key + + etag = "" + hpe = result["Oem"]["Hpe"] + urltosend = "/cgi-bin/uploadFile" + + if "PushUpdateUri" in hpe: + urltosend = hpe["PushUpdateUri"] + elif "HttpPushUri" in result: + urltosend = result["HttpPushUri"] + else: + return ReturnCodes.FAILED_TO_UPLOAD_COMPONENT + + for item in filelist: + ilo_upload_filename = item[0] + + ilo_upload_compsig_filename = ilo_upload_filename[: ilo_upload_filename.rfind(".")] + ".compsig" + + componentpath = item[1] + compsigpath = item[2] + + _, filename = os.path.split(componentpath) + + if not etag: + etag = "sum" + filename.replace(".", "") + etag = etag.replace("-", "") + etag = etag.replace("_", "") + + section_num = item[3] + if isinstance(sessionkey, bytes): + sessionkey = sessionkey.decode("utf-8") + + parameters = { + "UpdateRepository": options.update_repository, + "UpdateTarget": options.update_target, + "ETag": etag, + "Section": section_num, + "UpdateRecoverySet": options.update_srs, + "TPMOverride": options.tover, + } + + data = [("sessionKey", sessionkey), ("parameters", json.dumps(parameters))] + + if not compsigpath: + compsigpath = self.findcompsig(componentpath) + if compsigpath: + with open(compsigpath, "rb") as fle: + output = fle.read() + data.append( + ( + "compsig", + ( + ilo_upload_compsig_filename, + output, + "application/octet-stream", + ), + ) + ) + output = None + + with open(componentpath, "rb") as fle: + output = fle.read() + data.append(("file", (ilo_upload_filename, output, "application/octet-stream"))) + + if options.update_repository: + self.rdmc.ui.printer("Uploading component " + filename + ".\n") + if options.update_target: + self.rdmc.ui.printer("Flashing component " + filename + ".\n") + res = self.rdmc.app.post_handler( + str(urltosend), + data, + headers={"Cookie": "sessionKey=" + sessionkey}, + silent=False, + service=False, + ) + + if res.status == 400 and res.dict is None: + self.rdmc.ui.error( + "Component " + filename + " was not uploaded , iLO returned 400 error code. " + "Check if the user has all privileges to perform the operation.\n" + ) + return ReturnCodes.FAILED_TO_UPLOAD_COMPONENT + + if res.status != 200: + return ReturnCodes.FAILED_TO_UPLOAD_COMPONENT + else: + if options.update_repository: + self.rdmc.ui.printer("Component " + filename + " uploading successfully.\n") + if options.update_target: + self.rdmc.ui.printer("Component " + filename + " flashing successfully.\n") + + if not self.wait_for_state_change(): + if options.response: + self.rdmc.ui.printer("UpdateService Status:") + UI().print_out_json(self.status_content) + LOGGER.info("\nUpdate Service Status: {}".format(self.status_content)) + # Failed to upload the component. + raise UploadError("Error while processing the component.") + + if options.response: + self.rdmc.ui.printer("UpdateService Status:") + UI().print_out_json(self.status_content) + LOGGER.info("\nUpdate Service Status: {}".format(self.status_content)) + + return ReturnCodes.SUCCESS + + def get_update_service_status(self, result): + if "Result" in result["Oem"]["Hpe"]: + res = "MessageId:" + result["Oem"]["Hpe"]["Result"]["MessageId"] + complete_res = "{" + res + "}" + self.status_content["Result"] = complete_res + self.status_content["State"] = (result["Oem"]["Hpe"]["State"]).upper() + + def wait_for_state_change(self, wait_time=4800): + """Wait for the iLO UpdateService to a move to terminal state. + :param options: command line options + :type options: list. + :param wait_time: time to wait on upload + :type wait_time: int. + """ + total_time = 0 + result = dict() + spinner = ["|", "/", "-", "\\"] + state = "" + self.rdmc.ui.printer("Waiting for iLO UpdateService to finish processing the component\n") + + while total_time < wait_time: + state, result = self.get_update_service_state() + + if state == "ERROR": + self.get_update_service_status(result) + return False + elif state != "COMPLETED" and state != "IDLE" and state != "COMPLETE": + # Lets try again after 8 seconds + count = 0 + + # fancy spinner + while count <= 32: + self.rdmc.ui.printer("Updating: %s\r" % spinner[count % 4]) + time.sleep(0.25) + count += 1 + + total_time += 8 + else: + self.get_update_service_status(result) + break + + if total_time > wait_time: + raise TimeOutError("UpdateService in " + state + " state for " + str(wait_time) + "s") + + return True + + def get_update_service_state(self): + """Get the current UpdateService state + + :param options: command line options + :type options: list. + """ + path = "/redfish/v1/UpdateService" + results = self.rdmc.app.get_handler(path, service=True, silent=True) + + if results and results.status == 200 and results.dict: + output = results.dict + + if self.rdmc.opts.verbose: + self.rdmc.ui.printer("UpdateService state = " + (output["Oem"]["Hpe"]["State"]).upper() + "\n") + + return (output["Oem"]["Hpe"]["State"]).upper(), results.dict + else: + return "UNKNOWN", {} + + def findcompsig(self, comppath): + """Try to find compsig if not included + :param comppath: Path of file to find compsig for. + :type comppath: str. + """ + compsig = "" + + cutpath = comppath.split(os.sep) + _file = cutpath[-1] + _file_rev = _file[::-1] + filename = _file[: ((_file_rev.find(".")) * -1) - 1] + + try: + location = os.sep.join(cutpath[:-1]) + except: + location = os.curdir + + if not location: + location = os.curdir + + files = [f for f in os.listdir(location) if os.path.isfile(os.path.join(location, f))] + + for filehndl in files: + if filehndl.startswith(filename) and filehndl.endswith(".compsig"): + self.rdmc.ui.printer("Compsig found for file.\n") + + if location != ".": + compsig = location + os.sep + filehndl + else: + compsig = filehndl + + break + + return compsig + + def uploadlocally(self, filelist, options=None): + """Upload component locally + + :param filelist: List of files to upload. + :type filelist: list. + :param options: command line options + :type options: list. + """ + new_chif_needed = False + upload_failed = False + if not options.update_target: + options.upload_srs = False + + if options.update_srs: + if options.user and options.password: + new_chif_needed = True + else: + self.rdmc.ui.error( + "ERROR: --update_srs option needs to be passed with " + "--username and --password options, upload failed\n" + ) + return ReturnCodes.FAILED_TO_UPLOAD_COMPONENT + try: + dll = self.rdmc.app.current_client.connection._conn.channel.dll + multiupload = False + + if new_chif_needed: + # Backup old chif channel + dll_bk = dll + dll = None + user = options.user + passwrd = options.password + dll, fhandle = self.create_new_chif_for_upload(user, passwrd) + self.rdmc.app.current_client.connection._conn.channel.dll = dll + + dll.uploadComponent.argtypes = [c_char_p, c_char_p, c_char_p, c_uint32] + dll.uploadComponent.restype = c_int + + for item in filelist: + ilo_upload_filename = item[0] + componentpath = item[1] + compsigpath = item[2] + + if not compsigpath: + compsigpath = self.findcompsig(componentpath) + + _, filename = os.path.split(componentpath) + + # 0x00000001 // FUM_WRITE_NAND + # 0x00000002 // FUM_USE_NAND + # 0x00000004 // FUM_NO_FLASH + # 0x00000008 // FUM_FORCE + # 0x00000010 // FUM_SIDECAR + # 0x00000020 // FUM_APPEND + # 0x40 // FUM_UPDATE_RECOVERY + # 0x00000080 //FUM_RECOVERY + # 0x00000100 // FUM_TASK + # 0x00000200 //FUM_RECO_PRIV + + if not compsigpath and options.update_target: + if not options.update_repository: + # Just update the firmware + if options.update_srs: + dispatchflag = ctypes.c_uint32(0x00000000 | 0x40) + else: + dispatchflag = ctypes.c_uint32(0x00000000) + else: + # Update the firmware and Upload to Repository + if options.update_srs: + dispatchflag = ctypes.c_uint32(0x00000000 | 0x00000001 | 0x40) + else: + dispatchflag = ctypes.c_uint32(0x00000000 | 0x00000001) + elif not compsigpath and not options.update_target and options.update_repository: + # uploading a secuare flash binary image onto the NAND + if options.update_srs: + dispatchflag = ctypes.c_uint32(0x00000001 | 0x00000004 | 0x40) + else: + dispatchflag = ctypes.c_uint32(0x00000001 | 0x00000004) + else: + # Uploading a component with a side car file. + if options.update_srs: + dispatchflag = ctypes.c_uint32(0x00000001 | 0x00000004 | 0x00000010 | 0x40) + else: + dispatchflag = ctypes.c_uint32(0x00000001 | 0x00000004 | 0x00000010) + + if multiupload: + # For second upload to append if the component is > 32MB in size + if options.update_srs: + dispatchflag = ctypes.c_uint32(0x00000001 | 0x00000004 | 0x00000010 | 0x00000020 | 0x40) + else: + dispatchflag = ctypes.c_uint32(0x00000001 | 0x00000004 | 0x00000010 | 0x00000020) + + if options.update_repository: + self.rdmc.ui.printer("Uploading component " + filename + "\n") + if options.update_target: + self.rdmc.ui.printer("Flashing component " + filename + "\n") + + ret = dll.uploadComponent( + ctypes.create_string_buffer(compsigpath.encode("utf-8")), + ctypes.create_string_buffer(componentpath.encode("utf-8")), + ctypes.create_string_buffer(ilo_upload_filename.encode("utf-8")), + dispatchflag, + ) + + upload_failed = False + if ret != 0: + LOGGER.error("Component {} upload failed".format(filename)) + self.rdmc.ui.error("Component " + filename + " upload failed.\n") + upload_failed = True + else: + LOGGER.info("Component {} uploaded successfully".format(filename)) + if options.update_repository: + self.rdmc.ui.printer("Component " + filename + " uploaded successfully.\n") + if options.update_target: + self.rdmc.ui.printer("Component " + filename + " flashed successfully.\n") + self.rdmc.ui.printer("[200] The operation completed successfully.\n") + if not options.update_target: + if not self.wait_for_state_change(): + if options.response: + self.rdmc.ui.printer("UpdateService Status:") + UI().print_out_json(self.status_content) + LOGGER.info("\nUpdate Service Status: {}".format(self.status_content)) + # Failed to upload the component. + raise UploadError("Error while processing the component.") + + multiupload = True + + if new_chif_needed: + dll.ChifTerminate() + dll.ChifClose(fhandle) + fhandle = None + BlobStore2.unloadchifhandle(dll) + # Restore old chif channel + dll = dll_bk + self.rdmc.app.current_client.connection._conn.channel.dll = dll_bk + LOGGER.info("Restored the old chif channel\n") + + except Exception as excep: + LOGGER.error("Exception occured, {}".format(excep)) + raise excep + + if options.response: + path = "/redfish/v1/UpdateService" + results = self.rdmc.app.get_handler(path, service=True, silent=True) + res = results.dict + self.get_update_service_status(res) + self.rdmc.ui.printer("UpdateService Status:") + UI().print_out_json(self.status_content) + LOGGER.info("\nUpdate Service Status: {}".format(self.status_content)) + + if upload_failed: + return ReturnCodes.FAILED_TO_UPLOAD_COMPONENT + else: + return ReturnCodes.SUCCESS + + def create_new_chif_for_upload(self, user, passwrd): + dll = BlobStore2.gethprestchifhandle() + dll.ChifInitialize(None) + # Enable Security Flag in Chif + dll.ChifEnableSecurity() + fhandle = c_void_p() + dll.ChifCreate.argtypes = [c_void_p] + dll.ChifCreate.restype = c_uint32 + status = dll.ChifCreate(byref(fhandle)) + if status != BlobReturnCodes.SUCCESS: + if status == BlobReturnCodes.CHIFERR_NoDriver: + raise HpIloNoChifDriverError( + "Error %s - No Chif Driver occurred while trying to create a channel." % status + ) + else: + raise HpIloInitialError("Error %s occurred while trying to create a channel." % status) + dll.initiate_credentials.argtypes = [c_char_p, c_char_p] + dll.initiate_credentials.restype = POINTER(c_ubyte) + usernew = create_string_buffer(user.encode("utf-8")) + passnew = create_string_buffer(passwrd.encode("utf-8")) + dll.initiate_credentials(usernew, passnew) + status = dll.ChifPing(fhandle) + if status != BlobReturnCodes.SUCCESS: + raise HpIloInitialError("Error %s occurred while trying to create a channel." % status) + dll.ChifSetRecvTimeout(fhandle, 60000) + credreturn = dll.ChifVerifyCredentials() + if not credreturn == BlobReturnCodes.SUCCESS: + if credreturn == BlobReturnCodes.CHIFERR_AccessDenied: + raise HpIloChifAccessDeniedError( + "Error %s - Chif Access Denied occurred while trying " + "to open a channel to iLO. Verify iLO Credetials passed." % credreturn + ) + else: + raise HpIloInitialError("Error %s occurred while trying " "to open a channel to iLO" % credreturn) + return dll, fhandle + + def uploadcommandvalidation(self, options): + """upload command method validation function + + :param options: command line options + :type options: list. + """ + self.rdmc.login_select_validation(self, options) + + def definearguments(self, customparser): + """Define command line argument for the upload command + + :param customparser: command line input + :type customparser: parser. + """ + if not customparser: + return + + self.cmdbase.add_login_arguments_group(customparser) + + customparser.add_argument( + "-j", + "--json", + dest="json", + action="store_true", + help="Optionally include this flag if you wish to change the" + " displayed output to JSON format. Preserving the JSON data" + " structure makes the information easier to parse.", + default=False, + ) + customparser.add_argument( + "--response", + dest="response", + action="store_true", + help="Use this flag to return the iLO response body.", + default=False, + ) + customparser.add_argument( + "--component", + dest="component", + help="""Component or binary file path to upload to the update service.""", + default="", + required=True, + ) + customparser.add_argument( + "--compsig", + dest="componentsig", + help="Component signature file path needed by iLO to authenticate the " + "component file. If not provided will try to find the " + "signature file from component file path.", + default="", + ) + customparser.add_argument( + "--forceupload", + dest="forceupload", + action="store_true", + help="Add this flag to force upload components with the same name already on the repository.", + default=False, + ) + customparser.add_argument( + "--update_repository", + dest="update_repository", + action="store_false", + help="Add this flag to skip uploading component/binary to the iLO Repository. If this " + "flag is included with --update_srs, it will be ignored. Adding component to the " + "repository is required to update the system reovery set.", + default=True, + ) + customparser.add_argument( + "--update_target", + dest="update_target", + action="store_true", + help="Add this flag if you wish to flash the component/binary.", + default=False, + ) + customparser.add_argument( + "--update_srs", + dest="update_srs", + action="store_true", + help="Add this flag to update the System Recovery Set with the uploaded component. " + "NOTE: This requires an account login with the system recovery set privilege.", + default=False, + ) + customparser.add_argument( + "--tpmover", + dest="tover", + action="store_true", + help="If set then the TPMOverrideFlag is passed in on the associated flash operations", + default=False, + ) diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__init__.py b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/DeleteComponentCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/DeleteComponentCommand.cpython-310.pyc new file mode 100644 index 0000000..b96c41e Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/DeleteComponentCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/DownloadComponentCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/DownloadComponentCommand.cpython-310.pyc new file mode 100644 index 0000000..824fbff Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/DownloadComponentCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/FwpkgCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/FwpkgCommand.cpython-310.pyc new file mode 100644 index 0000000..eb1392f Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/FwpkgCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/InstallSetCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/InstallSetCommand.cpython-310.pyc new file mode 100644 index 0000000..9bb29a1 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/InstallSetCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/ListComponentCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/ListComponentCommand.cpython-310.pyc new file mode 100644 index 0000000..cb2e24f Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/ListComponentCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/MaintenanceWindowCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/MaintenanceWindowCommand.cpython-310.pyc new file mode 100644 index 0000000..98226fe Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/MaintenanceWindowCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/MakeInstallSetCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/MakeInstallSetCommand.cpython-310.pyc new file mode 100644 index 0000000..c0fc2d7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/MakeInstallSetCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/UpdateTaskQueueCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/UpdateTaskQueueCommand.cpython-310.pyc new file mode 100644 index 0000000..e250b48 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/UpdateTaskQueueCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/UploadComponentCommand.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/UploadComponentCommand.cpython-310.pyc new file mode 100644 index 0000000..26dfbbe Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/UploadComponentCommand.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..71147a0 Binary files /dev/null and b/venv/lib/python3.10/site-packages/ilorest/extensions/iLO_REPOSITORY_COMMANDS/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/ilorest/logging_config_path.py b/venv/lib/python3.10/site-packages/ilorest/logging_config_path.py new file mode 100644 index 0000000..b419edb --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/logging_config_path.py @@ -0,0 +1,27 @@ +"""Helper to determine logging_config.json path for different platforms. + +""" + +import os + + +def get_logging_config_path(): + r"""Return path for logging_config.json. + + Iterates through a list of different platform paths and returns the first one where + the directory exists and logging_config.json is present. + Platform paths: C:\Program Files\Hewlett Packard Enterprise\RESTful Interface Tool, + /opt/ilorest, /etc/ilorest, C:\Python313\Lib\site-packages\ilorest. + Falls back to the current working directory if none are found. + """ + platform_paths = [ + r"C:\Program Files\Hewlett Packard Enterprise\RESTful Interface Tool", + "/opt/ilorest", + "/etc/ilorest", + r"C:\Python313\Lib\site-packages\ilorest", + ] + for path in platform_paths: + if os.path.isdir(path) and os.path.isfile(os.path.join(path, "logging_config.json")): + return os.path.join(path, "logging_config.json") + # Fallback to current working directory + return os.path.join(os.getcwd(), "logging_config.json") diff --git a/venv/lib/python3.10/site-packages/ilorest/rdmc.py b/venv/lib/python3.10/site-packages/ilorest/rdmc.py new file mode 100644 index 0000000..cc09f20 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/rdmc.py @@ -0,0 +1,1295 @@ +### +# Copyright 2016-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +""" +This is the main module for Redfish Utility which handles all of the CLI and UI interfaces +""" + +# ---------Imports--------- +# -*- coding: utf-8 -*- + +import collections +import copy +import errno +import glob +import os +import shlex +import sys +import traceback +import warnings +import importlib +import ctypes +from argparse import ArgumentParser, RawTextHelpFormatter +from builtins import open, str, super + +import six +from prompt_toolkit import PromptSession +from prompt_toolkit.auto_suggest import AutoSuggestFromHistory +from prompt_toolkit.formatted_text import HTML +from prompt_toolkit.shortcuts import CompleteStyle +from six.moves import input + +import redfish.hpilo +import redfish.rest.v1 +import redfish.ris + + +try: + import cliutils + import versioning + import extensions + from config.rdmc_config import RdmcConfig + from rdmc_helper import HARDCODEDLIST + from rdmc_base_classes import RdmcCommandBase, RdmcOptionParser + from security_masking import SecurityMasker +except ModuleNotFoundError: + from ilorest import cliutils + from ilorest import versioning + from ilorest import extensions + from ilorest.config.rdmc_config import RdmcConfig + from ilorest.rdmc_helper import HARDCODEDLIST + from ilorest.rdmc_base_classes import RdmcCommandBase, RdmcOptionParser + from ilorest.security_masking import SecurityMasker + +try: + from rdmc_helper import ( + LOGGER, + UI, + setup_logging_from_json, + AlreadyCloudConnectedError, + BirthcertParseError, + BootOrderMissingEntriesError, + CloudConnectFailedError, + CloudConnectTimeoutError, + CommandNotEnabledError, + ConfigurationFileError, + DeviceDiscoveryInProgress, + DownloadError, + Encryption, + FailureDuringCommitError, + FirmwareUpdateError, + IloLicenseError, + IncompatableServerTypeError, + IncompatibleiLOVersionError, + InfoMissingEntriesError, + InvalidCListFileError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + InvalidFileFormattingError, + InvalidFileInputError, + InvalidKeyError, + InvalidMSCfileInputError, + InvalidOrNothingChangedSettingsError, + InvalidPasswordLengthError, + MultipleServerConfigError, + NicMissingOrConfigurationError, + NoChangesFoundOrMadeError, + NoContentsFoundForOperationError, + NoCurrentSessionEstablished, + NoDifferencesFoundError, + PartitionMoutingError, + PathUnavailableError, + ProxyConfigFailedError, + RdmcError, + ResourceExists, + ReturnCodes, + StandardBlobErrorHandler, + TabAndHistoryCompletionClass, + TaskQueueError, + TfaEnablePreRequisiteError, + TimeOutError, + UnableToDecodeError, + UnabletoFindDriveError, + UploadError, + UsernamePasswordRequiredError, + iLORisCorruptionError, + ResourceNotReadyError, + InstallsetError, + InvalidSmartArrayConfigurationError, + VnicExistsError, + GenerateAndSaveAccountError, + NoAppAccountError, + RemoveAccountError, + AppAccountExistsError, + ReactivateAppAccountTokenError, + InactiveAppAccountTokenError, + VnicLoginError, + SavinginTPMError, + SavinginiLOError, + GenBeforeLoginError, + AppIdListError, + FlashUnsupportedByIloError, + ) +except ModuleNotFoundError: + from ilorest.rdmc_helper import ( + ReturnCodes, + RdmcError, + setup_logging_from_json, + ConfigurationFileError, + CommandNotEnabledError, + InvalidCommandLineError, + InvalidCommandLineErrorOPTS, + UI, + LOGGER, + InvalidFileFormattingError, + NoChangesFoundOrMadeError, + InvalidFileInputError, + NoContentsFoundForOperationError, + InfoMissingEntriesError, + MultipleServerConfigError, + InvalidOrNothingChangedSettingsError, + NoDifferencesFoundError, + InvalidMSCfileInputError, + InvalidPasswordLengthError, + FirmwareUpdateError, + DeviceDiscoveryInProgress, + BootOrderMissingEntriesError, + NicMissingOrConfigurationError, + StandardBlobErrorHandler, + NoCurrentSessionEstablished, + InvalidCListFileError, + FailureDuringCommitError, + IncompatibleiLOVersionError, + PartitionMoutingError, + TimeOutError, + DownloadError, + UploadError, + BirthcertParseError, + ResourceExists, + IncompatableServerTypeError, + IloLicenseError, + InvalidKeyError, + UnableToDecodeError, + UnabletoFindDriveError, + Encryption, + PathUnavailableError, + TaskQueueError, + UsernamePasswordRequiredError, + TabAndHistoryCompletionClass, + iLORisCorruptionError, + TfaEnablePreRequisiteError, + CloudConnectTimeoutError, + CloudConnectFailedError, + ProxyConfigFailedError, + AlreadyCloudConnectedError, + ResourceNotReadyError, + InstallsetError, + InvalidSmartArrayConfigurationError, + VnicExistsError, + GenerateAndSaveAccountError, + NoAppAccountError, + RemoveAccountError, + AppAccountExistsError, + ReactivateAppAccountTokenError, + InactiveAppAccountTokenError, + VnicLoginError, + SavinginTPMError, + SavinginiLOError, + GenBeforeLoginError, + AppIdListError, + FlashUnsupportedByIloError, + ) + +warnings.filterwarnings("ignore", category=DeprecationWarning) + +if os.name != "nt": + try: + import setproctitle + except ImportError: + setproctitle = None + +# always flush stdout and stderr + +try: + CLI = cliutils.CLI() +except cliutils.ResourceAllocationError: + RdmcError("Unable to allocate more resources.") + RdmcError("ILOREST return code: %s\n" % ReturnCodes.RESOURCE_ALLOCATION_ISSUES_ERROR) + sys.exit(ReturnCodes.RESOURCE_ALLOCATION_ISSUES_ERROR) + +try: + # enable fips mode if our special functions are available in _ssl and OS is + # in FIPS mode + FIPSSTR = "" + if Encryption.check_fips_mode_os(): + LOGGER.info("FIPS mode is enabled in OS!") + # if Encryption.check_fips_mode_os() and not Encryption.check_fips_mode_ssl(): + # ssl.FIPS_mode_set(int(1)) + # if ssl.FIPS_mode(): + # FIPSSTR = "FIPS mode enabled using openssl version %s.\n" % ssl.OPENSSL_VERSION + # LOGGER.info("FIPS mode enabled!") + # else: + # LOGGER.info("FIPS mode can not be enabled!") +except AttributeError: + pass + + +class RdmcCommand(RdmcCommandBase): + """Constructor""" + + def __init__(self, name, usage, summary, aliases, argparser, Args=None): + super().__init__(name, usage, summary, aliases, argparser) + self._commands = collections.OrderedDict() + self.ui = UI(1) + self.commands_dict = dict() + self.interactive = False + self._progname = "%s : %s" % (versioning.__shortname__, versioning.__longname__) + self.opts = None + self.encoding = None + self.config = RdmcConfig() + self.app = redfish.ris.RmcApp(showwarnings=True) + self.retcode = 0 + self.candidates = dict() + self.comm_map = dict() # point command id names or alias to handle + self.commlist = list() + self._redobj = None + self.log_dir = None + self.loaded_commands = [] + + # import all extensions dynamically + for name in extensions.classNames: + pkgName, cName = name.rsplit(".", 1) + pkgName = "extensions" + pkgName + try: + if "__pycache__" not in pkgName and "Command" in cName: + try: + self.commands_dict[cName] = getattr(importlib.import_module(pkgName, __package__), cName)() + except: + self.commands_dict[cName] = getattr( + importlib.import_module("ilorest." + pkgName, __package__), + cName, + )() + sName = pkgName.split(".")[1] + self.add_command(cName, section=sName) + except cliutils.ResourceAllocationError as excp: + self.ui.error(excp) + retcode = ReturnCodes.RESOURCE_ALLOCATION_ISSUES_ERROR + self.ui.error("Unable to allocate more resources.") + self.ui.printer(("ILOREST return code: %s\n" % retcode)) + sys.exit(retcode) + except Exception: + self.ui.error(("loading command: %s" % cName), None) + + # command mapping + commands_to_remove = [] + for command in self.commands_dict: + try: + self.comm_map[self.commands_dict[command].ident.get("name")] = command + for alias in self.commands_dict[command].ident.get("aliases"): + self.comm_map[alias] = command + except Exception as excp: + self.ui.command_not_enabled( + ("Command '%s' unable to be " "initialized...Removing" % command), + excp, + ) + commands_to_remove.append(command) + + # removing commands marked for deletion + for cmd in commands_to_remove: + del self.commands_dict[cmd] + del commands_to_remove + + # ---------End of imports--------- + + def add_command(self, command_name, section=None): + """Handles to addition of new commands + + :param command_name: command name + :type command_name: str. + :param section: section for the new command + :type section: str. + """ + if section not in self._commands: + self._commands[section] = list() + + self._commands[section].append(command_name) + + def get_commands(self): + """Retrieves dictionary of commands""" + return self._commands + + def search_commands(self, cmdname): + """Function to see if command exist in added commands + + :param cmdname: command to be searched + :type cmdname: str. + """ + + try: + tmp = self.comm_map.get(cmdname) + if not tmp: + tmp = cmdname + return self.commands_dict[tmp] + except KeyError: + raise cliutils.CommandNotFoundException(cmdname) + + def load_command(self, cmd): + """Fully Loads command and returns the class instance + + :param cmd: command identifier + :type opts: class + :returns: defined class instance + + """ + try: + cmd.cmdbase = RdmcCommandBase( + cmd.ident["name"], + cmd.ident["usage"], + cmd.ident["summary"], + cmd.ident["aliases"], + ) + cmd.parser = ArgumentParser( + prog=cmd.ident["name"], + usage=cmd.ident["usage"], + description=cmd.ident["description"], + formatter_class=RawTextHelpFormatter, + ) + cmd.rdmc = self + cmd.definearguments(cmd.parser) + for auxcmd in cmd.ident["auxcommands"]: + auxcmd = self.search_commands(auxcmd) + if auxcmd not in self.loaded_commands: + self.loaded_commands.append(auxcmd) + cmd.auxcommands[auxcmd.ident["name"]] = self.load_command(auxcmd) + else: + cmd.auxcommands[auxcmd.ident["name"]] = self.commands_dict[self.comm_map[auxcmd.ident["name"]]] + return cmd + except Exception as excp: + raise RdmcError("Unable to load command {}: {}".format(cmd.ident["name"], excp)) + + def mask_passwords(self, args): + """Replaces password values in a command argument list with '****'.""" + return SecurityMasker.mask_command_arguments(args) + + def _run_command(self, opts, args, help_disp): + """Calls the command's run function. + + :param opts: Command options. + :type opts: argparse.Namespace + :param args: List of entered arguments. + :type args: list + :param help_disp: Help display flag. + :type help_disp: bool + :returns: Command execution result. + """ + if not args: + LOGGER.error("No command provided. Exiting execution.") + raise ValueError("No command provided.") + + masked_args = self.mask_passwords(args) + + LOGGER.info(f"Executing command: {args[0]} with arguments: {masked_args}") + + cmd = self.search_commands(args[0]) + if cmd is None: + LOGGER.error(f"Command '{args[0]}' not found.") + raise ValueError(f"Command '{args[0]}' not found.") + + self.load_command(cmd) + LOGGER.debug(f"Command '{args[0]}' loaded successfully.") + + # Handle JSON output flag + if any(a in ("-j", "--json") for a in args): + opts.nologo = True + LOGGER.debug("JSON output mode enabled.") + + # Show version information if needed + if not opts.nologo and not self.interactive: + LOGGER.info("Displaying version information.") + CLI.version(self._progname, versioning.__version__, versioning.__extracontent__) + + # Execute the command with remaining arguments + LOGGER.info(f"Running command: {args[0]} with arguments: {masked_args if len(args) > 1 else 'None'}") + return cmd.run(args[1:], help_disp=help_disp) if len(args) > 1 else cmd.run([], help_disp=help_disp) + + def run(self, line, help_disp=False): + """Main rdmc command worker function + + :param line: entered command line + :type line: list. + """ + if os.name == "nt": + if not ctypes.windll.shell32.IsUserAnAdmin() != 0: + self.app.typepath.adminpriv = False + elif not os.getuid() == 0: + self.app.typepath.adminpriv = False + + if "--version" in line or "-V" in line: + CLI.printer("%s %s\n" % (versioning.__longname__, versioning.__version__)) + sys.exit(self.retcode) + + help_disp = False + all_opts = True + help_indx = None + help_list = ["-h", "--help"] + for indx, elem in enumerate(line): + if elem in help_list: + help_indx = indx + if "-" in elem: + continue + else: + all_opts = False + if all_opts and (("-h" in line) or ("--help" in line)): + line = ["-h"] + elif help_indx: + # for comm in self.commands_dict: + # if self.commands_dict[comm].ident['name'] == line[0]: + # self.ui.printer(self.commands_dict[comm].ident['usage']) + # return ReturnCodes.SUCCESS + del line[help_indx] + help_disp = True + + if line and line[0] in ["-h", "--help"]: + cmddict = self.get_commands() + sorted_keys = sorted(list(cmddict.keys())) + + for key in sorted_keys: + if key[0] == "_": + continue + else: + self.parser.epilog = self.parser.epilog + "\n\n" + key + "\n" + for cmd in cmddict[key]: + c_help = "%-25s - %s\n" % ( + self.commands_dict[cmd].ident["name"], + self.commands_dict[cmd].ident["summary"], + ) + self.parser.epilog = self.parser.epilog + c_help + + (self.opts, nargv) = self.parser.parse_known_args(line) + + if self.opts.redirect: + try: + sys.stdout = open("console.log", "w") + except: + print("Unable to re-direct output for STDOUT.\n") + else: + print("Start of stdout file.\n\n") + try: + sys.stderr = open("console_err.log", "w") + except IOError: + print("Unable to re-direct output for STDERR.\n") + else: + print("Start of stderr file.\n\n") + + self.app.verbose = self.ui.verbosity = self.opts.verbose + + try: + # Test encoding functions are there + Encryption.encode_credentials("test") + self.app.set_encode_funct(Encryption.encode_credentials) + self.app.set_decode_funct(Encryption.decode_credentials) + self.encoding = True + except redfish.hpilo.risblobstore2.ChifDllMissingError: + self.encoding = False + + if self.opts.config is not None and len(self.opts.config) > 0: + if not os.path.isfile(self.opts.config): + self.retcode = ReturnCodes.CONFIGURATION_FILE_ERROR + sys.exit(self.retcode) + + self.config.configfile = self.opts.config + else: + # Default locations: Windows: executed directory Linux: /etc/ilorest/redfish.conf + self.config.configfile = ( + os.path.join(os.path.dirname(sys.executable), "redfish.conf") + if os.name == "nt" + else "/etc/ilorest/redfish.conf" + ) + + if not os.path.isfile(self.config.configfile): + LOGGER.warning("Config file '%s' not found\n\n", self.config.configfile) + + self.config.load() + + cachedir = None + if not self.opts.nocache: + self.config.cachedir = os.path.join(self.opts.config_dir, "cache") + cachedir = self.config.cachedir + + if cachedir: + self.app.cachedir = cachedir + try: + os.makedirs(cachedir) + except OSError as ex: + if ex.errno == errno.EEXIST: + pass + else: + raise + if self.opts.logdir and (self.opts.debug or not self.opts.noinfolog): + logdir = self.opts.logdir + else: + logdir = os.getcwd() + self.log_dir = logdir + try: + setup_logging_from_json(opts=self.opts) + except Exception: + # setup_logging_from_json contains its own fallback logging + # mechanisms. If something unexpected happens still ensure + # the package logger exists. + try: + LOGGER.warning("Failed to initialize JSON logging configuration; using fallback.") + except Exception: + pass + + self.app.LOGGER = LOGGER + + if ("login" in line or any(x.startswith("--url") for x in line) or not line) and not ( + any(x.startswith(("-h", "--h")) for x in nargv) or "help" in line + ): + if not any(x.startswith("--sessionid") for x in line): + self.app.logout() + else: + creds, enc = self._pull_creds(nargv) + self.app.restore(creds=creds, enc=enc) + self.opts.is_redfish = self.app.typepath.updatedefinesflag(redfishflag=self.opts.is_redfish) + + if nargv: + try: + self.retcode = self._run_command(self.opts, nargv, help_disp) + if "multiconnect" not in line: + if self.app.cache: + if ("logout" not in line) and ("--logout" not in line): + self.app.save() + self.app.redfishinst = None + else: + self.app.logout() + except AttributeError: + self.retcode = 0 + pass + except Exception as excp: + self.handle_exceptions(excp) + + return self.retcode + else: + self.cmdloop(self.opts) + + if self.app.cache: + self.app.save() + else: + self.app.logout() + + def cmdloop(self, opts): + """Interactive mode worker function. + + :param opts: Command options. + :type opts: argparse.Namespace + """ + LOGGER.info("Starting interactive mode.") + self.interactive = True + + if not opts.nologo: + sys.stdout.write(FIPSSTR) + CLI.version(self._progname, versioning.__version__, versioning.__extracontent__) + LOGGER.info("Displayed version information.") + + if not self.app.typepath.adminpriv: + self.ui.user_not_admin() + LOGGER.warning("User does not have admin privileges.") + + LOGGER.info("Loading available commands.") + for command, values in self.commands_dict.items(): + self.commlist.append(values.ident["name"]) + + for item in self.commlist: + self.candidates[item] = self.commlist if item == "help" else [] + + self._redobj = TabAndHistoryCompletionClass(dict(self.candidates)) + + def bottom_toolbar(): + return HTML("Restful Interface Tool") + + try: + session = PromptSession( + completer=self._redobj, + auto_suggest=AutoSuggestFromHistory(), + complete_style=CompleteStyle.READLINE_LIKE, + complete_while_typing=True, + ) + session.output.disable_bracketed_paste() + LOGGER.info("Tab completion enabled.") + except Exception as e: + LOGGER.exception("Console error: Tab complete is unavailable.") + session = None + + if self.opts.notab: + LOGGER.info("Tab completion is disabled as per user settings.") + session = None + + while True: + try: + prompt_string = str(versioning.__shortname__) + " > " + if session: + if self.opts.toolbar: + line = session.prompt(prompt_string, bottom_toolbar=bottom_toolbar) + else: + line = session.prompt(prompt_string) + else: + line = input(prompt_string) + + LOGGER.debug(f"User input: {SecurityMasker.mask_user_input(line)}") + + except (EOFError, KeyboardInterrupt): + LOGGER.info("User exited interactive mode.") + line = "quit\n" + + if not line.strip(): + continue + elif line.endswith(os.linesep): + line = line.rstrip(os.linesep) + + shlex.escape = "" + nargv = shlex.shlex(line, posix=True) + nargv.escape = "" + nargv.whitespace_split = True + nargv = list(nargv) + + LOGGER.debug(f"Parsed command arguments: {SecurityMasker.mask_command_arguments(nargv)}") + + try: + if not ( + any(x.startswith("-h") for x in nargv) or any(x.startswith("--h") for x in nargv) or "help" in line + ): + if "login " in line or line == "login" or any(x.startswith("--url") for x in nargv): + self.app.logout() + LOGGER.info("User triggered logout.") + + self.retcode = self._run_command(opts, nargv, help_disp=False) + LOGGER.info(f"Command executed with return code: {self.retcode}") + self.check_for_tab_lists(nargv) + + except Exception as excp: + LOGGER.exception("Exception occurred while executing command.") + self.handle_exceptions(excp) + + if self.opts.verbose: + sys.stdout.write(f"iLOrest return code: {self.retcode}\n") + + LOGGER.info("Exiting interactive mode.") + return self.retcode + + def handle_exceptions(self, excp): + """Main exception handler for both shell and interactive modes + + :param excp: captured exception to be handled + :type excp: exception. + """ + # pylint: disable=redefined-argument-from-local + try: + if not getattr(excp, "_already_logged", False): + LOGGER.info(f"Exception: {type(excp).__name__}: {excp}") + setattr(excp, "_already_logged", True) + raise + # ****** RDMC ERRORS ****** + except ConfigurationFileError as excp: + self.retcode = ReturnCodes.CONFIGURATION_FILE_ERROR + self.ui.error(excp) + sys.exit(excp.errcode) + except InstallsetError as excp: + self.retcode = ReturnCodes.INSTALLSET_ERROR + self.ui.error(excp) + except InvalidCommandLineError as excp: + self.retcode = ReturnCodes.INVALID_COMMAND_LINE_ERROR + self.ui.invalid_commmand_line(excp) + except NoCurrentSessionEstablished as excp: + self.retcode = ReturnCodes.NO_CURRENT_SESSION_ESTABLISHED + self.ui.error(excp) + except TfaEnablePreRequisiteError as excp: + self.retcode = ReturnCodes.TFA_ENABLED_ERROR + self.ui.error(excp) + except iLORisCorruptionError as excp: + self.retcode = ReturnCodes.ILO_RIS_CORRUPTION_ERROR + self.ui.error(excp) + except ResourceNotReadyError as excp: + self.retcode = ReturnCodes.RESOURCE_NOT_READY_ERROR + self.ui.error(excp) + except CloudConnectTimeoutError as excp: + self.retcode = ReturnCodes.CLOUD_CONNECT_TIMEOUT + self.ui.error(excp) + except CloudConnectFailedError as excp: + self.retcode = ReturnCodes.CLOUD_CONNECT_FAILED + self.ui.error(excp) + except AlreadyCloudConnectedError as excp: + self.retcode = ReturnCodes.CLOUD_ALREADY_CONNECTED + self.ui.error(excp) + except ProxyConfigFailedError as excp: + self.retcode = ReturnCodes.PROXY_CONFIG_FAILED + self.ui.error(excp) + except UsernamePasswordRequiredError as excp: + self.retcode = ReturnCodes.USERNAME_PASSWORD_REQUIRED_ERROR + self.ui.error(excp) + except InvalidPasswordLengthError as excp: + self.retcode = ReturnCodes.INVALID_PASSWORD_LENGTH_ERROR + self.ui.error(excp) + except NoChangesFoundOrMadeError as excp: + self.retcode = ReturnCodes.NO_CHANGES_MADE_OR_FOUND + self.ui.invalid_commmand_line(excp) + except StandardBlobErrorHandler as excp: + self.retcode = ReturnCodes.GENERAL_ERROR + self.ui.standard_blob_error(excp) + except InvalidFileInputError as excp: + self.retcode = ReturnCodes.INVALID_FILE_INPUT_ERROR + self.ui.invalid_commmand_line(excp) + except InvalidCommandLineErrorOPTS as excp: + self.retcode = ReturnCodes.INVALID_COMMAND_LINE_ERROR + self.ui.invalid_commmand_line(excp) + except InvalidFileFormattingError as excp: + self.retcode = ReturnCodes.INVALID_FILE_FORMATTING_ERROR + self.ui.invalid_file_formatting(excp) + except NoContentsFoundForOperationError as excp: + self.retcode = ReturnCodes.NO_CONTENTS_FOUND_FOR_OPERATION + self.ui.no_contents_found_for_operation(excp) + except InfoMissingEntriesError as excp: + self.retcode = ReturnCodes.NO_VALID_INFO_ERROR + self.ui.error(excp) + except ( + InvalidOrNothingChangedSettingsError, + redfish.ris.rmc_helper.IncorrectPropValue, + ) as excp: + self.retcode = ReturnCodes.SAME_SETTINGS_ERROR + self.ui.error(excp) + except NoDifferencesFoundError as excp: + self.retcode = ReturnCodes.NO_CHANGES_MADE_OR_FOUND + self.ui.no_differences_found(excp) + except MultipleServerConfigError as excp: + self.retcode = ReturnCodes.MULTIPLE_SERVER_CONFIG_FAIL + self.ui.multiple_server_config_fail(excp) + except InvalidMSCfileInputError as excp: + self.retcode = ReturnCodes.MULTIPLE_SERVER_INPUT_FILE_ERROR + self.ui.multiple_server_config_input_file(excp) + except FirmwareUpdateError as excp: + self.retcode = ReturnCodes.FIRMWARE_UPDATE_ERROR + self.ui.error(excp) + except FailureDuringCommitError as excp: + self.retcode = ReturnCodes.FAILURE_DURING_COMMIT_OPERATION + self.ui.error(excp) + except BootOrderMissingEntriesError as excp: + self.retcode = ReturnCodes.BOOT_ORDER_ENTRY_ERROR + self.ui.error(excp) + except NicMissingOrConfigurationError as excp: + self.retcode = ReturnCodes.NIC_MISSING_OR_INVALID_ERROR + self.ui.error(excp) + except ( + IncompatibleiLOVersionError, + redfish.ris.rmc_helper.IncompatibleiLOVersionError, + ) as excp: + self.retcode = ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + self.ui.printer(excp) + except IncompatableServerTypeError as excp: + self.retcode = ReturnCodes.INCOMPATIBLE_SERVER_TYPE + self.ui.printer(excp) + except IloLicenseError as excp: + self.ui.printer(excp) + self.retcode = ReturnCodes.ILO_LICENSE_ERROR + except InvalidCListFileError as excp: + self.retcode = ReturnCodes.INVALID_CLIST_FILE_ERROR + self.ui.error(excp) + except PartitionMoutingError as excp: + self.retcode = ReturnCodes.UNABLE_TO_MOUNT_BB_ERROR + self.ui.error(excp) + except TimeOutError as excp: + self.retcode = ReturnCodes.UPDATE_SERVICE_BUSY + self.ui.error(excp) + except DownloadError as excp: + self.retcode = ReturnCodes.FAILED_TO_DOWNLOAD_COMPONENT + self.ui.error(excp) + except UploadError as excp: + self.retcode = ReturnCodes.FAILED_TO_UPLOAD_COMPONENT + self.ui.error(excp) + except FlashUnsupportedByIloError as excp: + self.retcode = ReturnCodes.ILO_UNSUPPORTED_FLASH + self.ui.error(excp) + except BirthcertParseError as excp: + self.retcode = ReturnCodes.BIRTHCERT_PARSE_ERROR + self.ui.error(excp) + except ResourceExists as excp: + self.retcode = ReturnCodes.RESOURCE_EXISTS_ERROR + self.ui.error(excp) + except InvalidKeyError as excp: + self.retcode = ReturnCodes.ENCRYPTION_ERROR + self.ui.error("Invalid key has been entered for encryption/decryption.\n") + except UnableToDecodeError as excp: + self.retcode = ReturnCodes.ENCRYPTION_ERROR + self.ui.error(excp) + except UnabletoFindDriveError as excp: + self.retcode = ReturnCodes.DRIVE_MISSING_ERROR + self.ui.error(excp) + self.ui.printer("Error occurred while reading device labels.\n") + except PathUnavailableError as excp: + self.retcode = ReturnCodes.PATH_UNAVAILABLE_ERROR + if excp: + self.ui.error(excp) + else: + self.ui.printer("Requested path is unavailable.") + except TaskQueueError as excp: + self.retcode = ReturnCodes.TASKQUEUE_ERROR + self.ui.error(excp) + except DeviceDiscoveryInProgress as excp: + self.retcode = ReturnCodes.DEVICE_DISCOVERY_IN_PROGRESS + self.ui.error(excp) + except InvalidSmartArrayConfigurationError as excp: + self.retcode = ReturnCodes.INVALID_SMART_ARRAY_PAYLOAD + except GenBeforeLoginError as excp: + self.retcode = ReturnCodes.GEN_BEFORE_LOGIN_ERROR + if not getattr(excp, "_already_logged", False): + self.ui.error(f"{type(excp).__name__}: {excp}") + setattr(excp, "_already_logged", True) + # ****** CLI ERRORS ****** + except (CommandNotEnabledError, cliutils.CommandNotFoundException) as excp: + self.retcode = ReturnCodes.UI_CLI_COMMAND_NOT_FOUND_EXCEPTION + self.ui.command_not_found(excp) + # try: + # self.commands_dict['HelpCommand'].run('-h') + # except KeyError: + # pass + + # ****** RMC/RIS ERRORS ****** + except redfish.ris.UndefinedClientError: + self.retcode = ReturnCodes.RIS_UNDEFINED_CLIENT_ERROR + self.ui.error("Please login before making a selection.") + except ( + redfish.ris.InstanceNotFoundError, + redfish.ris.RisInstanceNotFoundError, + ) as excp: + self.retcode = ReturnCodes.RIS_INSTANCE_NOT_FOUND_ERROR + self.ui.printer(excp) + except redfish.ris.CurrentlyLoggedInError as excp: + self.retcode = ReturnCodes.RIS_CURRENTLY_LOGGED_IN_ERROR + self.ui.error(excp) + except redfish.ris.NothingSelectedError as excp: + self.retcode = ReturnCodes.RIS_NOTHING_SELECTED_ERROR + self.ui.nothing_selected() + except redfish.ris.NothingSelectedFilterError as excp: + self.retcode = ReturnCodes.RIS_NOTHING_SELECTED_FILTER_ERROR + self.ui.nothing_selected_filter() + except redfish.ris.NothingSelectedSetError as excp: + self.retcode = ReturnCodes.RIS_NOTHING_SELECTED_SET_ERROR + self.ui.nothing_selected_set() + except redfish.ris.InvalidSelectionError as excp: + self.retcode = ReturnCodes.RIS_INVALID_SELECTION_ERROR + self.ui.error(excp) + except redfish.ris.rmc_helper.UnableToObtainIloVersionError as excp: + self.retcode = ReturnCodes.INCOMPATIBLE_ILO_VERSION_ERROR + self.ui.error(excp) + except redfish.ris.IdTokenError as excp: + if hasattr(excp, "message"): + self.ui.printer(excp.message) + else: + self.ui.printer( + "Logged-in account does not have the privilege" + " required to fulfill the request or a required" + " token is missing." + "\nEX: biospassword flag if bios password present " + "or tpmenabled flag if TPM module present.\n" + ) + self.retcode = ReturnCodes.RIS_MISSING_ID_TOKEN + except redfish.ris.SessionExpired as excp: + self.retcode = ReturnCodes.RIS_SESSION_EXPIRED + self.app.logout() + self.ui.printer( + "Current session has expired or is invalid, " + "please login again with proper credentials to continue.\n" + ) + except redfish.ris.ValidationError as excp: + self.retcode = ReturnCodes.RIS_VALIDATION_ERROR + except redfish.ris.ValueChangedError as excp: + self.retcode = ReturnCodes.RIS_VALUE_CHANGED_ERROR + except redfish.ris.ris.SchemaValidationError as excp: + self.ui.printer("Error found in schema, try running with the " "--latestschema flag.\n") + self.retcode = ReturnCodes.RIS_SCHEMA_PARSE_ERROR + # ****** RMC/RIS ERRORS ****** + except redfish.rest.connections.RetriesExhaustedError as excp: + self.retcode = ReturnCodes.V1_RETRIES_EXHAUSTED_ERROR + self.ui.retries_exhausted_attemps() + except redfish.rest.connections.VnicNotEnabledError as excp: + self.retcode = ReturnCodes.VNIC_NOT_ENABLED_ERROR + self.ui.retries_exhausted_vnic_not_enabled() + except redfish.rest.v1.InvalidCredentialsError as excp: + self.retcode = ReturnCodes.V1_INVALID_CREDENTIALS_ERROR + self.ui.invalid_credentials(excp) + except redfish.rest.v1.JsonDecodingError as excp: + self.retcode = ReturnCodes.JSON_DECODE_ERROR + self.ui.error(excp) + except redfish.rest.v1.ServerDownOrUnreachableError as excp: + self.retcode = ReturnCodes.V1_SERVER_DOWN_OR_UNREACHABLE_ERROR + self.ui.error(excp) + except redfish.rest.connections.ChifDriverMissingOrNotFound as excp: + self.retcode = ReturnCodes.V1_CHIF_DRIVER_MISSING_ERROR + self.ui.printer( + "Chif driver not found, please check that the iLO channel interface" " driver (Chif) is installed.\n" + ) + except redfish.rest.connections.SecurityStateError as excp: + self.retcode = ReturnCodes.V1_SECURITY_STATE_ERROR + self.ui.printer( + "High security mode [%s] or Host Authentication has been enabled. " + "Please provide valid credentials.\n" % str(excp) + ) + except redfish.rest.connections.OneTimePasscodeError: + self.retcode = ReturnCodes.TFA_OTP_EMAILED + self.ui.printer( + "One Time Passcode Sent to registered email.\n" + "Retry the login command by including -o/--otp tag along with the OTP received.\n" + ) + except redfish.rest.connections.UnauthorizedLoginAttemptError as excp: + self.retcode = ReturnCodes.TFA_WRONG_OTP + self.ui.error(excp) + except redfish.rest.connections.TokenExpiredError as excp: + self.retcode = ReturnCodes.TFA_OTP_TIMEDOUT + self.ui.error(excp) + except redfish.hpilo.risblobstore2.ChifDllMissingError as excp: + self.retcode = ReturnCodes.REST_ILOREST_CHIF_DLL_MISSING_ERROR + self.ui.printer( + "iLOrest Chif library not found, please check that the chif " "ilorest_chif.dll/.so is present.\n" + ) + except redfish.hpilo.risblobstore2.UnexpectedResponseError as excp: + self.retcode = ReturnCodes.REST_ILOREST_UNEXPECTED_RESPONSE_ERROR + self.ui.printer("Unexpected data received from iLO.\n") + except redfish.hpilo.risblobstore2.HpIloError as excp: + self.retcode = ReturnCodes.REST_ILOREST_ILO_ERROR + self.ui.printer("iLO returned a failed error code.\n") + except redfish.hpilo.risblobstore2.Blob2CreateError as excp: + self.retcode = ReturnCodes.REST_ILOREST_CREATE_BLOB_ERROR + self.ui.printer("Blob create operation failed.\n") + except redfish.hpilo.risblobstore2.Blob2ReadError as excp: + self.retcode = ReturnCodes.REST_ILOREST_READ_BLOB_ERROR + self.ui.printer("Blob read operation failed.\n") + except redfish.hpilo.risblobstore2.Blob2WriteError as excp: + self.retcode = ReturnCodes.REST_ILOREST_WRITE_BLOB_ERROR + self.ui.printer("Blob write operation failed.\n") + except redfish.hpilo.risblobstore2.Blob2DeleteError as excp: + self.retcode = ReturnCodes.REST_ILOREST_BLOB_DELETE_ERROR + self.ui.printer("Blob delete operation failed.\n") + except redfish.hpilo.risblobstore2.Blob2OverrideError as excp: + self.retcode = ReturnCodes.REST_ILOREST_BLOB_OVERRIDE_ERROR + self.ui.error(excp) + self.ui.printer( + "\nAnother user access in progress. Pease ensure only one user is accessing at a time locally.\n" + ) + except redfish.hpilo.risblobstore2.BlobRetriesExhaustedError as excp: + self.retcode = ReturnCodes.REST_BLOB_RETRIES_EXHAUSETED_ERROR + self.ui.printer("\nBlob operation still fails after max retries.\n") + except redfish.hpilo.risblobstore2.Blob2FinalizeError as excp: + self.retcode = ReturnCodes.REST_ILOREST_BLOB_FINALIZE_ERROR + self.ui.printer("Blob finalize operation failed.") + except redfish.hpilo.risblobstore2.BlobNotFoundError as excp: + self.retcode = ReturnCodes.REST_ILOREST_BLOB_NOT_FOUND_ERROR + self.ui.printer("Blob not found with key and namespace provided.\n") + except redfish.ris.rmc_helper.InvalidPathError as excp: + self.retcode = ReturnCodes.RIS_REF_PATH_NOT_FOUND_ERROR + self.ui.printer("Reference path not found.") + except redfish.ris.rmc_helper.IloResponseError as excp: + self.retcode = ReturnCodes.RIS_ILO_RESPONSE_ERROR + except redfish.ris.rmc_helper.UserNotAdminError as excp: + self.ui.user_not_admin() + self.retcode = ReturnCodes.USER_NOT_ADMIN + except redfish.hpilo.rishpilo.HpIloInitialError as excp: + self.ui.error(excp) + self.retcode = ReturnCodes.RIS_ILO_INIT_ERROR + except redfish.hpilo.rishpilo.HpIloChifAccessDeniedError as excp: + self.ui.error(excp) + self.retcode = ReturnCodes.RIS_ILO_CHIF_ACCESS_DENIED_ERROR + except redfish.hpilo.rishpilo.HpIloPrepareAndCreateChannelError as excp: + self.ui.error(excp) + self.retcode = ReturnCodes.RIS_CREATE_AND_PREPARE_CHANNEL_ERROR + except redfish.hpilo.rishpilo.HpIloChifPacketExchangeError as excp: + self.ui.error(excp) + self.retcode = ReturnCodes.RIS_ILO_CHIF_PACKET_EXCHANGE_ERROR + except redfish.hpilo.rishpilo.HpIloNoDriverError as excp: + self.ui.error(excp) + self.retcode = ReturnCodes.RIS_ILO_CHIF_NO_DRIVER_ERROR + except redfish.hpilo.rishpilo.HpIloWriteError as excp: + self.ui.error(excp) + self.retcode = ReturnCodes.RESOURCE_ALLOCATION_ISSUES_ERROR + except redfish.hpilo.rishpilo.HpIloReadError as excp: + self.ui.error(excp) + self.retcode = ReturnCodes.RESOURCE_ALLOCATION_ISSUES_ERROR + # ****** RIS OBJECTS ERRORS ****** + except redfish.ris.ris.BiosUnregisteredError as excp: + self.retcode = ReturnCodes.RIS_RIS_BIOS_UNREGISTERED_ERROR + self.ui.bios_unregistered_error() + # ****** FILE/IO ERRORS ****** + except IOError: + self.retcode = ReturnCodes.INVALID_FILE_INPUT_ERROR + self.ui.printer( + "Error accessing the file path. Verify the file path is correct and " "you have proper permissions.\n" + ) + # ****** VNIC ERRORS ****** # + except VnicExistsError as excp: + self.retcode = ReturnCodes.VNIC_DOES_NOT_EXIST_ERROR + self.ui.error(excp) + except GenerateAndSaveAccountError as excp: + self.retcode = ReturnCodes.GENERAL_ACCOUNT_GENERATE_SAVE_ERROR + self.ui.error(excp) + except NoAppAccountError as excp: + self.retcode = ReturnCodes.ACCOUNT_DOES_NOT_EXIST_ERROR + self.ui.error(excp) + except RemoveAccountError as excp: + self.retcode = ReturnCodes.ACCOUNT_REMOVE_ERROR + self.ui.error(excp) + except AppAccountExistsError as excp: + self.retcode = ReturnCodes.ACCOUNT_EXISTS_CHECK_ERROR + self.ui.error(excp) + except ReactivateAppAccountTokenError as excp: + self.retcode = ReturnCodes.REACTIVATE_APP_ACCOUNT_TOKEN_ERROR + self.ui.error(excp) + except InactiveAppAccountTokenError as excp: + self.retcode = ReturnCodes.INACTIVE_APP_ACCOUNT_TOKEN + self.ui.error(excp) + except VnicLoginError as excp: + self.retcode = ReturnCodes.VNIC_LOGIN_ERROR + self.ui.error(excp) + except SavinginTPMError as excp: + self.retcode = ReturnCodes.ACCOUNT_SAVE_ERROR_TPM + self.ui.error(excp) + except SavinginiLOError as excp: + self.retcode = ReturnCodes.ACCOUNT_SAVE_ERROR_ILO + self.ui.error(excp) + except AppIdListError as excp: + self.retcode = ReturnCodes.APPID_LIST_ERROR + self.ui.error(excp) + # ****** GENERAL ERRORS ****** + except SystemExit: + self.retcode = ReturnCodes.GENERAL_ERROR + raise + except Exception as excp: + self.retcode = ReturnCodes.GENERAL_ERROR + sys.stderr.write("ERROR: %s\n" % excp) + + if self.opts.debug: + traceback.print_exc(file=sys.stderr) + + def check_for_tab_lists(self, command=None): + """Function to generate available options for tab tab + :param command: command for auto tab completion + :type command: string. + """ + # TODO: don't always update, only when we have a different selector. + # Try to use args to get specific nested posibilities? + changes = dict() + + # select options + typeslist = list() + + try: + typeslist = sorted(set(self.app.types())) + changes["select"] = typeslist + except: + pass + + # get/set/info options + getlist = list() + + try: + if self.app.typepath.defs: + typestr = self.app.typepath.defs.typestring + templist = self.app.getprops() + dictcopy = copy.copy(templist[0]) + + for content in templist: + for k in list(content.keys()): + if k.lower() in HARDCODEDLIST or "@odata" in k.lower(): + del content[k] + if "Bios." in dictcopy[typestr]: + if hasattr( + templist[0], + "Attributes", + ): + templist = templist[0]["Attributes"] + else: + templist = templist[0] + else: + templist = templist[0] + for key, _ in templist.items(): + getlist.append(key) + + getlist.sort() + + # if select command, get possible values + infovals = dict() + + if "select" in command: + if typestr in dictcopy: + (_, attributeregistry) = self.app.get_selection(setenable=True) + schema, reg = self.app.get_model(dictcopy, attributeregistry) + + if reg: + if "Attributes" in reg: + reg = reg["Attributes"] + for item in getlist: + for attribute in reg: + if item == attribute: + infovals.update({item: reg[attribute]}) + break + + changes["nestedinfo"] = infovals + + elif schema: + changes["nestedinfo"] = schema + + changes["get"] = getlist + changes["nestedprop"] = dictcopy["Attributes"] if "Attributes" in dictcopy else dictcopy + changes["set"] = getlist + changes["info"] = getlist + changes["val"] = [] + + readonly_list = [] + + for info in schema: + for checkread in schema[info]: + if "readonly" in checkread and schema[info]["readonly"] is True: + readonly_list.append(info) + + set_list = [x for x in getlist if x not in readonly_list] + + changes["set"] = set_list + + except: + pass + + if changes: + self._redobj.updates_tab_completion_lists(changes) + + def _pull_creds(self, args): + """Pull creds from the arguments for blobstore""" + cred_args = {} + enc = False + arg_iter = iter(args) + try: + for arg in arg_iter: + if arg in ("--enc", "-e"): + enc = True + if arg in ("-u", "--user"): + cred_args["username"] = next(arg_iter) + elif arg in ("-p", "--password"): + cred_args["password"] = next(arg_iter) + except StopIteration: + return {}, False + return cred_args, enc + + def rdmc_parse_arglist(self, cmdinstance, line=None, default=False): + """ + Parses command-line arguments with special consideration for quoting and optional arguments. + + :param cmdinstance: Command instance to be referenced. + :type cmdinstance: class object + :param line: String of arguments passed in. + :type line: str or list + :param default: Flag to determine if the parsed command requires the default workaround for + argparse in Python 2. Argparse incorrectly assumes a sub-command is always + required, so we have to include a default sub-command for no arguments. + :type default: bool + :returns: Parsed argument list + """ + + def checkargs(argopts): + """Check for invalid optional args.""" + (_, args) = argopts + for arg in args: + if arg.startswith("-") or arg.startswith("--"): + try: + cmdinstance.parser.error( + "The option %s is not available for %s" % (arg, cmdinstance.ident["name"]) + ) + except SystemExit: + raise InvalidCommandLineErrorOPTS("") + return argopts + + # Ensure `None` does not cause errors + if line is None: + return checkargs(cmdinstance.parser.parse_known_args([])) + + # Parse argument list correctly + if isinstance(line, six.string_types): + arglist = shlex.split(line, posix=False) + arglist = [val.strip("\"'") for val in arglist] # Strip surrounding quotes + elif isinstance(line, list): + arglist = line + else: + raise ValueError("Invalid type for 'line'. Expected str or list.") + + # Handle Windows-specific globbing + exarglist = [] + if os.name == "nt": + for arg in arglist: + try: + matches = glob.glob(arg) + exarglist.extend(matches if matches else [arg]) + except Exception as e: + if arg: + exarglist.append(arg) + else: + exarglist = [arg for arg in arglist if arg] + + # Insert 'default' if needed + if default and not any(_opt in ["-h", "--h", "-help", "--help"] for _opt in exarglist): + exarglist.insert(0, "default") + + return checkargs(cmdinstance.parser.parse_known_args(exarglist)) + + +def ilorestcommand(): + # Parse command line arguments early to extract logging flags + ARGUMENTS = sys.argv[1:] + # Create a temporary parser to extract logging flags early + temp_parser = ArgumentParser(add_help=False) + temp_parser.add_argument("--nostdoutlog", action="store_true", default=False) + temp_parser.add_argument("--noinfolog", action="store_true", default=False) + temp_parser.add_argument("--debug", action="store_true", default=False) + temp_parser.add_argument("--logdir", dest="logdir", help="Use the provided directory for logs", default=None) + # Parse known args to extract logging flags + temp_opts, _ = temp_parser.parse_known_args(ARGUMENTS) + + # Initialize logging from JSON configuration with parsed flags + setup_logging_from_json(temp_opts) + + RDMC = RdmcCommand( + Args=ARGUMENTS, + name=versioning.__shortname__, + usage=versioning.__shortname__ + " [command]", + summary="HPE RESTful Interface Tool", + aliases=[versioning.__shortname__], + argparser=RdmcOptionParser(), + ) + + # Main execution function call wrapper + if "setproctitle" in sys.modules: + FOUND = False + VARIABLE = setproctitle.getproctitle() + + for items in VARIABLE.split(" "): + if FOUND: + VARIABLE = VARIABLE.replace(items, "xxxxxxxx") + break + + if items == "--password" or items == "-p": + FOUND = True + + setproctitle.setproctitle(VARIABLE) + + RDMC.retcode = RDMC.run(ARGUMENTS) + + if RDMC.opts: + if RDMC.opts.verbose: + RDMC.ui.printer(("iLORest return code: %s\n" % RDMC.retcode)) + else: + RDMC.ui.printer(("iLORest return code: %s\n" % RDMC.retcode)) + + # Return code + sys.exit(RDMC.retcode) + + +if __name__ == "__main__": + ilorestcommand() diff --git a/venv/lib/python3.10/site-packages/ilorest/rdmc_base_classes.py b/venv/lib/python3.10/site-packages/ilorest/rdmc_base_classes.py new file mode 100644 index 0000000..b2119eb --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/rdmc_base_classes.py @@ -0,0 +1,537 @@ +### +# Copyright 2017-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""This is the helper module for RDMC""" + +# ---------Imports--------- +import os +import sys +from argparse import ( + SUPPRESS, + Action, + ArgumentParser, + RawDescriptionHelpFormatter, + _ArgumentGroup, +) +from redfish.ris import NothingSelectedError + +try: + from ilorest import cliutils, versioning, rdmc_helper + from ilorest.rdmc_helper import InvalidCommandLineErrorOPTS +except ImportError: + import cliutils + import versioning + import rdmc_helper + from rdmc_helper import InvalidCommandLineErrorOPTS + +# ---------End of imports--------- + + +class _Verbosity(Action): + def __init__(self, option_strings, dest, nargs, **kwargs): + super(_Verbosity, self).__init__(option_strings, dest, nargs, **kwargs) + + def __call__(self, parser, namespace, values, option_strings): + try: + if values: + tmp = next(iter(values)) + if tmp.isdigit(): + namespace.verbose = int(tmp) + else: + namespace.verbose = len(tmp) + 1 + return + namespace.verbose = 1 + except: + raise InvalidCommandLineErrorOPTS("Invalid verbosity selection ('-v').") + + +class CommandBase(object): + """Abstract base class for all Command objects. + + This class is used to build complex command line programs + """ + + def __init__(self, name, usage, summary, aliases=None, argparser=None): + self.name = name + self.summary = summary + self.aliases = aliases + self.config_required = True # does the command access config data + + if argparser is None: + self.parser = ArgumentParser() + else: + self.parser = argparser + + self.parser.usage = usage + + def run(self, line, help_disp=False): + """Called to actually perform the work. + + Override this method in your derived class. This is where your program + actually does work. + """ + pass + + +class RdmcCommandBase(CommandBase): + """Base class for rdmc commands which includes some common helper + methods. + """ + + def __init__(self, name, usage, summary, aliases, argparser=None, **kwargs): + """Constructor""" + CommandBase.__init__( + self, name=name, usage=usage, summary=summary, aliases=aliases, argparser=argparser, **kwargs + ) + self.json = False + self.cache = False + self.nologo = False + self.toolbar = False + + def login_validation(self, cmdinstance, options, skipbuild=False): + """Combined validation function to login and select with other commands. Not for use with + login or select commands themselves. Make sure your command imports options from + add_login_arguments_group or there will be errors. + + :param cmdinstance: the command object instance + :type cmdinstance: list. + :param options: command line options + :type options: list. + :param skipbuild: flag to only login and skip monolith build + :type skipbuild: bool. + """ + + logobj = cmdinstance.rdmc.load_command(cmdinstance.rdmc.search_commands("LoginCommand")) + inputline = list() + client = None + loggedin = False + + if hasattr(options, "json") and cmdinstance.rdmc.config.format.lower() == "json": + options.json = True + + try: + client = cmdinstance.rdmc.app.current_client + except: + if options.user or options.password or options.url or options.force_vnic: + if options.url: + inputline.extend([options.url]) + if options.user: + if options.encode: + options.user = rdmc_helper.Encryption.decode_credentials(options.user) + if isinstance(options.user, bytes): + options.user = options.user.decode("utf-8") + inputline.extend(["-u", options.user]) + if options.password: + if options.encode: + options.password = rdmc_helper.Encryption.decode_credentials(options.password) + if isinstance(options.password, bytes): + options.password = options.password.decode("utf-8") + inputline.extend(["-p", options.password]) + if options.force_vnic: + inputline.extend(["--force_vnic"]) + if getattr(options, "https_cert", False): + inputline.extend(["--https", options.https_cert]) + if getattr(options, "user_certificate", False): + inputline.extend(["--usercert", options.user_certificate]) + if getattr(options, "user_root_ca_key", False): + inputline.extend(["--userkey", options.user_root_ca_key]) + if getattr(options, "user_root_ca_password", False): + inputline.extend(["--userpassphrase", options.user_root_ca_password]) + else: + if cmdinstance.rdmc.config.url: + inputline.extend([cmdinstance.rdmc.config.url]) + if cmdinstance.rdmc.config.username: + inputline.extend(["-u", cmdinstance.rdmc.config.username]) + if cmdinstance.rdmc.config.password: + inputline.extend(["-p", cmdinstance.rdmc.config.password]) + if cmdinstance.rdmc.config.ssl_cert: + inputline.extend(["--https", cmdinstance.rdmc.config.ssl_cert]) + if getattr(options, "user_certificate", False): + inputline.extend(["--usercert", options.user_certificate]) + if getattr(options, "user_root_ca_key", False): + inputline.extend(["--userkey", options.user_root_ca_key]) + if getattr(options, "user_root_ca_password", False): + inputline.extend(["--userpassphrase", options.user_root_ca_password]) + if options.includelogs: + inputline.extend(["--includelogs"]) + if options.path: + inputline.extend(["--path", options.path]) + + if getattr(options, "biospassword", False): + inputline.extend(["--biospassword", options.biospassword]) + if getattr(options, "sessionid", False): + inputline.extend(["--sessionid", options.sessionid]) + + logobj.loginfunction(inputline, skipbuild=skipbuild) + loggedin = True + + if not (loggedin or client or options.url or cmdinstance.rdmc.app.typepath.url): + message = "Local login initiated...\n" + if cmdinstance.rdmc.opts.verbose: + sys.stdout.write(message) + else: + rdmc_helper.LOGGER.info(message) + + if not (loggedin or client): + logobj.loginfunction(inputline, skipbuild=skipbuild) + + def login_select_validation(self, cmdinstance, options, skipbuild=False): + """Combined validation function to login and select with other commands. Not for use with + login or select commands themselves. Make sure your command imports options from + add_login_arguments_group or there will be errors. + + :param cmdinstance: the command object instance + :type cmdinstance: list. + :param options: command line options + :type options: list. + :param skipbuild: flag to only login and skip monolith build + :type skipbuild: bool. + """ + inputline = list() + + if cmdinstance.ident["name"] == "detectilo" or cmdinstance.ident["name"] == "appaccount": + try: + client = cmdinstance.rdmc.app.current_client + return client + except Exception: + return None + + self.login_validation(cmdinstance, options, skipbuild=skipbuild) + logobj = cmdinstance.rdmc.load_command(cmdinstance.rdmc.search_commands("LoginCommand")) + selobj = cmdinstance.rdmc.load_command(cmdinstance.rdmc.search_commands("SelectCommand")) + if hasattr(options, "selector") and options.selector: + if inputline: + inputline.extend(["--selector", options.selector]) + logobj.loginfunction(inputline) + else: + if getattr(options, "ref", False): + inputline.extend(["--refresh"]) + + inputline.extend([options.selector]) + selobj.selectfunction(inputline) + elif hasattr(options, "selector"): + try: + inputline = list() + selector = cmdinstance.rdmc.app.selector + + if hasattr(options, "ref") and options.ref: + inputline.extend(["--refresh"]) + + if selector: + inputline.extend([selector]) + selobj.selectfunction(inputline) + except NothingSelectedError: + raise NothingSelectedError + + def logout_routine(self, cmdinstance, options): + """Routine to logout of a server automatically at the completion of a command. + + :param commandinstance: the command object instance + :type commandinstance: list. + :param options: command line options + :type options: list. + """ + + logoutobj = cmdinstance.rdmc.load_command(cmdinstance.rdmc.search_commands("LogoutCommand")) + + if getattr(options, "logout", False): + logoutobj.run("") + + def add_login_arguments_group(self, parser): + """Adds login arguments to the passed parser + + :param parser: The parser to add the login option group to + :type parser: ArgumentParser + """ + group = parser.add_argument_group("LOGIN OPTIONS", "Options for logging in to a system.") + group.add_argument("--url", dest="url", help="Use the provided iLO URL to login.", default=None) + group.add_argument( + "--sessionid", + dest="sessionid", + help="Use the provided sessionid to login.", + default=None, + ) + group.add_argument( + "-u", + "--user", + dest="user", + help="""If you are not logged in yet, including this flag along with the +password and URL flags can be used to login to a server in the same command.""", + default=None, + ) + group.add_argument( + "-p", + "--password", + dest="password", + help="""Use the provided iLO password to log in.""", + default=None, + ) + group.add_argument( + "-o", + "--otp", + dest="login_otp", + help="""Use the provided iLO OTP to log in.""", + default=None, + ) + group.add_argument( + "--biospassword", + dest="biospassword", + help="""Select this flag to input a BIOS password. Include this +flag if second-level BIOS authentication is needed for the command to execute. +This option is only used on Gen 9 systems.""", + default=None, + ) + group.add_argument( + "--https", + dest="https_cert", + help="""Use the provided CA bundle or SSL certificate with your login to +connect securely to the system in remote mode. This flag has no effect in local mode.""", + default=None, + ) + group.add_argument( + "--usercert", + dest="user_certificate", + type=str, + help="""Specify a user certificate file path for certificate based authentication +with iLO.\n**NOTE**: Inclusion of this argument will force certficate based +authentication. A root user certificate authority key or bundle will be required.""", + default=None, + ) + group.add_argument( + "--userkey", + dest="user_root_ca_key", + type=str, + help="""Specify a user root ca key file path for certificate based certificate +authentication with iLO. **NOTE 1**: Inclusion of this argument will force certficate based +authentication. A root user certificate authority key or bundle will be required. +**NOTE 2**: Inclusion of this argument will force certificate based authentication. +A user certificate will be required. +**NOTE 3**: A user will be prompted for a password if the root certificate authority key +is encrypted and \'-certpass/--userrootcapassword\' is omitted.""", + default=None, + ) + group.add_argument( + "--userpassphrase", + dest="user_root_ca_password", + type=str, + help="""Optionally specify a user root ca key file password for encrypted +user root certificate authority keys. **NOTE 1**: Inclusion of this argument will force +certficate based authentication. A root user certificate authority key or +bundle will be required. **NOTE 2**: The user will be prompted for a password +if the user root certificate authority key requires a password""", + default=None, + ) + # group.add_argument( + # '--certbundle', + # dest='ca_cert_bundle', + # type=str, + # help="""Specify a file path for the certificate authority bundle location + # (local repository for certificate collection) **NOTE**: Providing a custom certificate + # or root CA key will override the use of certificate bundles""", + # default=None) + group.add_argument( + "-e", + "--enc", + dest="encode", + action="store_true", + help=SUPPRESS, + default=False, + ) + group.add_argument( + "--includelogs", + dest="includelogs", + action="store_true", + help="Optionally include logs in the data retrieval process.", + default=False, + ) + group.add_argument( + "--path", + dest="path", + help="""Optionally set a starting point for data collection during login. +If you do not specify a starting point, the default path will be /redfish/v1/. +Note: The path flag can only be specified at the time of login. +Warning: Only for advanced users, and generally not needed for normal operations.""", + default=None, + ) + group.add_argument( + "--force_vnic", + dest="force_vnic", + action="store_true", + help="Force login through iLO Virtual NIC. **NOTE** " "iLO 5 required", + default=False, + ) + group.add_argument( + "--logout", + dest="logout", + action="store_true", + help="Logout after the completion of the command.", + default=None, + ) + + +class RdmcOptionParser(ArgumentParser): + """Constructor""" + + def __init__(self): + super().__init__( + usage="%s [GLOBAL OPTIONS] [COMMAND] [COMMAND ARGUMENTS] " "[COMMAND OPTIONS]" % versioning.__shortname__, + description="iLOrest is a command-line or interactive interface that allows users " + "to manage Hewlett Packard Enterprise products that take advantage" + " of RESTful APIs.\n\nIn order to view or manage a system you must" + " first login. You can login using the login command or during " + "execution of any other command.\nFrom here you can run any other " + "commands. To learn more about specific commands, run iLOrest " + "COMMAND -h.", + epilog="Examples:\n\nThe following is the standard flow of command" + "s to view system data.\n\tThe first example is each command " + "run individually: \n\n\tilorest login\n\tilorest select Bios.\n\t" + "ilorest get\n\n\tThe second is the list of all of the commands " + "run at once. First locally, then remotely.\n\tilorest get " + "--select Bios.\n\tilorest get --select Bios. --url -u" + " -p ", + formatter_class=RawDescriptionHelpFormatter, + ) + globalgroup = _ArgumentGroup(self, "GLOBAL OPTIONS") + + self.add_argument( + "--config", + dest="config", + help="Use the provided configuration file instead of the default one.", + metavar="FILE", + ) + + config_dir_default = os.path.join(cliutils.get_user_config_dir(), ".%s" % versioning.__shortname__) + self.add_argument( + "--cache-dir", + dest="config_dir", + default=config_dir_default, + help="Use the provided directory as the location to cache data" + " (default location: %s)" % config_dir_default, + metavar="PATH", + ) + self.add_argument( + "-v", + "--verbose", + dest="verbose", + action="count", + help="Display verbose information (with increasing level). '-v': Level 1, " + "Logging, Stdout, Stderr. '-vv': Level 2, Extends Level 1 with slightly " + "elaborated iLO and HTTP response message. '-vvv': Level3, Extends Level 2 " + "with message id, validation class, message text with embedded args, and " + "possible resolution/mitigation for iLO responses. Includes HTTP responses. " + "**NOTE 1**: Some responses may only contain limited information from the source." + "**NOTE 2**: Default level is 0.", + default=0, + ) + self.add_argument( + "-d", + "--debug", + dest="debug", + action="store_true", + help="""[DEPRECATED] This flag is deprecated and will be removed in future versions. + Use '--logconfig' for enabling debug logs. + Enable debug mode for detailed logging output.""", + default=False, + ) + self.add_argument( + "--logdir", + dest="logdir", + default=None, + help="""Use the provided directory as the location for log file.""", + metavar="PATH", + ) + self.add_argument( + "--nostdoutlog", + dest="nostdoutlog", + action="store_true", + help="""[DEPRECATED] This flag is deprecated and will be removed in future versions. + Use '--logconfig' for enabling debug logs. + Enable debug mode for detailed logging output.""", + default=False, + ) + self.add_argument( + "--noinfolog", + "--nodefaultlog", + "--no_default_log", + dest="noinfolog", + action="store_true", + help="""Disable default INFO logs.""", + default=False, + ) + self.add_argument( + "--nocache", + dest="nocache", + action="store_true", + help="During execution the application will temporarily store data only in memory.", + default=False, + ) + self.add_argument( + "--nologo", + dest="nologo", + action="store_true", + help="""Include to block copyright and logo.""", + default=False, + ) + self.add_argument( + "--toolbar", + dest="toolbar", + action="store_true", + help="""Show toolbar at the bottom.""", + default=False, + ) + self.add_argument( + "--notab", + dest="notab", + action="store_true", + help="""Disable tab complete.""", + default=False, + ) + self.add_argument( + "--redfish", + dest="is_redfish", + action="store_true", + help="Use this flag if you wish to to enable " + "Redfish only compliance. It is enabled by default " + "in systems with iLO5 and above.", + default=False, + ) + self.add_argument( + "--latestschema", + dest="latestschema", + action="store_true", + help="Optionally use the latest schema instead of the one " + "requested by the file. Note: May cause errors in some data " + "retrieval due to difference in schema versions.", + default=False, + ) + self.add_argument( + "--useproxy", + dest="proxy", + default=None, + help="""Use the provided proxy for communication.""", + metavar="URL", + ) + self.add_argument( + "--redirectconsole", + dest="redirect", + help="Optionally include this flag to redirect stdout/stderr console.", + nargs="?", + default=None, + const=True, + metavar="REDIRECT CONSOLE", + ) + self.add_argument_group(globalgroup) diff --git a/venv/lib/python3.10/site-packages/ilorest/rdmc_helper.py b/venv/lib/python3.10/site-packages/ilorest/rdmc_helper.py new file mode 100644 index 0000000..8094a7c --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/rdmc_helper.py @@ -0,0 +1,1429 @@ +### +# Copyright 2017-2025 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""This is the helper module for RDMC""" + +# ---------Imports--------- +from __future__ import unicode_literals + +import json +import logging +import os +import sys +import time +from ctypes import byref, c_char_p, create_string_buffer + +try: + from logging_config_path import get_logging_config_path +except ModuleNotFoundError: + from ilorest.logging_config_path import get_logging_config_path + + +import pyaes +import six +from prompt_toolkit.completion import Completer, Completion + +import redfish.hpilo.risblobstore2 as risblobstore2 +import redfish.ris +import logging.config + +try: + import versioning +except ImportError: + from ilorest import versioning + +if os.name == "nt": + from six.moves import winreg + + if six.PY2: + from win32con import HKEY_LOCAL_MACHINE + elif six.PY3: + from win32.lib.win32con import HKEY_LOCAL_MACHINE + +# ---------End of imports--------- + + +# Using hard coded list until better solution is found +HARDCODEDLIST = [ + "modified", + "attributeregistry", + "links", + "settingsresult", + "actions", + "availableactions", + "extref", +] + +# ---------Debug logger--------- +# Centralized logging behaviour: +# - The JSON config in ilorest/logging_config.json defines handlers/formatters. +# - `setup_logging_from_json()` loads that config and uses `prepare_log_directory()` to +# resolve and create a writable `logdir` (trying the configured path first and then +# platform-specific fallbacks). This centralizes all directory-selection logic. +# - `setup_fallback_logging()` is to support fallback logging if the JSON config +# cannot be loaded or applied for some reason. It uses command line flags to retain old functionality. + + +class InfoFilter(logging.Filter): + """Filter to allow only INFO and WARNING messages, blocking ERROR and above""" + + def filter(self, record): + # Allow only records below ERROR (i.e., INFO and WARNING) + return record.levelno < logging.ERROR + + +# Logger instance - configuration will be handled by JSON config file +LOGGER = logging.getLogger(__name__) + + +def clear_existing_handlers(): + """Clear any existing log handlers to avoid conflicts.""" + root_logger = logging.getLogger() + for handler in root_logger.handlers[:]: + root_logger.removeHandler(handler) + + +def setup_fallback_logging(reason="Unknown error", opts=None): + """ + Fallback logging setup when all else fails. + + Args: + reason: The reason for falling back to fallback logging + opts: Optional command line options that may contain logging flags + """ + try: + clear_existing_handlers() + + # Extract command line flags from opts or sys.argv + debug_flag = False + noinfo_flag = False + nostdout_flag = False + log_dir = os.path.join(os.getcwd(), "ilorest_logs") + + if opts: + debug_flag = getattr(opts, "debug", False) + noinfo_flag = getattr(opts, "noinfolog", False) + nostdout_flag = getattr(opts, "nostdoutlog", False) + custom_logdir = getattr(opts, "logdir", None) + if custom_logdir: + log_dir = os.path.normpath(custom_logdir) + else: + # Check sys.argv as fallback + debug_flag = any(arg in ["-d", "--debug"] for arg in sys.argv) + noinfo_flag = any(arg in ["--noinfolog"] for arg in sys.argv) + nostdout_flag = any(arg in ["--nostdoutlog"] for arg in sys.argv) + + # Create log directory + os.makedirs(log_dir, exist_ok=True) + log_file_path = os.path.join(log_dir, "iLORest.log") + + # Determine logging level (debug takes precedence over noinfo) + if noinfo_flag and not debug_flag: + file_level = logging.ERROR + elif debug_flag: + file_level = logging.DEBUG + else: + file_level = logging.INFO + + # Configure file handler + file_handler = logging.FileHandler(log_file_path) + file_handler.setLevel(file_level) + file_formatter = logging.Formatter("%(levelname)s\t: %(message)s") + file_handler.setFormatter(file_formatter) + + handlers = [file_handler] + + # Configure stdout handler so that INFO (and below) do NOT go to stdout. + # The console should show CRITICAL by default, but if the + # global file level is higher (e.g. ERROR due to --noinfolog), keep + # stdout at that higher level to respect the flag. + if not nostdout_flag: + stdout_handler = logging.StreamHandler(sys.stdout) + # If user requested debug output on the command line, allow stdout to + # receive DEBUG messages. Otherwise keep stdout quieter (CRITICAL) + if debug_flag: + stdout_level = logging.DEBUG + else: + # set to default CRITICAL + stdout_level = logging.CRITICAL + stdout_handler.setLevel(stdout_level) + stdout_handler.setFormatter(file_formatter) + handlers.append(stdout_handler) + + # Apply configuration to the root logger + root_logger = logging.getLogger() + root_logger.setLevel(file_level) + + # Clear any existing handlers (already done above) then add ours + for h in handlers: + root_logger.addHandler(h) + + logger = logging.getLogger(__name__) + logger.debug("Fallback to basic configurations for logging: %s (logdir=%s)", reason, log_dir) + return True + + except Exception as fallback_error: + # If even fallback setup failed, write to stderr directly + sys.stderr.write(f"Critical: All fallback logging attempts failed: {reason} - {fallback_error}\n") + return False + + +def load_config_file(config_path): + """Load and validate the JSON configuration file.""" + if not os.path.isfile(config_path): + raise FileNotFoundError(f"JSON config file not found: {config_path}") + + with open(config_path, "r", encoding="utf-8") as f: + config = json.load(f) + + # Validate basic structure + if not isinstance(config, dict): + raise ValueError("Config file must contain a JSON object") + + if "handlers" not in config: + raise ValueError("Config file must contain 'handlers' section") + + return config + + +def prepare_log_directory(config): + """Determine and prepare a writable log directory based on config and platform. + + Behavior (matching historical implementation): + - Try the JSON-configured path first (absolute). + - If that fails, fall back to a single directory: + - On Windows: use APPDATA if set + - Otherwise: use ~/.local/share + - Return the directory that is writable; raise PermissionError if neither works. + """ + primary = config.get("logdir", "./ilorest_logs") + if not os.path.isabs(primary): + primary = os.path.abspath(primary) + + def _writable_dir(path): + try: + os.makedirs(path, exist_ok=True) + testfile = os.path.join(path, ".write_test") + with open(testfile, "w") as fh: + fh.write("ok") + try: + os.remove(testfile) + except Exception: + pass + return True + except Exception: + return False + + # Try primary configured path first + if _writable_dir(primary): + LOGGER.debug("Selected logdir (configured): %s", os.path.normpath(primary)) + return os.path.normpath(primary) + + # Single fallback to APPDATA or ~/.local/share (matching legacy behavior) + appdata = os.getenv("APPDATA") + if appdata: + fallback_dir = appdata + else: + fallback_dir = os.path.join(os.path.expanduser("~"), ".local", "share") + + fallback_dir = os.path.join(fallback_dir, "ilorest_logs") + + if _writable_dir(fallback_dir): + LOGGER.debug("Selected logdir (fallback): %s", os.path.normpath(fallback_dir)) + return os.path.normpath(fallback_dir) + + # Nothing worked + raise PermissionError(f"Unable to create or write to configured logdir '{primary}' or fallback '{fallback_dir}'") + + +def configure_file_handler(config, logdir): + """Configure the file handler with proper path joining and validation.""" + file_handler = config.get("handlers", {}).get("file", {}) + if not file_handler: + return + + original_filename = file_handler.get("filename", "iLORest.log") + if "%(logdir)s" in original_filename: + filename = original_filename.replace("%(logdir)s", logdir) + else: + filename = os.path.join(logdir, "iLORest.log") + + file_handler["filename"] = os.path.normpath(filename) + + # Test if we can write to the log file + try: + with open(file_handler["filename"], "a") as test_file: + pass # Just test if we can open for writing + except (IOError, OSError, PermissionError) as e: + raise ValueError(f"Cannot write to log file {file_handler['filename']}: {e}") + + +def set_logging_config_with_flags(config, nostdout=False, noinfo=False, debug=False): + """Modify logging configuration based on command-line flags. + + :param config: The logging configuration dictionary to modify + :type config: dict + :param nostdout: Flag to suppress stdout logging + :type nostdout: bool + :param noinfo: Flag to suppress info level logging + :type noinfo: bool + :param debug: Flag to enable debug level logging + :type debug: bool + :returns: Modified configuration dictionary + :rtype: dict + """ + # Handle priority: debug takes precedence over noinfo when both are set + if noinfo and debug: + noinfo = False + + # Modify config based on early command-specific flags + if nostdout and "handlers" in config and "stdout" in config["handlers"]: + # Remove stdout handler from the configuration + del config["handlers"]["stdout"] + # Remove stdout from all logger handlers lists + for logger_name, logger_config in config.get("loggers", {}).items(): + if "handlers" in logger_config and "stdout" in logger_config["handlers"]: + logger_config["handlers"] = [h for h in logger_config["handlers"] if h != "stdout"] + + # Modify logging levels based on flags + if noinfo: + level = "ERROR" + elif debug: + level = "DEBUG" + else: + level = None + + if level: + # Apply level to all loggers + for logger_name, logger_config in config.get("loggers", {}).items(): + logger_config["level"] = level + # Also apply to handlers if needed + for handler_name, handler_config in config.get("handlers", {}).items(): + if handler_name != "stderr": # Keep stderr at ERROR level + handler_config["level"] = level + + return config + + +def setup_logging_from_json(opts=None): + """Setup logging from JSON config file using dictConfig with comprehensive fallback. + + This function attempts to load logging configuration from a JSON file and falls back + to existing configuration if the JSON config fails for any reason. + If `opts` is provided, it extracts logging flags from the options object. + + """ + config_path = get_logging_config_path() + + try: + # Load and validate configuration file + config = load_config_file(config_path) + + # Extract logging flags from opts if provided and apply_flags is True + if opts: + nostdout = getattr(opts, "nostdoutlog", False) + noinfo = getattr(opts, "noinfolog", False) + debug = getattr(opts, "debug", False) + logdir = getattr(opts, "logdir", None) + + # If caller passed an explicit logdir, use it to override the config value + if logdir: + # normalize and set + config["logdir"] = os.path.normpath(logdir) + + # Modify config based on flags using the dedicated function + config = set_logging_config_with_flags(config, nostdout, noinfo, debug) + + # Prepare log directory (this will create it if missing) + final_logdir = prepare_log_directory(config) + + # Configure file handler paths (will replace %(logdir)s etc.) + configure_file_handler(config, final_logdir) + + # Clear any existing handlers to avoid conflicts + clear_existing_handlers() + + # Apply the logging configuration + logging.config.dictConfig(config) + + # Test that logging is working (only if not suppressing info logs and flags applied) + if opts and not getattr(opts, "noinfolog", False): + logger = logging.getLogger(__name__) + logger.debug("Logging configuration loaded successfully from JSON (logdir=%s)", final_logdir) + except FileNotFoundError as e: + setup_fallback_logging(str(e), opts) + return None + except (PermissionError, IOError) as e: + setup_fallback_logging(f"File access error: {e}", opts) + return None + except json.JSONDecodeError as e: + setup_fallback_logging(f"Invalid JSON format: {e}", opts) + return None + except (KeyError, ValueError, TypeError) as e: + setup_fallback_logging(f"Invalid configuration structure: {e}", opts) + return None + except Exception as e: + setup_fallback_logging(f"Unexpected error loading config: {e}", opts) + return None + + +# ---------End of debug logger--------- + + +class ReturnCodes(object): + """Return code class to be used by all functions""" + + SUCCESS = 0 + + # ****** RDMC ERRORS ****** + CONFIGURATION_FILE_ERROR = 1 + COMMAND_NOT_ENABLED_ERROR = 2 + INVALID_COMMAND_LINE_ERROR = 3 + INVALID_FILE_FORMATTING_ERROR = 4 + USER_NOT_ADMIN = 5 + NO_CONTENTS_FOUND_FOR_OPERATION = 6 + INVALID_FILE_INPUT_ERROR = 7 + NO_CHANGES_MADE_OR_FOUND = 8 + NO_VALID_INFO_ERROR = 9 + + # ****** CLI ERRORS ****** + UI_CLI_ERROR_EXCEPTION = 10 + UI_CLI_WARN_EXCEPTION = 11 + UI_CLI_USAGE_EXCEPTION = 12 + UI_CLI_COMMAND_NOT_FOUND_EXCEPTION = 13 + INVALID_PASSWORD_LENGTH_ERROR = 14 + + # ****** RMC/RIS ERRORS ****** + RIS_UNDEFINED_CLIENT_ERROR = 21 + RIS_CURRENTLY_LOGGED_IN_ERROR = 22 + RIS_INSTANCE_NOT_FOUND_ERROR = 23 + RIS_NOTHING_SELECTED_ERROR = 24 + RIS_NOTHING_SELECTED_FILTER_ERROR = 25 + RIS_NOTHING_SELECTED_SET_ERROR = 26 + RIS_INVALID_SELECTION_ERROR = 27 + RIS_VALIDATION_ERROR = 28 + RIS_MISSING_ID_TOKEN = 29 + RIS_SESSION_EXPIRED = 30 + + # ****** REST V1 ERRORS ****** + V1_RETRIES_EXHAUSTED_ERROR = 31 + V1_INVALID_CREDENTIALS_ERROR = 32 + V1_SERVER_DOWN_OR_UNREACHABLE_ERROR = 33 + V1_CHIF_DRIVER_MISSING_ERROR = 34 + REST_ILOREST_CHIF_DLL_MISSING_ERROR = 35 + REST_ILOREST_UNEXPECTED_RESPONSE_ERROR = 36 + REST_ILOREST_ILO_ERROR = 37 + REST_ILOREST_CREATE_BLOB_ERROR = 38 + REST_ILOREST_READ_BLOB_ERROR = 39 + + # ****** RDMC ERRORS ****** + SAME_SETTINGS_ERROR = 40 + FIRMWARE_UPDATE_ERROR = 41 + BOOT_ORDER_ENTRY_ERROR = 42 + NIC_MISSING_OR_INVALID_ERROR = 43 + NO_CURRENT_SESSION_ESTABLISHED = 44 + FAILURE_DURING_COMMIT_OPERATION = 45 + USERNAME_PASSWORD_REQUIRED_ERROR = 46 + VNIC_NOT_ENABLED_ERROR = 47 + MULTIPLE_SERVER_CONFIG_FAIL = 51 + MULTIPLE_SERVER_INPUT_FILE_ERROR = 52 + LOAD_SKIP_SETTING_ERROR = 53 + INCOMPATIBLE_ILO_VERSION_ERROR = 54 + INVALID_CLIST_FILE_ERROR = 55 + UNABLE_TO_MOUNT_BB_ERROR = 56 + BIRTHCERT_PARSE_ERROR = 57 + INCOMPATIBLE_SERVER_TYPE = 58 + ILO_LICENSE_ERROR = 59 + RESOURCE_EXISTS_ERROR = 60 + + # ****** RMC/RIS ERRORS ****** + RIS_VALUE_CHANGED_ERROR = 61 + RIS_REF_PATH_NOT_FOUND_ERROR = 62 + RIS_ILO_RESPONSE_ERROR = 63 + RIS_ILO_INIT_ERROR = 64 + RIS_SCHEMA_PARSE_ERROR = 65 + RIS_ILO_CHIF_ACCESS_DENIED_ERROR = 66 + RIS_CREATE_AND_PREPARE_CHANNEL_ERROR = 67 + RIS_ILO_CHIF_PACKET_EXCHANGE_ERROR = 71 + RIS_ILO_CHIF_NO_DRIVER_ERROR = 69 + + # ****** REST V1 ERRORS ****** + REST_ILOREST_WRITE_BLOB_ERROR = 70 + REST_ILOREST_BLOB_DELETE_ERROR = 68 + REST_ILOREST_BLOB_FINALIZE_ERROR = 72 + REST_ILOREST_BLOB_NOT_FOUND_ERROR = 73 + JSON_DECODE_ERROR = 74 + V1_SECURITY_STATE_ERROR = 75 + REST_ILOREST_BLOB_OVERRIDE_ERROR = 76 + REST_BLOB_RETRIES_EXHAUSETED_ERROR = 77 + + # ****** RDMC ERRORS ****** + RESOURCE_ALLOCATION_ISSUES_ERROR = 80 + ENCRYPTION_ERROR = 81 + DRIVE_MISSING_ERROR = 82 + PATH_UNAVAILABLE_ERROR = 83 + ILO_RIS_CORRUPTION_ERROR = 84 + RESOURCE_NOT_READY_ERROR = 85 + INVALID_SMART_ARRAY_PAYLOAD = 86 + + # ****** RIS ERRORS ****** + RIS_RIS_BIOS_UNREGISTERED_ERROR = 100 + + # ***** Upload/Download ERRORS ****** + FAILED_TO_DOWNLOAD_COMPONENT = 101 + UPDATE_SERVICE_BUSY = 102 + FAILED_TO_UPLOAD_COMPONENT = 103 + TASKQUEUE_ERROR = 104 + DEVICE_DISCOVERY_IN_PROGRESS = 105 + INSTALLSET_ERROR = 106 + INVALID_TARGET_ERROR = 107 + ILO_UNSUPPORTED_FLASH = 108 + + # **** ComputeOpsManagement Errors**** + CLOUD_CONNECT_TIMEOUT = 111 + CLOUD_CONNECT_FAILED = 112 + CLOUD_ALREADY_CONNECTED = 113 + PROXY_CONFIG_FAILED = 114 + + # **** scep error **** + SCEP_ENABLED_ERROR = 121 + + # *** authentication error **** + TFA_WRONG_OTP = 131 + TFA_OTP_TIMEDOUT = 132 + TFA_ENABLED_ERROR = 133 + TFA_OTP_EMAILED = 134 + + # ****** GENERAL ERRORS ****** + GENERAL_ERROR = 255 + + # ****** VNIC ERRORS ****** + GENERAL_ACCOUNT_GENERATE_SAVE_ERROR = 141 + VNIC_DOES_NOT_EXIST_ERROR = 142 + ACCOUNT_DOES_NOT_EXIST_ERROR = 143 + ACCOUNT_REMOVE_ERROR = 144 + ACCOUNT_EXISTS_CHECK_ERROR = 145 + VNIC_LOGIN_ERROR = 146 + ACCOUNT_SAVE_ERROR_TPM = 147 + ACCOUNT_SAVE_ERROR_ILO = 148 + GEN_BEFORE_LOGIN_ERROR = 149 + APPID_LIST_ERROR = 150 + INACTIVE_APP_ACCOUNT_TOKEN = 151 + REACTIVATE_APP_ACCOUNT_TOKEN_ERROR = 152 + + +class RdmcError(Exception): + """Baseclass for all rdmc exceptions""" + + errcode = 1 + + def __init__(self, message=None): + Exception.__init__(self, message) + + +class ConfigurationFileError(RdmcError): + """Raised when something is wrong in the config file""" + + errcode = 3 + + +class ProxyConfigFailedError(RdmcError): + """Raised when ComputeOpsManagement connection fails""" + + pass + + +class CloudConnectTimeoutError(RdmcError): + """Raised when ComputeOpsManagement connection times out""" + + pass + + +class CloudConnectFailedError(RdmcError): + """Raised when ComputeOpsManagement connection fails""" + + pass + + +class TfaEnablePreRequisiteError(RdmcError): + """Raised when pre-requisites not met while enabling TFA""" + + pass + + +class AlreadyCloudConnectedError(RdmcError): + """Raised when ComputeOpsManagement is already connected""" + + pass + + +class CommandNotEnabledError(RdmcError): + """Raised when user tries to invoke a command that isn't enabled""" + + pass + + +class iLORisCorruptionError(RdmcError): + """Raised when user tries to invoke a command that isn't enabled""" + + pass + + +class ResourceNotReadyError(RdmcError): + """Raised when user tries to invoke a command that isn't enabled""" + + pass + + +class UsernamePasswordRequiredError(RdmcError): + """Raised when username and password are required for local chif login""" + + pass + + +class PathUnavailableError(Exception): + """Raised when the requested path is unavailable""" + + pass + + +class InvalidCommandLineError(RdmcError): + """Raised when user enter incorrect command line arguments""" + + pass + + +class NoCurrentSessionEstablished(RdmcError): + """Raised when user enter incorrect command line arguments""" + + pass + + +class NoChangesFoundOrMadeError(RdmcError): + """Raised when no changes were found or made on the commit function""" + + pass + + +class StandardBlobErrorHandler(RdmcError): + """Raised when error occured for blob operations""" + + pass + + +class InvalidCommandLineErrorOPTS(RdmcError): + """Raised when user enter incorrect command line arguments""" + + pass + + +class InvalidFileInputError(RdmcError): + """Raised when user enter an invalid file input""" + + pass + + +class InvalidFileFormattingError(RdmcError): + """Raised when user enter incorrect load file formatting""" + + pass + + +class WindowsUserNotAdmin(RdmcError): + """Raised when user is not running as admin""" + + pass + + +class NoContentsFoundForOperationError(RdmcError): + """Raised when no contents were found for the current operation""" + + pass + + +class InfoMissingEntriesError(RdmcError): + """Raised when no valid entries for info were found in the current + instance""" + + pass + + +class InvalidOrNothingChangedSettingsError(RdmcError): + """Raised when something is wrong with the settings""" + + pass + + +class InvalidPasswordLengthError(RdmcError): + """Raised when password length is invalid""" + + pass + + +class InvalidSmartArrayConfigurationError(RdmcError): + """Raised when no changes were found or made on the commit function""" + + pass + + +class NoDifferencesFoundError(RdmcError): + """Raised when no differences are found in the current configuration""" + + pass + + +class NothingSelectedError(RdmcError): + """Raised when type selection is reference but none have been provided""" + + pass + + +class InvalidPropertyError(RdmcError): + """Raised when one or more properties or attributes are in conflict with current or + specified configuration""" + + pass + + +class MultipleServerConfigError(RdmcError): + """Raised when one or more servers failed to load given configuration""" + + pass + + +class InvalidMSCfileInputError(RdmcError): + """Raised when servers input file for load has incorrect parameters""" + + pass + + +class FirmwareUpdateError(RdmcError): + """Raised when there is an error while updating firmware""" + + pass + + +class FailureDuringCommitError(RdmcError): + """Raised when there is an error during commit""" + + pass + + +class BootOrderMissingEntriesError(RdmcError): + """Raised when no entries were found for bios tools""" + + pass + + +class NicMissingOrConfigurationError(RdmcError): + """ Raised when no entries are found for given NIC or all NICs are \ + configured or when wrong inputs are presented for NIC entries""" + + pass + + +class IncompatibleiLOVersionError(RdmcError): + """Raised when the iLO version is above or below the required \ + version""" + + pass + + +class IncompatableServerTypeError(RdmcError): + """Raised when the server type is incompatable with the requested\ + command""" + + pass + + +class IloLicenseError(RdmcError): + """Raised when the proper iLO license is not available for a command""" + + pass + + +class ScepenabledError(RdmcError): + """Raised when the generation csr or deletion of https cert is issues when scep is enabled""" + + pass + + +class ResourceExists(RdmcError): + """Raised when the account to be added already exists""" + + pass + + +class InvalidCListFileError(RdmcError): + """Raised when an error occurs while reading the cfilelist \ + within AHS logs""" + + pass + + +class PartitionMoutingError(RdmcError): + """Raised when there is error or iLO fails to respond to \ + partition mounting request""" + + pass + + +class DownloadError(RdmcError): + """Raised when the component fails to download""" + + pass + + +class UploadError(RdmcError): + """Raised when the component fails to download""" + + pass + + +class FlashUnsupportedByIloError(RdmcError): + """Raised when the flashing of the component is not supported by iLO""" + + pass + + +class TimeOutError(RdmcError): + """Raised when the update service times out""" + + pass + + +class LibHPsrvMissingError(RdmcError): + """Raised when unable to obtain the libhpsrv handle""" + + pass + + +class BirthcertParseError(RdmcError): + """Raised when unable to parse the birthcert""" + + pass + + +class InvalidKeyError(RdmcError): + """Raised when an invalid encryption key is used""" + + pass + + +class UnableToDecodeError(RdmcError): + """Raised when the file is unable to be decoded using the given key""" + + pass + + +class UnabletoFindDriveError(RdmcError): + """Raised when there is an issue finding required label""" + + pass + + +class TaskQueueError(RdmcError): + """Raised when there is an issue with the current order of taskqueue""" + + pass + + +class DeviceDiscoveryInProgress(RdmcError): + """Raised when device discovery in progress""" + + pass + + +class FallbackChifUse(RdmcError): + """Fallback Chif Use""" + + pass + + +class InstallsetError(RdmcError): + """Error while deleting Recovery installset""" + + pass + + +class GenerateAndSaveAccountError(RdmcError): + """Raised when errors occurred while generating and saving app account""" + + pass + + +class NoAppAccountError(RdmcError): + """Raised when the app account to be deleted does not exist.""" + + pass + + +class RemoveAccountError(RdmcError): + """Raised when errors occurred while removing app account""" + + pass + + +class AppAccountExistsError(RdmcError): + """Raised when errors occurred while removing app account""" + + pass + + +class ReactivateAppAccountTokenError(RdmcError): + """Raised when errors occurred while reactivating app account""" + + pass + + +class InactiveAppAccountTokenError(RdmcError): + """Raised when inactive app account token""" + + pass + + +class VnicLoginError(RdmcError): + """Raised when error occurs while VNIC login""" + + pass + + +class VnicExistsError(RdmcError): + """Raised when VNIC is not enabled""" + + pass + + +class SavinginTPMError(RdmcError): + """Raised when error occurs while saving app account in TPM""" + + pass + + +class SavinginiLOError(RdmcError): + """Raised when error occurs while saving app account in iLO""" + + pass + + +class GenBeforeLoginError(RdmcError): + """Raised when error occurs while getting iLO Gen before login""" + + pass + + +class AppIdListError(RdmcError): + """Raised when error occurs while retrieving list of apptokens and appaccounts""" + + pass + + +class UI(object): + """UI class handles all of our printing etc so we have + consistency across the project""" + + def __init__(self, verbosity=1): + self.verbosity = verbosity + + def printer(self, data, flush=True, excp=None, verbose_override=False): + """Print wrapper for stdout vs fileout""" + if self.verbosity >= 0 or verbose_override: + if excp: + sys.stderr.write(str(data)) + else: + try: + sys.stdout.write(str(data)) + if flush: + sys.stdout.flush() + except IOError: + pass + + def command_not_found(self, cmd): + """Called when command was not found""" + self.printer( + ("\nCommand '%s' not found. Use the help command to " "see a list of available commands\n" % cmd), + excp=True, + ) + return ReturnCodes.UI_CLI_COMMAND_NOT_FOUND_EXCEPTION + + def command_not_enabled(self, cmd, excp): + """Called when command has not been enabled""" + self.printer(("\nCommand '%s' has not been enabled: %s\n" % (cmd, excp)), excp=excp) + + def invalid_commmand_line(self, excp): + """Called when user entered invalid command line entries""" + self.printer(("Error: %s\n" % excp), excp=excp) + + def ilo_ris_corruption(self, excp): + """Called when user entered invalid command line entries""" + self.printer(("\nError: %s\n" % excp), excp=excp) + + def standard_blob_error(self, excp): + """Called when user error encountered with blob""" + self.printer(("\nError: Blob operation failed with error code %s\n" % excp), excp=excp) + + def invalid_file_formatting(self, excp): + """Called when file formatting is unrecognizable""" + self.printer(("\nError: %s\n" % excp), excp=excp) + + def user_not_admin(self): + """Called when file formatting in unrecognizable""" + self.printer( + ( + "\nBoth remote and local mode is accessible when %s " + "is run as administrator. Only remote mode is available for non-" + "admin user groups.\n" % versioning.__longname__ + ), + excp=True, + ) + + def no_contents_found_for_operation(self, excp): + """Called when no contents were found for the current operation""" + self.printer(("\nError: %s\n" % excp), excp=True) + + def nothing_selected(self): + """Called when nothing has been select yet""" + self.printer( + "\nNo type currently selected. Please use the" + " 'types' command to\nget a list of types, or input" + " your type by using the '--selector' flag.\n", + excp=True, + ) + + def nothing_selected_filter(self): + """Called when nothing has been select after a filter set""" + self.printer("\nNothing was found to match your provided filter.\n", excp=True) + + def nothing_selected_set(self): + """Called when nothing has been select yet""" + self.printer("\nNothing is selected or selection is read-only.\n", excp=True) + + def no_differences_found(self, excp): + """Called when no difference is found in the current configuration""" + self.printer(("Error: %s\n" % excp), excp=True) + + def multiple_server_config_fail(self, excp): + """Called when one or more servers failed to load given configuration""" + self.printer(("Error: %s\n" % excp), excp=True) + + def multiple_server_config_input_file(self, excp): + """Called when servers input file has incorrect information""" + self.printer(("Error: %s\n" % excp), excp=True) + + def invalid_credentials(self, timeout): + """Called user has entered invalid credentials + :param timeout: timeout given for failed login attempt + :type timeout: int. + """ + self.printer("Authenticating to iLO", excp=True) + + timeout = 0 + for _ in range(0, (int(str(timeout)) + 3)): + time.sleep(2) + self.printer(".", excp=True) + + self.printer( + "\nInvalid credentials. Unable to authenticate to iLO. \n" + "Please ensure to specify correct username and password.\n", + excp=True, + ) + + def bios_unregistered_error(self): + """Called when ilo/bios unregistered error occurs""" + self.printer( + "\nERROR 100: Bios provider is unregistered. Please" + " refer to the documentation for details on this issue.\n", + excp=True, + ) + + def error(self, msg, inner_except=None): + """Used for general error handling + :param inner_except: raised exception to be logged + :type inner_except: exception. + :param msg: warning message + :type msg: string. + """ + if inner_except is not None: + LOGGER.error(msg, exc_info=True) + else: + LOGGER.error(msg) + + def warn(self, msg, inner_except=None): + """Used for general warning handling + :param inner_except: raised exception to be logged + :type inner_except: exception. + :param msg: warning message + :type msg: string. + """ + if inner_except is not None: + LOGGER.warning(msg, exc_info=True) + else: + LOGGER.warning(msg) + + def retries_exhausted_attemps(self): + """Called when url retries have been exhausted""" + LOGGER.error("Could not reach URL. Retries have been exhausted.") + + def retries_exhausted_vnic_not_enabled(self): + """Called when there is no VNIC is Enabled""" + self.printer("\nError: Could not reach URL, VNIC is not enabled. \n", excp=True) + + def print_out_json(self, content): + """Print out json content to std.out with sorted keys + :param content: content to be printed out + :type content: str. + """ + # stringify + content = json.dumps(content, indent=2, cls=redfish.ris.JSONEncoder, sort_keys=True) + self.printer(content, verbose_override=True) + self.printer("\n") + + def print_out_json_ordered(self, content): + """Print out sorted json content to std.out + :param content: content to be printed out + :type content: str. + """ + content = json.dumps(content, indent=2, cls=redfish.ris.JSONEncoder) + self.printer(content, verbose_override=True) + self.printer("\n") + + def print_out_human_readable(self, content): + """Print out human readable content to std.out + :param content: content to be printed out + :type content: str. + """ + self.pretty_human_readable(content, enterloop=True) + self.printer("\n") + + def pretty_human_readable(self, content, indent=0, start=0, enterloop=False): + """Convert content to human readable and print out to std.out + :param content: content to be printed out + :type content: str. + :param indent: indent string to be used as seperator + :type indent: str. + :param start: used to determine the indent level + :type start: int. + """ + space = "\n" + "\t" * indent + " " * start + if isinstance(content, list): + for item in content: + if item is None: + continue + + self.pretty_human_readable(item, indent, start) + + if content.index(item) != (len(content) - 1): + self.printer(space) + elif isinstance(content, dict): + for key, value in content.items(): + if space and not enterloop: + self.printer(space) + + enterloop = False + self.printer((str(key) + "=")) + self.pretty_human_readable(value, indent, (start + len(key) + 2)) + else: + content = content if isinstance(content, six.string_types) else str(content) + + content = '""' if not content else content + # Changed to support py3, verify if there is a unicode prit issue. + + self.printer(content) + + +class Encryption(object): + """Encryption/Decryption object""" + + @staticmethod + def check_fips_mode_os(): + """Function to check for the OS fips mode + :param key: string to encrypt with + :type key: str. + :returns: returns True if FIPS mode is active, False otherwise + """ + fips = False + if os.name == "nt": + reg = winreg.ConnectRegistry(None, HKEY_LOCAL_MACHINE) + try: + reg = winreg.OpenKey( + reg, + "System\\CurrentControlSet\\Control\\" "Lsa\\FipsAlgorithmPolicy", + ) + winreg.QueryInfoKey(reg) + value, _ = winreg.QueryValueEx(reg, "Enabled") + if value: + fips = True + except: + fips = False + else: + try: + fipsfile = open("/proc/sys/crypto/fips_enabled") + result = fipsfile.readline() + if int(result) > 0: + fipsfile = True + fipsfile.close() + except: + fips = False + return fips + + @staticmethod + def check_fips_mode_ssl(): + """Function to check for the SSL fips mode + Uses custom cpython ssl module API, if available. Otheriwse + probes using ctypes.cdll APIs. + :returns: returns True if FIPS mode is active, False otherwise + """ + import ssl + + if hasattr(ssl, "FIPS_mode"): + return ssl.FIPS_mode() + + from ctypes import cdll + + libcrypto = cdll.LoadLibrary(ssl._ssl.__file__) + return libcrypto.FIPS_mode() + + def encrypt_file(self, filetxt, key): + """encrypt a file given a key + :param filetxt: content to be encrypted + :type content: str. + :param key: string to encrypt with + :type key: str. + """ + try: + filetxt = filetxt.encode() + except (UnicodeDecodeError, AttributeError): + pass # must be encoded already + try: + key = key.encode() + except (UnicodeDecodeError, AttributeError): + pass # must be encoded already + if Encryption.check_fips_mode_os(): + raise CommandNotEnabledError("Encrypting of files is not available" " in FIPS mode.") + if len(key) not in [16, 24, 32]: + raise InvalidKeyError("") + else: + encryptedfile = pyaes.AESModeOfOperationCTR(key).encrypt(filetxt) + + return encryptedfile + + def decrypt_file(self, filetxt, key): + """decrypt a file given a key + :param filetxt: content to be decrypted + :type content: str. + :param key: string to decrypt with + :type key: str. + :returns: returns the decrypted file + """ + try: + filetxt = filetxt.encode() + except (UnicodeDecodeError, AttributeError): + pass # must be encoded already + try: + key = key.encode() + except (UnicodeDecodeError, AttributeError): + pass # must be encoded already + if len(key) not in [16, 24, 32]: + raise InvalidKeyError("") + else: + decryptedfile = pyaes.AESModeOfOperationCTR(key).decrypt(filetxt) + try: + json.loads(decryptedfile) + except: + raise UnableToDecodeError( + "Unable to decrypt the file, make " "sure the key is the same as used in encryption." + ) + + return decryptedfile + + @staticmethod + def decode_credentials(credential): + """decode an encoded credential + :param credential: credential to be decoded + :type credential: str. + :returns: returns the decoded credential + """ + + lib = risblobstore2.BlobStore2.gethprestchifhandle() + credbuff = create_string_buffer(credential.encode("utf-8")) + retbuff = create_string_buffer(128) + + lib.decode_credentials.argtypes = [c_char_p] + + lib.decode_credentials(credbuff, byref(retbuff)) + + risblobstore2.BlobStore2.unloadchifhandle(lib) + # try: + # if isinstance(retbuff.value, bytes): + # retbuff.value = retbuff.value.decode('utf-8', 'ignore') + # if not retbuff.value: + # raise UnableToDecodeError("") + # except: + # raise UnableToDecodeError("Unable to decode credential %s." % credential) + + return retbuff.value + + @staticmethod + def encode_credentials(credential): + """encode a credential + :param credential: credential to be encoded + :type credential: str. + :returns: returns the encoded credential + """ + + lib = risblobstore2.BlobStore2.gethprestchifhandle() + if isinstance(credential, bytes): + credential = credential.decode("utf-8") + credbuff = create_string_buffer(credential.encode("utf-8")) + + retbuff = create_string_buffer(128) + + lib.encode_credentials.argtypes = [c_char_p] + + lib.encode_credentials(credbuff, byref(retbuff)) + + risblobstore2.BlobStore2.unloadchifhandle(lib) + try: + if six.PY2: + enc_val = retbuff.value.encode("utf-8") + elif six.PY3: + enc_val = retbuff.value.decode("utf-8") # .encode('utf-8') + if not retbuff.value: + raise UnableToDecodeError("") + except Exception: + raise UnableToDecodeError("Unable to decode credential %s." % credential) + + return enc_val + + +class TabAndHistoryCompletionClass(Completer): + """Tab and History Class used by interactive mode""" + + def __init__(self, options): + self.options = options + self.toolbar_text = None + self.last_complete = None + + def get_completions(self, document, complete_event): + """Function to return the options for autocomplete""" + word = "" + self.toolbar_text = "" + lstoption = self.options + if document.text: + tokens = document.text.split() + # We aren't completing options yet + tokens = [token for token in tokens if not token.startswith("-")] + + self.last_complete = tokens[-1] + nestedtokens = self.last_complete.split("/") + + if not document.text.endswith(" "): + tokens.pop() + word = document.get_word_under_cursor() + else: + nestedtokens = [] + if word == "/": + word = "" + + if len(tokens) >= 1: + if tokens[0] == "select": + # only first type + if len(tokens) >= 2: + lstoption = [] + else: + lstoption = self.options.get(tokens[0], {}) + elif tokens[0] in ["get", "list", "info", "set"]: + # Match properties + nested_data = self.options.get("nestedprop", {}) + nested_info = self.options.get("nestedinfo", {}) + for token in nestedtokens: + try: + nested_data = nested_data[token] + if tokens[0] == "get" and isinstance(nested_data, dict): + for k in list(nested_data.keys()): + if ( + k.lower() in HARDCODEDLIST + or "@odata" in k.lower() + or "@redfish.allowablevalues" in k.lower() + ): + del nested_data[k] + if nested_info: + if "properties" in nested_info: + nested_info = nested_info["properties"] + if "AttributeName" not in nested_info[token]: + nested_info = ( + nested_info["properties"][token] + if "properties" in nested_info + else nested_info[token] + ) + else: + nested_info = nested_info[token] + except Exception: + break + nested_data = list(nested_data.keys()) if isinstance(nested_data, dict) else [] + lstoption = nested_data + + # Try to get info for help bar + help_text = nested_info.get("HelpText", "") + enum_tab = [] + if "Type" in nested_info and nested_info["Type"].lower() == "enumeration": + help_text += "\nPossible Values:\n" + for value in nested_info["Value"]: + enum_tab.append(value["ValueName"]) + help_text += six.u(str(value["ValueName"])) + " " + + if not help_text: + try: + nested_info = nested_info["properties"] + except KeyError: + pass + help_text = nested_info.get("description", "") + if "enum" in nested_info: + help_text += "\nPossible Values:\n" + for value in nested_info["enum"]: + enum_tab.append(value) + help_text += six.u(str(value)) + " " + if isinstance(help_text, str): + help_text = help_text.replace(". ", ".\n") + self.toolbar_text = help_text + if tokens[0] in ["set"]: + lstoption = self.options.get("set") + else: + lstoption = {} + else: + for token in tokens: + # just match commands + lstoption = self.options.get(token, {}) + + for opt in lstoption: + if opt == word: + self.last_complete = opt + if opt.startswith(word): + yield Completion(opt + "", start_position=-len(word)) + + def bottom_toolbar(self): + return self.toolbar_text if self.toolbar_text else None + + def updates_tab_completion_lists(self, options): + """Function to update tab completion lists + :param options: options list + :type options: list. + """ + # Loop through options passed and add them to them + # to the current tab options list + for key, value in options.items(): + self.options[key] = value diff --git a/venv/lib/python3.10/site-packages/ilorest/security_masking.py b/venv/lib/python3.10/site-packages/ilorest/security_masking.py new file mode 100644 index 0000000..c88e731 --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/security_masking.py @@ -0,0 +1,678 @@ +### +# Copyright 2025 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Centralized security masking framework for iLORest""" + +import json +import re +import copy +from typing import Dict, List, Any, Union, Optional + + +class SecurityMasker: + """Centralized framework for masking sensitive information in logs and outputs.""" + + # Default mask character + MASK_VALUE = "******" + + # Number of characters to show for partial masking + PARTIAL_MASK_SHOW_CHARS = 4 + + # Sensitive command-line argument patterns + SENSITIVE_CLI_KEYWORDS = { + "--password", + "--pass", + "-p", + "--pwd", + "--token", + "--auth-token", + "--session-token", + "--key", + "--api-key", + "--secret", + "--credential", + "--cert-key", + "--private-key", + "--passphrase", + "--sessionkey", + "--auth", + "--authorization", + "--filepass", + "--backup-password", + "--biospassword", + } + + # Fields requiring COMPLETE masking (no characters visible) + COMPLETE_MASK_FIELDS = { + "password", + "oldpassword", + "newpassword", + "currentpassword", + "passwd", + "pwd", + "pass", + "passphrase", + "secret", + "client_secret", + "private_key", + "privatekey", + "cert_key", + "certkey", + "pin", + "otp", + "totp", + "mfa_token", + "csrf_token", + "csrftoken", + } + + # Fields allowing PARTIAL masking (last 4-6 chars visible for debugging) + PARTIAL_MASK_FIELDS = { + "token", + "auth_token", + "authtoken", + "session_token", + "sessiontoken", + "access_token", + "accesstoken", + "refresh_token", + "refreshtoken", + "bearer_token", + "api_key", + "apikey", + "sessionkey", + "session_key", + "credential", + "credentials", + "appid", + "app_id", + "applicationid", + "application_id", + } + + # Headers requiring COMPLETE masking + COMPLETE_MASK_HEADERS = { + "authorization", # Often contains Basic auth with password + "www-authenticate", + "proxy-authorization", + "cookie", + "set-cookie", # May contain sensitive session data + } + + # Headers allowing PARTIAL masking (for debugging/correlation) + PARTIAL_MASK_HEADERS = { + "x-auth-token", + "x-session-token", + "x-api-key", + "api-key", + "bearer", # When separate from Authorization header + "x-session-id", + "session-id", + } + + # All sensitive headers (union of complete and partial) + SENSITIVE_HEADERS = ( + COMPLETE_MASK_HEADERS + | PARTIAL_MASK_HEADERS + | {"authentication", "basic", "digest", "oauth", "jwt", "x-csrf-token", "x-request-id"} + ) + + # All sensitive body fields (union of complete and partial) + SENSITIVE_BODY_FIELDS = COMPLETE_MASK_FIELDS | PARTIAL_MASK_FIELDS | {"auth", "authorization"} + + # URL parameter patterns that might contain sensitive data + SENSITIVE_URL_PARAMS = {"password", "token", "key", "secret", "auth", "session", "credential", "pass", "pwd"} + + @classmethod + def mask_command_arguments(cls, args: List[str]) -> List[str]: + """ + Mask sensitive information in command-line arguments. + + Args: + args: List of command-line arguments + + Returns: + List of arguments with sensitive values masked + + Example: + >>> SecurityMasker.mask_command_arguments(['--user', 'admin', '--password', 'MyP@ss']) + ['--user', 'admin', '--password', '******'] + """ + if not args: + return args + + masked_args = [] + i = 0 + + while i < len(args): + arg = args[i] + + # Check for inline sensitive arguments (--password=value) + if cls._has_inline_sensitive_value(arg): + masked_args.append(cls._mask_inline_argument(arg)) + i += 1 + continue + + # Check if current arg is a sensitive flag + if cls._is_sensitive_cli_flag(arg): + masked_args.append(arg) # Keep the flag + # Mask the next argument if it exists and isn't another flag + if i + 1 < len(args) and not args[i + 1].startswith("-"): + i += 1 # Move to the next argument + masked_args.append(cls.MASK_VALUE) # Mask the value + i += 1 + continue + + # Keep non-sensitive arguments as-is + masked_args.append(arg) + i += 1 + + return masked_args + + @classmethod + def mask_http_headers( + cls, headers: Union[Dict[str, str], List[tuple], Any] + ) -> Union[Dict[str, str], List[tuple], str]: + """ + Mask sensitive information in HTTP headers. + + Args: + headers: HTTP headers in various formats (dict, list of tuples, or other) + + Returns: + Headers with sensitive values masked + + Examples: + # dict format + >>> SecurityMasker.mask_http_headers({'Authorization': 'Basic abc123', 'Accept': 'application/json'}) + {'Authorization': '******', 'Accept': 'application/json'} + + # list-of-tuples format + >>> SecurityMasker.mask_http_headers([('Authorization', 'Bearer abcd'), ('Host', 'example')]) + [('Authorization', '******'), ('Host', 'example')] + """ + if not headers: + return headers + + try: + # Handle dictionary format + if isinstance(headers, dict): + return cls._mask_headers_dict(headers) + + # Handle list of tuples format + elif isinstance(headers, (list, tuple)): + return cls._mask_headers_list(headers) + + # Handle string representation of headers + elif isinstance(headers, str): + return cls._mask_headers_string(headers) + + # Handle other object types (e.g., HTTPMessage) + else: + return cls._mask_headers_object(headers) + + except Exception: + # If masking fails, return a safe placeholder + return "[HEADERS MASKED DUE TO PROCESSING ERROR]" + + @classmethod + def mask_simple_body(cls, body: Union[str, bytes, Dict[str, Any]]) -> Union[str, bytes, Dict[str, Any]]: + """ + Mask sensitive information in simple HTTP request/response bodies. + + This method handles basic body masking for simple payloads. + For complex bodies with advanced JSON parsing and error handling, + use mask_complex_body() instead. + + Args: + body: Request/response body in various formats + + Returns: + Body with sensitive values masked + + Examples: + # JSON string + >>> SecurityMasker.mask_simple_body('{"password":"secret"}') + '{"password":"******"}' + + # dict + >>> SecurityMasker.mask_simple_body({"token": "abcd"}) + {"token": "******"} + """ + if not body: + return body + + try: + # Handle string body (likely JSON) + if isinstance(body, str): + return cls._mask_string_body(body) + + # Handle bytes body + elif isinstance(body, bytes): + return cls._mask_bytes_body(body) + + # Handle dictionary body + elif isinstance(body, dict): + return cls._mask_dict_body(body) + + # Handle other types + else: + return body + + except Exception: + # If masking fails, return original or safe placeholder + if isinstance(body, (str, bytes)): + return "[BODY MASKED DUE TO PROCESSING ERROR]" + return body + + @classmethod + def mask_url_parameters(cls, url: str) -> str: + """ + Mask sensitive information in URL parameters. + + Args: + url: URL string that may contain parameters + + Returns: + URL with sensitive parameter values masked + + Example: + >>> SecurityMasker.mask_url_parameters('https://api.example.com/login?user=admin&password=1234') + 'https://api.example.com/login?user=admin&password=******' + """ + if not url or "?" not in url: + return url + + try: + base_url, params = url.split("?", 1) + param_pairs = params.split("&") + masked_pairs = [] + + for pair in param_pairs: + if "=" in pair: + key, value = pair.split("=", 1) + if any(sensitive in key.lower() for sensitive in cls.SENSITIVE_URL_PARAMS): + masked_pairs.append(f"{key}={cls.MASK_VALUE}") + else: + masked_pairs.append(pair) + else: + masked_pairs.append(pair) + + return f"{base_url}?{'&'.join(masked_pairs)}" + + except Exception: + return url + + @classmethod + def mask_complex_body(cls, body: Union[str, bytes, Dict[str, Any]]) -> Union[str, bytes]: + """ + Mask sensitive information in complex HTTP request/response bodies for logging. + + This is a specialized method for complex body masking that handles: + - Advanced JSON parsing with error recovery + - Automatic encoding/decoding (str ↔ bytes) + - Type preservation (returns same type as input) + - Graceful error handling to ensure logging never fails + + Use this method for complex request/response bodies. For simple bodies, + mask_simple_body() may be sufficient. + + Args: + body: The request/response body in various formats (str, bytes, dict) + + Returns: + Masked body as string or bytes (matching input type) + + Example: + >>> SecurityMasker.mask_complex_body(b'{"users":[{"name":"u","password":"p"}]}') + b'{"users": [{"name": "u", "password": "******"}]}' + """ + if not body: + return body + + original_type = type(body) + + try: + # Convert to string if bytes + if isinstance(body, bytes): + try: + body = body.decode("utf-8") + except UnicodeDecodeError: + # If decode fails, return original bytes + return body + + # If already a dict, mask and return as JSON string + if isinstance(body, dict): + masked_dict = cls._mask_dict_body(body) + return json.dumps(masked_dict) + + # If string, check if it's JSON + if isinstance(body, str): + body = body.strip() + if body.startswith(("{", "[")): + try: + # Parse JSON and mask + body_json = json.loads(body) + masked_json = cls._mask_dict_body(body_json) + masked_string = json.dumps(masked_json) + + # Return in original type + if original_type == bytes: + return masked_string.encode("utf-8") + return masked_string + except json.JSONDecodeError: + # Not valid JSON, return as-is + pass + + # Return body as-is if not JSON or other type + if original_type == bytes and isinstance(body, str): + return body.encode("utf-8") + return body + + except Exception: + # If anything fails, return original to ensure logging continues + # Converting to string if needed for logging + if isinstance(body, bytes): + try: + return body.decode("utf-8", errors="replace") + except: + return "[BODY - BINARY DATA]" + return body if body else "" + + @classmethod + def mask_user_input(cls, user_input: str, command_context: Optional[str] = None) -> str: + """ + Mask sensitive information in user input based on context. + + Args: + user_input: Raw user input string + command_context: Optional context about what command is being executed + + Returns: + User input with sensitive parts masked + + Example: + >>> SecurityMasker.mask_user_input('--login admin --password MyP@ss') + '--login admin --password ******' + """ + if not user_input: + return user_input + + # Split input into tokens for analysis + tokens = user_input.split() + + # Apply command argument masking + masked_tokens = cls.mask_command_arguments(tokens) + + return " ".join(masked_tokens) + + # Private helper methods + @classmethod + def _is_sensitive_cli_flag(cls, arg: str) -> bool: + """Check if an argument is a sensitive command-line flag.""" + return arg.lower() in {flag.lower() for flag in cls.SENSITIVE_CLI_KEYWORDS} + + @classmethod + def _has_inline_sensitive_value(cls, arg: str) -> bool: + """Check if an argument contains inline sensitive value (flag=value).""" + if "=" not in arg: + return False + flag = arg.split("=")[0] + return cls._is_sensitive_cli_flag(flag) + + @classmethod + def _mask_inline_argument(cls, arg: str) -> str: + """Mask the value part of an inline argument.""" + return re.sub(r"=(.*)", f"={cls.MASK_VALUE}", arg) + + @classmethod + def _mask_headers_dict(cls, headers: Dict[str, str]) -> Dict[str, str]: + """Mask sensitive headers in dictionary format.""" + masked = {} + for key, value in headers.items(): + if cls._is_sensitive_header(key): + # Determine if complete or partial masking + masked[key] = cls._mask_value(value, key, is_header=True) + else: + masked[key] = value + return masked + + @classmethod + def _mask_headers_list(cls, headers: List[tuple]) -> List[tuple]: + """Mask sensitive headers in list of tuples format.""" + masked = [] + for item in headers: + if isinstance(item, (list, tuple)) and len(item) >= 2: + key, value = item[0], item[1] + if cls._is_sensitive_header(key): + masked_value = cls._mask_value(value, key, is_header=True) + masked.append((key, masked_value)) + else: + masked.append(item) + else: + masked.append(item) + return masked + + @classmethod + def _mask_headers_string(cls, headers: str) -> str: + """Mask sensitive headers in string format.""" + lines = headers.split("\n") + masked_lines = [] + + for line in lines: + if ":" in line: + key, value = line.split(":", 1) + if cls._is_sensitive_header(key.strip()): + # Preserve leading whitespace (indentation) from original key + leading_space = key[: len(key) - len(key.lstrip())] + masked_value = cls._mask_value(value.strip(), key.strip(), is_header=True) + masked_lines.append(f"{leading_space}{key.strip()}: {masked_value}") + else: + masked_lines.append(line) + else: + masked_lines.append(line) + + return "\n".join(masked_lines) + + @classmethod + def _mask_headers_object(cls, headers: Any) -> str: + """Mask sensitive headers in object format (e.g., HTTPMessage).""" + try: + # Try to convert to dictionary first + if hasattr(headers, "items"): + header_dict = dict(headers.items()) + masked_dict = cls._mask_headers_dict(header_dict) + return str(masked_dict) + elif hasattr(headers, "__dict__"): + return cls._mask_headers_dict(headers.__dict__) + else: + return "[HEADERS MASKED - UNKNOWN FORMAT]" + except Exception: + return "[HEADERS MASKED DUE TO PROCESSING ERROR]" + + @classmethod + def _is_sensitive_header(cls, header_name: str) -> bool: + """Check if a header name is considered sensitive.""" + header_lower = header_name.lower().strip() + return any(sensitive in header_lower for sensitive in cls.SENSITIVE_HEADERS) + + @classmethod + def _mask_string_body(cls, body: str) -> str: + """Mask sensitive information in string body.""" + try: + # Try to parse as JSON + if body.strip().startswith(("{", "[")): + json_body = json.loads(body) + masked_body = cls._mask_dict_body(json_body) + return json.dumps(masked_body) + else: + # Handle form data or other string formats + return cls._mask_form_data(body) + except json.JSONDecodeError: + # Not JSON, treat as form data or plain text + return cls._mask_form_data(body) + + @classmethod + def _mask_bytes_body(cls, body: bytes) -> bytes: + """Mask sensitive information in bytes body.""" + try: + # Convert to string, mask, then back to bytes + string_body = body.decode("utf-8") + masked_string = cls._mask_string_body(string_body) + return masked_string.encode("utf-8") + except UnicodeDecodeError: + # If can't decode, return as-is (likely binary data) + return body + + @classmethod + def _mask_dict_body(cls, body: Dict[str, Any]) -> Dict[str, Any]: + """Mask sensitive information in dictionary body.""" + masked = copy.deepcopy(body) + + def mask_recursive(obj): + if isinstance(obj, dict): + for key, value in obj.items(): + if cls._is_sensitive_body_field(key): + # Apply appropriate masking based on field type + obj[key] = cls._mask_value(str(value) if value else "", key, is_header=False) + else: + mask_recursive(value) + elif isinstance(obj, list): + for item in obj: + mask_recursive(item) + + mask_recursive(masked) + return masked + + @classmethod + def _mask_form_data(cls, data: str) -> str: + """Mask sensitive information in form data.""" + # Handle URL-encoded form data + if "&" in data and "=" in data: + pairs = data.split("&") + masked_pairs = [] + for pair in pairs: + if "=" in pair: + key, value = pair.split("=", 1) + if cls._is_sensitive_body_field(key): + masked_pairs.append(f"{key}={cls.MASK_VALUE}") + else: + masked_pairs.append(pair) + else: + masked_pairs.append(pair) + return "&".join(masked_pairs) + + # For other string formats, return as-is + return data + + @classmethod + def _is_sensitive_body_field(cls, field_name: str) -> bool: + """Check if a body field name is considered sensitive.""" + field_lower = field_name.lower().strip() + return field_lower in cls.SENSITIVE_BODY_FIELDS + + @classmethod + def _mask_value(cls, value: str, field_name: str, is_header: bool = False) -> str: + """ + Mask a sensitive value with either complete or partial masking. + + Args: + value: The value to mask + field_name: The field/header name (for determining mask type) + is_header: Whether this is a header (vs body field) + + Returns: + Masked value (either completely or partially) + """ + if not value: + return cls.MASK_VALUE + + field_lower = field_name.lower().strip() + + # Determine if we should use complete or partial masking + use_complete_mask = False + + if is_header: + # Check if header requires complete masking + use_complete_mask = field_lower in {h.lower() for h in cls.COMPLETE_MASK_HEADERS} + # Special case: Authorization header with "Basic" is password-based + if field_lower == "authorization" and value.strip().lower().startswith("basic"): + use_complete_mask = True + else: + # Check if body field requires complete masking + use_complete_mask = field_lower in {f.lower() for f in cls.COMPLETE_MASK_FIELDS} + + # Apply appropriate masking + if use_complete_mask: + return cls.MASK_VALUE + else: + # Partial masking: show last N characters + return cls._partial_mask(value) + + @classmethod + def _partial_mask(cls, value: str, show_chars: int = None) -> str: + """ + Apply partial masking to a value, showing only the last N characters. + + Args: + value: The value to mask + show_chars: Number of characters to show (default: PARTIAL_MASK_SHOW_CHARS) + + Returns: + Partially masked value like "******a0fb" + """ + if show_chars is None: + show_chars = cls.PARTIAL_MASK_SHOW_CHARS + + if not value or len(value) <= show_chars: + # If value is too short, completely mask it + return cls.MASK_VALUE + + # Show last N characters + visible_part = value[-show_chars:] + return f"{cls.MASK_VALUE}{visible_part}" + + +# Convenience functions for backward compatibility and easy usage +def mask_passwords(args: List[str]) -> List[str]: + """Backward compatible function for masking passwords in command arguments.""" + return SecurityMasker.mask_command_arguments(args) + + +def mask_http_request_data(method: str, path: str, headers: Any, body: Any) -> tuple: + """ + Mask all sensitive data in an HTTP request for logging. + + Args: + method: HTTP method + path: Request path/URL + headers: Request headers + body: Request body + + Returns: + Tuple of (method, masked_path, masked_headers, masked_body) + """ + masked_path = SecurityMasker.mask_url_parameters(path) + masked_headers = SecurityMasker.mask_http_headers(headers) + masked_body = SecurityMasker.mask_simple_body(body) + + return method, masked_path, masked_headers, masked_body + + +def mask_user_input(user_input: str, command_context: Optional[str] = None) -> str: + """Convenience function for masking user input.""" + return SecurityMasker.mask_user_input(user_input, command_context) diff --git a/venv/lib/python3.10/site-packages/ilorest/versioning.py b/venv/lib/python3.10/site-packages/ilorest/versioning.py new file mode 100644 index 0000000..448680f --- /dev/null +++ b/venv/lib/python3.10/site-packages/ilorest/versioning.py @@ -0,0 +1,23 @@ +### +# Copyright 2017 Hewlett Packard Enterprise, Inc. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +### + +# -*- coding: utf-8 -*- +"""Version strings for the utility""" + +__version__ = "7.0.0.0" +__shortname__ = "iLORest" +__longname__ = "RESTful Interface Tool" +__extracontent__ = "Copyright (c) 2014-2025 Hewlett Packard Enterprise Development LP\n" diff --git a/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt new file mode 100644 index 0000000..7b190ca --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2011 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/METADATA b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/METADATA new file mode 100644 index 0000000..ddf5464 --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/METADATA @@ -0,0 +1,60 @@ +Metadata-Version: 2.1 +Name: itsdangerous +Version: 2.2.0 +Summary: Safely pass data to untrusted environments and back. +Maintainer-email: Pallets +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +Project-URL: Changes, https://itsdangerous.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://itsdangerous.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/itsdangerous/ + +# ItsDangerous + +... so better sign this + +Various helpers to pass data to untrusted environments and to get it +back safe and sound. Data is cryptographically signed to ensure that a +token has not been tampered with. + +It's possible to customize how data is serialized. Data is compressed as +needed. A timestamp can be added and verified automatically while +loading a token. + + +## A Simple Example + +Here's how you could generate a token for transmitting a user's id and +name between web requests. + +```python +from itsdangerous import URLSafeSerializer +auth_s = URLSafeSerializer("secret key", "auth") +token = auth_s.dumps({"id": 5, "name": "itsdangerous"}) + +print(token) +# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.6YP6T0BaO67XP--9UzTrmurXSmg + +data = auth_s.loads(token) +print(data["name"]) +# itsdangerous +``` + + +## Donate + +The Pallets organization develops and supports ItsDangerous and other +popular packages. In order to grow the community of contributors and +users, and allow the maintainers to devote more time to the projects, +[please donate today][]. + +[please donate today]: https://palletsprojects.com/donate + diff --git a/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/RECORD b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/RECORD new file mode 100644 index 0000000..255db20 --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/RECORD @@ -0,0 +1,22 @@ +itsdangerous-2.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +itsdangerous-2.2.0.dist-info/LICENSE.txt,sha256=Y68JiRtr6K0aQlLtQ68PTvun_JSOIoNnvtfzxa4LCdc,1475 +itsdangerous-2.2.0.dist-info/METADATA,sha256=0rk0-1ZwihuU5DnwJVwPWoEI4yWOyCexih3JyZHblhE,1924 +itsdangerous-2.2.0.dist-info/RECORD,, +itsdangerous-2.2.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +itsdangerous/__init__.py,sha256=4SK75sCe29xbRgQE1ZQtMHnKUuZYAf3bSpZOrff1IAY,1427 +itsdangerous/__pycache__/__init__.cpython-310.pyc,, +itsdangerous/__pycache__/_json.cpython-310.pyc,, +itsdangerous/__pycache__/encoding.cpython-310.pyc,, +itsdangerous/__pycache__/exc.cpython-310.pyc,, +itsdangerous/__pycache__/serializer.cpython-310.pyc,, +itsdangerous/__pycache__/signer.cpython-310.pyc,, +itsdangerous/__pycache__/timed.cpython-310.pyc,, +itsdangerous/__pycache__/url_safe.cpython-310.pyc,, +itsdangerous/_json.py,sha256=wPQGmge2yZ9328EHKF6gadGeyGYCJQKxtU-iLKE6UnA,473 +itsdangerous/encoding.py,sha256=wwTz5q_3zLcaAdunk6_vSoStwGqYWe307Zl_U87aRFM,1409 +itsdangerous/exc.py,sha256=Rr3exo0MRFEcPZltwecyK16VV1bE2K9_F1-d-ljcUn4,3201 +itsdangerous/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +itsdangerous/serializer.py,sha256=PmdwADLqkSyQLZ0jOKAgDsAW4k_H0TlA71Ei3z0C5aI,15601 +itsdangerous/signer.py,sha256=YO0CV7NBvHA6j549REHJFUjUojw2pHqwcUpQnU7yNYQ,9647 +itsdangerous/timed.py,sha256=6RvDMqNumGMxf0-HlpaZdN9PUQQmRvrQGplKhxuivUs,8083 +itsdangerous/url_safe.py,sha256=az4e5fXi_vs-YbWj8YZwn4wiVKfeD--GEKRT5Ueu4P4,2505 diff --git a/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/WHEEL b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/WHEEL new file mode 100644 index 0000000..3b5e64b --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous-2.2.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.9.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.10/site-packages/itsdangerous/__init__.py b/venv/lib/python3.10/site-packages/itsdangerous/__init__.py new file mode 100644 index 0000000..ea55256 --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous/__init__.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import typing as t + +from .encoding import base64_decode as base64_decode +from .encoding import base64_encode as base64_encode +from .encoding import want_bytes as want_bytes +from .exc import BadData as BadData +from .exc import BadHeader as BadHeader +from .exc import BadPayload as BadPayload +from .exc import BadSignature as BadSignature +from .exc import BadTimeSignature as BadTimeSignature +from .exc import SignatureExpired as SignatureExpired +from .serializer import Serializer as Serializer +from .signer import HMACAlgorithm as HMACAlgorithm +from .signer import NoneAlgorithm as NoneAlgorithm +from .signer import Signer as Signer +from .timed import TimedSerializer as TimedSerializer +from .timed import TimestampSigner as TimestampSigner +from .url_safe import URLSafeSerializer as URLSafeSerializer +from .url_safe import URLSafeTimedSerializer as URLSafeTimedSerializer + + +def __getattr__(name: str) -> t.Any: + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " ItsDangerous 2.3. Use feature detection or" + " 'importlib.metadata.version(\"itsdangerous\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("itsdangerous") + + raise AttributeError(name) diff --git a/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..198ac21 Binary files /dev/null and b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/_json.cpython-310.pyc b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/_json.cpython-310.pyc new file mode 100644 index 0000000..591b368 Binary files /dev/null and b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/_json.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/encoding.cpython-310.pyc b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/encoding.cpython-310.pyc new file mode 100644 index 0000000..acc25a9 Binary files /dev/null and b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/encoding.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/exc.cpython-310.pyc b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/exc.cpython-310.pyc new file mode 100644 index 0000000..27531ac Binary files /dev/null and b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/exc.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/serializer.cpython-310.pyc b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/serializer.cpython-310.pyc new file mode 100644 index 0000000..099c687 Binary files /dev/null and b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/serializer.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/signer.cpython-310.pyc b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/signer.cpython-310.pyc new file mode 100644 index 0000000..d1090c4 Binary files /dev/null and b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/signer.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/timed.cpython-310.pyc b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/timed.cpython-310.pyc new file mode 100644 index 0000000..45c72be Binary files /dev/null and b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/timed.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/url_safe.cpython-310.pyc b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/url_safe.cpython-310.pyc new file mode 100644 index 0000000..2c1b460 Binary files /dev/null and b/venv/lib/python3.10/site-packages/itsdangerous/__pycache__/url_safe.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/itsdangerous/_json.py b/venv/lib/python3.10/site-packages/itsdangerous/_json.py new file mode 100644 index 0000000..fc23fea --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous/_json.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import json as _json +import typing as t + + +class _CompactJSON: + """Wrapper around json module that strips whitespace.""" + + @staticmethod + def loads(payload: str | bytes) -> t.Any: + return _json.loads(payload) + + @staticmethod + def dumps(obj: t.Any, **kwargs: t.Any) -> str: + kwargs.setdefault("ensure_ascii", False) + kwargs.setdefault("separators", (",", ":")) + return _json.dumps(obj, **kwargs) diff --git a/venv/lib/python3.10/site-packages/itsdangerous/encoding.py b/venv/lib/python3.10/site-packages/itsdangerous/encoding.py new file mode 100644 index 0000000..f5ca80f --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous/encoding.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import base64 +import string +import struct +import typing as t + +from .exc import BadData + + +def want_bytes( + s: str | bytes, encoding: str = "utf-8", errors: str = "strict" +) -> bytes: + if isinstance(s, str): + s = s.encode(encoding, errors) + + return s + + +def base64_encode(string: str | bytes) -> bytes: + """Base64 encode a string of bytes or text. The resulting bytes are + safe to use in URLs. + """ + string = want_bytes(string) + return base64.urlsafe_b64encode(string).rstrip(b"=") + + +def base64_decode(string: str | bytes) -> bytes: + """Base64 decode a URL-safe string of bytes or text. The result is + bytes. + """ + string = want_bytes(string, encoding="ascii", errors="ignore") + string += b"=" * (-len(string) % 4) + + try: + return base64.urlsafe_b64decode(string) + except (TypeError, ValueError) as e: + raise BadData("Invalid base64-encoded data") from e + + +# The alphabet used by base64.urlsafe_* +_base64_alphabet = f"{string.ascii_letters}{string.digits}-_=".encode("ascii") + +_int64_struct = struct.Struct(">Q") +_int_to_bytes = _int64_struct.pack +_bytes_to_int = t.cast("t.Callable[[bytes], tuple[int]]", _int64_struct.unpack) + + +def int_to_bytes(num: int) -> bytes: + return _int_to_bytes(num).lstrip(b"\x00") + + +def bytes_to_int(bytestr: bytes) -> int: + return _bytes_to_int(bytestr.rjust(8, b"\x00"))[0] diff --git a/venv/lib/python3.10/site-packages/itsdangerous/exc.py b/venv/lib/python3.10/site-packages/itsdangerous/exc.py new file mode 100644 index 0000000..a75adcd --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous/exc.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import typing as t +from datetime import datetime + + +class BadData(Exception): + """Raised if bad data of any sort was encountered. This is the base + for all exceptions that ItsDangerous defines. + + .. versionadded:: 0.15 + """ + + def __init__(self, message: str): + super().__init__(message) + self.message = message + + def __str__(self) -> str: + return self.message + + +class BadSignature(BadData): + """Raised if a signature does not match.""" + + def __init__(self, message: str, payload: t.Any | None = None): + super().__init__(message) + + #: The payload that failed the signature test. In some + #: situations you might still want to inspect this, even if + #: you know it was tampered with. + #: + #: .. versionadded:: 0.14 + self.payload: t.Any | None = payload + + +class BadTimeSignature(BadSignature): + """Raised if a time-based signature is invalid. This is a subclass + of :class:`BadSignature`. + """ + + def __init__( + self, + message: str, + payload: t.Any | None = None, + date_signed: datetime | None = None, + ): + super().__init__(message, payload) + + #: If the signature expired this exposes the date of when the + #: signature was created. This can be helpful in order to + #: tell the user how long a link has been gone stale. + #: + #: .. versionchanged:: 2.0 + #: The datetime value is timezone-aware rather than naive. + #: + #: .. versionadded:: 0.14 + self.date_signed = date_signed + + +class SignatureExpired(BadTimeSignature): + """Raised if a signature timestamp is older than ``max_age``. This + is a subclass of :exc:`BadTimeSignature`. + """ + + +class BadHeader(BadSignature): + """Raised if a signed header is invalid in some form. This only + happens for serializers that have a header that goes with the + signature. + + .. versionadded:: 0.24 + """ + + def __init__( + self, + message: str, + payload: t.Any | None = None, + header: t.Any | None = None, + original_error: Exception | None = None, + ): + super().__init__(message, payload) + + #: If the header is actually available but just malformed it + #: might be stored here. + self.header: t.Any | None = header + + #: If available, the error that indicates why the payload was + #: not valid. This might be ``None``. + self.original_error: Exception | None = original_error + + +class BadPayload(BadData): + """Raised if a payload is invalid. This could happen if the payload + is loaded despite an invalid signature, or if there is a mismatch + between the serializer and deserializer. The original exception + that occurred during loading is stored on as :attr:`original_error`. + + .. versionadded:: 0.15 + """ + + def __init__(self, message: str, original_error: Exception | None = None): + super().__init__(message) + + #: If available, the error that indicates why the payload was + #: not valid. This might be ``None``. + self.original_error: Exception | None = original_error diff --git a/venv/lib/python3.10/site-packages/itsdangerous/py.typed b/venv/lib/python3.10/site-packages/itsdangerous/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.10/site-packages/itsdangerous/serializer.py b/venv/lib/python3.10/site-packages/itsdangerous/serializer.py new file mode 100644 index 0000000..5ddf387 --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous/serializer.py @@ -0,0 +1,406 @@ +from __future__ import annotations + +import collections.abc as cabc +import json +import typing as t + +from .encoding import want_bytes +from .exc import BadPayload +from .exc import BadSignature +from .signer import _make_keys_list +from .signer import Signer + +if t.TYPE_CHECKING: + import typing_extensions as te + + # This should be either be str or bytes. To avoid having to specify the + # bound type, it falls back to a union if structural matching fails. + _TSerialized = te.TypeVar( + "_TSerialized", bound=t.Union[str, bytes], default=t.Union[str, bytes] + ) +else: + # Still available at runtime on Python < 3.13, but without the default. + _TSerialized = t.TypeVar("_TSerialized", bound=t.Union[str, bytes]) + + +class _PDataSerializer(t.Protocol[_TSerialized]): + def loads(self, payload: _TSerialized, /) -> t.Any: ... + # A signature with additional arguments is not handled correctly by type + # checkers right now, so an overload is used below for serializers that + # don't match this strict protocol. + def dumps(self, obj: t.Any, /) -> _TSerialized: ... + + +# Use TypeIs once it's available in typing_extensions or 3.13. +def is_text_serializer( + serializer: _PDataSerializer[t.Any], +) -> te.TypeGuard[_PDataSerializer[str]]: + """Checks whether a serializer generates text or binary.""" + return isinstance(serializer.dumps({}), str) + + +class Serializer(t.Generic[_TSerialized]): + """A serializer wraps a :class:`~itsdangerous.signer.Signer` to + enable serializing and securely signing data other than bytes. It + can unsign to verify that the data hasn't been changed. + + The serializer provides :meth:`dumps` and :meth:`loads`, similar to + :mod:`json`, and by default uses :mod:`json` internally to serialize + the data to bytes. + + The secret key should be a random string of ``bytes`` and should not + be saved to code or version control. Different salts should be used + to distinguish signing in different contexts. See :doc:`/concepts` + for information about the security of the secret key and salt. + + :param secret_key: The secret key to sign and verify with. Can be a + list of keys, oldest to newest, to support key rotation. + :param salt: Extra key to combine with ``secret_key`` to distinguish + signatures in different contexts. + :param serializer: An object that provides ``dumps`` and ``loads`` + methods for serializing data to a string. Defaults to + :attr:`default_serializer`, which defaults to :mod:`json`. + :param serializer_kwargs: Keyword arguments to pass when calling + ``serializer.dumps``. + :param signer: A ``Signer`` class to instantiate when signing data. + Defaults to :attr:`default_signer`, which defaults to + :class:`~itsdangerous.signer.Signer`. + :param signer_kwargs: Keyword arguments to pass when instantiating + the ``Signer`` class. + :param fallback_signers: List of signer parameters to try when + unsigning with the default signer fails. Each item can be a dict + of ``signer_kwargs``, a ``Signer`` class, or a tuple of + ``(signer, signer_kwargs)``. Defaults to + :attr:`default_fallback_signers`. + + .. versionchanged:: 2.0 + Added support for key rotation by passing a list to + ``secret_key``. + + .. versionchanged:: 2.0 + Removed the default SHA-512 fallback signer from + ``default_fallback_signers``. + + .. versionchanged:: 1.1 + Added support for ``fallback_signers`` and configured a default + SHA-512 fallback. This fallback is for users who used the yanked + 1.0.0 release which defaulted to SHA-512. + + .. versionchanged:: 0.14 + The ``signer`` and ``signer_kwargs`` parameters were added to + the constructor. + """ + + #: The default serialization module to use to serialize data to a + #: string internally. The default is :mod:`json`, but can be changed + #: to any object that provides ``dumps`` and ``loads`` methods. + default_serializer: _PDataSerializer[t.Any] = json + + #: The default ``Signer`` class to instantiate when signing data. + #: The default is :class:`itsdangerous.signer.Signer`. + default_signer: type[Signer] = Signer + + #: The default fallback signers to try when unsigning fails. + default_fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] = [] + + # Serializer[str] if no data serializer is provided, or if it returns str. + @t.overload + def __init__( + self: Serializer[str], + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + serializer: None | _PDataSerializer[str] = None, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Serializer[bytes] with a bytes data serializer positional argument. + @t.overload + def __init__( + self: Serializer[bytes], + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None, + serializer: _PDataSerializer[bytes], + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Serializer[bytes] with a bytes data serializer keyword argument. + @t.overload + def __init__( + self: Serializer[bytes], + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + *, + serializer: _PDataSerializer[bytes], + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Fall back with a positional argument. If the strict signature of + # _PDataSerializer doesn't match, fall back to a union, requiring the user + # to specify the type. + @t.overload + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None, + serializer: t.Any, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Fall back with a keyword argument. + @t.overload + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + *, + serializer: t.Any, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + serializer: t.Any | None = None, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): + #: The list of secret keys to try for verifying signatures, from + #: oldest to newest. The newest (last) key is used for signing. + #: + #: This allows a key rotation system to keep a list of allowed + #: keys and remove expired ones. + self.secret_keys: list[bytes] = _make_keys_list(secret_key) + + if salt is not None: + salt = want_bytes(salt) + # if salt is None then the signer's default is used + + self.salt = salt + + if serializer is None: + serializer = self.default_serializer + + self.serializer: _PDataSerializer[_TSerialized] = serializer + self.is_text_serializer: bool = is_text_serializer(serializer) + + if signer is None: + signer = self.default_signer + + self.signer: type[Signer] = signer + self.signer_kwargs: dict[str, t.Any] = signer_kwargs or {} + + if fallback_signers is None: + fallback_signers = list(self.default_fallback_signers) + + self.fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] = fallback_signers + self.serializer_kwargs: dict[str, t.Any] = serializer_kwargs or {} + + @property + def secret_key(self) -> bytes: + """The newest (last) entry in the :attr:`secret_keys` list. This + is for compatibility from before key rotation support was added. + """ + return self.secret_keys[-1] + + def load_payload( + self, payload: bytes, serializer: _PDataSerializer[t.Any] | None = None + ) -> t.Any: + """Loads the encoded object. This function raises + :class:`.BadPayload` if the payload is not valid. The + ``serializer`` parameter can be used to override the serializer + stored on the class. The encoded ``payload`` should always be + bytes. + """ + if serializer is None: + use_serializer = self.serializer + is_text = self.is_text_serializer + else: + use_serializer = serializer + is_text = is_text_serializer(serializer) + + try: + if is_text: + return use_serializer.loads(payload.decode("utf-8")) # type: ignore[arg-type] + + return use_serializer.loads(payload) # type: ignore[arg-type] + except Exception as e: + raise BadPayload( + "Could not load the payload because an exception" + " occurred on unserializing the data.", + original_error=e, + ) from e + + def dump_payload(self, obj: t.Any) -> bytes: + """Dumps the encoded object. The return value is always bytes. + If the internal serializer returns text, the value will be + encoded as UTF-8. + """ + return want_bytes(self.serializer.dumps(obj, **self.serializer_kwargs)) + + def make_signer(self, salt: str | bytes | None = None) -> Signer: + """Creates a new instance of the signer to be used. The default + implementation uses the :class:`.Signer` base class. + """ + if salt is None: + salt = self.salt + + return self.signer(self.secret_keys, salt=salt, **self.signer_kwargs) + + def iter_unsigners(self, salt: str | bytes | None = None) -> cabc.Iterator[Signer]: + """Iterates over all signers to be tried for unsigning. Starts + with the configured signer, then constructs each signer + specified in ``fallback_signers``. + """ + if salt is None: + salt = self.salt + + yield self.make_signer(salt) + + for fallback in self.fallback_signers: + if isinstance(fallback, dict): + kwargs = fallback + fallback = self.signer + elif isinstance(fallback, tuple): + fallback, kwargs = fallback + else: + kwargs = self.signer_kwargs + + for secret_key in self.secret_keys: + yield fallback(secret_key, salt=salt, **kwargs) + + def dumps(self, obj: t.Any, salt: str | bytes | None = None) -> _TSerialized: + """Returns a signed string serialized with the internal + serializer. The return value can be either a byte or unicode + string depending on the format of the internal serializer. + """ + payload = want_bytes(self.dump_payload(obj)) + rv = self.make_signer(salt).sign(payload) + + if self.is_text_serializer: + return rv.decode("utf-8") # type: ignore[return-value] + + return rv # type: ignore[return-value] + + def dump(self, obj: t.Any, f: t.IO[t.Any], salt: str | bytes | None = None) -> None: + """Like :meth:`dumps` but dumps into a file. The file handle has + to be compatible with what the internal serializer expects. + """ + f.write(self.dumps(obj, salt)) + + def loads( + self, s: str | bytes, salt: str | bytes | None = None, **kwargs: t.Any + ) -> t.Any: + """Reverse of :meth:`dumps`. Raises :exc:`.BadSignature` if the + signature validation fails. + """ + s = want_bytes(s) + last_exception = None + + for signer in self.iter_unsigners(salt): + try: + return self.load_payload(signer.unsign(s)) + except BadSignature as err: + last_exception = err + + raise t.cast(BadSignature, last_exception) + + def load(self, f: t.IO[t.Any], salt: str | bytes | None = None) -> t.Any: + """Like :meth:`loads` but loads from a file.""" + return self.loads(f.read(), salt) + + def loads_unsafe( + self, s: str | bytes, salt: str | bytes | None = None + ) -> tuple[bool, t.Any]: + """Like :meth:`loads` but without verifying the signature. This + is potentially very dangerous to use depending on how your + serializer works. The return value is ``(signature_valid, + payload)`` instead of just the payload. The first item will be a + boolean that indicates if the signature is valid. This function + never fails. + + Use it for debugging only and if you know that your serializer + module is not exploitable (for example, do not use it with a + pickle serializer). + + .. versionadded:: 0.15 + """ + return self._loads_unsafe_impl(s, salt) + + def _loads_unsafe_impl( + self, + s: str | bytes, + salt: str | bytes | None, + load_kwargs: dict[str, t.Any] | None = None, + load_payload_kwargs: dict[str, t.Any] | None = None, + ) -> tuple[bool, t.Any]: + """Low level helper function to implement :meth:`loads_unsafe` + in serializer subclasses. + """ + if load_kwargs is None: + load_kwargs = {} + + try: + return True, self.loads(s, salt=salt, **load_kwargs) + except BadSignature as e: + if e.payload is None: + return False, None + + if load_payload_kwargs is None: + load_payload_kwargs = {} + + try: + return ( + False, + self.load_payload(e.payload, **load_payload_kwargs), + ) + except BadPayload: + return False, None + + def load_unsafe( + self, f: t.IO[t.Any], salt: str | bytes | None = None + ) -> tuple[bool, t.Any]: + """Like :meth:`loads_unsafe` but loads from a file. + + .. versionadded:: 0.15 + """ + return self.loads_unsafe(f.read(), salt=salt) diff --git a/venv/lib/python3.10/site-packages/itsdangerous/signer.py b/venv/lib/python3.10/site-packages/itsdangerous/signer.py new file mode 100644 index 0000000..e324dc0 --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous/signer.py @@ -0,0 +1,266 @@ +from __future__ import annotations + +import collections.abc as cabc +import hashlib +import hmac +import typing as t + +from .encoding import _base64_alphabet +from .encoding import base64_decode +from .encoding import base64_encode +from .encoding import want_bytes +from .exc import BadSignature + + +class SigningAlgorithm: + """Subclasses must implement :meth:`get_signature` to provide + signature generation functionality. + """ + + def get_signature(self, key: bytes, value: bytes) -> bytes: + """Returns the signature for the given key and value.""" + raise NotImplementedError() + + def verify_signature(self, key: bytes, value: bytes, sig: bytes) -> bool: + """Verifies the given signature matches the expected + signature. + """ + return hmac.compare_digest(sig, self.get_signature(key, value)) + + +class NoneAlgorithm(SigningAlgorithm): + """Provides an algorithm that does not perform any signing and + returns an empty signature. + """ + + def get_signature(self, key: bytes, value: bytes) -> bytes: + return b"" + + +def _lazy_sha1(string: bytes = b"") -> t.Any: + """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include + SHA-1, in which case the import and use as a default would fail before the + developer can configure something else. + """ + return hashlib.sha1(string) + + +class HMACAlgorithm(SigningAlgorithm): + """Provides signature generation using HMACs.""" + + #: The digest method to use with the MAC algorithm. This defaults to + #: SHA1, but can be changed to any other function in the hashlib + #: module. + default_digest_method: t.Any = staticmethod(_lazy_sha1) + + def __init__(self, digest_method: t.Any = None): + if digest_method is None: + digest_method = self.default_digest_method + + self.digest_method: t.Any = digest_method + + def get_signature(self, key: bytes, value: bytes) -> bytes: + mac = hmac.new(key, msg=value, digestmod=self.digest_method) + return mac.digest() + + +def _make_keys_list( + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], +) -> list[bytes]: + if isinstance(secret_key, (str, bytes)): + return [want_bytes(secret_key)] + + return [want_bytes(s) for s in secret_key] # pyright: ignore + + +class Signer: + """A signer securely signs bytes, then unsigns them to verify that + the value hasn't been changed. + + The secret key should be a random string of ``bytes`` and should not + be saved to code or version control. Different salts should be used + to distinguish signing in different contexts. See :doc:`/concepts` + for information about the security of the secret key and salt. + + :param secret_key: The secret key to sign and verify with. Can be a + list of keys, oldest to newest, to support key rotation. + :param salt: Extra key to combine with ``secret_key`` to distinguish + signatures in different contexts. + :param sep: Separator between the signature and value. + :param key_derivation: How to derive the signing key from the secret + key and salt. Possible values are ``concat``, ``django-concat``, + or ``hmac``. Defaults to :attr:`default_key_derivation`, which + defaults to ``django-concat``. + :param digest_method: Hash function to use when generating the HMAC + signature. Defaults to :attr:`default_digest_method`, which + defaults to :func:`hashlib.sha1`. Note that the security of the + hash alone doesn't apply when used intermediately in HMAC. + :param algorithm: A :class:`SigningAlgorithm` instance to use + instead of building a default :class:`HMACAlgorithm` with the + ``digest_method``. + + .. versionchanged:: 2.0 + Added support for key rotation by passing a list to + ``secret_key``. + + .. versionchanged:: 0.18 + ``algorithm`` was added as an argument to the class constructor. + + .. versionchanged:: 0.14 + ``key_derivation`` and ``digest_method`` were added as arguments + to the class constructor. + """ + + #: The default digest method to use for the signer. The default is + #: :func:`hashlib.sha1`, but can be changed to any :mod:`hashlib` or + #: compatible object. Note that the security of the hash alone + #: doesn't apply when used intermediately in HMAC. + #: + #: .. versionadded:: 0.14 + default_digest_method: t.Any = staticmethod(_lazy_sha1) + + #: The default scheme to use to derive the signing key from the + #: secret key and salt. The default is ``django-concat``. Possible + #: values are ``concat``, ``django-concat``, and ``hmac``. + #: + #: .. versionadded:: 0.14 + default_key_derivation: str = "django-concat" + + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous.Signer", + sep: str | bytes = b".", + key_derivation: str | None = None, + digest_method: t.Any | None = None, + algorithm: SigningAlgorithm | None = None, + ): + #: The list of secret keys to try for verifying signatures, from + #: oldest to newest. The newest (last) key is used for signing. + #: + #: This allows a key rotation system to keep a list of allowed + #: keys and remove expired ones. + self.secret_keys: list[bytes] = _make_keys_list(secret_key) + self.sep: bytes = want_bytes(sep) + + if self.sep in _base64_alphabet: + raise ValueError( + "The given separator cannot be used because it may be" + " contained in the signature itself. ASCII letters," + " digits, and '-_=' must not be used." + ) + + if salt is not None: + salt = want_bytes(salt) + else: + salt = b"itsdangerous.Signer" + + self.salt = salt + + if key_derivation is None: + key_derivation = self.default_key_derivation + + self.key_derivation: str = key_derivation + + if digest_method is None: + digest_method = self.default_digest_method + + self.digest_method: t.Any = digest_method + + if algorithm is None: + algorithm = HMACAlgorithm(self.digest_method) + + self.algorithm: SigningAlgorithm = algorithm + + @property + def secret_key(self) -> bytes: + """The newest (last) entry in the :attr:`secret_keys` list. This + is for compatibility from before key rotation support was added. + """ + return self.secret_keys[-1] + + def derive_key(self, secret_key: str | bytes | None = None) -> bytes: + """This method is called to derive the key. The default key + derivation choices can be overridden here. Key derivation is not + intended to be used as a security method to make a complex key + out of a short password. Instead you should use large random + secret keys. + + :param secret_key: A specific secret key to derive from. + Defaults to the last item in :attr:`secret_keys`. + + .. versionchanged:: 2.0 + Added the ``secret_key`` parameter. + """ + if secret_key is None: + secret_key = self.secret_keys[-1] + else: + secret_key = want_bytes(secret_key) + + if self.key_derivation == "concat": + return t.cast(bytes, self.digest_method(self.salt + secret_key).digest()) + elif self.key_derivation == "django-concat": + return t.cast( + bytes, self.digest_method(self.salt + b"signer" + secret_key).digest() + ) + elif self.key_derivation == "hmac": + mac = hmac.new(secret_key, digestmod=self.digest_method) + mac.update(self.salt) + return mac.digest() + elif self.key_derivation == "none": + return secret_key + else: + raise TypeError("Unknown key derivation method") + + def get_signature(self, value: str | bytes) -> bytes: + """Returns the signature for the given value.""" + value = want_bytes(value) + key = self.derive_key() + sig = self.algorithm.get_signature(key, value) + return base64_encode(sig) + + def sign(self, value: str | bytes) -> bytes: + """Signs the given string.""" + value = want_bytes(value) + return value + self.sep + self.get_signature(value) + + def verify_signature(self, value: str | bytes, sig: str | bytes) -> bool: + """Verifies the signature for the given value.""" + try: + sig = base64_decode(sig) + except Exception: + return False + + value = want_bytes(value) + + for secret_key in reversed(self.secret_keys): + key = self.derive_key(secret_key) + + if self.algorithm.verify_signature(key, value, sig): + return True + + return False + + def unsign(self, signed_value: str | bytes) -> bytes: + """Unsigns the given string.""" + signed_value = want_bytes(signed_value) + + if self.sep not in signed_value: + raise BadSignature(f"No {self.sep!r} found in value") + + value, sig = signed_value.rsplit(self.sep, 1) + + if self.verify_signature(value, sig): + return value + + raise BadSignature(f"Signature {sig!r} does not match", payload=value) + + def validate(self, signed_value: str | bytes) -> bool: + """Only validates the given signed value. Returns ``True`` if + the signature exists and is valid. + """ + try: + self.unsign(signed_value) + return True + except BadSignature: + return False diff --git a/venv/lib/python3.10/site-packages/itsdangerous/timed.py b/venv/lib/python3.10/site-packages/itsdangerous/timed.py new file mode 100644 index 0000000..7384375 --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous/timed.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +import collections.abc as cabc +import time +import typing as t +from datetime import datetime +from datetime import timezone + +from .encoding import base64_decode +from .encoding import base64_encode +from .encoding import bytes_to_int +from .encoding import int_to_bytes +from .encoding import want_bytes +from .exc import BadSignature +from .exc import BadTimeSignature +from .exc import SignatureExpired +from .serializer import _TSerialized +from .serializer import Serializer +from .signer import Signer + + +class TimestampSigner(Signer): + """Works like the regular :class:`.Signer` but also records the time + of the signing and can be used to expire signatures. The + :meth:`unsign` method can raise :exc:`.SignatureExpired` if the + unsigning failed because the signature is expired. + """ + + def get_timestamp(self) -> int: + """Returns the current timestamp. The function must return an + integer. + """ + return int(time.time()) + + def timestamp_to_datetime(self, ts: int) -> datetime: + """Convert the timestamp from :meth:`get_timestamp` into an + aware :class`datetime.datetime` in UTC. + + .. versionchanged:: 2.0 + The timestamp is returned as a timezone-aware ``datetime`` + in UTC rather than a naive ``datetime`` assumed to be UTC. + """ + return datetime.fromtimestamp(ts, tz=timezone.utc) + + def sign(self, value: str | bytes) -> bytes: + """Signs the given string and also attaches time information.""" + value = want_bytes(value) + timestamp = base64_encode(int_to_bytes(self.get_timestamp())) + sep = want_bytes(self.sep) + value = value + sep + timestamp + return value + sep + self.get_signature(value) + + # Ignore overlapping signatures check, return_timestamp is the only + # parameter that affects the return type. + + @t.overload + def unsign( # type: ignore[overload-overlap] + self, + signed_value: str | bytes, + max_age: int | None = None, + return_timestamp: t.Literal[False] = False, + ) -> bytes: ... + + @t.overload + def unsign( + self, + signed_value: str | bytes, + max_age: int | None = None, + return_timestamp: t.Literal[True] = True, + ) -> tuple[bytes, datetime]: ... + + def unsign( + self, + signed_value: str | bytes, + max_age: int | None = None, + return_timestamp: bool = False, + ) -> tuple[bytes, datetime] | bytes: + """Works like the regular :meth:`.Signer.unsign` but can also + validate the time. See the base docstring of the class for + the general behavior. If ``return_timestamp`` is ``True`` the + timestamp of the signature will be returned as an aware + :class:`datetime.datetime` object in UTC. + + .. versionchanged:: 2.0 + The timestamp is returned as a timezone-aware ``datetime`` + in UTC rather than a naive ``datetime`` assumed to be UTC. + """ + try: + result = super().unsign(signed_value) + sig_error = None + except BadSignature as e: + sig_error = e + result = e.payload or b"" + + sep = want_bytes(self.sep) + + # If there is no timestamp in the result there is something + # seriously wrong. In case there was a signature error, we raise + # that one directly, otherwise we have a weird situation in + # which we shouldn't have come except someone uses a time-based + # serializer on non-timestamp data, so catch that. + if sep not in result: + if sig_error: + raise sig_error + + raise BadTimeSignature("timestamp missing", payload=result) + + value, ts_bytes = result.rsplit(sep, 1) + ts_int: int | None = None + ts_dt: datetime | None = None + + try: + ts_int = bytes_to_int(base64_decode(ts_bytes)) + except Exception: + pass + + # Signature is *not* okay. Raise a proper error now that we have + # split the value and the timestamp. + if sig_error is not None: + if ts_int is not None: + try: + ts_dt = self.timestamp_to_datetime(ts_int) + except (ValueError, OSError, OverflowError) as exc: + # Windows raises OSError + # 32-bit raises OverflowError + raise BadTimeSignature( + "Malformed timestamp", payload=value + ) from exc + + raise BadTimeSignature(str(sig_error), payload=value, date_signed=ts_dt) + + # Signature was okay but the timestamp is actually not there or + # malformed. Should not happen, but we handle it anyway. + if ts_int is None: + raise BadTimeSignature("Malformed timestamp", payload=value) + + # Check timestamp is not older than max_age + if max_age is not None: + age = self.get_timestamp() - ts_int + + if age > max_age: + raise SignatureExpired( + f"Signature age {age} > {max_age} seconds", + payload=value, + date_signed=self.timestamp_to_datetime(ts_int), + ) + + if age < 0: + raise SignatureExpired( + f"Signature age {age} < 0 seconds", + payload=value, + date_signed=self.timestamp_to_datetime(ts_int), + ) + + if return_timestamp: + return value, self.timestamp_to_datetime(ts_int) + + return value + + def validate(self, signed_value: str | bytes, max_age: int | None = None) -> bool: + """Only validates the given signed value. Returns ``True`` if + the signature exists and is valid.""" + try: + self.unsign(signed_value, max_age=max_age) + return True + except BadSignature: + return False + + +class TimedSerializer(Serializer[_TSerialized]): + """Uses :class:`TimestampSigner` instead of the default + :class:`.Signer`. + """ + + default_signer: type[TimestampSigner] = TimestampSigner + + def iter_unsigners( + self, salt: str | bytes | None = None + ) -> cabc.Iterator[TimestampSigner]: + return t.cast("cabc.Iterator[TimestampSigner]", super().iter_unsigners(salt)) + + # TODO: Signature is incompatible because parameters were added + # before salt. + + def loads( # type: ignore[override] + self, + s: str | bytes, + max_age: int | None = None, + return_timestamp: bool = False, + salt: str | bytes | None = None, + ) -> t.Any: + """Reverse of :meth:`dumps`, raises :exc:`.BadSignature` if the + signature validation fails. If a ``max_age`` is provided it will + ensure the signature is not older than that time in seconds. In + case the signature is outdated, :exc:`.SignatureExpired` is + raised. All arguments are forwarded to the signer's + :meth:`~TimestampSigner.unsign` method. + """ + s = want_bytes(s) + last_exception = None + + for signer in self.iter_unsigners(salt): + try: + base64d, timestamp = signer.unsign( + s, max_age=max_age, return_timestamp=True + ) + payload = self.load_payload(base64d) + + if return_timestamp: + return payload, timestamp + + return payload + except SignatureExpired: + # The signature was unsigned successfully but was + # expired. Do not try the next signer. + raise + except BadSignature as err: + last_exception = err + + raise t.cast(BadSignature, last_exception) + + def loads_unsafe( # type: ignore[override] + self, + s: str | bytes, + max_age: int | None = None, + salt: str | bytes | None = None, + ) -> tuple[bool, t.Any]: + return self._loads_unsafe_impl(s, salt, load_kwargs={"max_age": max_age}) diff --git a/venv/lib/python3.10/site-packages/itsdangerous/url_safe.py b/venv/lib/python3.10/site-packages/itsdangerous/url_safe.py new file mode 100644 index 0000000..56a0793 --- /dev/null +++ b/venv/lib/python3.10/site-packages/itsdangerous/url_safe.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import typing as t +import zlib + +from ._json import _CompactJSON +from .encoding import base64_decode +from .encoding import base64_encode +from .exc import BadPayload +from .serializer import _PDataSerializer +from .serializer import Serializer +from .timed import TimedSerializer + + +class URLSafeSerializerMixin(Serializer[str]): + """Mixed in with a regular serializer it will attempt to zlib + compress the string to make it shorter if necessary. It will also + base64 encode the string so that it can safely be placed in a URL. + """ + + default_serializer: _PDataSerializer[str] = _CompactJSON + + def load_payload( + self, + payload: bytes, + *args: t.Any, + serializer: t.Any | None = None, + **kwargs: t.Any, + ) -> t.Any: + decompress = False + + if payload.startswith(b"."): + payload = payload[1:] + decompress = True + + try: + json = base64_decode(payload) + except Exception as e: + raise BadPayload( + "Could not base64 decode the payload because of an exception", + original_error=e, + ) from e + + if decompress: + try: + json = zlib.decompress(json) + except Exception as e: + raise BadPayload( + "Could not zlib decompress the payload before decoding the payload", + original_error=e, + ) from e + + return super().load_payload(json, *args, **kwargs) + + def dump_payload(self, obj: t.Any) -> bytes: + json = super().dump_payload(obj) + is_compressed = False + compressed = zlib.compress(json) + + if len(compressed) < (len(json) - 1): + json = compressed + is_compressed = True + + base64d = base64_encode(json) + + if is_compressed: + base64d = b"." + base64d + + return base64d + + +class URLSafeSerializer(URLSafeSerializerMixin, Serializer[str]): + """Works like :class:`.Serializer` but dumps and loads into a URL + safe string consisting of the upper and lowercase character of the + alphabet as well as ``'_'``, ``'-'`` and ``'.'``. + """ + + +class URLSafeTimedSerializer(URLSafeSerializerMixin, TimedSerializer[str]): + """Works like :class:`.TimedSerializer` but dumps and loads into a + URL safe string consisting of the upper and lowercase character of + the alphabet as well as ``'_'``, ``'-'`` and ``'.'``. + """ diff --git a/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/INSTALLER b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/METADATA b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/METADATA new file mode 100644 index 0000000..ffef2ff --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/METADATA @@ -0,0 +1,84 @@ +Metadata-Version: 2.4 +Name: Jinja2 +Version: 3.1.6 +Summary: A very fast and expressive template engine. +Maintainer-email: Pallets +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Text Processing :: Markup :: HTML +Classifier: Typing :: Typed +License-File: LICENSE.txt +Requires-Dist: MarkupSafe>=2.0 +Requires-Dist: Babel>=2.7 ; extra == "i18n" +Project-URL: Changes, https://jinja.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://jinja.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/jinja/ +Provides-Extra: i18n + +# Jinja + +Jinja is a fast, expressive, extensible templating engine. Special +placeholders in the template allow writing code similar to Python +syntax. Then the template is passed data to render the final document. + +It includes: + +- Template inheritance and inclusion. +- Define and import macros within templates. +- HTML templates can use autoescaping to prevent XSS from untrusted + user input. +- A sandboxed environment can safely render untrusted templates. +- AsyncIO support for generating templates and calling async + functions. +- I18N support with Babel. +- Templates are compiled to optimized Python code just-in-time and + cached, or can be compiled ahead-of-time. +- Exceptions point to the correct line in templates to make debugging + easier. +- Extensible filters, tests, functions, and even syntax. + +Jinja's philosophy is that while application logic belongs in Python if +possible, it shouldn't make the template designer's job difficult by +restricting functionality too much. + + +## In A Nutshell + +```jinja +{% extends "base.html" %} +{% block title %}Members{% endblock %} +{% block content %} +

+{% endblock %} +``` + +## Donate + +The Pallets organization develops and supports Jinja and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today][]. + +[please donate today]: https://palletsprojects.com/donate + +## Contributing + +See our [detailed contributing documentation][contrib] for many ways to +contribute, including reporting issues, requesting features, asking or answering +questions, and making PRs. + +[contrib]: https://palletsprojects.com/contributing/ + diff --git a/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/RECORD b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/RECORD new file mode 100644 index 0000000..20d4fee --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/RECORD @@ -0,0 +1,57 @@ +jinja2-3.1.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jinja2-3.1.6.dist-info/METADATA,sha256=aMVUj7Z8QTKhOJjZsx7FDGvqKr3ZFdkh8hQ1XDpkmcg,2871 +jinja2-3.1.6.dist-info/RECORD,, +jinja2-3.1.6.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82 +jinja2-3.1.6.dist-info/entry_points.txt,sha256=OL85gYU1eD8cuPlikifFngXpeBjaxl6rIJ8KkC_3r-I,58 +jinja2-3.1.6.dist-info/licenses/LICENSE.txt,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475 +jinja2/__init__.py,sha256=xxepO9i7DHsqkQrgBEduLtfoz2QCuT6_gbL4XSN1hbU,1928 +jinja2/__pycache__/__init__.cpython-310.pyc,, +jinja2/__pycache__/_identifier.cpython-310.pyc,, +jinja2/__pycache__/async_utils.cpython-310.pyc,, +jinja2/__pycache__/bccache.cpython-310.pyc,, +jinja2/__pycache__/compiler.cpython-310.pyc,, +jinja2/__pycache__/constants.cpython-310.pyc,, +jinja2/__pycache__/debug.cpython-310.pyc,, +jinja2/__pycache__/defaults.cpython-310.pyc,, +jinja2/__pycache__/environment.cpython-310.pyc,, +jinja2/__pycache__/exceptions.cpython-310.pyc,, +jinja2/__pycache__/ext.cpython-310.pyc,, +jinja2/__pycache__/filters.cpython-310.pyc,, +jinja2/__pycache__/idtracking.cpython-310.pyc,, +jinja2/__pycache__/lexer.cpython-310.pyc,, +jinja2/__pycache__/loaders.cpython-310.pyc,, +jinja2/__pycache__/meta.cpython-310.pyc,, +jinja2/__pycache__/nativetypes.cpython-310.pyc,, +jinja2/__pycache__/nodes.cpython-310.pyc,, +jinja2/__pycache__/optimizer.cpython-310.pyc,, +jinja2/__pycache__/parser.cpython-310.pyc,, +jinja2/__pycache__/runtime.cpython-310.pyc,, +jinja2/__pycache__/sandbox.cpython-310.pyc,, +jinja2/__pycache__/tests.cpython-310.pyc,, +jinja2/__pycache__/utils.cpython-310.pyc,, +jinja2/__pycache__/visitor.cpython-310.pyc,, +jinja2/_identifier.py,sha256=_zYctNKzRqlk_murTNlzrju1FFJL7Va_Ijqqd7ii2lU,1958 +jinja2/async_utils.py,sha256=vK-PdsuorOMnWSnEkT3iUJRIkTnYgO2T6MnGxDgHI5o,2834 +jinja2/bccache.py,sha256=gh0qs9rulnXo0PhX5jTJy2UHzI8wFnQ63o_vw7nhzRg,14061 +jinja2/compiler.py,sha256=9RpCQl5X88BHllJiPsHPh295Hh0uApvwFJNQuutULeM,74131 +jinja2/constants.py,sha256=GMoFydBF_kdpaRKPoM5cl5MviquVRLVyZtfp5-16jg0,1433 +jinja2/debug.py,sha256=CnHqCDHd-BVGvti_8ZsTolnXNhA3ECsY-6n_2pwU8Hw,6297 +jinja2/defaults.py,sha256=boBcSw78h-lp20YbaXSJsqkAI2uN_mD_TtCydpeq5wU,1267 +jinja2/environment.py,sha256=9nhrP7Ch-NbGX00wvyr4yy-uhNHq2OCc60ggGrni_fk,61513 +jinja2/exceptions.py,sha256=ioHeHrWwCWNaXX1inHmHVblvc4haO7AXsjCp3GfWvx0,5071 +jinja2/ext.py,sha256=5PF5eHfh8mXAIxXHHRB2xXbXohi8pE3nHSOxa66uS7E,31875 +jinja2/filters.py,sha256=PQ_Egd9n9jSgtnGQYyF4K5j2nYwhUIulhPnyimkdr-k,55212 +jinja2/idtracking.py,sha256=-ll5lIp73pML3ErUYiIJj7tdmWxcH_IlDv3yA_hiZYo,10555 +jinja2/lexer.py,sha256=LYiYio6br-Tep9nPcupWXsPEtjluw3p1mU-lNBVRUfk,29786 +jinja2/loaders.py,sha256=wIrnxjvcbqh5VwW28NSkfotiDq8qNCxIOSFbGUiSLB4,24055 +jinja2/meta.py,sha256=OTDPkaFvU2Hgvx-6akz7154F8BIWaRmvJcBFvwopHww,4397 +jinja2/nativetypes.py,sha256=7GIGALVJgdyL80oZJdQUaUfwSt5q2lSSZbXt0dNf_M4,4210 +jinja2/nodes.py,sha256=m1Duzcr6qhZI8JQ6VyJgUNinjAf5bQzijSmDnMsvUx8,34579 +jinja2/optimizer.py,sha256=rJnCRlQ7pZsEEmMhsQDgC_pKyDHxP5TPS6zVPGsgcu8,1651 +jinja2/parser.py,sha256=lLOFy3sEmHc5IaEHRiH1sQVnId2moUQzhyeJZTtdY30,40383 +jinja2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jinja2/runtime.py,sha256=gDk-GvdriJXqgsGbHgrcKTP0Yp6zPXzhzrIpCFH3jAU,34249 +jinja2/sandbox.py,sha256=Mw2aitlY2I8la7FYhcX2YG9BtUYcLnD0Gh3d29cDWrY,15009 +jinja2/tests.py,sha256=VLsBhVFnWg-PxSBz1MhRnNWgP1ovXk3neO1FLQMeC9Q,5926 +jinja2/utils.py,sha256=rRp3o9e7ZKS4fyrWRbELyLcpuGVTFcnooaOa1qx_FIk,24129 +jinja2/visitor.py,sha256=EcnL1PIwf_4RVCOMxsRNuR8AXHbS1qfAdMOE2ngKJz4,3557 diff --git a/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/WHEEL b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/WHEEL new file mode 100644 index 0000000..23d2d7e --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.11.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/entry_points.txt b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/entry_points.txt new file mode 100644 index 0000000..abc3eae --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[babel.extractors] +jinja2=jinja2.ext:babel_extract[i18n] + diff --git a/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..c37cae4 --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/venv/lib/python3.10/site-packages/jinja2/__init__.py b/venv/lib/python3.10/site-packages/jinja2/__init__.py new file mode 100644 index 0000000..1a423a3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2/__init__.py @@ -0,0 +1,38 @@ +"""Jinja is a template engine written in pure Python. It provides a +non-XML syntax that supports inline expressions and an optional +sandboxed environment. +""" + +from .bccache import BytecodeCache as BytecodeCache +from .bccache import FileSystemBytecodeCache as FileSystemBytecodeCache +from .bccache import MemcachedBytecodeCache as MemcachedBytecodeCache +from .environment import Environment as Environment +from .environment import Template as Template +from .exceptions import TemplateAssertionError as TemplateAssertionError +from .exceptions import TemplateError as TemplateError +from .exceptions import TemplateNotFound as TemplateNotFound +from .exceptions import TemplateRuntimeError as TemplateRuntimeError +from .exceptions import TemplatesNotFound as TemplatesNotFound +from .exceptions import TemplateSyntaxError as TemplateSyntaxError +from .exceptions import UndefinedError as UndefinedError +from .loaders import BaseLoader as BaseLoader +from .loaders import ChoiceLoader as ChoiceLoader +from .loaders import DictLoader as DictLoader +from .loaders import FileSystemLoader as FileSystemLoader +from .loaders import FunctionLoader as FunctionLoader +from .loaders import ModuleLoader as ModuleLoader +from .loaders import PackageLoader as PackageLoader +from .loaders import PrefixLoader as PrefixLoader +from .runtime import ChainableUndefined as ChainableUndefined +from .runtime import DebugUndefined as DebugUndefined +from .runtime import make_logging_undefined as make_logging_undefined +from .runtime import StrictUndefined as StrictUndefined +from .runtime import Undefined as Undefined +from .utils import clear_caches as clear_caches +from .utils import is_undefined as is_undefined +from .utils import pass_context as pass_context +from .utils import pass_environment as pass_environment +from .utils import pass_eval_context as pass_eval_context +from .utils import select_autoescape as select_autoescape + +__version__ = "3.1.6" diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/__init__.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 0000000..7c2ba99 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/__init__.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/_identifier.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/_identifier.cpython-310.pyc new file mode 100644 index 0000000..63cdd62 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/_identifier.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/async_utils.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/async_utils.cpython-310.pyc new file mode 100644 index 0000000..c4e9ba6 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/async_utils.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/bccache.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/bccache.cpython-310.pyc new file mode 100644 index 0000000..7703db7 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/bccache.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/compiler.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/compiler.cpython-310.pyc new file mode 100644 index 0000000..b3a3a4f Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/compiler.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/constants.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/constants.cpython-310.pyc new file mode 100644 index 0000000..ffa8f2d Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/constants.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/debug.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/debug.cpython-310.pyc new file mode 100644 index 0000000..764fea4 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/debug.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/defaults.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/defaults.cpython-310.pyc new file mode 100644 index 0000000..5293ff3 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/defaults.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/environment.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/environment.cpython-310.pyc new file mode 100644 index 0000000..484f62f Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/environment.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/exceptions.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/exceptions.cpython-310.pyc new file mode 100644 index 0000000..613ade3 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/exceptions.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/ext.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/ext.cpython-310.pyc new file mode 100644 index 0000000..7543de2 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/ext.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/filters.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/filters.cpython-310.pyc new file mode 100644 index 0000000..63b8c70 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/filters.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/idtracking.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/idtracking.cpython-310.pyc new file mode 100644 index 0000000..3a3c660 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/idtracking.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/lexer.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/lexer.cpython-310.pyc new file mode 100644 index 0000000..918f78a Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/lexer.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/loaders.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/loaders.cpython-310.pyc new file mode 100644 index 0000000..89a285e Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/loaders.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/meta.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/meta.cpython-310.pyc new file mode 100644 index 0000000..194e232 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/meta.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/nativetypes.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/nativetypes.cpython-310.pyc new file mode 100644 index 0000000..a2a3261 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/nativetypes.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/nodes.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/nodes.cpython-310.pyc new file mode 100644 index 0000000..63b0905 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/nodes.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/optimizer.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/optimizer.cpython-310.pyc new file mode 100644 index 0000000..97f4f80 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/optimizer.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/parser.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/parser.cpython-310.pyc new file mode 100644 index 0000000..57fcb3d Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/parser.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/runtime.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/runtime.cpython-310.pyc new file mode 100644 index 0000000..86bd228 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/runtime.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/sandbox.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/sandbox.cpython-310.pyc new file mode 100644 index 0000000..19963a6 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/sandbox.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/tests.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/tests.cpython-310.pyc new file mode 100644 index 0000000..62bad45 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/tests.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/utils.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/utils.cpython-310.pyc new file mode 100644 index 0000000..e006e49 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/utils.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/__pycache__/visitor.cpython-310.pyc b/venv/lib/python3.10/site-packages/jinja2/__pycache__/visitor.cpython-310.pyc new file mode 100644 index 0000000..adf0185 Binary files /dev/null and b/venv/lib/python3.10/site-packages/jinja2/__pycache__/visitor.cpython-310.pyc differ diff --git a/venv/lib/python3.10/site-packages/jinja2/_identifier.py b/venv/lib/python3.10/site-packages/jinja2/_identifier.py new file mode 100644 index 0000000..928c150 --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2/_identifier.py @@ -0,0 +1,6 @@ +import re + +# generated by scripts/generate_identifier_pattern.py +pattern = re.compile( + r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߽߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛࣓-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣ৾ਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣૺ-૿ଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఄా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഀ-ഃ഻഼ാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳷-᳹᷀-᷹᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꣿꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𐴤-𐽆𐴧-𐽐𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑄴𑅅𑅆𑅳𑆀-𑆂𑆳-𑇀𑇉-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌻𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑑞𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑠬-𑠺𑨁-𑨊𑨳-𑨹𑨻-𑨾𑩇𑩑-𑩛𑪊-𑪙𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𑴱-𑴶𑴺𑴼𑴽𑴿-𑵅𑵇𑶊-𑶎𑶐𑶑𑶓-𑶗𑻳-𑻶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950 +) diff --git a/venv/lib/python3.10/site-packages/jinja2/async_utils.py b/venv/lib/python3.10/site-packages/jinja2/async_utils.py new file mode 100644 index 0000000..f0c1402 --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2/async_utils.py @@ -0,0 +1,99 @@ +import inspect +import typing as t +from functools import WRAPPER_ASSIGNMENTS +from functools import wraps + +from .utils import _PassArg +from .utils import pass_eval_context + +if t.TYPE_CHECKING: + import typing_extensions as te + +V = t.TypeVar("V") + + +def async_variant(normal_func): # type: ignore + def decorator(async_func): # type: ignore + pass_arg = _PassArg.from_obj(normal_func) + need_eval_context = pass_arg is None + + if pass_arg is _PassArg.environment: + + def is_async(args: t.Any) -> bool: + return t.cast(bool, args[0].is_async) + + else: + + def is_async(args: t.Any) -> bool: + return t.cast(bool, args[0].environment.is_async) + + # Take the doc and annotations from the sync function, but the + # name from the async function. Pallets-Sphinx-Themes + # build_function_directive expects __wrapped__ to point to the + # sync function. + async_func_attrs = ("__module__", "__name__", "__qualname__") + normal_func_attrs = tuple(set(WRAPPER_ASSIGNMENTS).difference(async_func_attrs)) + + @wraps(normal_func, assigned=normal_func_attrs) + @wraps(async_func, assigned=async_func_attrs, updated=()) + def wrapper(*args, **kwargs): # type: ignore + b = is_async(args) + + if need_eval_context: + args = args[1:] + + if b: + return async_func(*args, **kwargs) + + return normal_func(*args, **kwargs) + + if need_eval_context: + wrapper = pass_eval_context(wrapper) + + wrapper.jinja_async_variant = True # type: ignore[attr-defined] + return wrapper + + return decorator + + +_common_primitives = {int, float, bool, str, list, dict, tuple, type(None)} + + +async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V": + # Avoid a costly call to isawaitable + if type(value) in _common_primitives: + return t.cast("V", value) + + if inspect.isawaitable(value): + return await t.cast("t.Awaitable[V]", value) + + return value + + +class _IteratorToAsyncIterator(t.Generic[V]): + def __init__(self, iterator: "t.Iterator[V]"): + self._iterator = iterator + + def __aiter__(self) -> "te.Self": + return self + + async def __anext__(self) -> V: + try: + return next(self._iterator) + except StopIteration as e: + raise StopAsyncIteration(e.value) from e + + +def auto_aiter( + iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", +) -> "t.AsyncIterator[V]": + if hasattr(iterable, "__aiter__"): + return iterable.__aiter__() + else: + return _IteratorToAsyncIterator(iter(iterable)) + + +async def auto_to_list( + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", +) -> t.List["V"]: + return [x async for x in auto_aiter(value)] diff --git a/venv/lib/python3.10/site-packages/jinja2/bccache.py b/venv/lib/python3.10/site-packages/jinja2/bccache.py new file mode 100644 index 0000000..ada8b09 --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2/bccache.py @@ -0,0 +1,408 @@ +"""The optional bytecode cache system. This is useful if you have very +complex template situations and the compilation of all those templates +slows down your application too much. + +Situations where this is useful are often forking web applications that +are initialized on the first request. +""" + +import errno +import fnmatch +import marshal +import os +import pickle +import stat +import sys +import tempfile +import typing as t +from hashlib import sha1 +from io import BytesIO +from types import CodeType + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .environment import Environment + + class _MemcachedClient(te.Protocol): + def get(self, key: str) -> bytes: ... + + def set( + self, key: str, value: bytes, timeout: t.Optional[int] = None + ) -> None: ... + + +bc_version = 5 +# Magic bytes to identify Jinja bytecode cache files. Contains the +# Python major and minor version to avoid loading incompatible bytecode +# if a project upgrades its Python version. +bc_magic = ( + b"j2" + + pickle.dumps(bc_version, 2) + + pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1], 2) +) + + +class Bucket: + """Buckets are used to store the bytecode for one template. It's created + and initialized by the bytecode cache and passed to the loading functions. + + The buckets get an internal checksum from the cache assigned and use this + to automatically reject outdated cache material. Individual bytecode + cache subclasses don't have to care about cache invalidation. + """ + + def __init__(self, environment: "Environment", key: str, checksum: str) -> None: + self.environment = environment + self.key = key + self.checksum = checksum + self.reset() + + def reset(self) -> None: + """Resets the bucket (unloads the bytecode).""" + self.code: t.Optional[CodeType] = None + + def load_bytecode(self, f: t.BinaryIO) -> None: + """Loads bytecode from a file or file like object.""" + # make sure the magic header is correct + magic = f.read(len(bc_magic)) + if magic != bc_magic: + self.reset() + return + # the source code of the file changed, we need to reload + checksum = pickle.load(f) + if self.checksum != checksum: + self.reset() + return + # if marshal_load fails then we need to reload + try: + self.code = marshal.load(f) + except (EOFError, ValueError, TypeError): + self.reset() + return + + def write_bytecode(self, f: t.IO[bytes]) -> None: + """Dump the bytecode into the file or file like object passed.""" + if self.code is None: + raise TypeError("can't write empty bucket") + f.write(bc_magic) + pickle.dump(self.checksum, f, 2) + marshal.dump(self.code, f) + + def bytecode_from_string(self, string: bytes) -> None: + """Load bytecode from bytes.""" + self.load_bytecode(BytesIO(string)) + + def bytecode_to_string(self) -> bytes: + """Return the bytecode as bytes.""" + out = BytesIO() + self.write_bytecode(out) + return out.getvalue() + + +class BytecodeCache: + """To implement your own bytecode cache you have to subclass this class + and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of + these methods are passed a :class:`~jinja2.bccache.Bucket`. + + A very basic bytecode cache that saves the bytecode on the file system:: + + from os import path + + class MyCache(BytecodeCache): + + def __init__(self, directory): + self.directory = directory + + def load_bytecode(self, bucket): + filename = path.join(self.directory, bucket.key) + if path.exists(filename): + with open(filename, 'rb') as f: + bucket.load_bytecode(f) + + def dump_bytecode(self, bucket): + filename = path.join(self.directory, bucket.key) + with open(filename, 'wb') as f: + bucket.write_bytecode(f) + + A more advanced version of a filesystem based bytecode cache is part of + Jinja. + """ + + def load_bytecode(self, bucket: Bucket) -> None: + """Subclasses have to override this method to load bytecode into a + bucket. If they are not able to find code in the cache for the + bucket, it must not do anything. + """ + raise NotImplementedError() + + def dump_bytecode(self, bucket: Bucket) -> None: + """Subclasses have to override this method to write the bytecode + from a bucket back to the cache. If it unable to do so it must not + fail silently but raise an exception. + """ + raise NotImplementedError() + + def clear(self) -> None: + """Clears the cache. This method is not used by Jinja but should be + implemented to allow applications to clear the bytecode cache used + by a particular environment. + """ + + def get_cache_key( + self, name: str, filename: t.Optional[t.Union[str]] = None + ) -> str: + """Returns the unique hash key for this template name.""" + hash = sha1(name.encode("utf-8")) + + if filename is not None: + hash.update(f"|{filename}".encode()) + + return hash.hexdigest() + + def get_source_checksum(self, source: str) -> str: + """Returns a checksum for the source.""" + return sha1(source.encode("utf-8")).hexdigest() + + def get_bucket( + self, + environment: "Environment", + name: str, + filename: t.Optional[str], + source: str, + ) -> Bucket: + """Return a cache bucket for the given template. All arguments are + mandatory but filename may be `None`. + """ + key = self.get_cache_key(name, filename) + checksum = self.get_source_checksum(source) + bucket = Bucket(environment, key, checksum) + self.load_bytecode(bucket) + return bucket + + def set_bucket(self, bucket: Bucket) -> None: + """Put the bucket into the cache.""" + self.dump_bytecode(bucket) + + +class FileSystemBytecodeCache(BytecodeCache): + """A bytecode cache that stores bytecode on the filesystem. It accepts + two arguments: The directory where the cache items are stored and a + pattern string that is used to build the filename. + + If no directory is specified a default cache directory is selected. On + Windows the user's temp directory is used, on UNIX systems a directory + is created for the user in the system temp directory. + + The pattern can be used to have multiple separate caches operate on the + same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s`` + is replaced with the cache key. + + >>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache') + + This bytecode cache supports clearing of the cache using the clear method. + """ + + def __init__( + self, directory: t.Optional[str] = None, pattern: str = "__jinja2_%s.cache" + ) -> None: + if directory is None: + directory = self._get_default_cache_dir() + self.directory = directory + self.pattern = pattern + + def _get_default_cache_dir(self) -> str: + def _unsafe_dir() -> "te.NoReturn": + raise RuntimeError( + "Cannot determine safe temp directory. You " + "need to explicitly provide one." + ) + + tmpdir = tempfile.gettempdir() + + # On windows the temporary directory is used specific unless + # explicitly forced otherwise. We can just use that. + if os.name == "nt": + return tmpdir + if not hasattr(os, "getuid"): + _unsafe_dir() + + dirname = f"_jinja2-cache-{os.getuid()}" + actual_dir = os.path.join(tmpdir, dirname) + + try: + os.mkdir(actual_dir, stat.S_IRWXU) + except OSError as e: + if e.errno != errno.EEXIST: + raise + try: + os.chmod(actual_dir, stat.S_IRWXU) + actual_dir_stat = os.lstat(actual_dir) + if ( + actual_dir_stat.st_uid != os.getuid() + or not stat.S_ISDIR(actual_dir_stat.st_mode) + or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU + ): + _unsafe_dir() + except OSError as e: + if e.errno != errno.EEXIST: + raise + + actual_dir_stat = os.lstat(actual_dir) + if ( + actual_dir_stat.st_uid != os.getuid() + or not stat.S_ISDIR(actual_dir_stat.st_mode) + or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU + ): + _unsafe_dir() + + return actual_dir + + def _get_cache_filename(self, bucket: Bucket) -> str: + return os.path.join(self.directory, self.pattern % (bucket.key,)) + + def load_bytecode(self, bucket: Bucket) -> None: + filename = self._get_cache_filename(bucket) + + # Don't test for existence before opening the file, since the + # file could disappear after the test before the open. + try: + f = open(filename, "rb") + except (FileNotFoundError, IsADirectoryError, PermissionError): + # PermissionError can occur on Windows when an operation is + # in progress, such as calling clear(). + return + + with f: + bucket.load_bytecode(f) + + def dump_bytecode(self, bucket: Bucket) -> None: + # Write to a temporary file, then rename to the real name after + # writing. This avoids another process reading the file before + # it is fully written. + name = self._get_cache_filename(bucket) + f = tempfile.NamedTemporaryFile( + mode="wb", + dir=os.path.dirname(name), + prefix=os.path.basename(name), + suffix=".tmp", + delete=False, + ) + + def remove_silent() -> None: + try: + os.remove(f.name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + pass + + try: + with f: + bucket.write_bytecode(f) + except BaseException: + remove_silent() + raise + + try: + os.replace(f.name, name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + remove_silent() + except BaseException: + remove_silent() + raise + + def clear(self) -> None: + # imported lazily here because google app-engine doesn't support + # write access on the file system and the function does not exist + # normally. + from os import remove + + files = fnmatch.filter(os.listdir(self.directory), self.pattern % ("*",)) + for filename in files: + try: + remove(os.path.join(self.directory, filename)) + except OSError: + pass + + +class MemcachedBytecodeCache(BytecodeCache): + """This class implements a bytecode cache that uses a memcache cache for + storing the information. It does not enforce a specific memcache library + (tummy's memcache or cmemcache) but will accept any class that provides + the minimal interface required. + + Libraries compatible with this class: + + - `cachelib `_ + - `python-memcached `_ + + (Unfortunately the django cache interface is not compatible because it + does not support storing binary data, only text. You can however pass + the underlying cache client to the bytecode cache which is available + as `django.core.cache.cache._client`.) + + The minimal interface for the client passed to the constructor is this: + + .. class:: MinimalClientInterface + + .. method:: set(key, value[, timeout]) + + Stores the bytecode in the cache. `value` is a string and + `timeout` the timeout of the key. If timeout is not provided + a default timeout or no timeout should be assumed, if it's + provided it's an integer with the number of seconds the cache + item should exist. + + .. method:: get(key) + + Returns the value for the cache key. If the item does not + exist in the cache the return value must be `None`. + + The other arguments to the constructor are the prefix for all keys that + is added before the actual cache key and the timeout for the bytecode in + the cache system. We recommend a high (or no) timeout. + + This bytecode cache does not support clearing of used items in the cache. + The clear method is a no-operation function. + + .. versionadded:: 2.7 + Added support for ignoring memcache errors through the + `ignore_memcache_errors` parameter. + """ + + def __init__( + self, + client: "_MemcachedClient", + prefix: str = "jinja2/bytecode/", + timeout: t.Optional[int] = None, + ignore_memcache_errors: bool = True, + ): + self.client = client + self.prefix = prefix + self.timeout = timeout + self.ignore_memcache_errors = ignore_memcache_errors + + def load_bytecode(self, bucket: Bucket) -> None: + try: + code = self.client.get(self.prefix + bucket.key) + except Exception: + if not self.ignore_memcache_errors: + raise + else: + bucket.bytecode_from_string(code) + + def dump_bytecode(self, bucket: Bucket) -> None: + key = self.prefix + bucket.key + value = bucket.bytecode_to_string() + + try: + if self.timeout is not None: + self.client.set(key, value, self.timeout) + else: + self.client.set(key, value) + except Exception: + if not self.ignore_memcache_errors: + raise diff --git a/venv/lib/python3.10/site-packages/jinja2/compiler.py b/venv/lib/python3.10/site-packages/jinja2/compiler.py new file mode 100644 index 0000000..a4ff6a1 --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2/compiler.py @@ -0,0 +1,1998 @@ +"""Compiles nodes from the parser into Python code.""" + +import typing as t +from contextlib import contextmanager +from functools import update_wrapper +from io import StringIO +from itertools import chain +from keyword import iskeyword as is_python_keyword + +from markupsafe import escape +from markupsafe import Markup + +from . import nodes +from .exceptions import TemplateAssertionError +from .idtracking import Symbols +from .idtracking import VAR_LOAD_ALIAS +from .idtracking import VAR_LOAD_PARAMETER +from .idtracking import VAR_LOAD_RESOLVE +from .idtracking import VAR_LOAD_UNDEFINED +from .nodes import EvalContext +from .optimizer import Optimizer +from .utils import _PassArg +from .utils import concat +from .visitor import NodeVisitor + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .environment import Environment + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + +operators = { + "eq": "==", + "ne": "!=", + "gt": ">", + "gteq": ">=", + "lt": "<", + "lteq": "<=", + "in": "in", + "notin": "not in", +} + + +def optimizeconst(f: F) -> F: + def new_func( + self: "CodeGenerator", node: nodes.Expr, frame: "Frame", **kwargs: t.Any + ) -> t.Any: + # Only optimize if the frame is not volatile + if self.optimizer is not None and not frame.eval_ctx.volatile: + new_node = self.optimizer.visit(node, frame.eval_ctx) + + if new_node != node: + return self.visit(new_node, frame) + + return f(self, node, frame, **kwargs) + + return update_wrapper(new_func, f) # type: ignore[return-value] + + +def _make_binop(op: str) -> t.Callable[["CodeGenerator", nodes.BinExpr, "Frame"], None]: + @optimizeconst + def visitor(self: "CodeGenerator", node: nodes.BinExpr, frame: Frame) -> None: + if ( + self.environment.sandboxed and op in self.environment.intercepted_binops # type: ignore + ): + self.write(f"environment.call_binop(context, {op!r}, ") + self.visit(node.left, frame) + self.write(", ") + self.visit(node.right, frame) + else: + self.write("(") + self.visit(node.left, frame) + self.write(f" {op} ") + self.visit(node.right, frame) + + self.write(")") + + return visitor + + +def _make_unop( + op: str, +) -> t.Callable[["CodeGenerator", nodes.UnaryExpr, "Frame"], None]: + @optimizeconst + def visitor(self: "CodeGenerator", node: nodes.UnaryExpr, frame: Frame) -> None: + if ( + self.environment.sandboxed and op in self.environment.intercepted_unops # type: ignore + ): + self.write(f"environment.call_unop(context, {op!r}, ") + self.visit(node.node, frame) + else: + self.write("(" + op) + self.visit(node.node, frame) + + self.write(")") + + return visitor + + +def generate( + node: nodes.Template, + environment: "Environment", + name: t.Optional[str], + filename: t.Optional[str], + stream: t.Optional[t.TextIO] = None, + defer_init: bool = False, + optimized: bool = True, +) -> t.Optional[str]: + """Generate the python source for a node tree.""" + if not isinstance(node, nodes.Template): + raise TypeError("Can't compile non template nodes") + + generator = environment.code_generator_class( + environment, name, filename, stream, defer_init, optimized + ) + generator.visit(node) + + if stream is None: + return generator.stream.getvalue() # type: ignore + + return None + + +def has_safe_repr(value: t.Any) -> bool: + """Does the node have a safe representation?""" + if value is None or value is NotImplemented or value is Ellipsis: + return True + + if type(value) in {bool, int, float, complex, range, str, Markup}: + return True + + if type(value) in {tuple, list, set, frozenset}: + return all(has_safe_repr(v) for v in value) + + if type(value) is dict: # noqa E721 + return all(has_safe_repr(k) and has_safe_repr(v) for k, v in value.items()) + + return False + + +def find_undeclared( + nodes: t.Iterable[nodes.Node], names: t.Iterable[str] +) -> t.Set[str]: + """Check if the names passed are accessed undeclared. The return value + is a set of all the undeclared names from the sequence of names found. + """ + visitor = UndeclaredNameVisitor(names) + try: + for node in nodes: + visitor.visit(node) + except VisitorExit: + pass + return visitor.undeclared + + +class MacroRef: + def __init__(self, node: t.Union[nodes.Macro, nodes.CallBlock]) -> None: + self.node = node + self.accesses_caller = False + self.accesses_kwargs = False + self.accesses_varargs = False + + +class Frame: + """Holds compile time information for us.""" + + def __init__( + self, + eval_ctx: EvalContext, + parent: t.Optional["Frame"] = None, + level: t.Optional[int] = None, + ) -> None: + self.eval_ctx = eval_ctx + + # the parent of this frame + self.parent = parent + + if parent is None: + self.symbols = Symbols(level=level) + + # in some dynamic inheritance situations the compiler needs to add + # write tests around output statements. + self.require_output_check = False + + # inside some tags we are using a buffer rather than yield statements. + # this for example affects {% filter %} or {% macro %}. If a frame + # is buffered this variable points to the name of the list used as + # buffer. + self.buffer: t.Optional[str] = None + + # the name of the block we're in, otherwise None. + self.block: t.Optional[str] = None + + else: + self.symbols = Symbols(parent.symbols, level=level) + self.require_output_check = parent.require_output_check + self.buffer = parent.buffer + self.block = parent.block + + # a toplevel frame is the root + soft frames such as if conditions. + self.toplevel = False + + # the root frame is basically just the outermost frame, so no if + # conditions. This information is used to optimize inheritance + # situations. + self.rootlevel = False + + # variables set inside of loops and blocks should not affect outer frames, + # but they still needs to be kept track of as part of the active context. + self.loop_frame = False + self.block_frame = False + + # track whether the frame is being used in an if-statement or conditional + # expression as it determines which errors should be raised during runtime + # or compile time. + self.soft_frame = False + + def copy(self) -> "te.Self": + """Create a copy of the current one.""" + rv = object.__new__(self.__class__) + rv.__dict__.update(self.__dict__) + rv.symbols = self.symbols.copy() + return rv + + def inner(self, isolated: bool = False) -> "Frame": + """Return an inner frame.""" + if isolated: + return Frame(self.eval_ctx, level=self.symbols.level + 1) + return Frame(self.eval_ctx, self) + + def soft(self) -> "te.Self": + """Return a soft frame. A soft frame may not be modified as + standalone thing as it shares the resources with the frame it + was created of, but it's not a rootlevel frame any longer. + + This is only used to implement if-statements and conditional + expressions. + """ + rv = self.copy() + rv.rootlevel = False + rv.soft_frame = True + return rv + + __copy__ = copy + + +class VisitorExit(RuntimeError): + """Exception used by the `UndeclaredNameVisitor` to signal a stop.""" + + +class DependencyFinderVisitor(NodeVisitor): + """A visitor that collects filter and test calls.""" + + def __init__(self) -> None: + self.filters: t.Set[str] = set() + self.tests: t.Set[str] = set() + + def visit_Filter(self, node: nodes.Filter) -> None: + self.generic_visit(node) + self.filters.add(node.name) + + def visit_Test(self, node: nodes.Test) -> None: + self.generic_visit(node) + self.tests.add(node.name) + + def visit_Block(self, node: nodes.Block) -> None: + """Stop visiting at blocks.""" + + +class UndeclaredNameVisitor(NodeVisitor): + """A visitor that checks if a name is accessed without being + declared. This is different from the frame visitor as it will + not stop at closure frames. + """ + + def __init__(self, names: t.Iterable[str]) -> None: + self.names = set(names) + self.undeclared: t.Set[str] = set() + + def visit_Name(self, node: nodes.Name) -> None: + if node.ctx == "load" and node.name in self.names: + self.undeclared.add(node.name) + if self.undeclared == self.names: + raise VisitorExit() + else: + self.names.discard(node.name) + + def visit_Block(self, node: nodes.Block) -> None: + """Stop visiting a blocks.""" + + +class CompilerExit(Exception): + """Raised if the compiler encountered a situation where it just + doesn't make sense to further process the code. Any block that + raises such an exception is not further processed. + """ + + +class CodeGenerator(NodeVisitor): + def __init__( + self, + environment: "Environment", + name: t.Optional[str], + filename: t.Optional[str], + stream: t.Optional[t.TextIO] = None, + defer_init: bool = False, + optimized: bool = True, + ) -> None: + if stream is None: + stream = StringIO() + self.environment = environment + self.name = name + self.filename = filename + self.stream = stream + self.created_block_context = False + self.defer_init = defer_init + self.optimizer: t.Optional[Optimizer] = None + + if optimized: + self.optimizer = Optimizer(environment) + + # aliases for imports + self.import_aliases: t.Dict[str, str] = {} + + # a registry for all blocks. Because blocks are moved out + # into the global python scope they are registered here + self.blocks: t.Dict[str, nodes.Block] = {} + + # the number of extends statements so far + self.extends_so_far = 0 + + # some templates have a rootlevel extends. In this case we + # can safely assume that we're a child template and do some + # more optimizations. + self.has_known_extends = False + + # the current line number + self.code_lineno = 1 + + # registry of all filters and tests (global, not block local) + self.tests: t.Dict[str, str] = {} + self.filters: t.Dict[str, str] = {} + + # the debug information + self.debug_info: t.List[t.Tuple[int, int]] = [] + self._write_debug_info: t.Optional[int] = None + + # the number of new lines before the next write() + self._new_lines = 0 + + # the line number of the last written statement + self._last_line = 0 + + # true if nothing was written so far. + self._first_write = True + + # used by the `temporary_identifier` method to get new + # unique, temporary identifier + self._last_identifier = 0 + + # the current indentation + self._indentation = 0 + + # Tracks toplevel assignments + self._assign_stack: t.List[t.Set[str]] = [] + + # Tracks parameter definition blocks + self._param_def_block: t.List[t.Set[str]] = [] + + # Tracks the current context. + self._context_reference_stack = ["context"] + + @property + def optimized(self) -> bool: + return self.optimizer is not None + + # -- Various compilation helpers + + def fail(self, msg: str, lineno: int) -> "te.NoReturn": + """Fail with a :exc:`TemplateAssertionError`.""" + raise TemplateAssertionError(msg, lineno, self.name, self.filename) + + def temporary_identifier(self) -> str: + """Get a new unique identifier.""" + self._last_identifier += 1 + return f"t_{self._last_identifier}" + + def buffer(self, frame: Frame) -> None: + """Enable buffering for the frame from that point onwards.""" + frame.buffer = self.temporary_identifier() + self.writeline(f"{frame.buffer} = []") + + def return_buffer_contents( + self, frame: Frame, force_unescaped: bool = False + ) -> None: + """Return the buffer contents of the frame.""" + if not force_unescaped: + if frame.eval_ctx.volatile: + self.writeline("if context.eval_ctx.autoescape:") + self.indent() + self.writeline(f"return Markup(concat({frame.buffer}))") + self.outdent() + self.writeline("else:") + self.indent() + self.writeline(f"return concat({frame.buffer})") + self.outdent() + return + elif frame.eval_ctx.autoescape: + self.writeline(f"return Markup(concat({frame.buffer}))") + return + self.writeline(f"return concat({frame.buffer})") + + def indent(self) -> None: + """Indent by one.""" + self._indentation += 1 + + def outdent(self, step: int = 1) -> None: + """Outdent by step.""" + self._indentation -= step + + def start_write(self, frame: Frame, node: t.Optional[nodes.Node] = None) -> None: + """Yield or write into the frame buffer.""" + if frame.buffer is None: + self.writeline("yield ", node) + else: + self.writeline(f"{frame.buffer}.append(", node) + + def end_write(self, frame: Frame) -> None: + """End the writing process started by `start_write`.""" + if frame.buffer is not None: + self.write(")") + + def simple_write( + self, s: str, frame: Frame, node: t.Optional[nodes.Node] = None + ) -> None: + """Simple shortcut for start_write + write + end_write.""" + self.start_write(frame, node) + self.write(s) + self.end_write(frame) + + def blockvisit(self, nodes: t.Iterable[nodes.Node], frame: Frame) -> None: + """Visit a list of nodes as block in a frame. If the current frame + is no buffer a dummy ``if 0: yield None`` is written automatically. + """ + try: + self.writeline("pass") + for node in nodes: + self.visit(node, frame) + except CompilerExit: + pass + + def write(self, x: str) -> None: + """Write a string into the output stream.""" + if self._new_lines: + if not self._first_write: + self.stream.write("\n" * self._new_lines) + self.code_lineno += self._new_lines + if self._write_debug_info is not None: + self.debug_info.append((self._write_debug_info, self.code_lineno)) + self._write_debug_info = None + self._first_write = False + self.stream.write(" " * self._indentation) + self._new_lines = 0 + self.stream.write(x) + + def writeline( + self, x: str, node: t.Optional[nodes.Node] = None, extra: int = 0 + ) -> None: + """Combination of newline and write.""" + self.newline(node, extra) + self.write(x) + + def newline(self, node: t.Optional[nodes.Node] = None, extra: int = 0) -> None: + """Add one or more newlines before the next write.""" + self._new_lines = max(self._new_lines, 1 + extra) + if node is not None and node.lineno != self._last_line: + self._write_debug_info = node.lineno + self._last_line = node.lineno + + def signature( + self, + node: t.Union[nodes.Call, nodes.Filter, nodes.Test], + frame: Frame, + extra_kwargs: t.Optional[t.Mapping[str, t.Any]] = None, + ) -> None: + """Writes a function call to the stream for the current node. + A leading comma is added automatically. The extra keyword + arguments may not include python keywords otherwise a syntax + error could occur. The extra keyword arguments should be given + as python dict. + """ + # if any of the given keyword arguments is a python keyword + # we have to make sure that no invalid call is created. + kwarg_workaround = any( + is_python_keyword(t.cast(str, k)) + for k in chain((x.key for x in node.kwargs), extra_kwargs or ()) + ) + + for arg in node.args: + self.write(", ") + self.visit(arg, frame) + + if not kwarg_workaround: + for kwarg in node.kwargs: + self.write(", ") + self.visit(kwarg, frame) + if extra_kwargs is not None: + for key, value in extra_kwargs.items(): + self.write(f", {key}={value}") + if node.dyn_args: + self.write(", *") + self.visit(node.dyn_args, frame) + + if kwarg_workaround: + if node.dyn_kwargs is not None: + self.write(", **dict({") + else: + self.write(", **{") + for kwarg in node.kwargs: + self.write(f"{kwarg.key!r}: ") + self.visit(kwarg.value, frame) + self.write(", ") + if extra_kwargs is not None: + for key, value in extra_kwargs.items(): + self.write(f"{key!r}: {value}, ") + if node.dyn_kwargs is not None: + self.write("}, **") + self.visit(node.dyn_kwargs, frame) + self.write(")") + else: + self.write("}") + + elif node.dyn_kwargs is not None: + self.write(", **") + self.visit(node.dyn_kwargs, frame) + + def pull_dependencies(self, nodes: t.Iterable[nodes.Node]) -> None: + """Find all filter and test names used in the template and + assign them to variables in the compiled namespace. Checking + that the names are registered with the environment is done when + compiling the Filter and Test nodes. If the node is in an If or + CondExpr node, the check is done at runtime instead. + + .. versionchanged:: 3.0 + Filters and tests in If and CondExpr nodes are checked at + runtime instead of compile time. + """ + visitor = DependencyFinderVisitor() + + for node in nodes: + visitor.visit(node) + + for id_map, names, dependency in ( + (self.filters, visitor.filters, "filters"), + ( + self.tests, + visitor.tests, + "tests", + ), + ): + for name in sorted(names): + if name not in id_map: + id_map[name] = self.temporary_identifier() + + # add check during runtime that dependencies used inside of executed + # blocks are defined, as this step may be skipped during compile time + self.writeline("try:") + self.indent() + self.writeline(f"{id_map[name]} = environment.{dependency}[{name!r}]") + self.outdent() + self.writeline("except KeyError:") + self.indent() + self.writeline("@internalcode") + self.writeline(f"def {id_map[name]}(*unused):") + self.indent() + self.writeline( + f'raise TemplateRuntimeError("No {dependency[:-1]}' + f' named {name!r} found.")' + ) + self.outdent() + self.outdent() + + def enter_frame(self, frame: Frame) -> None: + undefs = [] + for target, (action, param) in frame.symbols.loads.items(): + if action == VAR_LOAD_PARAMETER: + pass + elif action == VAR_LOAD_RESOLVE: + self.writeline(f"{target} = {self.get_resolve_func()}({param!r})") + elif action == VAR_LOAD_ALIAS: + self.writeline(f"{target} = {param}") + elif action == VAR_LOAD_UNDEFINED: + undefs.append(target) + else: + raise NotImplementedError("unknown load instruction") + if undefs: + self.writeline(f"{' = '.join(undefs)} = missing") + + def leave_frame(self, frame: Frame, with_python_scope: bool = False) -> None: + if not with_python_scope: + undefs = [] + for target in frame.symbols.loads: + undefs.append(target) + if undefs: + self.writeline(f"{' = '.join(undefs)} = missing") + + def choose_async(self, async_value: str = "async ", sync_value: str = "") -> str: + return async_value if self.environment.is_async else sync_value + + def func(self, name: str) -> str: + return f"{self.choose_async()}def {name}" + + def macro_body( + self, node: t.Union[nodes.Macro, nodes.CallBlock], frame: Frame + ) -> t.Tuple[Frame, MacroRef]: + """Dump the function def of a macro or call block.""" + frame = frame.inner() + frame.symbols.analyze_node(node) + macro_ref = MacroRef(node) + + explicit_caller = None + skip_special_params = set() + args = [] + + for idx, arg in enumerate(node.args): + if arg.name == "caller": + explicit_caller = idx + if arg.name in ("kwargs", "varargs"): + skip_special_params.add(arg.name) + args.append(frame.symbols.ref(arg.name)) + + undeclared = find_undeclared(node.body, ("caller", "kwargs", "varargs")) + + if "caller" in undeclared: + # In older Jinja versions there was a bug that allowed caller + # to retain the special behavior even if it was mentioned in + # the argument list. However thankfully this was only really + # working if it was the last argument. So we are explicitly + # checking this now and error out if it is anywhere else in + # the argument list. + if explicit_caller is not None: + try: + node.defaults[explicit_caller - len(node.args)] + except IndexError: + self.fail( + "When defining macros or call blocks the " + 'special "caller" argument must be omitted ' + "or be given a default.", + node.lineno, + ) + else: + args.append(frame.symbols.declare_parameter("caller")) + macro_ref.accesses_caller = True + if "kwargs" in undeclared and "kwargs" not in skip_special_params: + args.append(frame.symbols.declare_parameter("kwargs")) + macro_ref.accesses_kwargs = True + if "varargs" in undeclared and "varargs" not in skip_special_params: + args.append(frame.symbols.declare_parameter("varargs")) + macro_ref.accesses_varargs = True + + # macros are delayed, they never require output checks + frame.require_output_check = False + frame.symbols.analyze_node(node) + self.writeline(f"{self.func('macro')}({', '.join(args)}):", node) + self.indent() + + self.buffer(frame) + self.enter_frame(frame) + + self.push_parameter_definitions(frame) + for idx, arg in enumerate(node.args): + ref = frame.symbols.ref(arg.name) + self.writeline(f"if {ref} is missing:") + self.indent() + try: + default = node.defaults[idx - len(node.args)] + except IndexError: + self.writeline( + f'{ref} = undefined("parameter {arg.name!r} was not provided",' + f" name={arg.name!r})" + ) + else: + self.writeline(f"{ref} = ") + self.visit(default, frame) + self.mark_parameter_stored(ref) + self.outdent() + self.pop_parameter_definitions() + + self.blockvisit(node.body, frame) + self.return_buffer_contents(frame, force_unescaped=True) + self.leave_frame(frame, with_python_scope=True) + self.outdent() + + return frame, macro_ref + + def macro_def(self, macro_ref: MacroRef, frame: Frame) -> None: + """Dump the macro definition for the def created by macro_body.""" + arg_tuple = ", ".join(repr(x.name) for x in macro_ref.node.args) + name = getattr(macro_ref.node, "name", None) + if len(macro_ref.node.args) == 1: + arg_tuple += "," + self.write( + f"Macro(environment, macro, {name!r}, ({arg_tuple})," + f" {macro_ref.accesses_kwargs!r}, {macro_ref.accesses_varargs!r}," + f" {macro_ref.accesses_caller!r}, context.eval_ctx.autoescape)" + ) + + def position(self, node: nodes.Node) -> str: + """Return a human readable position for the node.""" + rv = f"line {node.lineno}" + if self.name is not None: + rv = f"{rv} in {self.name!r}" + return rv + + def dump_local_context(self, frame: Frame) -> str: + items_kv = ", ".join( + f"{name!r}: {target}" + for name, target in frame.symbols.dump_stores().items() + ) + return f"{{{items_kv}}}" + + def write_commons(self) -> None: + """Writes a common preamble that is used by root and block functions. + Primarily this sets up common local helpers and enforces a generator + through a dead branch. + """ + self.writeline("resolve = context.resolve_or_missing") + self.writeline("undefined = environment.undefined") + self.writeline("concat = environment.concat") + # always use the standard Undefined class for the implicit else of + # conditional expressions + self.writeline("cond_expr_undefined = Undefined") + self.writeline("if 0: yield None") + + def push_parameter_definitions(self, frame: Frame) -> None: + """Pushes all parameter targets from the given frame into a local + stack that permits tracking of yet to be assigned parameters. In + particular this enables the optimization from `visit_Name` to skip + undefined expressions for parameters in macros as macros can reference + otherwise unbound parameters. + """ + self._param_def_block.append(frame.symbols.dump_param_targets()) + + def pop_parameter_definitions(self) -> None: + """Pops the current parameter definitions set.""" + self._param_def_block.pop() + + def mark_parameter_stored(self, target: str) -> None: + """Marks a parameter in the current parameter definitions as stored. + This will skip the enforced undefined checks. + """ + if self._param_def_block: + self._param_def_block[-1].discard(target) + + def push_context_reference(self, target: str) -> None: + self._context_reference_stack.append(target) + + def pop_context_reference(self) -> None: + self._context_reference_stack.pop() + + def get_context_ref(self) -> str: + return self._context_reference_stack[-1] + + def get_resolve_func(self) -> str: + target = self._context_reference_stack[-1] + if target == "context": + return "resolve" + return f"{target}.resolve" + + def derive_context(self, frame: Frame) -> str: + return f"{self.get_context_ref()}.derived({self.dump_local_context(frame)})" + + def parameter_is_undeclared(self, target: str) -> bool: + """Checks if a given target is an undeclared parameter.""" + if not self._param_def_block: + return False + return target in self._param_def_block[-1] + + def push_assign_tracking(self) -> None: + """Pushes a new layer for assignment tracking.""" + self._assign_stack.append(set()) + + def pop_assign_tracking(self, frame: Frame) -> None: + """Pops the topmost level for assignment tracking and updates the + context variables if necessary. + """ + vars = self._assign_stack.pop() + if ( + not frame.block_frame + and not frame.loop_frame + and not frame.toplevel + or not vars + ): + return + public_names = [x for x in vars if x[:1] != "_"] + if len(vars) == 1: + name = next(iter(vars)) + ref = frame.symbols.ref(name) + if frame.loop_frame: + self.writeline(f"_loop_vars[{name!r}] = {ref}") + return + if frame.block_frame: + self.writeline(f"_block_vars[{name!r}] = {ref}") + return + self.writeline(f"context.vars[{name!r}] = {ref}") + else: + if frame.loop_frame: + self.writeline("_loop_vars.update({") + elif frame.block_frame: + self.writeline("_block_vars.update({") + else: + self.writeline("context.vars.update({") + for idx, name in enumerate(sorted(vars)): + if idx: + self.write(", ") + ref = frame.symbols.ref(name) + self.write(f"{name!r}: {ref}") + self.write("})") + if not frame.block_frame and not frame.loop_frame and public_names: + if len(public_names) == 1: + self.writeline(f"context.exported_vars.add({public_names[0]!r})") + else: + names_str = ", ".join(map(repr, sorted(public_names))) + self.writeline(f"context.exported_vars.update(({names_str}))") + + # -- Statement Visitors + + def visit_Template( + self, node: nodes.Template, frame: t.Optional[Frame] = None + ) -> None: + assert frame is None, "no root frame allowed" + eval_ctx = EvalContext(self.environment, self.name) + + from .runtime import async_exported + from .runtime import exported + + if self.environment.is_async: + exported_names = sorted(exported + async_exported) + else: + exported_names = sorted(exported) + + self.writeline("from jinja2.runtime import " + ", ".join(exported_names)) + + # if we want a deferred initialization we cannot move the + # environment into a local name + envenv = "" if self.defer_init else ", environment=environment" + + # do we have an extends tag at all? If not, we can save some + # overhead by just not processing any inheritance code. + have_extends = node.find(nodes.Extends) is not None + + # find all blocks + for block in node.find_all(nodes.Block): + if block.name in self.blocks: + self.fail(f"block {block.name!r} defined twice", block.lineno) + self.blocks[block.name] = block + + # find all imports and import them + for import_ in node.find_all(nodes.ImportedName): + if import_.importname not in self.import_aliases: + imp = import_.importname + self.import_aliases[imp] = alias = self.temporary_identifier() + if "." in imp: + module, obj = imp.rsplit(".", 1) + self.writeline(f"from {module} import {obj} as {alias}") + else: + self.writeline(f"import {imp} as {alias}") + + # add the load name + self.writeline(f"name = {self.name!r}") + + # generate the root render function. + self.writeline( + f"{self.func('root')}(context, missing=missing{envenv}):", extra=1 + ) + self.indent() + self.write_commons() + + # process the root + frame = Frame(eval_ctx) + if "self" in find_undeclared(node.body, ("self",)): + ref = frame.symbols.declare_parameter("self") + self.writeline(f"{ref} = TemplateReference(context)") + frame.symbols.analyze_node(node) + frame.toplevel = frame.rootlevel = True + frame.require_output_check = have_extends and not self.has_known_extends + if have_extends: + self.writeline("parent_template = None") + self.enter_frame(frame) + self.pull_dependencies(node.body) + self.blockvisit(node.body, frame) + self.leave_frame(frame, with_python_scope=True) + self.outdent() + + # make sure that the parent root is called. + if have_extends: + if not self.has_known_extends: + self.indent() + self.writeline("if parent_template is not None:") + self.indent() + if not self.environment.is_async: + self.writeline("yield from parent_template.root_render_func(context)") + else: + self.writeline("agen = parent_template.root_render_func(context)") + self.writeline("try:") + self.indent() + self.writeline("async for event in agen:") + self.indent() + self.writeline("yield event") + self.outdent() + self.outdent() + self.writeline("finally: await agen.aclose()") + self.outdent(1 + (not self.has_known_extends)) + + # at this point we now have the blocks collected and can visit them too. + for name, block in self.blocks.items(): + self.writeline( + f"{self.func('block_' + name)}(context, missing=missing{envenv}):", + block, + 1, + ) + self.indent() + self.write_commons() + # It's important that we do not make this frame a child of the + # toplevel template. This would cause a variety of + # interesting issues with identifier tracking. + block_frame = Frame(eval_ctx) + block_frame.block_frame = True + undeclared = find_undeclared(block.body, ("self", "super")) + if "self" in undeclared: + ref = block_frame.symbols.declare_parameter("self") + self.writeline(f"{ref} = TemplateReference(context)") + if "super" in undeclared: + ref = block_frame.symbols.declare_parameter("super") + self.writeline(f"{ref} = context.super({name!r}, block_{name})") + block_frame.symbols.analyze_node(block) + block_frame.block = name + self.writeline("_block_vars = {}") + self.enter_frame(block_frame) + self.pull_dependencies(block.body) + self.blockvisit(block.body, block_frame) + self.leave_frame(block_frame, with_python_scope=True) + self.outdent() + + blocks_kv_str = ", ".join(f"{x!r}: block_{x}" for x in self.blocks) + self.writeline(f"blocks = {{{blocks_kv_str}}}", extra=1) + debug_kv_str = "&".join(f"{k}={v}" for k, v in self.debug_info) + self.writeline(f"debug_info = {debug_kv_str!r}") + + def visit_Block(self, node: nodes.Block, frame: Frame) -> None: + """Call a block and register it for the template.""" + level = 0 + if frame.toplevel: + # if we know that we are a child template, there is no need to + # check if we are one + if self.has_known_extends: + return + if self.extends_so_far > 0: + self.writeline("if parent_template is None:") + self.indent() + level += 1 + + if node.scoped: + context = self.derive_context(frame) + else: + context = self.get_context_ref() + + if node.required: + self.writeline(f"if len(context.blocks[{node.name!r}]) <= 1:", node) + self.indent() + self.writeline( + f'raise TemplateRuntimeError("Required block {node.name!r} not found")', + node, + ) + self.outdent() + + if not self.environment.is_async and frame.buffer is None: + self.writeline( + f"yield from context.blocks[{node.name!r}][0]({context})", node + ) + else: + self.writeline(f"gen = context.blocks[{node.name!r}][0]({context})") + self.writeline("try:") + self.indent() + self.writeline( + f"{self.choose_async()}for event in gen:", + node, + ) + self.indent() + self.simple_write("event", frame) + self.outdent() + self.outdent() + self.writeline( + f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}" + ) + + self.outdent(level) + + def visit_Extends(self, node: nodes.Extends, frame: Frame) -> None: + """Calls the extender.""" + if not frame.toplevel: + self.fail("cannot use extend from a non top-level scope", node.lineno) + + # if the number of extends statements in general is zero so + # far, we don't have to add a check if something extended + # the template before this one. + if self.extends_so_far > 0: + # if we have a known extends we just add a template runtime + # error into the generated code. We could catch that at compile + # time too, but i welcome it not to confuse users by throwing the + # same error at different times just "because we can". + if not self.has_known_extends: + self.writeline("if parent_template is not None:") + self.indent() + self.writeline('raise TemplateRuntimeError("extended multiple times")') + + # if we have a known extends already we don't need that code here + # as we know that the template execution will end here. + if self.has_known_extends: + raise CompilerExit() + else: + self.outdent() + + self.writeline("parent_template = environment.get_template(", node) + self.visit(node.template, frame) + self.write(f", {self.name!r})") + self.writeline("for name, parent_block in parent_template.blocks.items():") + self.indent() + self.writeline("context.blocks.setdefault(name, []).append(parent_block)") + self.outdent() + + # if this extends statement was in the root level we can take + # advantage of that information and simplify the generated code + # in the top level from this point onwards + if frame.rootlevel: + self.has_known_extends = True + + # and now we have one more + self.extends_so_far += 1 + + def visit_Include(self, node: nodes.Include, frame: Frame) -> None: + """Handles includes.""" + if node.ignore_missing: + self.writeline("try:") + self.indent() + + func_name = "get_or_select_template" + if isinstance(node.template, nodes.Const): + if isinstance(node.template.value, str): + func_name = "get_template" + elif isinstance(node.template.value, (tuple, list)): + func_name = "select_template" + elif isinstance(node.template, (nodes.Tuple, nodes.List)): + func_name = "select_template" + + self.writeline(f"template = environment.{func_name}(", node) + self.visit(node.template, frame) + self.write(f", {self.name!r})") + if node.ignore_missing: + self.outdent() + self.writeline("except TemplateNotFound:") + self.indent() + self.writeline("pass") + self.outdent() + self.writeline("else:") + self.indent() + + def loop_body() -> None: + self.indent() + self.simple_write("event", frame) + self.outdent() + + if node.with_context: + self.writeline( + f"gen = template.root_render_func(" + "template.new_context(context.get_all(), True," + f" {self.dump_local_context(frame)}))" + ) + self.writeline("try:") + self.indent() + self.writeline(f"{self.choose_async()}for event in gen:") + loop_body() + self.outdent() + self.writeline( + f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}" + ) + elif self.environment.is_async: + self.writeline( + "for event in (await template._get_default_module_async())" + "._body_stream:" + ) + loop_body() + else: + self.writeline("yield from template._get_default_module()._body_stream") + + if node.ignore_missing: + self.outdent() + + def _import_common( + self, node: t.Union[nodes.Import, nodes.FromImport], frame: Frame + ) -> None: + self.write(f"{self.choose_async('await ')}environment.get_template(") + self.visit(node.template, frame) + self.write(f", {self.name!r}).") + + if node.with_context: + f_name = f"make_module{self.choose_async('_async')}" + self.write( + f"{f_name}(context.get_all(), True, {self.dump_local_context(frame)})" + ) + else: + self.write(f"_get_default_module{self.choose_async('_async')}(context)") + + def visit_Import(self, node: nodes.Import, frame: Frame) -> None: + """Visit regular imports.""" + self.writeline(f"{frame.symbols.ref(node.target)} = ", node) + if frame.toplevel: + self.write(f"context.vars[{node.target!r}] = ") + + self._import_common(node, frame) + + if frame.toplevel and not node.target.startswith("_"): + self.writeline(f"context.exported_vars.discard({node.target!r})") + + def visit_FromImport(self, node: nodes.FromImport, frame: Frame) -> None: + """Visit named imports.""" + self.newline(node) + self.write("included_template = ") + self._import_common(node, frame) + var_names = [] + discarded_names = [] + for name in node.names: + if isinstance(name, tuple): + name, alias = name + else: + alias = name + self.writeline( + f"{frame.symbols.ref(alias)} =" + f" getattr(included_template, {name!r}, missing)" + ) + self.writeline(f"if {frame.symbols.ref(alias)} is missing:") + self.indent() + # The position will contain the template name, and will be formatted + # into a string that will be compiled into an f-string. Curly braces + # in the name must be replaced with escapes so that they will not be + # executed as part of the f-string. + position = self.position(node).replace("{", "{{").replace("}", "}}") + message = ( + "the template {included_template.__name__!r}" + f" (imported on {position})" + f" does not export the requested name {name!r}" + ) + self.writeline( + f"{frame.symbols.ref(alias)} = undefined(f{message!r}, name={name!r})" + ) + self.outdent() + if frame.toplevel: + var_names.append(alias) + if not alias.startswith("_"): + discarded_names.append(alias) + + if var_names: + if len(var_names) == 1: + name = var_names[0] + self.writeline(f"context.vars[{name!r}] = {frame.symbols.ref(name)}") + else: + names_kv = ", ".join( + f"{name!r}: {frame.symbols.ref(name)}" for name in var_names + ) + self.writeline(f"context.vars.update({{{names_kv}}})") + if discarded_names: + if len(discarded_names) == 1: + self.writeline(f"context.exported_vars.discard({discarded_names[0]!r})") + else: + names_str = ", ".join(map(repr, discarded_names)) + self.writeline( + f"context.exported_vars.difference_update(({names_str}))" + ) + + def visit_For(self, node: nodes.For, frame: Frame) -> None: + loop_frame = frame.inner() + loop_frame.loop_frame = True + test_frame = frame.inner() + else_frame = frame.inner() + + # try to figure out if we have an extended loop. An extended loop + # is necessary if the loop is in recursive mode if the special loop + # variable is accessed in the body if the body is a scoped block. + extended_loop = ( + node.recursive + or "loop" + in find_undeclared(node.iter_child_nodes(only=("body",)), ("loop",)) + or any(block.scoped for block in node.find_all(nodes.Block)) + ) + + loop_ref = None + if extended_loop: + loop_ref = loop_frame.symbols.declare_parameter("loop") + + loop_frame.symbols.analyze_node(node, for_branch="body") + if node.else_: + else_frame.symbols.analyze_node(node, for_branch="else") + + if node.test: + loop_filter_func = self.temporary_identifier() + test_frame.symbols.analyze_node(node, for_branch="test") + self.writeline(f"{self.func(loop_filter_func)}(fiter):", node.test) + self.indent() + self.enter_frame(test_frame) + self.writeline(self.choose_async("async for ", "for ")) + self.visit(node.target, loop_frame) + self.write(" in ") + self.write(self.choose_async("auto_aiter(fiter)", "fiter")) + self.write(":") + self.indent() + self.writeline("if ", node.test) + self.visit(node.test, test_frame) + self.write(":") + self.indent() + self.writeline("yield ") + self.visit(node.target, loop_frame) + self.outdent(3) + self.leave_frame(test_frame, with_python_scope=True) + + # if we don't have an recursive loop we have to find the shadowed + # variables at that point. Because loops can be nested but the loop + # variable is a special one we have to enforce aliasing for it. + if node.recursive: + self.writeline( + f"{self.func('loop')}(reciter, loop_render_func, depth=0):", node + ) + self.indent() + self.buffer(loop_frame) + + # Use the same buffer for the else frame + else_frame.buffer = loop_frame.buffer + + # make sure the loop variable is a special one and raise a template + # assertion error if a loop tries to write to loop + if extended_loop: + self.writeline(f"{loop_ref} = missing") + + for name in node.find_all(nodes.Name): + if name.ctx == "store" and name.name == "loop": + self.fail( + "Can't assign to special loop variable in for-loop target", + name.lineno, + ) + + if node.else_: + iteration_indicator = self.temporary_identifier() + self.writeline(f"{iteration_indicator} = 1") + + self.writeline(self.choose_async("async for ", "for "), node) + self.visit(node.target, loop_frame) + if extended_loop: + self.write(f", {loop_ref} in {self.choose_async('Async')}LoopContext(") + else: + self.write(" in ") + + if node.test: + self.write(f"{loop_filter_func}(") + if node.recursive: + self.write("reciter") + else: + if self.environment.is_async and not extended_loop: + self.write("auto_aiter(") + self.visit(node.iter, frame) + if self.environment.is_async and not extended_loop: + self.write(")") + if node.test: + self.write(")") + + if node.recursive: + self.write(", undefined, loop_render_func, depth):") + else: + self.write(", undefined):" if extended_loop else ":") + + self.indent() + self.enter_frame(loop_frame) + + self.writeline("_loop_vars = {}") + self.blockvisit(node.body, loop_frame) + if node.else_: + self.writeline(f"{iteration_indicator} = 0") + self.outdent() + self.leave_frame( + loop_frame, with_python_scope=node.recursive and not node.else_ + ) + + if node.else_: + self.writeline(f"if {iteration_indicator}:") + self.indent() + self.enter_frame(else_frame) + self.blockvisit(node.else_, else_frame) + self.leave_frame(else_frame) + self.outdent() + + # if the node was recursive we have to return the buffer contents + # and start the iteration code + if node.recursive: + self.return_buffer_contents(loop_frame) + self.outdent() + self.start_write(frame, node) + self.write(f"{self.choose_async('await ')}loop(") + if self.environment.is_async: + self.write("auto_aiter(") + self.visit(node.iter, frame) + if self.environment.is_async: + self.write(")") + self.write(", loop)") + self.end_write(frame) + + # at the end of the iteration, clear any assignments made in the + # loop from the top level + if self._assign_stack: + self._assign_stack[-1].difference_update(loop_frame.symbols.stores) + + def visit_If(self, node: nodes.If, frame: Frame) -> None: + if_frame = frame.soft() + self.writeline("if ", node) + self.visit(node.test, if_frame) + self.write(":") + self.indent() + self.blockvisit(node.body, if_frame) + self.outdent() + for elif_ in node.elif_: + self.writeline("elif ", elif_) + self.visit(elif_.test, if_frame) + self.write(":") + self.indent() + self.blockvisit(elif_.body, if_frame) + self.outdent() + if node.else_: + self.writeline("else:") + self.indent() + self.blockvisit(node.else_, if_frame) + self.outdent() + + def visit_Macro(self, node: nodes.Macro, frame: Frame) -> None: + macro_frame, macro_ref = self.macro_body(node, frame) + self.newline() + if frame.toplevel: + if not node.name.startswith("_"): + self.write(f"context.exported_vars.add({node.name!r})") + self.writeline(f"context.vars[{node.name!r}] = ") + self.write(f"{frame.symbols.ref(node.name)} = ") + self.macro_def(macro_ref, macro_frame) + + def visit_CallBlock(self, node: nodes.CallBlock, frame: Frame) -> None: + call_frame, macro_ref = self.macro_body(node, frame) + self.writeline("caller = ") + self.macro_def(macro_ref, call_frame) + self.start_write(frame, node) + self.visit_Call(node.call, frame, forward_caller=True) + self.end_write(frame) + + def visit_FilterBlock(self, node: nodes.FilterBlock, frame: Frame) -> None: + filter_frame = frame.inner() + filter_frame.symbols.analyze_node(node) + self.enter_frame(filter_frame) + self.buffer(filter_frame) + self.blockvisit(node.body, filter_frame) + self.start_write(frame, node) + self.visit_Filter(node.filter, filter_frame) + self.end_write(frame) + self.leave_frame(filter_frame) + + def visit_With(self, node: nodes.With, frame: Frame) -> None: + with_frame = frame.inner() + with_frame.symbols.analyze_node(node) + self.enter_frame(with_frame) + for target, expr in zip(node.targets, node.values): + self.newline() + self.visit(target, with_frame) + self.write(" = ") + self.visit(expr, frame) + self.blockvisit(node.body, with_frame) + self.leave_frame(with_frame) + + def visit_ExprStmt(self, node: nodes.ExprStmt, frame: Frame) -> None: + self.newline(node) + self.visit(node.node, frame) + + class _FinalizeInfo(t.NamedTuple): + const: t.Optional[t.Callable[..., str]] + src: t.Optional[str] + + @staticmethod + def _default_finalize(value: t.Any) -> t.Any: + """The default finalize function if the environment isn't + configured with one. Or, if the environment has one, this is + called on that function's output for constants. + """ + return str(value) + + _finalize: t.Optional[_FinalizeInfo] = None + + def _make_finalize(self) -> _FinalizeInfo: + """Build the finalize function to be used on constants and at + runtime. Cached so it's only created once for all output nodes. + + Returns a ``namedtuple`` with the following attributes: + + ``const`` + A function to finalize constant data at compile time. + + ``src`` + Source code to output around nodes to be evaluated at + runtime. + """ + if self._finalize is not None: + return self._finalize + + finalize: t.Optional[t.Callable[..., t.Any]] + finalize = default = self._default_finalize + src = None + + if self.environment.finalize: + src = "environment.finalize(" + env_finalize = self.environment.finalize + pass_arg = { + _PassArg.context: "context", + _PassArg.eval_context: "context.eval_ctx", + _PassArg.environment: "environment", + }.get( + _PassArg.from_obj(env_finalize) # type: ignore + ) + finalize = None + + if pass_arg is None: + + def finalize(value: t.Any) -> t.Any: # noqa: F811 + return default(env_finalize(value)) + + else: + src = f"{src}{pass_arg}, " + + if pass_arg == "environment": + + def finalize(value: t.Any) -> t.Any: # noqa: F811 + return default(env_finalize(self.environment, value)) + + self._finalize = self._FinalizeInfo(finalize, src) + return self._finalize + + def _output_const_repr(self, group: t.Iterable[t.Any]) -> str: + """Given a group of constant values converted from ``Output`` + child nodes, produce a string to write to the template module + source. + """ + return repr(concat(group)) + + def _output_child_to_const( + self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo + ) -> str: + """Try to optimize a child of an ``Output`` node by trying to + convert it to constant, finalized data at compile time. + + If :exc:`Impossible` is raised, the node is not constant and + will be evaluated at runtime. Any other exception will also be + evaluated at runtime for easier debugging. + """ + const = node.as_const(frame.eval_ctx) + + if frame.eval_ctx.autoescape: + const = escape(const) + + # Template data doesn't go through finalize. + if isinstance(node, nodes.TemplateData): + return str(const) + + return finalize.const(const) # type: ignore + + def _output_child_pre( + self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo + ) -> None: + """Output extra source code before visiting a child of an + ``Output`` node. + """ + if frame.eval_ctx.volatile: + self.write("(escape if context.eval_ctx.autoescape else str)(") + elif frame.eval_ctx.autoescape: + self.write("escape(") + else: + self.write("str(") + + if finalize.src is not None: + self.write(finalize.src) + + def _output_child_post( + self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo + ) -> None: + """Output extra source code after visiting a child of an + ``Output`` node. + """ + self.write(")") + + if finalize.src is not None: + self.write(")") + + def visit_Output(self, node: nodes.Output, frame: Frame) -> None: + # If an extends is active, don't render outside a block. + if frame.require_output_check: + # A top-level extends is known to exist at compile time. + if self.has_known_extends: + return + + self.writeline("if parent_template is None:") + self.indent() + + finalize = self._make_finalize() + body: t.List[t.Union[t.List[t.Any], nodes.Expr]] = [] + + # Evaluate constants at compile time if possible. Each item in + # body will be either a list of static data or a node to be + # evaluated at runtime. + for child in node.nodes: + try: + if not ( + # If the finalize function requires runtime context, + # constants can't be evaluated at compile time. + finalize.const + # Unless it's basic template data that won't be + # finalized anyway. + or isinstance(child, nodes.TemplateData) + ): + raise nodes.Impossible() + + const = self._output_child_to_const(child, frame, finalize) + except (nodes.Impossible, Exception): + # The node was not constant and needs to be evaluated at + # runtime. Or another error was raised, which is easier + # to debug at runtime. + body.append(child) + continue + + if body and isinstance(body[-1], list): + body[-1].append(const) + else: + body.append([const]) + + if frame.buffer is not None: + if len(body) == 1: + self.writeline(f"{frame.buffer}.append(") + else: + self.writeline(f"{frame.buffer}.extend((") + + self.indent() + + for item in body: + if isinstance(item, list): + # A group of constant data to join and output. + val = self._output_const_repr(item) + + if frame.buffer is None: + self.writeline("yield " + val) + else: + self.writeline(val + ",") + else: + if frame.buffer is None: + self.writeline("yield ", item) + else: + self.newline(item) + + # A node to be evaluated at runtime. + self._output_child_pre(item, frame, finalize) + self.visit(item, frame) + self._output_child_post(item, frame, finalize) + + if frame.buffer is not None: + self.write(",") + + if frame.buffer is not None: + self.outdent() + self.writeline(")" if len(body) == 1 else "))") + + if frame.require_output_check: + self.outdent() + + def visit_Assign(self, node: nodes.Assign, frame: Frame) -> None: + self.push_assign_tracking() + + # ``a.b`` is allowed for assignment, and is parsed as an NSRef. However, + # it is only valid if it references a Namespace object. Emit a check for + # that for each ref here, before assignment code is emitted. This can't + # be done in visit_NSRef as the ref could be in the middle of a tuple. + seen_refs: t.Set[str] = set() + + for nsref in node.find_all(nodes.NSRef): + if nsref.name in seen_refs: + # Only emit the check for each reference once, in case the same + # ref is used multiple times in a tuple, `ns.a, ns.b = c, d`. + continue + + seen_refs.add(nsref.name) + ref = frame.symbols.ref(nsref.name) + self.writeline(f"if not isinstance({ref}, Namespace):") + self.indent() + self.writeline( + "raise TemplateRuntimeError" + '("cannot assign attribute on non-namespace object")' + ) + self.outdent() + + self.newline(node) + self.visit(node.target, frame) + self.write(" = ") + self.visit(node.node, frame) + self.pop_assign_tracking(frame) + + def visit_AssignBlock(self, node: nodes.AssignBlock, frame: Frame) -> None: + self.push_assign_tracking() + block_frame = frame.inner() + # This is a special case. Since a set block always captures we + # will disable output checks. This way one can use set blocks + # toplevel even in extended templates. + block_frame.require_output_check = False + block_frame.symbols.analyze_node(node) + self.enter_frame(block_frame) + self.buffer(block_frame) + self.blockvisit(node.body, block_frame) + self.newline(node) + self.visit(node.target, frame) + self.write(" = (Markup if context.eval_ctx.autoescape else identity)(") + if node.filter is not None: + self.visit_Filter(node.filter, block_frame) + else: + self.write(f"concat({block_frame.buffer})") + self.write(")") + self.pop_assign_tracking(frame) + self.leave_frame(block_frame) + + # -- Expression Visitors + + def visit_Name(self, node: nodes.Name, frame: Frame) -> None: + if node.ctx == "store" and ( + frame.toplevel or frame.loop_frame or frame.block_frame + ): + if self._assign_stack: + self._assign_stack[-1].add(node.name) + ref = frame.symbols.ref(node.name) + + # If we are looking up a variable we might have to deal with the + # case where it's undefined. We can skip that case if the load + # instruction indicates a parameter which are always defined. + if node.ctx == "load": + load = frame.symbols.find_load(ref) + if not ( + load is not None + and load[0] == VAR_LOAD_PARAMETER + and not self.parameter_is_undeclared(ref) + ): + self.write( + f"(undefined(name={node.name!r}) if {ref} is missing else {ref})" + ) + return + + self.write(ref) + + def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None: + # NSRef is a dotted assignment target a.b=c, but uses a[b]=c internally. + # visit_Assign emits code to validate that each ref is to a Namespace + # object only. That can't be emitted here as the ref could be in the + # middle of a tuple assignment. + ref = frame.symbols.ref(node.name) + self.writeline(f"{ref}[{node.attr!r}]") + + def visit_Const(self, node: nodes.Const, frame: Frame) -> None: + val = node.as_const(frame.eval_ctx) + if isinstance(val, float): + self.write(str(val)) + else: + self.write(repr(val)) + + def visit_TemplateData(self, node: nodes.TemplateData, frame: Frame) -> None: + try: + self.write(repr(node.as_const(frame.eval_ctx))) + except nodes.Impossible: + self.write( + f"(Markup if context.eval_ctx.autoescape else identity)({node.data!r})" + ) + + def visit_Tuple(self, node: nodes.Tuple, frame: Frame) -> None: + self.write("(") + idx = -1 + for idx, item in enumerate(node.items): + if idx: + self.write(", ") + self.visit(item, frame) + self.write(",)" if idx == 0 else ")") + + def visit_List(self, node: nodes.List, frame: Frame) -> None: + self.write("[") + for idx, item in enumerate(node.items): + if idx: + self.write(", ") + self.visit(item, frame) + self.write("]") + + def visit_Dict(self, node: nodes.Dict, frame: Frame) -> None: + self.write("{") + for idx, item in enumerate(node.items): + if idx: + self.write(", ") + self.visit(item.key, frame) + self.write(": ") + self.visit(item.value, frame) + self.write("}") + + visit_Add = _make_binop("+") + visit_Sub = _make_binop("-") + visit_Mul = _make_binop("*") + visit_Div = _make_binop("/") + visit_FloorDiv = _make_binop("//") + visit_Pow = _make_binop("**") + visit_Mod = _make_binop("%") + visit_And = _make_binop("and") + visit_Or = _make_binop("or") + visit_Pos = _make_unop("+") + visit_Neg = _make_unop("-") + visit_Not = _make_unop("not ") + + @optimizeconst + def visit_Concat(self, node: nodes.Concat, frame: Frame) -> None: + if frame.eval_ctx.volatile: + func_name = "(markup_join if context.eval_ctx.volatile else str_join)" + elif frame.eval_ctx.autoescape: + func_name = "markup_join" + else: + func_name = "str_join" + self.write(f"{func_name}((") + for arg in node.nodes: + self.visit(arg, frame) + self.write(", ") + self.write("))") + + @optimizeconst + def visit_Compare(self, node: nodes.Compare, frame: Frame) -> None: + self.write("(") + self.visit(node.expr, frame) + for op in node.ops: + self.visit(op, frame) + self.write(")") + + def visit_Operand(self, node: nodes.Operand, frame: Frame) -> None: + self.write(f" {operators[node.op]} ") + self.visit(node.expr, frame) + + @optimizeconst + def visit_Getattr(self, node: nodes.Getattr, frame: Frame) -> None: + if self.environment.is_async: + self.write("(await auto_await(") + + self.write("environment.getattr(") + self.visit(node.node, frame) + self.write(f", {node.attr!r})") + + if self.environment.is_async: + self.write("))") + + @optimizeconst + def visit_Getitem(self, node: nodes.Getitem, frame: Frame) -> None: + # slices bypass the environment getitem method. + if isinstance(node.arg, nodes.Slice): + self.visit(node.node, frame) + self.write("[") + self.visit(node.arg, frame) + self.write("]") + else: + if self.environment.is_async: + self.write("(await auto_await(") + + self.write("environment.getitem(") + self.visit(node.node, frame) + self.write(", ") + self.visit(node.arg, frame) + self.write(")") + + if self.environment.is_async: + self.write("))") + + def visit_Slice(self, node: nodes.Slice, frame: Frame) -> None: + if node.start is not None: + self.visit(node.start, frame) + self.write(":") + if node.stop is not None: + self.visit(node.stop, frame) + if node.step is not None: + self.write(":") + self.visit(node.step, frame) + + @contextmanager + def _filter_test_common( + self, node: t.Union[nodes.Filter, nodes.Test], frame: Frame, is_filter: bool + ) -> t.Iterator[None]: + if self.environment.is_async: + self.write("(await auto_await(") + + if is_filter: + self.write(f"{self.filters[node.name]}(") + func = self.environment.filters.get(node.name) + else: + self.write(f"{self.tests[node.name]}(") + func = self.environment.tests.get(node.name) + + # When inside an If or CondExpr frame, allow the filter to be + # undefined at compile time and only raise an error if it's + # actually called at runtime. See pull_dependencies. + if func is None and not frame.soft_frame: + type_name = "filter" if is_filter else "test" + self.fail(f"No {type_name} named {node.name!r}.", node.lineno) + + pass_arg = { + _PassArg.context: "context", + _PassArg.eval_context: "context.eval_ctx", + _PassArg.environment: "environment", + }.get( + _PassArg.from_obj(func) # type: ignore + ) + + if pass_arg is not None: + self.write(f"{pass_arg}, ") + + # Back to the visitor function to handle visiting the target of + # the filter or test. + yield + + self.signature(node, frame) + self.write(")") + + if self.environment.is_async: + self.write("))") + + @optimizeconst + def visit_Filter(self, node: nodes.Filter, frame: Frame) -> None: + with self._filter_test_common(node, frame, True): + # if the filter node is None we are inside a filter block + # and want to write to the current buffer + if node.node is not None: + self.visit(node.node, frame) + elif frame.eval_ctx.volatile: + self.write( + f"(Markup(concat({frame.buffer}))" + f" if context.eval_ctx.autoescape else concat({frame.buffer}))" + ) + elif frame.eval_ctx.autoescape: + self.write(f"Markup(concat({frame.buffer}))") + else: + self.write(f"concat({frame.buffer})") + + @optimizeconst + def visit_Test(self, node: nodes.Test, frame: Frame) -> None: + with self._filter_test_common(node, frame, False): + self.visit(node.node, frame) + + @optimizeconst + def visit_CondExpr(self, node: nodes.CondExpr, frame: Frame) -> None: + frame = frame.soft() + + def write_expr2() -> None: + if node.expr2 is not None: + self.visit(node.expr2, frame) + return + + self.write( + f'cond_expr_undefined("the inline if-expression on' + f" {self.position(node)} evaluated to false and no else" + f' section was defined.")' + ) + + self.write("(") + self.visit(node.expr1, frame) + self.write(" if ") + self.visit(node.test, frame) + self.write(" else ") + write_expr2() + self.write(")") + + @optimizeconst + def visit_Call( + self, node: nodes.Call, frame: Frame, forward_caller: bool = False + ) -> None: + if self.environment.is_async: + self.write("(await auto_await(") + if self.environment.sandboxed: + self.write("environment.call(context, ") + else: + self.write("context.call(") + self.visit(node.node, frame) + extra_kwargs = {"caller": "caller"} if forward_caller else None + loop_kwargs = {"_loop_vars": "_loop_vars"} if frame.loop_frame else {} + block_kwargs = {"_block_vars": "_block_vars"} if frame.block_frame else {} + if extra_kwargs: + extra_kwargs.update(loop_kwargs, **block_kwargs) + elif loop_kwargs or block_kwargs: + extra_kwargs = dict(loop_kwargs, **block_kwargs) + self.signature(node, frame, extra_kwargs) + self.write(")") + if self.environment.is_async: + self.write("))") + + def visit_Keyword(self, node: nodes.Keyword, frame: Frame) -> None: + self.write(node.key + "=") + self.visit(node.value, frame) + + # -- Unused nodes for extensions + + def visit_MarkSafe(self, node: nodes.MarkSafe, frame: Frame) -> None: + self.write("Markup(") + self.visit(node.expr, frame) + self.write(")") + + def visit_MarkSafeIfAutoescape( + self, node: nodes.MarkSafeIfAutoescape, frame: Frame + ) -> None: + self.write("(Markup if context.eval_ctx.autoescape else identity)(") + self.visit(node.expr, frame) + self.write(")") + + def visit_EnvironmentAttribute( + self, node: nodes.EnvironmentAttribute, frame: Frame + ) -> None: + self.write("environment." + node.name) + + def visit_ExtensionAttribute( + self, node: nodes.ExtensionAttribute, frame: Frame + ) -> None: + self.write(f"environment.extensions[{node.identifier!r}].{node.name}") + + def visit_ImportedName(self, node: nodes.ImportedName, frame: Frame) -> None: + self.write(self.import_aliases[node.importname]) + + def visit_InternalName(self, node: nodes.InternalName, frame: Frame) -> None: + self.write(node.name) + + def visit_ContextReference( + self, node: nodes.ContextReference, frame: Frame + ) -> None: + self.write("context") + + def visit_DerivedContextReference( + self, node: nodes.DerivedContextReference, frame: Frame + ) -> None: + self.write(self.derive_context(frame)) + + def visit_Continue(self, node: nodes.Continue, frame: Frame) -> None: + self.writeline("continue", node) + + def visit_Break(self, node: nodes.Break, frame: Frame) -> None: + self.writeline("break", node) + + def visit_Scope(self, node: nodes.Scope, frame: Frame) -> None: + scope_frame = frame.inner() + scope_frame.symbols.analyze_node(node) + self.enter_frame(scope_frame) + self.blockvisit(node.body, scope_frame) + self.leave_frame(scope_frame) + + def visit_OverlayScope(self, node: nodes.OverlayScope, frame: Frame) -> None: + ctx = self.temporary_identifier() + self.writeline(f"{ctx} = {self.derive_context(frame)}") + self.writeline(f"{ctx}.vars = ") + self.visit(node.context, frame) + self.push_context_reference(ctx) + + scope_frame = frame.inner(isolated=True) + scope_frame.symbols.analyze_node(node) + self.enter_frame(scope_frame) + self.blockvisit(node.body, scope_frame) + self.leave_frame(scope_frame) + self.pop_context_reference() + + def visit_EvalContextModifier( + self, node: nodes.EvalContextModifier, frame: Frame + ) -> None: + for keyword in node.options: + self.writeline(f"context.eval_ctx.{keyword.key} = ") + self.visit(keyword.value, frame) + try: + val = keyword.value.as_const(frame.eval_ctx) + except nodes.Impossible: + frame.eval_ctx.volatile = True + else: + setattr(frame.eval_ctx, keyword.key, val) + + def visit_ScopedEvalContextModifier( + self, node: nodes.ScopedEvalContextModifier, frame: Frame + ) -> None: + old_ctx_name = self.temporary_identifier() + saved_ctx = frame.eval_ctx.save() + self.writeline(f"{old_ctx_name} = context.eval_ctx.save()") + self.visit_EvalContextModifier(node, frame) + for child in node.body: + self.visit(child, frame) + frame.eval_ctx.revert(saved_ctx) + self.writeline(f"context.eval_ctx.revert({old_ctx_name})") diff --git a/venv/lib/python3.10/site-packages/jinja2/constants.py b/venv/lib/python3.10/site-packages/jinja2/constants.py new file mode 100644 index 0000000..41a1c23 --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2/constants.py @@ -0,0 +1,20 @@ +#: list of lorem ipsum words used by the lipsum() helper function +LOREM_IPSUM_WORDS = """\ +a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at +auctor augue bibendum blandit class commodo condimentum congue consectetuer +consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus +diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend +elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames +faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac +hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum +justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem +luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie +mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non +nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque +penatibus per pharetra phasellus placerat platea porta porttitor posuere +potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus +ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit +sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor +tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices +ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus +viverra volutpat vulputate""" diff --git a/venv/lib/python3.10/site-packages/jinja2/debug.py b/venv/lib/python3.10/site-packages/jinja2/debug.py new file mode 100644 index 0000000..eeeeee7 --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2/debug.py @@ -0,0 +1,191 @@ +import sys +import typing as t +from types import CodeType +from types import TracebackType + +from .exceptions import TemplateSyntaxError +from .utils import internal_code +from .utils import missing + +if t.TYPE_CHECKING: + from .runtime import Context + + +def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException: + """Rewrite the current exception to replace any tracebacks from + within compiled template code with tracebacks that look like they + came from the template source. + + This must be called within an ``except`` block. + + :param source: For ``TemplateSyntaxError``, the original source if + known. + :return: The original exception with the rewritten traceback. + """ + _, exc_value, tb = sys.exc_info() + exc_value = t.cast(BaseException, exc_value) + tb = t.cast(TracebackType, tb) + + if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated: + exc_value.translated = True + exc_value.source = source + # Remove the old traceback, otherwise the frames from the + # compiler still show up. + exc_value.with_traceback(None) + # Outside of runtime, so the frame isn't executing template + # code, but it still needs to point at the template. + tb = fake_traceback( + exc_value, None, exc_value.filename or "", exc_value.lineno + ) + else: + # Skip the frame for the render function. + tb = tb.tb_next + + stack = [] + + # Build the stack of traceback object, replacing any in template + # code with the source file and line information. + while tb is not None: + # Skip frames decorated with @internalcode. These are internal + # calls that aren't useful in template debugging output. + if tb.tb_frame.f_code in internal_code: + tb = tb.tb_next + continue + + template = tb.tb_frame.f_globals.get("__jinja_template__") + + if template is not None: + lineno = template.get_corresponding_lineno(tb.tb_lineno) + fake_tb = fake_traceback(exc_value, tb, template.filename, lineno) + stack.append(fake_tb) + else: + stack.append(tb) + + tb = tb.tb_next + + tb_next = None + + # Assign tb_next in reverse to avoid circular references. + for tb in reversed(stack): + tb.tb_next = tb_next + tb_next = tb + + return exc_value.with_traceback(tb_next) + + +def fake_traceback( # type: ignore + exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int +) -> TracebackType: + """Produce a new traceback object that looks like it came from the + template source instead of the compiled code. The filename, line + number, and location name will point to the template, and the local + variables will be the current template context. + + :param exc_value: The original exception to be re-raised to create + the new traceback. + :param tb: The original traceback to get the local variables and + code info from. + :param filename: The template filename. + :param lineno: The line number in the template source. + """ + if tb is not None: + # Replace the real locals with the context that would be + # available at that point in the template. + locals = get_template_locals(tb.tb_frame.f_locals) + locals.pop("__jinja_exception__", None) + else: + locals = {} + + globals = { + "__name__": filename, + "__file__": filename, + "__jinja_exception__": exc_value, + } + # Raise an exception at the correct line number. + code: CodeType = compile( + "\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec" + ) + + # Build a new code object that points to the template file and + # replaces the location with a block name. + location = "template" + + if tb is not None: + function = tb.tb_frame.f_code.co_name + + if function == "root": + location = "top-level template code" + elif function.startswith("block_"): + location = f"block {function[6:]!r}" + + if sys.version_info >= (3, 8): + code = code.replace(co_name=location) + else: + code = CodeType( + code.co_argcount, + code.co_kwonlyargcount, + code.co_nlocals, + code.co_stacksize, + code.co_flags, + code.co_code, + code.co_consts, + code.co_names, + code.co_varnames, + code.co_filename, + location, + code.co_firstlineno, + code.co_lnotab, + code.co_freevars, + code.co_cellvars, + ) + + # Execute the new code, which is guaranteed to raise, and return + # the new traceback without this frame. + try: + exec(code, globals, locals) + except BaseException: + return sys.exc_info()[2].tb_next # type: ignore + + +def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + """Based on the runtime locals, get the context that would be + available at that point in the template. + """ + # Start with the current template context. + ctx: t.Optional[Context] = real_locals.get("context") + + if ctx is not None: + data: t.Dict[str, t.Any] = ctx.get_all().copy() + else: + data = {} + + # Might be in a derived context that only sets local variables + # rather than pushing a context. Local variables follow the scheme + # l_depth_name. Find the highest-depth local that has a value for + # each name. + local_overrides: t.Dict[str, t.Tuple[int, t.Any]] = {} + + for name, value in real_locals.items(): + if not name.startswith("l_") or value is missing: + # Not a template variable, or no longer relevant. + continue + + try: + _, depth_str, name = name.split("_", 2) + depth = int(depth_str) + except ValueError: + continue + + cur_depth = local_overrides.get(name, (-1,))[0] + + if cur_depth < depth: + local_overrides[name] = (depth, value) + + # Modify the context with any derived context. + for name, (_, value) in local_overrides.items(): + if value is missing: + data.pop(name, None) + else: + data[name] = value + + return data diff --git a/venv/lib/python3.10/site-packages/jinja2/defaults.py b/venv/lib/python3.10/site-packages/jinja2/defaults.py new file mode 100644 index 0000000..638cad3 --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2/defaults.py @@ -0,0 +1,48 @@ +import typing as t + +from .filters import FILTERS as DEFAULT_FILTERS # noqa: F401 +from .tests import TESTS as DEFAULT_TESTS # noqa: F401 +from .utils import Cycler +from .utils import generate_lorem_ipsum +from .utils import Joiner +from .utils import Namespace + +if t.TYPE_CHECKING: + import typing_extensions as te + +# defaults for the parser / lexer +BLOCK_START_STRING = "{%" +BLOCK_END_STRING = "%}" +VARIABLE_START_STRING = "{{" +VARIABLE_END_STRING = "}}" +COMMENT_START_STRING = "{#" +COMMENT_END_STRING = "#}" +LINE_STATEMENT_PREFIX: t.Optional[str] = None +LINE_COMMENT_PREFIX: t.Optional[str] = None +TRIM_BLOCKS = False +LSTRIP_BLOCKS = False +NEWLINE_SEQUENCE: "te.Literal['\\n', '\\r\\n', '\\r']" = "\n" +KEEP_TRAILING_NEWLINE = False + +# default filters, tests and namespace + +DEFAULT_NAMESPACE = { + "range": range, + "dict": dict, + "lipsum": generate_lorem_ipsum, + "cycler": Cycler, + "joiner": Joiner, + "namespace": Namespace, +} + +# default policies +DEFAULT_POLICIES: t.Dict[str, t.Any] = { + "compiler.ascii_str": True, + "urlize.rel": "noopener", + "urlize.target": None, + "urlize.extra_schemes": None, + "truncate.leeway": 5, + "json.dumps_function": None, + "json.dumps_kwargs": {"sort_keys": True}, + "ext.i18n.trimmed": False, +} diff --git a/venv/lib/python3.10/site-packages/jinja2/environment.py b/venv/lib/python3.10/site-packages/jinja2/environment.py new file mode 100644 index 0000000..0fc6e5b --- /dev/null +++ b/venv/lib/python3.10/site-packages/jinja2/environment.py @@ -0,0 +1,1672 @@ +"""Classes for managing templates and their runtime and compile time +options. +""" + +import os +import typing +import typing as t +import weakref +from collections import ChainMap +from functools import lru_cache +from functools import partial +from functools import reduce +from types import CodeType + +from markupsafe import Markup + +from . import nodes +from .compiler import CodeGenerator +from .compiler import generate +from .defaults import BLOCK_END_STRING +from .defaults import BLOCK_START_STRING +from .defaults import COMMENT_END_STRING +from .defaults import COMMENT_START_STRING +from .defaults import DEFAULT_FILTERS # type: ignore[attr-defined] +from .defaults import DEFAULT_NAMESPACE +from .defaults import DEFAULT_POLICIES +from .defaults import DEFAULT_TESTS # type: ignore[attr-defined] +from .defaults import KEEP_TRAILING_NEWLINE +from .defaults import LINE_COMMENT_PREFIX +from .defaults import LINE_STATEMENT_PREFIX +from .defaults import LSTRIP_BLOCKS +from .defaults import NEWLINE_SEQUENCE +from .defaults import TRIM_BLOCKS +from .defaults import VARIABLE_END_STRING +from .defaults import VARIABLE_START_STRING +from .exceptions import TemplateNotFound +from .exceptions import TemplateRuntimeError +from .exceptions import TemplatesNotFound +from .exceptions import TemplateSyntaxError +from .exceptions import UndefinedError +from .lexer import get_lexer +from .lexer import Lexer +from .lexer import TokenStream +from .nodes import EvalContext +from .parser import Parser +from .runtime import Context +from .runtime import new_context +from .runtime import Undefined +from .utils import _PassArg +from .utils import concat +from .utils import consume +from .utils import import_string +from .utils import internalcode +from .utils import LRUCache +from .utils import missing + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .bccache import BytecodeCache + from .ext import Extension + from .loaders import BaseLoader + +_env_bound = t.TypeVar("_env_bound", bound="Environment") + + +# for direct template usage we have up to ten living environments +@lru_cache(maxsize=10) +def get_spontaneous_environment(cls: t.Type[_env_bound], *args: t.Any) -> _env_bound: + """Return a new spontaneous environment. A spontaneous environment + is used for templates created directly rather than through an + existing environment. + + :param cls: Environment class to create. + :param args: Positional arguments passed to environment. + """ + env = cls(*args) + env.shared = True + return env + + +def create_cache( + size: int, +) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[t.Any]", str], "Template"]]: + """Return the cache class for the given size.""" + if size == 0: + return None + + if size < 0: + return {} + + return LRUCache(size) # type: ignore + + +def copy_cache( + cache: t.Optional[t.MutableMapping[t.Any, t.Any]], +) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[t.Any]", str], "Template"]]: + """Create an empty copy of the given cache.""" + if cache is None: + return None + + if type(cache) is dict: # noqa E721 + return {} + + return LRUCache(cache.capacity) # type: ignore + + +def load_extensions( + environment: "Environment", + extensions: t.Sequence[t.Union[str, t.Type["Extension"]]], +) -> t.Dict[str, "Extension"]: + """Load the extensions from the list and bind it to the environment. + Returns a dict of instantiated extensions. + """ + result = {} + + for extension in extensions: + if isinstance(extension, str): + extension = t.cast(t.Type["Extension"], import_string(extension)) + + result[extension.identifier] = extension(environment) + + return result + + +def _environment_config_check(environment: _env_bound) -> _env_bound: + """Perform a sanity check on the environment.""" + assert issubclass( + environment.undefined, Undefined + ), "'undefined' must be a subclass of 'jinja2.Undefined'." + assert ( + environment.block_start_string + != environment.variable_start_string + != environment.comment_start_string + ), "block, variable and comment start strings must be different." + assert environment.newline_sequence in { + "\r", + "\r\n", + "\n", + }, "'newline_sequence' must be one of '\\n', '\\r\\n', or '\\r'." + return environment + + +class Environment: + r"""The core component of Jinja is the `Environment`. It contains + important shared variables like configuration, filters, tests, + globals and others. Instances of this class may be modified if + they are not shared and if no template was loaded so far. + Modifications on environments after the first template was loaded + will lead to surprising effects and undefined behavior. + + Here are the possible initialization parameters: + + `block_start_string` + The string marking the beginning of a block. Defaults to ``'{%'``. + + `block_end_string` + The string marking the end of a block. Defaults to ``'%}'``. + + `variable_start_string` + The string marking the beginning of a print statement. + Defaults to ``'{{'``. + + `variable_end_string` + The string marking the end of a print statement. Defaults to + ``'}}'``. + + `comment_start_string` + The string marking the beginning of a comment. Defaults to ``'{#'``. + + `comment_end_string` + The string marking the end of a comment. Defaults to ``'#}'``. + + `line_statement_prefix` + If given and a string, this will be used as prefix for line based + statements. See also :ref:`line-statements`. + + `line_comment_prefix` + If given and a string, this will be used as prefix for line based + comments. See also :ref:`line-statements`. + + .. versionadded:: 2.2 + + `trim_blocks` + If this is set to ``True`` the first newline after a block is + removed (block, not variable tag!). Defaults to `False`. + + `lstrip_blocks` + If this is set to ``True`` leading spaces and tabs are stripped + from the start of a line to a block. Defaults to `False`. + + `newline_sequence` + The sequence that starts a newline. Must be one of ``'\r'``, + ``'\n'`` or ``'\r\n'``. The default is ``'\n'`` which is a + useful default for Linux and OS X systems as well as web + applications. + + `keep_trailing_newline` + Preserve the trailing newline when rendering templates. + The default is ``False``, which causes a single newline, + if present, to be stripped from the end of the template. + + .. versionadded:: 2.7 + + `extensions` + List of Jinja extensions to use. This can either be import paths + as strings or extension classes. For more information have a + look at :ref:`the extensions documentation `. + + `optimized` + should the optimizer be enabled? Default is ``True``. + + `undefined` + :class:`Undefined` or a subclass of it that is used to represent + undefined values in the template. + + `finalize` + A callable that can be used to process the result of a variable + expression before it is output. For example one can convert + ``None`` implicitly into an empty string here. + + `autoescape` + If set to ``True`` the XML/HTML autoescaping feature is enabled by + default. For more details about autoescaping see + :class:`~markupsafe.Markup`. As of Jinja 2.4 this can also + be a callable that is passed the template name and has to + return ``True`` or ``False`` depending on autoescape should be + enabled by default. + + .. versionchanged:: 2.4 + `autoescape` can now be a function + + `loader` + The template loader for this environment. + + `cache_size` + The size of the cache. Per default this is ``400`` which means + that if more than 400 templates are loaded the loader will clean + out the least recently used template. If the cache size is set to + ``0`` templates are recompiled all the time, if the cache size is + ``-1`` the cache will not be cleaned. + + .. versionchanged:: 2.8 + The cache size was increased to 400 from a low 50. + + `auto_reload` + Some loaders load templates from locations where the template + sources may change (ie: file system or database). If + ``auto_reload`` is set to ``True`` (default) every time a template is + requested the loader checks if the source changed and if yes, it + will reload the template. For higher performance it's possible to + disable that. + + `bytecode_cache` + If set to a bytecode cache object, this object will provide a + cache for the internal Jinja bytecode so that templates don't + have to be parsed if they were not changed. + + See :ref:`bytecode-cache` for more information. + + `enable_async` + If set to true this enables async template execution which + allows using async functions and generators. + """ + + #: if this environment is sandboxed. Modifying this variable won't make + #: the environment sandboxed though. For a real sandboxed environment + #: have a look at jinja2.sandbox. This flag alone controls the code + #: generation by the compiler. + sandboxed = False + + #: True if the environment is just an overlay + overlayed = False + + #: the environment this environment is linked to if it is an overlay + linked_to: t.Optional["Environment"] = None + + #: shared environments have this set to `True`. A shared environment + #: must not be modified + shared = False + + #: the class that is used for code generation. See + #: :class:`~jinja2.compiler.CodeGenerator` for more information. + code_generator_class: t.Type["CodeGenerator"] = CodeGenerator + + concat = "".join + + #: the context class that is used for templates. See + #: :class:`~jinja2.runtime.Context` for more information. + context_class: t.Type[Context] = Context + + template_class: t.Type["Template"] + + def __init__( + self, + block_start_string: str = BLOCK_START_STRING, + block_end_string: str = BLOCK_END_STRING, + variable_start_string: str = VARIABLE_START_STRING, + variable_end_string: str = VARIABLE_END_STRING, + comment_start_string: str = COMMENT_START_STRING, + comment_end_string: str = COMMENT_END_STRING, + line_statement_prefix: t.Optional[str] = LINE_STATEMENT_PREFIX, + line_comment_prefix: t.Optional[str] = LINE_COMMENT_PREFIX, + trim_blocks: bool = TRIM_BLOCKS, + lstrip_blocks: bool = LSTRIP_BLOCKS, + newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = NEWLINE_SEQUENCE, + keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE, + extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = (), + optimized: bool = True, + undefined: t.Type[Undefined] = Undefined, + finalize: t.Optional[t.Callable[..., t.Any]] = None, + autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = False, + loader: t.Optional["BaseLoader"] = None, + cache_size: int = 400, + auto_reload: bool = True, + bytecode_cache: t.Optional["BytecodeCache"] = None, + enable_async: bool = False, + ): + # !!Important notice!! + # The constructor accepts quite a few arguments that should be + # passed by keyword rather than position. However it's important to + # not change the order of arguments because it's used at least + # internally in those cases: + # - spontaneous environments (i18n extension and Template) + # - unittests + # If parameter changes are required only add parameters at the end + # and don't change the arguments (or the defaults!) of the arguments + # existing already. + + # lexer / parser information + self.block_start_string = block_start_string + self.block_end_string = block_end_string + self.variable_start_string = variable_start_string + self.variable_end_string = variable_end_string + self.comment_start_string = comment_start_string + self.comment_end_string = comment_end_string + self.line_statement_prefix = line_statement_prefix + self.line_comment_prefix = line_comment_prefix + self.trim_blocks = trim_blocks + self.lstrip_blocks = lstrip_blocks + self.newline_sequence = newline_sequence + self.keep_trailing_newline = keep_trailing_newline + + # runtime information + self.undefined: t.Type[Undefined] = undefined + self.optimized = optimized + self.finalize = finalize + self.autoescape = autoescape + + # defaults + self.filters = DEFAULT_FILTERS.copy() + self.tests = DEFAULT_TESTS.copy() + self.globals = DEFAULT_NAMESPACE.copy() + + # set the loader provided + self.loader = loader + self.cache = create_cache(cache_size) + self.bytecode_cache = bytecode_cache + self.auto_reload = auto_reload + + # configurable policies + self.policies = DEFAULT_POLICIES.copy() + + # load extensions + self.extensions = load_extensions(self, extensions) + + self.is_async = enable_async + _environment_config_check(self) + + def add_extension(self, extension: t.Union[str, t.Type["Extension"]]) -> None: + """Adds an extension after the environment was created. + + .. versionadded:: 2.5 + """ + self.extensions.update(load_extensions(self, [extension])) + + def extend(self, **attributes: t.Any) -> None: + """Add the items to the instance of the environment if they do not exist + yet. This is used by :ref:`extensions ` to register + callbacks and configuration values without breaking inheritance. + """ + for key, value in attributes.items(): + if not hasattr(self, key): + setattr(self, key, value) + + def overlay( + self, + block_start_string: str = missing, + block_end_string: str = missing, + variable_start_string: str = missing, + variable_end_string: str = missing, + comment_start_string: str = missing, + comment_end_string: str = missing, + line_statement_prefix: t.Optional[str] = missing, + line_comment_prefix: t.Optional[str] = missing, + trim_blocks: bool = missing, + lstrip_blocks: bool = missing, + newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = missing, + keep_trailing_newline: bool = missing, + extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = missing, + optimized: bool = missing, + undefined: t.Type[Undefined] = missing, + finalize: t.Optional[t.Callable[..., t.Any]] = missing, + autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = missing, + loader: t.Optional["BaseLoader"] = missing, + cache_size: int = missing, + auto_reload: bool = missing, + bytecode_cache: t.Optional["BytecodeCache"] = missing, + enable_async: bool = missing, + ) -> "te.Self": + """Create a new overlay environment that shares all the data with the + current environment except for cache and the overridden attributes. + Extensions cannot be removed for an overlayed environment. An overlayed + environment automatically gets all the extensions of the environment it + is linked to plus optional extra extensions. + + Creating overlays should happen after the initial environment was set + up completely. Not all attributes are truly linked, some are just + copied over so modifications on the original environment may not shine + through. + + .. versionchanged:: 3.1.5 + ``enable_async`` is applied correctly. + + .. versionchanged:: 3.1.2 + Added the ``newline_sequence``, ``keep_trailing_newline``, + and ``enable_async`` parameters to match ``__init__``. + """ + args = dict(locals()) + del args["self"], args["cache_size"], args["extensions"], args["enable_async"] + + rv = object.__new__(self.__class__) + rv.__dict__.update(self.__dict__) + rv.overlayed = True + rv.linked_to = self + + for key, value in args.items(): + if value is not missing: + setattr(rv, key, value) + + if cache_size is not missing: + rv.cache = create_cache(cache_size) + else: + rv.cache = copy_cache(self.cache) + + rv.extensions = {} + for key, value in self.extensions.items(): + rv.extensions[key] = value.bind(rv) + if extensions is not missing: + rv.extensions.update(load_extensions(rv, extensions)) + + if enable_async is not missing: + rv.is_async = enable_async + + return _environment_config_check(rv) + + @property + def lexer(self) -> Lexer: + """The lexer for this environment.""" + return get_lexer(self) + + def iter_extensions(self) -> t.Iterator["Extension"]: + """Iterates over the extensions by priority.""" + return iter(sorted(self.extensions.values(), key=lambda x: x.priority)) + + def getitem( + self, obj: t.Any, argument: t.Union[str, t.Any] + ) -> t.Union[t.Any, Undefined]: + """Get an item or attribute of an object but prefer the item.""" + try: + return obj[argument] + except (AttributeError, TypeError, LookupError): + if isinstance(argument, str): + try: + attr = str(argument) + except Exception: + pass + else: + try: + return getattr(obj, attr) + except AttributeError: + pass + return self.undefined(obj=obj, name=argument) + + def getattr(self, obj: t.Any, attribute: str) -> t.Any: + """Get an item or attribute of an object but prefer the attribute. + Unlike :meth:`getitem` the attribute *must* be a string. + """ + try: + return getattr(obj, attribute) + except AttributeError: + pass + try: + return obj[attribute] + except (TypeError, LookupError, AttributeError): + return self.undefined(obj=obj, name=attribute) + + def _filter_test_common( + self, + name: t.Union[str, Undefined], + value: t.Any, + args: t.Optional[t.Sequence[t.Any]], + kwargs: t.Optional[t.Mapping[str, t.Any]], + context: t.Optional[Context], + eval_ctx: t.Optional[EvalContext], + is_filter: bool, + ) -> t.Any: + if is_filter: + env_map = self.filters + type_name = "filter" + else: + env_map = self.tests + type_name = "test" + + func = env_map.get(name) # type: ignore + + if func is None: + msg = f"No {type_name} named {name!r}." + + if isinstance(name, Undefined): + try: + name._fail_with_undefined_error() + except Exception as e: + msg = f"{msg} ({e}; did you forget to quote the callable name?)" + + raise TemplateRuntimeError(msg) + + args = [value, *(args if args is not None else ())] + kwargs = kwargs if kwargs is not None else {} + pass_arg = _PassArg.from_obj(func) + + if pass_arg is _PassArg.context: + if context is None: + raise TemplateRuntimeError( + f"Attempted to invoke a context {type_name} without context." + ) + + args.insert(0, context) + elif pass_arg is _PassArg.eval_context: + if eval_ctx is None: + if context is not None: + eval_ctx = context.eval_ctx + else: + eval_ctx = EvalContext(self) + + args.insert(0, eval_ctx) + elif pass_arg is _PassArg.environment: + args.insert(0, self) + + return func(*args, **kwargs) + + def call_filter( + self, + name: str, + value: t.Any, + args: t.Optional[t.Sequence[t.Any]] = None, + kwargs: t.Optional[t.Mapping[str, t.Any]] = None, + context: t.Optional[Context] = None, + eval_ctx: t.Optional[EvalContext] = None, + ) -> t.Any: + """Invoke a filter on a value the same way the compiler does. + + This might return a coroutine if the filter is running from an + environment in async mode and the filter supports async + execution. It's your responsibility to await this if needed. + + .. versionadded:: 2.7 + """ + return self._filter_test_common( + name, value, args, kwargs, context, eval_ctx, True + ) + + def call_test( + self, + name: str, + value: t.Any, + args: t.Optional[t.Sequence[t.Any]] = None, + kwargs: t.Optional[t.Mapping[str, t.Any]] = None, + context: t.Optional[Context] = None, + eval_ctx: t.Optional[EvalContext] = None, + ) -> t.Any: + """Invoke a test on a value the same way the compiler does. + + This might return a coroutine if the test is running from an + environment in async mode and the test supports async execution. + It's your responsibility to await this if needed. + + .. versionchanged:: 3.0 + Tests support ``@pass_context``, etc. decorators. Added + the ``context`` and ``eval_ctx`` parameters. + + .. versionadded:: 2.7 + """ + return self._filter_test_common( + name, value, args, kwargs, context, eval_ctx, False + ) + + @internalcode + def parse( + self, + source: str, + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + ) -> nodes.Template: + """Parse the sourcecode and return the abstract syntax tree. This + tree of nodes is used by the compiler to convert the template into + executable source- or bytecode. This is useful for debugging or to + extract information from templates. + + If you are :ref:`developing Jinja extensions ` + this gives you a good overview of the node tree generated. + """ + try: + return self._parse(source, name, filename) + except TemplateSyntaxError: + self.handle_exception(source=source) + + def _parse( + self, source: str, name: t.Optional[str], filename: t.Optional[str] + ) -> nodes.Template: + """Internal parsing function used by `parse` and `compile`.""" + return Parser(self, source, name, filename).parse() + + def lex( + self, + source: str, + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + ) -> t.Iterator[t.Tuple[int, str, str]]: + """Lex the given sourcecode and return a generator that yields + tokens as tuples in the form ``(lineno, token_type, value)``. + This can be useful for :ref:`extension development ` + and debugging templates. + + This does not perform preprocessing. If you want the preprocessing + of the extensions to be applied you have to filter source through + the :meth:`preprocess` method. + """ + source = str(source) + try: + return self.lexer.tokeniter(source, name, filename) + except TemplateSyntaxError: + self.handle_exception(source=source) + + def preprocess( + self, + source: str, + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + ) -> str: + """Preprocesses the source with all extensions. This is automatically + called for all parsing and compiling methods but *not* for :meth:`lex` + because there you usually only want the actual source tokenized. + """ + return reduce( + lambda s, e: e.preprocess(s, name, filename), + self.iter_extensions(), + str(source), + ) + + def _tokenize( + self, + source: str, + name: t.Optional[str], + filename: t.Optional[str] = None, + state: t.Optional[str] = None, + ) -> TokenStream: + """Called by the parser to do the preprocessing and filtering + for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`. + """ + source = self.preprocess(source, name, filename) + stream = self.lexer.tokenize(source, name, filename, state) + + for ext in self.iter_extensions(): + stream = ext.filter_stream(stream) # type: ignore + + if not isinstance(stream, TokenStream): + stream = TokenStream(stream, name, filename) + + return stream + + def _generate( + self, + source: nodes.Template, + name: t.Optional[str], + filename: t.Optional[str], + defer_init: bool = False, + ) -> str: + """Internal hook that can be overridden to hook a different generate + method in. + + .. versionadded:: 2.5 + """ + return generate( # type: ignore + source, + self, + name, + filename, + defer_init=defer_init, + optimized=self.optimized, + ) + + def _compile(self, source: str, filename: str) -> CodeType: + """Internal hook that can be overridden to hook a different compile + method in. + + .. versionadded:: 2.5 + """ + return compile(source, filename, "exec") + + @typing.overload + def compile( + self, + source: t.Union[str, nodes.Template], + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + raw: "te.Literal[False]" = False, + defer_init: bool = False, + ) -> CodeType: ... + + @typing.overload + def compile( + self, + source: t.Union[str, nodes.Template], + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + raw: "te.Literal[True]" = ..., + defer_init: bool = False, + ) -> str: ... + + @internalcode + def compile( + self, + source: t.Union[str, nodes.Template], + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + raw: bool = False, + defer_init: bool = False, + ) -> t.Union[str, CodeType]: + """Compile a node or template source code. The `name` parameter is + the load name of the template after it was joined using + :meth:`join_path` if necessary, not the filename on the file system. + the `filename` parameter is the estimated filename of the template on + the file system. If the template came from a database or memory this + can be omitted. + + The return value of this method is a python code object. If the `raw` + parameter is `True` the return value will be a string with python + code equivalent to the bytecode returned otherwise. This method is + mainly used internally. + + `defer_init` is use internally to aid the module code generator. This + causes the generated code to be able to import without the global + environment variable to be set. + + .. versionadded:: 2.4 + `defer_init` parameter added. + """ + source_hint = None + try: + if isinstance(source, str): + source_hint = source + source = self._parse(source, name, filename) + source = self._generate(source, name, filename, defer_init=defer_init) + if raw: + return source + if filename is None: + filename = "