commit d4bf750c9e483ec3482d795cdc45582d648befcf Author: EG Date: Wed Jul 1 15:15:07 2026 +0300 On branch DiscordProfile Initial commit diff --git a/backend/__pycache__/server.cpython-311.pyc b/backend/__pycache__/server.cpython-311.pyc new file mode 100644 index 0000000..9ed1c20 Binary files /dev/null and b/backend/__pycache__/server.cpython-311.pyc differ diff --git a/backend/server.py b/backend/server.py new file mode 100644 index 0000000..3ac950c --- /dev/null +++ b/backend/server.py @@ -0,0 +1,137 @@ +from fastapi import FastAPI, UploadFile, File +import shutil +import uuid +import json +import os +import asyncio +from contextlib import asynccontextmanager +from datetime import datetime, timedelta +from services.time_service import get_local_time, update_time +from services.discord_sync import sync +async def widget_loop(): + + last_force_sync = None + + while True: + + now = datetime.now() + + print( + "SYNC TICK", + now.strftime("%H:%M:%S"), + flush=True + ) + + try: + update_time() + + result = sync() + + if result: + print( + "Widget sync:", + result.status_code, + result.text, + flush=True + ) + + # force sync в полночь + today = now.date() + + if now.hour == 0 and now.minute == 0: + + if last_force_sync != today: + + print( + "MIDNIGHT FORCE SYNC", + flush=True + ) + + update_time() + + result = sync() + + print( + "FORCE RESULT:", + result.status_code if result else "FAILED", + flush=True + ) + + last_force_sync = today + + + except Exception as e: + print( + "Widget error:", + repr(e), + flush=True + ) + + + await asyncio.sleep(60) + +@asynccontextmanager +async def lifespan(app): + + print("AUTOSYNC STARTED", flush=True) + + asyncio.create_task( + widget_loop() + ) + + yield + + + +app = FastAPI( + lifespan=lifespan +) + +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +PROFILE_FILE = os.path.join(BASE_DIR, "..", "config", "profile.json") +HERO_DIR = os.path.join(BASE_DIR, "..", "assets", "heroes") + +os.makedirs(HERO_DIR, exist_ok=True) + +def load_profile(): + try: + with open(PROFILE_FILE, "r", encoding="utf-8") as f: + return json.load(f) + except Exception: + return {} + + +def save_profile(data): + with open(PROFILE_FILE, "w", encoding="utf-8") as f: + json.dump(data, f, indent=2) + + +@app.get("/profile") +def get_profile(): + return load_profile() + + +@app.post("/profile") +def update_profile(data: dict): + save_profile(data) + return {"status": "ok"} + + +@app.get("/time") +def get_time(): + return { + "time": get_local_time() + } +@app.post("/hero/upload") +async def upload_hero(file: UploadFile = File(...)): + filename = f"{uuid.uuid4()}_{file.filename}" + + path = os.path.join(HERO_DIR, filename) + + with open(path, "wb") as buffer: + shutil.copyfileobj(file.file, buffer) + + return { + "status": "ok", + "hero": filename + } diff --git a/bot/__init__.py b/bot/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/bot/__pycache__/__init__.cpython-311.pyc b/bot/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..0cef026 Binary files /dev/null and b/bot/__pycache__/__init__.cpython-311.pyc differ diff --git a/bot/__pycache__/bot.cpython-311.pyc b/bot/__pycache__/bot.cpython-311.pyc new file mode 100644 index 0000000..0e4e61a Binary files /dev/null and b/bot/__pycache__/bot.cpython-311.pyc differ diff --git a/bot/bot.py b/bot/bot.py new file mode 100644 index 0000000..283e442 --- /dev/null +++ b/bot/bot.py @@ -0,0 +1,208 @@ +import os +import discord +from discord.ext import commands, tasks + +from services.profile_manager import set_value +from services.time_service import update_time +from services.discord_sync import sync + +from dotenv import load_dotenv + +load_dotenv() + + +TOKEN = os.getenv("TOKEN") + + +intents = discord.Intents.default() + + +bot = commands.Bot( + command_prefix="!", + intents=intents +) + + + +@bot.event +async def on_ready(): + + print( + "Bot online", + bot.user, + flush=True + ) + + if not clock.is_running(): + clock.start() + + + +@tasks.loop(minutes=1) +async def clock(): + + update_time() + + try: + + result = sync() + + if result: + print( + "AUTO SYNC:", + result.status_code, + flush=True + ) + + else: + print( + "AUTO SYNC: no response", + flush=True + ) + + + except Exception as e: + + print( + "SYNC ERROR", + e, + flush=True + ) + + + +@bot.slash_command( + name="ping", + description="Проверка бота" +) +async def ping(ctx): + + await ctx.respond( + "Pong!" + ) + + + +@bot.slash_command( + name="refresh", + description="Принудительное обновление" +) +async def refresh(ctx): + + update_time() + + + result = sync() + + + if result: + + await ctx.respond( + f"✅ Widget updated\nHTTP {result.status_code}" + ) + + else: + + await ctx.respond( + "❌ Sync failed" + ) + + + +@bot.slash_command( + name="sub2", + description="Изменить второй текст" +) +async def sub2( + ctx, + text: str +): + + set_value( + "sub2", + text + ) + + + sync() + + + await ctx.respond( + "✅ sub2 обновлён" + ) + + + +@bot.slash_command( + name="sub3", + description="Изменить третий текст" +) +async def sub3( + ctx, + text: str +): + + set_value( + "sub3", + text + ) + + + sync() + + + await ctx.respond( + "✅ sub3 обновлён" + ) + + + +@bot.slash_command( + name="subi2", + description="Иконка второго слота" +) +async def subi2( + ctx, + url: str +): + + set_value( + "subi2", + url + ) + + + sync() + + + await ctx.respond( + "🖼️ subi2 обновлена" + ) + + + +@bot.slash_command( + name="subi3", + description="Иконка третьего слота" +) +async def subi3( + ctx, + url: str +): + + set_value( + "subi3", + url + ) + + + sync() + + + await ctx.respond( + "🖼️ subi3 обновлена" + ) + + + +bot.run(TOKEN) diff --git a/bot/test.py b/bot/test.py new file mode 100644 index 0000000..03de066 --- /dev/null +++ b/bot/test.py @@ -0,0 +1,10 @@ +import aiohttp +import asyncio + +async def main(): + async with aiohttp.ClientSession(trust_env=True) as session: + async with session.get("https://discord.com/api/v10") as r: + print(r.status) + print(await r.text()) + +asyncio.run(main()) diff --git a/config/profile.json b/config/profile.json new file mode 100644 index 0000000..41a8186 --- /dev/null +++ b/config/profile.json @@ -0,0 +1,49 @@ +{ + "username": "Egor Widget", + "dynamic": [ + { + "type": 1, + "name": "time", + "value": "My time rn is 15:11" + }, + { + "type": 1, + "name": "description", + "value": "My Discord Widget" + }, + { + "type": 1, + "name": "server", + "value": "profile-system" + }, + { + "type": 1, + "name": "sub2", + "value": "Youmama" + } + ], + "hero": "", + "attach": { + "name": "", + "description": "", + "icon": "" + }, + "attachments": [ + { + "name": "Test", + "description": "Hello", + "icon": "https://test.png", + "mini_icon": "https://mini.png" + }, + {}, + {}, + {}, + {}, + {} + ], + "descriptions": { + "1": "", + "2": "Первый мой текст", + "3": "Второй мой текст" + } +} \ No newline at end of file diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/services/__pycache__/__init__.cpython-311.pyc b/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..fc99c55 Binary files /dev/null and b/services/__pycache__/__init__.cpython-311.pyc differ diff --git a/services/__pycache__/attach_service.cpython-311.pyc b/services/__pycache__/attach_service.cpython-311.pyc new file mode 100644 index 0000000..ff9c692 Binary files /dev/null and b/services/__pycache__/attach_service.cpython-311.pyc differ diff --git a/services/__pycache__/description_service.cpython-311.pyc b/services/__pycache__/description_service.cpython-311.pyc new file mode 100644 index 0000000..73d2b74 Binary files /dev/null and b/services/__pycache__/description_service.cpython-311.pyc differ diff --git a/services/__pycache__/discord_sync.cpython-311.pyc b/services/__pycache__/discord_sync.cpython-311.pyc new file mode 100644 index 0000000..1916753 Binary files /dev/null and b/services/__pycache__/discord_sync.cpython-311.pyc differ diff --git a/services/__pycache__/profile_manager.cpython-311.pyc b/services/__pycache__/profile_manager.cpython-311.pyc new file mode 100644 index 0000000..95a2377 Binary files /dev/null and b/services/__pycache__/profile_manager.cpython-311.pyc differ diff --git a/services/__pycache__/time_service.cpython-311.pyc b/services/__pycache__/time_service.cpython-311.pyc new file mode 100644 index 0000000..ee8b16a Binary files /dev/null and b/services/__pycache__/time_service.cpython-311.pyc differ diff --git a/services/attach_service.py b/services/attach_service.py new file mode 100644 index 0000000..093c924 --- /dev/null +++ b/services/attach_service.py @@ -0,0 +1,91 @@ +from services.profile_manager import load_profile, save_profile + + +def set_attach( + slot: int, + name: str, + description: str, + icon: str = "", + mini_icon: str = "" +): + """ + Меняет один из 6 attach-слотов + """ + + if slot < 1 or slot > 6: + raise ValueError("Attach slot должен быть от 1 до 6") + + + profile = load_profile() + + + attachments = profile.setdefault( + "attachments", + [ + {}, + {}, + {}, + {}, + {}, + {} + ] + ) + + + attachments[slot - 1] = { + "name": name, + "description": description, + "icon": icon, + "mini_icon": mini_icon + } + + + profile["attachments"] = attachments + + + save_profile(profile) + + + return attachments[slot - 1] + + + +def get_attach(slot: int): + + if slot < 1 or slot > 6: + raise ValueError("Attach slot должен быть от 1 до 6") + + + profile = load_profile() + + + attachments = profile.get( + "attachments", + [ + {}, + {}, + {}, + {}, + {}, + {} + ] + ) + + + return attachments[slot - 1] + + + +def clear_attach(slot: int): + + if slot < 1 or slot > 6: + raise ValueError("Attach slot должен быть от 1 до 6") + + + profile = load_profile() + + + profile["attachments"][slot - 1] = {} + + + save_profile(profile) diff --git a/services/description_service.py b/services/description_service.py new file mode 100644 index 0000000..16e37fd --- /dev/null +++ b/services/description_service.py @@ -0,0 +1,70 @@ +from services.profile_manager import load_profile, save_profile + + +def set_description(slot: int, text: str): + """ + Меняет пользовательское описание. + slot: + 1 -> Discord description 2 + 2 -> Discord description 3 + """ + + if slot < 1 or slot > 2: + raise ValueError( + "Доступны только описания 1 и 2" + ) + + + profile = load_profile() + + + descriptions = profile.setdefault( + "descriptions", + { + "1": "", + "2": "", + "3": "" + } + ) + + + discord_slot = slot + 1 + + + descriptions[str(discord_slot)] = text + + + profile["descriptions"] = descriptions + + + save_profile(profile) + + + return text + + + +def get_description(slot: int): + + if slot < 1 or slot > 2: + raise ValueError( + "Доступны только описания 1 и 2" + ) + + + profile = load_profile() + + + descriptions = profile.get( + "descriptions", + {} + ) + + + discord_slot = slot + 1 + + + return descriptions.get( + str(discord_slot), + "" + ) diff --git a/services/discord_sync.py b/services/discord_sync.py new file mode 100644 index 0000000..a450186 --- /dev/null +++ b/services/discord_sync.py @@ -0,0 +1,204 @@ +import os +import requests + +from dotenv import load_dotenv +from services.profile_manager import load_profile + + +load_dotenv() + + +APP_ID = os.getenv("APP_ID") +TOKEN = os.getenv("TOKEN") +UID = os.getenv("UID") + + +def build_dynamic(profile): + + dynamic = [] + + + # ====================== + # TEXT DYNAMIC + # ====================== + + old_dynamic = profile.get( + "dynamic", + [] + ) + + + for item in old_dynamic: + + if item.get("name") in [ + "time", + "sub2", + "sub3" + ]: + + dynamic.append( + { + "type": 1, + "name": item["name"], + "value": item["value"] + } + ) + + + # ====================== + # ATTACHMENTS + # ====================== + + attachments = profile.get( + "attachments", + [] + ) + + + for index, attach in enumerate( + attachments, + start=1 + ): + + if not attach: + continue + + + if attach.get("name"): + + dynamic.append( + { + "type": 1, + "name": f"attach_{index}_name", + "value": attach["name"] + } + ) + + + if attach.get("description"): + + dynamic.append( + { + "type": 1, + "name": f"attach_{index}_description", + "value": attach["description"] + } + ) + + + if attach.get("icon"): + + dynamic.append( + { + "type": 3, + "name": f"attach_{index}_icon", + "value": { + "url": attach["icon"] + } + } + ) + + + if attach.get("mini_icon"): + + dynamic.append( + { + "type": 3, + "name": f"attach_{index}_mini_icon", + "value": { + "url": attach["mini_icon"] + } + } + ) + + + return dynamic + + + +def sync(): + + profile = load_profile() + + + payload = { + + "username": profile.get( + "username", + "Profile Widget" + ), + + "data": { + + "dynamic": build_dynamic(profile) + + } + + } + + + print( + "PAYLOAD:", + payload, + flush=True + ) + + + url = ( + "https://discord.com/api/v9/" + f"applications/{APP_ID}/users/" + f"{UID}/identities/0/profile" + ) + + + headers = { + + "Authorization": + f"Bot {TOKEN}", + + "User-Agent": + "DiscordBot (https://github.com/discord/discord-api-docs, 1.0.0)", + + "Content-Type": + "application/json" + + } + + + print( + "PATCH START", + flush=True + ) + + + try: + + response = requests.patch( + url, + json=payload, + headers=headers, + timeout=20 + ) + + + print( + "PATCH END", + response.status_code, + response.text, + flush=True + ) + + + return response + + + except Exception as e: + + print( + "PATCH ERROR", + repr(e), + flush=True + ) + + + return None diff --git a/services/profile_manager.py b/services/profile_manager.py new file mode 100644 index 0000000..7a7b470 --- /dev/null +++ b/services/profile_manager.py @@ -0,0 +1,64 @@ +import json +import os + + +BASE = "/home/eg/profile-system" +FILE = os.path.join( + BASE, + "config/profile.json" +) + + +def load_profile(): + + with open( + FILE, + "r", + encoding="utf-8" + ) as f: + + return json.load(f) + + + +def save_profile(data): + + with open( + FILE, + "w", + encoding="utf-8" + ) as f: + + json.dump( + data, + f, + indent=4, + ensure_ascii=False + ) + + + +def set_value(name,value): + + data = load_profile() + + + for item in data["dynamic"]: + + if item["name"] == name: + + item["value"] = value + save_profile(data) + return + + + data["dynamic"].append( + { + "type":1, + "name":name, + "value":value + } + ) + + + save_profile(data) diff --git a/services/time_service.py b/services/time_service.py new file mode 100644 index 0000000..7d73af1 --- /dev/null +++ b/services/time_service.py @@ -0,0 +1,23 @@ +import datetime + +from services.profile_manager import set_value + + +def get_local_time(): + + now = datetime.datetime.now() + + return now.strftime("%H:%M") + + + +def update_time(): + + time = get_local_time() + + set_value( + "time", + f"My time rn is {time}" + ) + + return time 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..efada37 --- /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/eg/profile-system/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..2d8f690 --- /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/eg/profile-system/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..883065f --- /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/eg/profile-system/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/dotenv b/venv/bin/dotenv new file mode 100755 index 0000000..a666e07 --- /dev/null +++ b/venv/bin/dotenv @@ -0,0 +1,8 @@ +#!/home/eg/profile-system/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from dotenv.__main__ import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/venv/bin/fastapi b/venv/bin/fastapi new file mode 100755 index 0000000..d66058c --- /dev/null +++ b/venv/bin/fastapi @@ -0,0 +1,8 @@ +#!/home/eg/profile-system/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from fastapi.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/idna b/venv/bin/idna new file mode 100755 index 0000000..9a69164 --- /dev/null +++ b/venv/bin/idna @@ -0,0 +1,8 @@ +#!/home/eg/profile-system/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from idna.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/normalizer b/venv/bin/normalizer new file mode 100755 index 0000000..237f999 --- /dev/null +++ b/venv/bin/normalizer @@ -0,0 +1,8 @@ +#!/home/eg/profile-system/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/pip b/venv/bin/pip new file mode 100755 index 0000000..8fc3fa8 --- /dev/null +++ b/venv/bin/pip @@ -0,0 +1,8 @@ +#!/home/eg/profile-system/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..8fc3fa8 --- /dev/null +++ b/venv/bin/pip3 @@ -0,0 +1,8 @@ +#!/home/eg/profile-system/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.11 b/venv/bin/pip3.11 new file mode 100755 index 0000000..8fc3fa8 --- /dev/null +++ b/venv/bin/pip3.11 @@ -0,0 +1,8 @@ +#!/home/eg/profile-system/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.11 b/venv/bin/python3.11 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/venv/bin/python3.11 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/venv/bin/uvicorn b/venv/bin/uvicorn new file mode 100755 index 0000000..23b9717 --- /dev/null +++ b/venv/bin/uvicorn @@ -0,0 +1,8 @@ +#!/home/eg/profile-system/venv/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from uvicorn.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/lib/python3.11/site-packages/81d243bd2c585b0f4821__mypyc.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/81d243bd2c585b0f4821__mypyc.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..10de24e Binary files /dev/null and b/venv/lib/python3.11/site-packages/81d243bd2c585b0f4821__mypyc.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc b/venv/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc new file mode 100644 index 0000000..9ffd040 Binary files /dev/null and b/venv/lib/python3.11/site-packages/__pycache__/typing_extensions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py b/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py new file mode 100644 index 0000000..f987a53 --- /dev/null +++ b/venv/lib/python3.11/site-packages/_distutils_hack/__init__.py @@ -0,0 +1,222 @@ +# don't import any costly modules +import sys +import os + + +is_pypy = '__pypy__' in sys.builtin_module_names + + +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 + import warnings + + 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 + import warnings + + warnings.warn("Setuptools is replacing distutils.") + mods = [ + name + for name in sys.modules + if name == "distutils" or name.startswith("distutils.") + ] + for name in mods: + del sys.modules[name] + + +def enabled(): + """ + Allow selection of distutils by environment variable. + """ + which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local') + return which == 'local' + + +def ensure_local_distutils(): + import importlib + + clear_distutils() + + # With the DistutilsMetaFinder in place, + # perform an import to cause distutils to be + # loaded from setuptools._distutils. Ref #2906. + with shim(): + importlib.import_module('distutils') + + # check that submodules load as expected + core = importlib.import_module('distutils.core') + assert '_distutils' in core.__file__, core.__file__ + assert 'setuptools._distutils.log' not in sys.modules + + +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 _TrivialRe: + def __init__(self, *patterns): + self._patterns = patterns + + def match(self, string): + return all(pat in string for pat in self._patterns) + + +class DistutilsMetaFinder: + def find_spec(self, fullname, path, target=None): + # optimization: only consider top level modules and those + # found in the CPython test suite. + if path is not None and not fullname.startswith('test.'): + return + + method_name = 'spec_for_{fullname}'.format(**locals()) + method = getattr(self, method_name, lambda: None) + return method() + + def spec_for_distutils(self): + if self.is_cpython(): + return + + import importlib + import importlib.abc + import importlib.util + + try: + mod = importlib.import_module('setuptools._distutils') + except Exception: + # There are a couple of cases where setuptools._distutils + # may not be present: + # - An older Setuptools without a local distutils is + # taking precedence. Ref #2957. + # - Path manipulation during sitecustomize removes + # setuptools from the path but only after the hook + # has been loaded. Ref #2980. + # In either case, fall back to stdlib behavior. + return + + class DistutilsLoader(importlib.abc.Loader): + def create_module(self, spec): + mod.__name__ = 'distutils' + return mod + + def exec_module(self, module): + pass + + return importlib.util.spec_from_loader( + 'distutils', DistutilsLoader(), origin=mod.__file__ + ) + + @staticmethod + def is_cpython(): + """ + Suppress supplying distutils for CPython (build and tests). + Ref #2965 and #3007. + """ + return os.path.isfile('pybuilddir.txt') + + 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 + + @classmethod + def pip_imported_during_build(cls): + """ + Detect if pip is being imported in a build script. Ref #2355. + """ + import traceback + + return any( + cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None) + ) + + @staticmethod + def frame_file_is_setup(frame): + """ + Return True if the indicated frame suggests a setup.py file. + """ + # some frames may not have __file__ (#2940) + return frame.f_globals.get('__file__', '').endswith('setup.py') + + def spec_for_sensitive_tests(self): + """ + Ensure stdlib distutils when running select tests under CPython. + + python/cpython#91169 + """ + clear_distutils() + self.spec_for_distutils = lambda: None + + sensitive_tests = ( + [ + 'test.test_distutils', + 'test.test_peg_generator', + 'test.test_importlib', + ] + if sys.version_info < (3, 10) + else [ + 'test.test_distutils', + ] + ) + + +for name in DistutilsMetaFinder.sensitive_tests: + setattr( + DistutilsMetaFinder, + f'spec_for_{name}', + DistutilsMetaFinder.spec_for_sensitive_tests, + ) + + +DISTUTILS_FINDER = DistutilsMetaFinder() + + +def add_shim(): + DISTUTILS_FINDER in sys.meta_path or insert_shim() + + +class shim: + def __enter__(self): + insert_shim() + + def __exit__(self, exc, value, tb): + remove_shim() + + +def insert_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.11/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a1b120d Binary files /dev/null and b/venv/lib/python3.11/site-packages/_distutils_hack/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc b/venv/lib/python3.11/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc new file mode 100644 index 0000000..c57e862 Binary files /dev/null and b/venv/lib/python3.11/site-packages/_distutils_hack/__pycache__/override.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/_distutils_hack/override.py b/venv/lib/python3.11/site-packages/_distutils_hack/override.py new file mode 100644 index 0000000..2cc433a --- /dev/null +++ b/venv/lib/python3.11/site-packages/_distutils_hack/override.py @@ -0,0 +1 @@ +__import__('_distutils_hack').do_override() diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/METADATA b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/METADATA new file mode 100644 index 0000000..c8a61b0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/METADATA @@ -0,0 +1,123 @@ +Metadata-Version: 2.4 +Name: aiohappyeyeballs +Version: 2.6.2 +Summary: Happy Eyeballs for asyncio +License: PSF-2.0 +License-File: LICENSE +Author: J. Nick Koston +Author-email: nick@koston.org +Requires-Python: >=3.10 +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: Operating System :: OS Independent +Classifier: Topic :: Software Development :: Libraries +Classifier: Programming Language :: Python :: 3 +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: License :: OSI Approved :: Python Software Foundation License +Project-URL: Bug Tracker, https://github.com/aio-libs/aiohappyeyeballs/issues +Project-URL: Changelog, https://github.com/aio-libs/aiohappyeyeballs/blob/main/CHANGELOG.md +Project-URL: Documentation, https://aiohappyeyeballs.readthedocs.io +Project-URL: Repository, https://github.com/aio-libs/aiohappyeyeballs +Description-Content-Type: text/markdown + +# aiohappyeyeballs + +

+ + CI Status + + + Documentation Status + + + Test coverage percentage + +

+

+ + Poetry + + + Ruff + + + pre-commit + +

+

+ + PyPI Version + + Supported Python versions + License +

+ +--- + +**Documentation**: https://aiohappyeyeballs.readthedocs.io + +**Source Code**: https://github.com/aio-libs/aiohappyeyeballs + +--- + +[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs) +([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html)) + +## Use case + +This library exists to allow connecting with +[Happy Eyeballs](https://en.wikipedia.org/wiki/Happy_Eyeballs) +([RFC 8305](https://www.rfc-editor.org/rfc/rfc8305.html)) +when you +already have a list of addrinfo and not a DNS name. + +The stdlib version of `loop.create_connection()` +will only work when you pass in an unresolved name which +is not a good fit when using DNS caching or resolving +names via another method such as `zeroconf`. + +## Installation + +Install this via pip (or your favourite package manager): + +`pip install aiohappyeyeballs` + +## License + +[aiohappyeyeballs is licensed under the same terms as cpython itself.](https://github.com/python/cpython/blob/main/LICENSE) + +## Example usage + +```python + +addr_infos = await loop.getaddrinfo("example.org", 80) + +socket = await start_connection(addr_infos) +socket = await start_connection(addr_infos, local_addr_infos=local_addr_infos, happy_eyeballs_delay=0.2) + +transport, protocol = await loop.create_connection( + MyProtocol, sock=socket, ...) + +# Remove the first address for each family from addr_info +pop_addr_infos_interleave(addr_info, 1) + +# Remove all matching address from addr_info +remove_addr_infos(addr_info, "dead::beef::") + +# Convert a local_addr to local_addr_infos +local_addr_infos = addr_to_addr_infos(("127.0.0.1",0)) +``` + +## Credits + +This package contains code from cpython and is licensed under the same terms as cpython itself. + +This package was created with +[Copier](https://copier.readthedocs.io/) and the +[browniebroke/pypackage-template](https://github.com/browniebroke/pypackage-template) +project template. + diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/RECORD b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/RECORD new file mode 100644 index 0000000..dd64ef3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/RECORD @@ -0,0 +1,16 @@ +aiohappyeyeballs-2.6.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +aiohappyeyeballs-2.6.2.dist-info/METADATA,sha256=cqs2VY8TwE2e_4qqnW409Si3suvj-aM-8dCiWG2Angk,5888 +aiohappyeyeballs-2.6.2.dist-info/RECORD,, +aiohappyeyeballs-2.6.2.dist-info/WHEEL,sha256=EGEvSphFYqXKs23-kQBeyNoJP1nrT8ZJKQoi5p5DYL8,88 +aiohappyeyeballs-2.6.2.dist-info/licenses/LICENSE,sha256=Oy-B_iHRgcSZxZolbI4ZaEVdZonSaaqFNzv7avQdo78,13936 +aiohappyeyeballs/__init__.py,sha256=Af9ADZj3BWLfGaA7ITOFPlKIenh3ozSNWL3yezSZ2Jw,361 +aiohappyeyeballs/__pycache__/__init__.cpython-311.pyc,, +aiohappyeyeballs/__pycache__/_staggered.cpython-311.pyc,, +aiohappyeyeballs/__pycache__/impl.cpython-311.pyc,, +aiohappyeyeballs/__pycache__/types.cpython-311.pyc,, +aiohappyeyeballs/__pycache__/utils.cpython-311.pyc,, +aiohappyeyeballs/_staggered.py,sha256=aj3cSwHEDX88UMfO9bUau9tfrRAszhjg99dpEMiAOGM,6698 +aiohappyeyeballs/impl.py,sha256=TIkAK4xfACvKBp1s7DAKSobHefUTOC2HGE-n0tOthRk,9667 +aiohappyeyeballs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +aiohappyeyeballs/types.py,sha256=_8JmHFix6MeM1e7hRP7BleEaGy93GswGtzQv068zKY8,288 +aiohappyeyeballs/utils.py,sha256=dPAcNcrU_VhaTolTEEA94hgh9ONDN9_dYT4Xo9ORqQw,2922 diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/WHEEL b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/WHEEL new file mode 100644 index 0000000..6a35213 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: poetry-core 2.4.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/licenses/LICENSE new file mode 100644 index 0000000..f26bcf4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs-2.6.2.dist-info/licenses/LICENSE @@ -0,0 +1,279 @@ +A. HISTORY OF THE SOFTWARE +========================== + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see https://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see https://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations, which became +Zope Corporation. In 2001, the Python Software Foundation (PSF, see +https://www.python.org/psf/) was formed, a non-profit organization +created specifically to own Python-related Intellectual Property. +Zope Corporation was a sponsoring member of the PSF. + +All Python releases are Open Source (see https://opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases. + + Release Derived Year Owner GPL- + from compatible? (1) + + 0.9.0 thru 1.2 1991-1995 CWI yes + 1.3 thru 1.5.2 1.2 1995-1999 CNRI yes + 1.6 1.5.2 2000 CNRI no + 2.0 1.6 2000 BeOpen.com no + 1.6.1 1.6 2001 CNRI yes (2) + 2.1 2.0+1.6.1 2001 PSF no + 2.0.1 2.0+1.6.1 2001 PSF yes + 2.1.1 2.1+2.0.1 2001 PSF yes + 2.1.2 2.1.1 2002 PSF yes + 2.1.3 2.1.2 2002 PSF yes + 2.2 and above 2.1.1 2001-now PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +(2) According to Richard Stallman, 1.6.1 is not GPL-compatible, + because its license has a choice of law clause. According to + CNRI, however, Stallman's lawyer has told CNRI's lawyer that 1.6.1 + is "not incompatible" with the GPL. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +Python software and documentation are licensed under the +Python Software Foundation License Version 2. + +Starting with Python 3.8.6, examples, recipes, and other code in +the documentation are dual licensed under the PSF License Version 2 +and the Zero-Clause BSD license. + +Some software incorporated into Python is under different licenses. +The licenses are listed with code falling under that license. + + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 Python Software Foundation; +All Rights Reserved" are retained in Python alone or in any derivative version +prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +BEOPEN.COM LICENSE AGREEMENT FOR PYTHON 2.0 +------------------------------------------- + +BEOPEN PYTHON OPEN SOURCE LICENSE AGREEMENT VERSION 1 + +1. This LICENSE AGREEMENT is between BeOpen.com ("BeOpen"), having an +office at 160 Saratoga Avenue, Santa Clara, CA 95051, and the +Individual or Organization ("Licensee") accessing and otherwise using +this software in source or binary form and its associated +documentation ("the Software"). + +2. Subject to the terms and conditions of this BeOpen Python License +Agreement, BeOpen hereby grants Licensee a non-exclusive, +royalty-free, world-wide license to reproduce, analyze, test, perform +and/or display publicly, prepare derivative works, distribute, and +otherwise use the Software alone or in any derivative version, +provided, however, that the BeOpen Python License is retained in the +Software, alone or in any derivative version prepared by Licensee. + +3. BeOpen is making the Software available to Licensee on an "AS IS" +basis. BEOPEN MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, BEOPEN MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF THE SOFTWARE WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +4. BEOPEN SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF THE +SOFTWARE FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS +AS A RESULT OF USING, MODIFYING OR DISTRIBUTING THE SOFTWARE, OR ANY +DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +5. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +6. This License Agreement shall be governed by and interpreted in all +respects by the law of the State of California, excluding conflict of +law provisions. Nothing in this License Agreement shall be deemed to +create any relationship of agency, partnership, or joint venture +between BeOpen and Licensee. This License Agreement does not grant +permission to use BeOpen trademarks or trade names in a trademark +sense to endorse or promote products or services of Licensee, or any +third party. As an exception, the "BeOpen Python" logos available at +http://www.pythonlabs.com/logos.html may be used according to the +permissions granted on that web page. + +7. By copying, installing or otherwise using the software, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. + + +CNRI LICENSE AGREEMENT FOR PYTHON 1.6.1 +--------------------------------------- + +1. This LICENSE AGREEMENT is between the Corporation for National +Research Initiatives, having an office at 1895 Preston White Drive, +Reston, VA 20191 ("CNRI"), and the Individual or Organization +("Licensee") accessing and otherwise using Python 1.6.1 software in +source or binary form and its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, CNRI +hereby grants Licensee a nonexclusive, royalty-free, world-wide +license to reproduce, analyze, test, perform and/or display publicly, +prepare derivative works, distribute, and otherwise use Python 1.6.1 +alone or in any derivative version, provided, however, that CNRI's +License Agreement and CNRI's notice of copyright, i.e., "Copyright (c) +1995-2001 Corporation for National Research Initiatives; All Rights +Reserved" are retained in Python 1.6.1 alone or in any derivative +version prepared by Licensee. Alternately, in lieu of CNRI's License +Agreement, Licensee may substitute the following text (omitting the +quotes): "Python 1.6.1 is made available subject to the terms and +conditions in CNRI's License Agreement. This Agreement together with +Python 1.6.1 may be located on the internet using the following +unique, persistent identifier (known as a handle): 1895.22/1013. This +Agreement may also be obtained from a proxy server on the internet +using the following URL: http://hdl.handle.net/1895.22/1013". + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python 1.6.1 or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python 1.6.1. + +4. CNRI is making Python 1.6.1 available to Licensee on an "AS IS" +basis. CNRI MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, CNRI MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON 1.6.1 WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. CNRI SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +1.6.1 FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON 1.6.1, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. This License Agreement shall be governed by the federal +intellectual property law of the United States, including without +limitation the federal copyright law, and, to the extent such +U.S. federal law does not apply, by the law of the Commonwealth of +Virginia, excluding Virginia's conflict of law provisions. +Notwithstanding the foregoing, with regard to derivative works based +on Python 1.6.1 that incorporate non-separable material that was +previously distributed under the GNU General Public License (GPL), the +law of the Commonwealth of Virginia shall govern this License +Agreement only as to issues arising under or with respect to +Paragraphs 4, 5, and 7 of this License Agreement. Nothing in this +License Agreement shall be deemed to create any relationship of +agency, partnership, or joint venture between CNRI and Licensee. This +License Agreement does not grant permission to use CNRI trademarks or +trade name in a trademark sense to endorse or promote products or +services of Licensee, or any third party. + +8. By clicking on the "ACCEPT" button where indicated, or by copying, +installing or otherwise using Python 1.6.1, Licensee agrees to be +bound by the terms and conditions of this License Agreement. + + ACCEPT + + +CWI LICENSE AGREEMENT FOR PYTHON 0.9.0 THROUGH 1.2 +-------------------------------------------------- + +Copyright (c) 1991 - 1995, Stichting Mathematisch Centrum Amsterdam, +The Netherlands. 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 Stichting Mathematisch +Centrum or CWI not be used in advertising or publicity pertaining to +distribution of the software without specific, written prior +permission. + +STICHTING MATHEMATISCH CENTRUM DISCLAIMS ALL WARRANTIES WITH REGARD TO +THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS, IN NO EVENT SHALL STICHTING MATHEMATISCH CENTRUM 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. + +ZERO-CLAUSE BSD LICENSE FOR CODE IN THE PYTHON DOCUMENTATION +---------------------------------------------------------------------- + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +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. diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/__init__.py b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__init__.py new file mode 100644 index 0000000..ad6bfb5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__init__.py @@ -0,0 +1,14 @@ +__version__ = "2.6.2" + +from .impl import start_connection +from .types import AddrInfoType, SocketFactoryType +from .utils import addr_to_addr_infos, pop_addr_infos_interleave, remove_addr_infos + +__all__ = ( + "AddrInfoType", + "SocketFactoryType", + "addr_to_addr_infos", + "pop_addr_infos_interleave", + "remove_addr_infos", + "start_connection", +) diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..de78167 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/_staggered.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/_staggered.cpython-311.pyc new file mode 100644 index 0000000..eddf105 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/_staggered.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/impl.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/impl.cpython-311.pyc new file mode 100644 index 0000000..dbf2bde Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/impl.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/types.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/types.cpython-311.pyc new file mode 100644 index 0000000..7f981af Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/types.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..6975084 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohappyeyeballs/__pycache__/utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/_staggered.py b/venv/lib/python3.11/site-packages/aiohappyeyeballs/_staggered.py new file mode 100644 index 0000000..bb745dc --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs/_staggered.py @@ -0,0 +1,197 @@ +import asyncio +import contextlib +from collections.abc import Awaitable, Callable, Iterable +from typing import ( + TYPE_CHECKING, + Any, + TypeVar, +) + +_T = TypeVar("_T") + +RE_RAISE_EXCEPTIONS = (SystemExit, KeyboardInterrupt) + + +def _set_result(wait_next: "asyncio.Future[None]") -> None: + """Set the result of a future if it is not already done.""" + if not wait_next.done(): + wait_next.set_result(None) + + +async def _wait_one( + futures: "Iterable[asyncio.Future[Any]]", + loop: asyncio.AbstractEventLoop, +) -> _T: + """Wait for the first future to complete.""" + wait_next = loop.create_future() + + def _on_completion(fut: "asyncio.Future[Any]") -> None: + if not wait_next.done(): + wait_next.set_result(fut) + + for f in futures: + f.add_done_callback(_on_completion) + + try: + return await wait_next + finally: + for f in futures: + f.remove_done_callback(_on_completion) + + +async def staggered_race( + coro_fns: Iterable[Callable[[], Awaitable[_T]]], + delay: float | None, + *, + loop: asyncio.AbstractEventLoop | None = None, +) -> tuple[_T | None, int | None, list[BaseException | None]]: + """ + Run coroutines with staggered start times and take the first to finish. + + This method takes an iterable of coroutine functions. The first one is + started immediately. From then on, whenever the immediately preceding one + fails (raises an exception), or when *delay* seconds has passed, the next + coroutine is started. This continues until one of the coroutines complete + successfully, in which case all others are cancelled, or until all + coroutines fail. + + The coroutines provided should be well-behaved in the following way: + + * They should only ``return`` if completed successfully. + + * They should always raise an exception if they did not complete + successfully. In particular, if they handle cancellation, they should + probably reraise, like this:: + + try: + # do work + except asyncio.CancelledError: + # undo partially completed work + raise + + Args: + ---- + coro_fns: an iterable of coroutine functions, i.e. callables that + return a coroutine object when called. Use ``functools.partial`` or + lambdas to pass arguments. + + delay: amount of time, in seconds, between starting coroutines. If + ``None``, the coroutines will run sequentially. + + loop: the event loop to use. If ``None``, the running loop is used. + + Returns: + ------- + tuple *(winner_result, winner_index, exceptions)* where + + - *winner_result*: the result of the winning coroutine, or ``None`` + if no coroutines won. + + - *winner_index*: the index of the winning coroutine in + ``coro_fns``, or ``None`` if no coroutines won. If the winning + coroutine may return None on success, *winner_index* can be used + to definitively determine whether any coroutine won. + + - *exceptions*: list of exceptions returned by the coroutines. + ``len(exceptions)`` is equal to the number of coroutines actually + started, and the order is the same as in ``coro_fns``. The winning + coroutine's entry is ``None``. + + """ + loop = loop or asyncio.get_running_loop() + exceptions: list[BaseException | None] = [] + tasks: set[asyncio.Task[tuple[_T, int] | None]] = set() + + async def run_one_coro( + coro_fn: Callable[[], Awaitable[_T]], + this_index: int, + start_next: "asyncio.Future[None]", + ) -> tuple[_T, int] | None: + """ + Run a single coroutine. + + If the coroutine fails, set the exception in the exceptions list and + start the next coroutine by setting the result of the start_next. + + If the coroutine succeeds, return the result and the index of the + coroutine in the coro_fns list. + + If SystemExit or KeyboardInterrupt is raised, re-raise it. + """ + try: + result = await coro_fn() + except RE_RAISE_EXCEPTIONS: + raise + except BaseException as e: + exceptions[this_index] = e + _set_result(start_next) # Kickstart the next coroutine + return None + + return result, this_index + + start_next_timer: asyncio.TimerHandle | None = None + start_next: asyncio.Future[None] | None + task: asyncio.Task[tuple[_T, int] | None] + done: asyncio.Future[None] | asyncio.Task[tuple[_T, int] | None] + coro_iter = iter(coro_fns) + this_index = -1 + try: + while True: + if coro_fn := next(coro_iter, None): + this_index += 1 + exceptions.append(None) + start_next = loop.create_future() + task = loop.create_task(run_one_coro(coro_fn, this_index, start_next)) + tasks.add(task) + start_next_timer = ( + loop.call_later(delay, _set_result, start_next) if delay else None + ) + elif not tasks: + # We exhausted the coro_fns list and no tasks are running + # so we have no winner and all coroutines failed. + break + + while tasks or start_next: + done = await _wait_one( + (*tasks, start_next) if start_next else tasks, loop + ) + if done is start_next: + # The current task has failed or the timer has expired + # so we need to start the next task. + start_next = None + if start_next_timer: + start_next_timer.cancel() + start_next_timer = None + + # Break out of the task waiting loop to start the next + # task. + break + + if TYPE_CHECKING: + assert isinstance(done, asyncio.Task) + + tasks.remove(done) + if winner := done.result(): + return *winner, exceptions + finally: + # We either have: + # - a winner + # - all tasks failed + # - a KeyboardInterrupt or SystemExit. + + # + # If the timer is still running, cancel it. + # + if start_next_timer: + start_next_timer.cancel() + + # + # If there are any tasks left, cancel them and than + # wait them so they fill the exceptions list. + # + for task in tasks: + task.cancel() + with contextlib.suppress(asyncio.CancelledError): + await task + + return None, None, exceptions diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/impl.py b/venv/lib/python3.11/site-packages/aiohappyeyeballs/impl.py new file mode 100644 index 0000000..4112ab4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs/impl.py @@ -0,0 +1,261 @@ +"""Base implementation.""" + +import asyncio +import collections +import contextlib +import functools +import itertools +import socket +from collections.abc import Sequence + +from . import _staggered +from .types import AddrInfoType, SocketFactoryType + + +async def start_connection( + addr_infos: Sequence[AddrInfoType], + *, + local_addr_infos: Sequence[AddrInfoType] | None = None, + happy_eyeballs_delay: float | None = None, + interleave: int | None = None, + loop: asyncio.AbstractEventLoop | None = None, + socket_factory: SocketFactoryType | None = None, +) -> socket.socket: + """ + Connect to a TCP server. + + Create a socket connection to a specified destination. The + destination is specified as a list of AddrInfoType tuples as + returned from getaddrinfo(). + + The arguments are, in order: + + * ``family``: the address family, e.g. ``socket.AF_INET`` or + ``socket.AF_INET6``. + * ``type``: the socket type, e.g. ``socket.SOCK_STREAM`` or + ``socket.SOCK_DGRAM``. + * ``proto``: the protocol, e.g. ``socket.IPPROTO_TCP`` or + ``socket.IPPROTO_UDP``. + * ``canonname``: the canonical name of the address, e.g. + ``"www.python.org"``. + * ``sockaddr``: the socket address + + This method is a coroutine which will try to establish the connection + in the background. When successful, the coroutine returns a + socket. + + The expected use case is to use this method in conjunction with + loop.create_connection() to establish a connection to a server:: + + socket = await start_connection(addr_infos) + transport, protocol = await loop.create_connection( + MyProtocol, sock=socket, ...) + """ + if not addr_infos: + raise ValueError("addr_infos must not be empty") + + current_loop = loop or asyncio.get_running_loop() + + single_addr_info = len(addr_infos) == 1 + + if happy_eyeballs_delay is not None and interleave is None: + # If using happy eyeballs, default to interleave addresses by family + interleave = 1 + + if interleave and not single_addr_info: + addr_infos = _interleave_addrinfos(addr_infos, interleave) + + sock: socket.socket | None = None + # uvloop can raise RuntimeError instead of OSError + exceptions: list[list[OSError | RuntimeError]] = [] + if happy_eyeballs_delay is None or single_addr_info: + # not using happy eyeballs + for addrinfo in addr_infos: + try: + sock = await _connect_sock( + current_loop, + exceptions, + addrinfo, + local_addr_infos, + None, + socket_factory, + ) + break + except (RuntimeError, OSError): + continue + else: # using happy eyeballs + open_sockets: set[socket.socket] = set() + try: + sock, _, _ = await _staggered.staggered_race( + ( + functools.partial( + _connect_sock, + current_loop, + exceptions, + addrinfo, + local_addr_infos, + open_sockets, + socket_factory, + ) + for addrinfo in addr_infos + ), + happy_eyeballs_delay, + ) + finally: + # If we have a winner, staggered_race will + # cancel the other tasks, however there is a + # small race window where any of the other tasks + # can be done before they are cancelled which + # will leave the socket open. To avoid this problem + # we pass a set to _connect_sock to keep track of + # the open sockets and close them here if there + # are any "runner up" sockets. + for s in open_sockets: + if s is not sock: + with contextlib.suppress(OSError): + s.close() + open_sockets = None # type: ignore[assignment] + + if sock is None: + all_exceptions = [exc for sub in exceptions for exc in sub] + try: + first_exception = all_exceptions[0] + if len(all_exceptions) == 1: + raise first_exception + else: + # If they all have the same str(), raise one. + model = str(first_exception) + if all(str(exc) == model for exc in all_exceptions): + raise first_exception + # Raise a combined exception so the user can see all + # the various error messages. + msg = "Multiple exceptions: {}".format( + ", ".join(str(exc) for exc in all_exceptions) + ) + # If the errno is the same for all exceptions, raise + # an OSError with that errno. + if isinstance(first_exception, OSError): + first_errno = first_exception.errno + if all( + isinstance(exc, OSError) and exc.errno == first_errno + for exc in all_exceptions + ): + raise OSError(first_errno, msg) + elif isinstance(first_exception, RuntimeError) and all( + isinstance(exc, RuntimeError) for exc in all_exceptions + ): + raise RuntimeError(msg) + # We have a mix of OSError and RuntimeError + # so we have to pick which one to raise. + # and we raise OSError for compatibility + raise OSError(msg) + finally: + all_exceptions = None # type: ignore[assignment] + exceptions = None # type: ignore[assignment] + + return sock + + +async def _connect_sock( + loop: asyncio.AbstractEventLoop, + exceptions: list[list[OSError | RuntimeError]], + addr_info: AddrInfoType, + local_addr_infos: Sequence[AddrInfoType] | None = None, + open_sockets: set[socket.socket] | None = None, + socket_factory: SocketFactoryType | None = None, +) -> socket.socket: + """ + Create, bind and connect one socket. + + If open_sockets is passed, add the socket to the set of open sockets. + Any failure caught here will remove the socket from the set and close it. + + Callers can use this set to close any sockets that are not the winner + of all staggered tasks in the result there are runner up sockets aka + multiple winners. + """ + my_exceptions: list[OSError | RuntimeError] = [] + exceptions.append(my_exceptions) + family, type_, proto, _, address = addr_info + sock = None + try: + if socket_factory is not None: + sock = socket_factory(addr_info) + else: + sock = socket.socket(family=family, type=type_, proto=proto) + if open_sockets is not None: + open_sockets.add(sock) + sock.setblocking(False) + if local_addr_infos is not None: + for lfamily, _, _, _, laddr in local_addr_infos: + # skip local addresses of different family + if lfamily != family: + continue + try: + sock.bind(laddr) + break + except OSError as exc: + msg = ( + f"error while attempting to bind on " + f"address {laddr!r}: " + f"{(exc.strerror or '').lower()}" + ) + exc = OSError(exc.errno, msg) + my_exceptions.append(exc) + else: # all bind attempts failed + if my_exceptions: + raise my_exceptions.pop() + else: + raise OSError(f"no matching local address with {family=} found") + await loop.sock_connect(sock, address) + return sock + except (RuntimeError, OSError) as exc: + my_exceptions.append(exc) + if sock is not None: + if open_sockets is not None: + open_sockets.remove(sock) + try: + sock.close() + except OSError as e: + my_exceptions.append(e) + raise + raise + except: + if sock is not None: + if open_sockets is not None: + open_sockets.remove(sock) + try: + sock.close() + except OSError as e: + my_exceptions.append(e) + raise + raise + finally: + exceptions = my_exceptions = None # type: ignore[assignment] + + +def _interleave_addrinfos( + addrinfos: Sequence[AddrInfoType], first_address_family_count: int = 1 +) -> list[AddrInfoType]: + """Interleave list of addrinfo tuples by family.""" + # Group addresses by family + addrinfos_by_family: collections.OrderedDict[int, list[AddrInfoType]] = ( + collections.OrderedDict() + ) + for addr in addrinfos: + family = addr[0] + if family not in addrinfos_by_family: + addrinfos_by_family[family] = [] + addrinfos_by_family[family].append(addr) + addrinfos_lists = list(addrinfos_by_family.values()) + + reordered: list[AddrInfoType] = [] + if first_address_family_count > 1: + reordered.extend(addrinfos_lists[0][: first_address_family_count - 1]) + del addrinfos_lists[0][: first_address_family_count - 1] + reordered.extend( + a + for a in itertools.chain.from_iterable(itertools.zip_longest(*addrinfos_lists)) + if a is not None + ) + return reordered diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/py.typed b/venv/lib/python3.11/site-packages/aiohappyeyeballs/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/types.py b/venv/lib/python3.11/site-packages/aiohappyeyeballs/types.py new file mode 100644 index 0000000..80f09af --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs/types.py @@ -0,0 +1,14 @@ +"""Types for aiohappyeyeballs.""" + +import socket +from collections.abc import Callable + +AddrInfoType = tuple[ + int | socket.AddressFamily, + int | socket.SocketKind, + int, + str, + tuple, # type: ignore[type-arg] +] + +SocketFactoryType = Callable[[AddrInfoType], socket.socket] diff --git a/venv/lib/python3.11/site-packages/aiohappyeyeballs/utils.py b/venv/lib/python3.11/site-packages/aiohappyeyeballs/utils.py new file mode 100644 index 0000000..5969691 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohappyeyeballs/utils.py @@ -0,0 +1,92 @@ +"""Utility functions for aiohappyeyeballs.""" + +import ipaddress +import socket + +from .types import AddrInfoType + + +def addr_to_addr_infos( + addr: tuple[str, int, int, int] | tuple[str, int, int] | tuple[str, int] | None, +) -> list[AddrInfoType] | None: + """Convert an address tuple to a list of addr_info tuples.""" + if addr is None: + return None + host = addr[0] + port = addr[1] + is_ipv6 = ":" in host + if is_ipv6: + flowinfo = 0 + scopeid = 0 + addr_len = len(addr) + if addr_len >= 4: + scopeid = addr[3] # type: ignore[misc] + if addr_len >= 3: + flowinfo = addr[2] # type: ignore[misc] + addr = (host, port, flowinfo, scopeid) + family = socket.AF_INET6 + else: + addr = (host, port) + family = socket.AF_INET + return [(family, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", addr)] + + +def pop_addr_infos_interleave( + addr_infos: list[AddrInfoType], interleave: int | None = None +) -> None: + """ + Pop addr_info from the list of addr_infos by family up to interleave times. + + The interleave parameter is used to know how many addr_infos for + each family should be popped of the top of the list. + """ + seen: dict[int, int] = {} + if interleave is None: + interleave = 1 + to_remove: list[AddrInfoType] = [] + for addr_info in addr_infos: + family = addr_info[0] + if family not in seen: + seen[family] = 0 + if seen[family] < interleave: + to_remove.append(addr_info) + seen[family] += 1 + for addr_info in to_remove: + addr_infos.remove(addr_info) + + +def _addr_tuple_to_ip_address( + addr: tuple[str, int] | tuple[str, int, int, int], +) -> tuple[ipaddress.IPv4Address, int] | tuple[ipaddress.IPv6Address, int, int, int]: + """Convert an address tuple to an IPv4Address.""" + return (ipaddress.ip_address(addr[0]), *addr[1:]) + + +def remove_addr_infos( + addr_infos: list[AddrInfoType], + addr: tuple[str, int] | tuple[str, int, int, int], +) -> None: + """ + Remove an address from the list of addr_infos. + + The addr value is typically the return value of + sock.getpeername(). + """ + bad_addrs_infos: list[AddrInfoType] = [] + for addr_info in addr_infos: + if addr_info[-1] == addr: + bad_addrs_infos.append(addr_info) + if bad_addrs_infos: + for bad_addr_info in bad_addrs_infos: + addr_infos.remove(bad_addr_info) + return + # Slow path in case addr is formatted differently + match_addr = _addr_tuple_to_ip_address(addr) + for addr_info in addr_infos: + if match_addr == _addr_tuple_to_ip_address(addr_info[-1]): + bad_addrs_infos.append(addr_info) + if bad_addrs_infos: + for bad_addr_info in bad_addrs_infos: + addr_infos.remove(bad_addr_info) + return + raise ValueError(f"Address {addr} not found in addr_infos") diff --git a/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/METADATA b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/METADATA new file mode 100644 index 0000000..07ed9d1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/METADATA @@ -0,0 +1,257 @@ +Metadata-Version: 2.4 +Name: aiohttp +Version: 3.14.1 +Summary: Async http client/server framework (asyncio) +Maintainer-email: aiohttp team +License: Apache-2.0 AND MIT +Project-URL: Homepage, https://github.com/aio-libs/aiohttp +Project-URL: Chat: Matrix, https://matrix.to/#/#aio-libs:matrix.org +Project-URL: Chat: Matrix Space, https://matrix.to/#/#aio-libs-space:matrix.org +Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI +Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiohttp +Project-URL: Docs: Changelog, https://docs.aiohttp.org/en/stable/changes.html +Project-URL: Docs: RTD, https://docs.aiohttp.org +Project-URL: GitHub: issues, https://github.com/aio-libs/aiohttp/issues +Project-URL: GitHub: repo, https://github.com/aio-libs/aiohttp +Classifier: Development Status :: 5 - Production/Stable +Classifier: Framework :: AsyncIO +Classifier: Intended Audience :: Developers +Classifier: Operating System :: POSIX +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +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: Topic :: Internet :: WWW/HTTP +Requires-Python: >=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE.txt +License-File: vendor/llhttp/LICENSE +Requires-Dist: aiohappyeyeballs>=2.5.0 +Requires-Dist: aiosignal>=1.4.0 +Requires-Dist: async-timeout<6.0,>=4.0; python_version < "3.11" +Requires-Dist: attrs>=17.3.0 +Requires-Dist: frozenlist>=1.1.1 +Requires-Dist: multidict<7.0,>=4.5 +Requires-Dist: propcache>=0.2.0 +Requires-Dist: typing_extensions>=4.4; python_version < "3.13" +Requires-Dist: yarl<2.0,>=1.17.0 +Provides-Extra: speedups +Requires-Dist: aiodns>=3.3.0; (sys_platform != "android" and sys_platform != "ios") and extra == "speedups" +Requires-Dist: Brotli>=1.2; (platform_python_implementation == "CPython" and sys_platform != "android" and sys_platform != "ios") and extra == "speedups" +Requires-Dist: brotlicffi>=1.2; platform_python_implementation != "CPython" and extra == "speedups" +Requires-Dist: backports.zstd; (platform_python_implementation == "CPython" and python_version < "3.14" and sys_platform != "android" and sys_platform != "ios") and extra == "speedups" +Dynamic: license-file + +================================== +Async http client/server framework +================================== + +.. image:: https://raw.githubusercontent.com/aio-libs/aiohttp/master/docs/aiohttp-plain.svg + :height: 64px + :width: 64px + :alt: aiohttp logo + +| + +.. image:: https://github.com/aio-libs/aiohttp/workflows/CI/badge.svg + :target: https://github.com/aio-libs/aiohttp/actions?query=workflow%3ACI + :alt: GitHub Actions status for master branch + +.. image:: https://codecov.io/gh/aio-libs/aiohttp/branch/master/graph/badge.svg + :target: https://codecov.io/gh/aio-libs/aiohttp + :alt: codecov.io status for master branch + +.. image:: https://badge.fury.io/py/aiohttp.svg + :target: https://pypi.org/project/aiohttp + :alt: Latest PyPI package version + +.. image:: https://img.shields.io/pypi/dm/aiohttp + :target: https://pypistats.org/packages/aiohttp + :alt: Downloads count + +.. image:: https://readthedocs.org/projects/aiohttp/badge/?version=latest + :target: https://docs.aiohttp.org/ + :alt: Latest Read The Docs + +.. image:: https://img.shields.io/endpoint?url=https://codspeed.io/badge.json + :target: https://codspeed.io/aio-libs/aiohttp + :alt: Codspeed.io status for aiohttp + + +Key Features +============ + +- Supports both client and server side of HTTP protocol. +- Supports both client and server Web-Sockets out-of-the-box and avoids + Callback Hell. +- Provides Web-server with middleware and pluggable routing. + + +Getting started +=============== + +Client +------ + +To get something from the web: + +.. code-block:: python + + import aiohttp + import asyncio + + async def main(): + + async with aiohttp.ClientSession() as session: + async with session.get('http://python.org') as response: + + print("Status:", response.status) + print("Content-type:", response.headers['content-type']) + + html = await response.text() + print("Body:", html[:15], "...") + + asyncio.run(main()) + +This prints: + +.. code-block:: + + Status: 200 + Content-type: text/html; charset=utf-8 + Body: ... + +Coming from `requests `_ ? Read `why we need so many lines `_. + +Server +------ + +An example using a simple server: + +.. code-block:: python + + # examples/server_simple.py + from aiohttp import web + + async def handle(request): + name = request.match_info.get('name', "Anonymous") + text = "Hello, " + name + return web.Response(text=text) + + async def wshandle(request): + ws = web.WebSocketResponse() + await ws.prepare(request) + + async for msg in ws: + if msg.type == web.WSMsgType.text: + await ws.send_str("Hello, {}".format(msg.data)) + elif msg.type == web.WSMsgType.binary: + await ws.send_bytes(msg.data) + elif msg.type == web.WSMsgType.close: + break + + return ws + + + app = web.Application() + app.add_routes([web.get('/', handle), + web.get('/echo', wshandle), + web.get('/{name}', handle)]) + + if __name__ == '__main__': + web.run_app(app) + + +Documentation +============= + +https://aiohttp.readthedocs.io/ + + +Demos +===== + +https://github.com/aio-libs/aiohttp-demos + + +External links +============== + +* `Third party libraries + `_ +* `Built with aiohttp + `_ +* `Powered by aiohttp + `_ + +Feel free to make a Pull Request for adding your link to these pages! + + +Communication channels +====================== + +*aio-libs Discussions*: https://github.com/aio-libs/aiohttp/discussions + +*Matrix*: `#aio-libs:matrix.org `_ + +We support `Stack Overflow +`_. +Please add *aiohttp* tag to your question there. + +Requirements +============ + +- attrs_ +- multidict_ +- yarl_ +- frozenlist_ + +Optionally you may install the aiodns_ library (highly recommended for sake of speed). + +.. _aiodns: https://pypi.python.org/pypi/aiodns +.. _attrs: https://github.com/python-attrs/attrs +.. _multidict: https://pypi.python.org/pypi/multidict +.. _frozenlist: https://pypi.org/project/frozenlist/ +.. _yarl: https://pypi.python.org/pypi/yarl +.. _async-timeout: https://pypi.python.org/pypi/async_timeout + + +Keepsafe +======== + +The aiohttp community would like to thank Keepsafe +(https://www.getkeepsafe.com) for its support in the early days of +the project. + + +Source code +=========== + +The latest developer version is available in a GitHub repository: +https://github.com/aio-libs/aiohttp + +Benchmarks +========== + +If you are interested in efficiency, the AsyncIO community maintains a +list of benchmarks on the official wiki: +https://github.com/python/asyncio/wiki/Benchmarks + +-------- + +.. image:: https://img.shields.io/matrix/aio-libs:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs:matrix.org + :alt: Matrix Room — #aio-libs:matrix.org + +.. image:: https://img.shields.io/matrix/aio-libs-space:matrix.org?label=Discuss%20on%20Matrix%20at%20%23aio-libs-space%3Amatrix.org&logo=matrix&server_fqdn=matrix.org&style=flat + :target: https://matrix.to/#/%23aio-libs-space:matrix.org + :alt: Matrix Space — #aio-libs-space:matrix.org + +.. image:: https://insights.linuxfoundation.org/api/badge/health-score?project=aiohttp + :target: https://insights.linuxfoundation.org/project/aiohttp + :alt: LFX Health Score diff --git a/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/RECORD b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/RECORD new file mode 100644 index 0000000..ddc156d --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/RECORD @@ -0,0 +1,139 @@ +aiohttp-3.14.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +aiohttp-3.14.1.dist-info/METADATA,sha256=lSrt4tDtmd7yhoDSTEq0NNoU8cSDjfDJAuOCCHWKil0,8262 +aiohttp-3.14.1.dist-info/RECORD,, +aiohttp-3.14.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +aiohttp-3.14.1.dist-info/WHEEL,sha256=M5wZqvm0RPz434ksvS2jyqHKJ9fsZFXCH8JlrvbFuNs,190 +aiohttp-3.14.1.dist-info/licenses/LICENSE.txt,sha256=Lkvl_GxMcqRm_LZl1ybgSaaJGYH-U2xPBLY2Z0lGHSM,11313 +aiohttp-3.14.1.dist-info/licenses/vendor/llhttp/LICENSE,sha256=YoFo1ou1qKF-C777O9dOMm4e3CBYPXb1L0V8gskhhn0,1069 +aiohttp-3.14.1.dist-info/top_level.txt,sha256=iv-JIaacmTl-hSho3QmphcKnbRRYx1st47yjz_178Ro,8 +aiohttp/.hash/_cparser.pxd.hash,sha256=1xYKvgB1DahaZj6GMSLJ9qi5KmoJDD8ZPp3CIxah0ig,121 +aiohttp/.hash/_find_header.pxd.hash,sha256=_mbpD6vM-CVCKq3ulUvsOAz5Wdo88wrDzfpOsMQaMNA,125 +aiohttp/.hash/_http_parser.pyx.hash,sha256=D15N0PIazROldZ2ezxeuuu4VuQP9OxrN8sutpdcL6TA,125 +aiohttp/.hash/_http_writer.pyx.hash,sha256=eqqhtJepEtpikWbd_gUoZOWyEBUwmHCd1fq2jfIrXSg,125 +aiohttp/.hash/hdrs.py.hash,sha256=cBtTPqXxWpYN--0v2ukVWjjfVbnT3Csii1PkBJBCz8E,116 +aiohttp/__init__.py,sha256=1dDCbOmlrF1FnB_LRnU-jVKY1XngQ9guX1ZasoTH-TY,8339 +aiohttp/__pycache__/__init__.cpython-311.pyc,, +aiohttp/__pycache__/_cookie_helpers.cpython-311.pyc,, +aiohttp/__pycache__/abc.cpython-311.pyc,, +aiohttp/__pycache__/base_protocol.cpython-311.pyc,, +aiohttp/__pycache__/client.cpython-311.pyc,, +aiohttp/__pycache__/client_exceptions.cpython-311.pyc,, +aiohttp/__pycache__/client_middleware_digest_auth.cpython-311.pyc,, +aiohttp/__pycache__/client_middlewares.cpython-311.pyc,, +aiohttp/__pycache__/client_proto.cpython-311.pyc,, +aiohttp/__pycache__/client_reqrep.cpython-311.pyc,, +aiohttp/__pycache__/client_ws.cpython-311.pyc,, +aiohttp/__pycache__/compression_utils.cpython-311.pyc,, +aiohttp/__pycache__/connector.cpython-311.pyc,, +aiohttp/__pycache__/cookiejar.cpython-311.pyc,, +aiohttp/__pycache__/formdata.cpython-311.pyc,, +aiohttp/__pycache__/hdrs.cpython-311.pyc,, +aiohttp/__pycache__/helpers.cpython-311.pyc,, +aiohttp/__pycache__/http.cpython-311.pyc,, +aiohttp/__pycache__/http_exceptions.cpython-311.pyc,, +aiohttp/__pycache__/http_parser.cpython-311.pyc,, +aiohttp/__pycache__/http_websocket.cpython-311.pyc,, +aiohttp/__pycache__/http_writer.cpython-311.pyc,, +aiohttp/__pycache__/log.cpython-311.pyc,, +aiohttp/__pycache__/multipart.cpython-311.pyc,, +aiohttp/__pycache__/payload.cpython-311.pyc,, +aiohttp/__pycache__/payload_streamer.cpython-311.pyc,, +aiohttp/__pycache__/pytest_plugin.cpython-311.pyc,, +aiohttp/__pycache__/resolver.cpython-311.pyc,, +aiohttp/__pycache__/streams.cpython-311.pyc,, +aiohttp/__pycache__/tcp_helpers.cpython-311.pyc,, +aiohttp/__pycache__/test_utils.cpython-311.pyc,, +aiohttp/__pycache__/tracing.cpython-311.pyc,, +aiohttp/__pycache__/typedefs.cpython-311.pyc,, +aiohttp/__pycache__/web.cpython-311.pyc,, +aiohttp/__pycache__/web_app.cpython-311.pyc,, +aiohttp/__pycache__/web_exceptions.cpython-311.pyc,, +aiohttp/__pycache__/web_fileresponse.cpython-311.pyc,, +aiohttp/__pycache__/web_log.cpython-311.pyc,, +aiohttp/__pycache__/web_middlewares.cpython-311.pyc,, +aiohttp/__pycache__/web_protocol.cpython-311.pyc,, +aiohttp/__pycache__/web_request.cpython-311.pyc,, +aiohttp/__pycache__/web_response.cpython-311.pyc,, +aiohttp/__pycache__/web_routedef.cpython-311.pyc,, +aiohttp/__pycache__/web_runner.cpython-311.pyc,, +aiohttp/__pycache__/web_server.cpython-311.pyc,, +aiohttp/__pycache__/web_urldispatcher.cpython-311.pyc,, +aiohttp/__pycache__/web_ws.cpython-311.pyc,, +aiohttp/__pycache__/worker.cpython-311.pyc,, +aiohttp/_cookie_helpers.py,sha256=uOe8v5yx9N9N2JHFJiQOjuN2SiIV7D1lxnTfrXc6oEU,14092 +aiohttp/_cparser.pxd,sha256=nvnwTkOaMAIazM-OUB6H66LpsNNeUG3nFxbSSLQoQEM,4336 +aiohttp/_find_header.pxd,sha256=0GfwFCPN2zxEKTO1_MA5sYq2UfzsG8kcV3aTqvwlz3g,68 +aiohttp/_headers.pxi,sha256=n701k28dVPjwRnx5j6LpJhLTfj7dqu2vJt7f0O60Oyg,2007 +aiohttp/_http_parser.cpython-311-x86_64-linux-gnu.so,sha256=WUGz4lLZe8Hq1WpaBWjgFc7Uq1UYdmvl8YLn3aL0MKY,2913832 +aiohttp/_http_parser.pyx,sha256=mNLUenKTRZkNlXXc9oxVDeXVJ31gsrI4XR69jrDWxPw,33860 +aiohttp/_http_writer.cpython-311-x86_64-linux-gnu.so,sha256=F5TEKHD28_o55LurAqvMT8oK0ud7vAgUb-mNaO2CTyE,455168 +aiohttp/_http_writer.pyx,sha256=FmhqRLv-qyvL2BJvVwk128VFqwYMb1b1vHLPCt3awXA,4823 +aiohttp/_websocket/.hash/mask.pxd.hash,sha256=Y0zBddk_ck3pi9-BFzMcpkcvCKvwvZ4GTtZFb9u1nxQ,128 +aiohttp/_websocket/.hash/mask.pyx.hash,sha256=90owpXYM8_kIma4KUcOxhWSk-Uv4NVMBoCYeFM1B3d0,128 +aiohttp/_websocket/.hash/reader_c.pxd.hash,sha256=DM51AsKpHR3Bl2EIx0eUSy1uvusTDfylNeRCMT66erk,132 +aiohttp/_websocket/__init__.py,sha256=Mar3R9_vBN_Ea4lsW7iTAVXD7OKswKPGqF5xgSyt77k,44 +aiohttp/_websocket/__pycache__/__init__.cpython-311.pyc,, +aiohttp/_websocket/__pycache__/helpers.cpython-311.pyc,, +aiohttp/_websocket/__pycache__/models.cpython-311.pyc,, +aiohttp/_websocket/__pycache__/reader.cpython-311.pyc,, +aiohttp/_websocket/__pycache__/reader_c.cpython-311.pyc,, +aiohttp/_websocket/__pycache__/reader_py.cpython-311.pyc,, +aiohttp/_websocket/__pycache__/writer.cpython-311.pyc,, +aiohttp/_websocket/helpers.py,sha256=lMDbpiZefmHTGNxjpe6K09ZlF048KxzcOvbh-ZYqvg0,5026 +aiohttp/_websocket/mask.cpython-311-x86_64-linux-gnu.so,sha256=P3O6pZ1iNQF9ePhc15tyVjA8BBSOwN477q5xGdh7EJ8,229856 +aiohttp/_websocket/mask.pxd,sha256=sBmZ1Amym9kW4Ge8lj1fLZ7mPPya4LzLdpkQExQXv5M,112 +aiohttp/_websocket/mask.pyx,sha256=BHjOtV0O0w7xp9p0LNADRJvGmgfPn9sGeJvSs0fL__4,1397 +aiohttp/_websocket/models.py,sha256=gJLskxkUKakYFsVMdjOclSjt41rQWRCL4RfRB_wmwO0,2952 +aiohttp/_websocket/reader.py,sha256=eC4qS0c5sOeQ2ebAHLaBpIaTVFaSKX79pY2xvh3Pqyw,1030 +aiohttp/_websocket/reader_c.cpython-311-x86_64-linux-gnu.so,sha256=8lf3gcF6jhUrKI8ltvHJCy5Y6-tntzPBh9AzBTSsgjg,1832944 +aiohttp/_websocket/reader_c.pxd,sha256=l-ODGpJpOx4FxpsCtkRyITmmRvBlRo8mv87qNgeQZbo,2683 +aiohttp/_websocket/reader_c.py,sha256=vf7Y6JB6hpLE17OQ39IhO70ga_DXFYksfv45M5n1wOU,20073 +aiohttp/_websocket/reader_py.py,sha256=vf7Y6JB6hpLE17OQ39IhO70ga_DXFYksfv45M5n1wOU,20073 +aiohttp/_websocket/writer.py,sha256=JA4ejOQMCvAvIXe_0f6uRx_aGSO4-zVUc1HJBZ125ak,11259 +aiohttp/abc.py,sha256=KvpM3wDEWl_iIdNQX3NEnJf3pYAtgxynAvp5s4JkNrU,7536 +aiohttp/base_protocol.py,sha256=bL6dSlNacbmln20Plez_gqYXTcFeg-V1hkHVDk4s8vU,4688 +aiohttp/client.py,sha256=PzaLAKgPMytcA75z1ZIEjTBzTWR3U7wT66f38OKTwmI,64312 +aiohttp/client_exceptions.py,sha256=XvJ57ppYs2IubcY3efBUkSpPElwQDqt3SWU_yssvjyI,11609 +aiohttp/client_middleware_digest_auth.py,sha256=Z8ufMnBF8RQ_JaVfG3bUbAoRua_kC7RSIexQM2zm9wQ,19042 +aiohttp/client_middlewares.py,sha256=kP5N9CMzQPMGPIEydeVUiLUTLsw8Vl8Gr4qAWYdu3vM,1918 +aiohttp/client_proto.py,sha256=u6AZU05mZxAFG4saTaxn5lVN9vy4cmgvF9k7HQG260A,12633 +aiohttp/client_reqrep.py,sha256=RWyXzuIi0NETNeuNADMbWPGd-mxcpVLsVQM0N582pU8,54704 +aiohttp/client_ws.py,sha256=PiMLbWN6IBEXGiWuqL1KyQqIAydDGsWMitX_pd8cjLo,19468 +aiohttp/compression_utils.py,sha256=_HTGK9dZOojGd4kyrk2A5yAt1pzVyKJh4AH5eVH0DQ8,15840 +aiohttp/connector.py,sha256=0iYWUWjhc8zDYwWd-J4mtfhjojzGRRcbnQbnuwY-njU,70036 +aiohttp/cookiejar.py,sha256=Y80b6_hSiDi4lN0iOb3X5983VONf-i4GhRH447gzTr0,25815 +aiohttp/formdata.py,sha256=zRWW2ODYkWeahDOd-hwra-__EUizdB2bf6hAMgkCKhc,6514 +aiohttp/hdrs.py,sha256=pGrWw6L6-NJqLGr8GiIQzjeaI_J5n857JqAfbOWkBkI,5106 +aiohttp/helpers.py,sha256=dSbEmGl8XsLU2zwmD0dyp4wHHvHrH5TFaofkSH-OmsA,33017 +aiohttp/http.py,sha256=CHXxFtOXK85RNijYkfpjvjCbKlJc3hIGRsCFAnh83hk,2077 +aiohttp/http_exceptions.py,sha256=I1nyopt1sYV0GqpW2ra4pnCmFcOqMoDC4dELghEk2lI,3115 +aiohttp/http_parser.py,sha256=nrXK1evVj04TjpbOu4tOx4w0yUlzG2PaEFxq0wvmfNY,45170 +aiohttp/http_websocket.py,sha256=8jVNshtgNPSDnab0Dc1lAO7HFvtT7y79eZKjUXX6e28,983 +aiohttp/http_writer.py,sha256=bXHXXWkEnySdYX3cCmYCFgNFsnxwPNCs-0nMjSPDRAo,12602 +aiohttp/log.py,sha256=BbNKx9e3VMIm0xYjZI0IcBBoS7wjdeIeSaiJE7-qK2g,325 +aiohttp/multipart.py,sha256=CJB4szZ8Zhv4htswXvSDfwY8HjlVGZFyvzGXIYo7b50,44179 +aiohttp/payload.py,sha256=MXcl_K0gcblkN_YFXE_Up6N7IQwjH8uM4HTfn9Axbmg,41480 +aiohttp/payload_streamer.py,sha256=PGqJKt_1ca_7i6p1UgU8jrpO1toOt5EQDQMOK9LjtV4,2225 +aiohttp/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7 +aiohttp/pytest_plugin.py,sha256=eGhRoy0TKNOQ3mtabkpoMgt0JThnVDr406h_kyZkQe8,12976 +aiohttp/resolver.py,sha256=jmrAUnhayFdSjTsCt2_coosdVmc5RgEB5Agr65Q0sQk,9954 +aiohttp/streams.py,sha256=k-XDoAfb3P8t5sHvWyEONXzYHoZ4JmJYr_aZB9CQF6c,24127 +aiohttp/tcp_helpers.py,sha256=BSadqVWaBpMFDRWnhaaR941N9MiDZ7bdTrxgCb0CW-M,961 +aiohttp/test_utils.py,sha256=RqqoQTTI4VpZDvOcLMmQgxisuRMCIX0qOjG019EtEpM,24636 +aiohttp/tracing.py,sha256=4mokNTKdX5YuoSrvVnG4D7WTPMwe-UlYHVYxQjRv4Fw,14500 +aiohttp/typedefs.py,sha256=I-V6S4T73fI_wkJfBYiL7otEeRvVHmSdsk-YYPR6Fj0,1672 +aiohttp/web.py,sha256=gsyxRSPWV3iZjPUbOR3dIGcp1jm2ltBtXwkPh91co3Q,18426 +aiohttp/web_app.py,sha256=hmT_DWXKwHMqmFBLE8JHhevLaaTST73ikovIIx_Tw6k,19379 +aiohttp/web_exceptions.py,sha256=CsEG88dQINYSFpklCE_7bZgL1IVjHoy5dlTSVQGHPRQ,10354 +aiohttp/web_fileresponse.py,sha256=5vlf-OmfgFxG5O1zGrzk-SPRkbs58izLlbQldT-DHXE,16404 +aiohttp/web_log.py,sha256=lJCDsZ2xJp7HqSuijaTZyqxJ0oNWPhwxJE1FVGUWwWU,8487 +aiohttp/web_middlewares.py,sha256=OIfnnCHJgRnKjzDjHoc_VzNcLuVf9y8kv5aTzM2VGvc,4152 +aiohttp/web_protocol.py,sha256=RyGK7u4Lpl8yC2rSbHcKiYhSDbKSbHuA-GZTaN8q-RY,31092 +aiohttp/web_request.py,sha256=o99Os7_M_C4poSc58el9q44VGO6VctJaJMFKhi7F6R8,31867 +aiohttp/web_response.py,sha256=7EjxpTW4a6du5X0utXenUbSO6fbY_MgfINppJUEre-M,30783 +aiohttp/web_routedef.py,sha256=S_uRfJ87LCM_fFTAIrqYTWiIqDAk0pLLYdUr1vGU9BM,6057 +aiohttp/web_runner.py,sha256=PGUdZs_d2qzM3Tnb6yCQQoxfwqQZFuVr_NGIJBZq9Ms,12708 +aiohttp/web_server.py,sha256=LawT47SJuLSIYgkAWL_haFgH6lVyuoD3zyFTvBatvsE,3170 +aiohttp/web_urldispatcher.py,sha256=lc9Cou6tP8YJTJS2MEQWVcJV9C5dFKTyhdCLjLtkOzU,43893 +aiohttp/web_ws.py,sha256=fqE-c95dxvwkZfm8M-vKeP5pCteHCmrhAdI-1o4J348,27455 +aiohttp/worker.py,sha256=osuMCad7kBmL7FeR6EEEdsfkDqOaBDYvAzHMgW_hsIc,8506 diff --git a/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/REQUESTED b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/WHEEL b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/WHEEL new file mode 100644 index 0000000..1c20eb8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/WHEEL @@ -0,0 +1,7 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: false +Tag: cp311-cp311-manylinux_2_17_x86_64 +Tag: cp311-cp311-manylinux2014_x86_64 +Tag: cp311-cp311-manylinux_2_28_x86_64 + diff --git a/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/licenses/LICENSE.txt b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..0b2f7b0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/licenses/LICENSE.txt @@ -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 aio-libs contributors + + 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.11/site-packages/aiohttp-3.14.1.dist-info/licenses/vendor/llhttp/LICENSE b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/licenses/vendor/llhttp/LICENSE new file mode 100644 index 0000000..23682c0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/licenses/vendor/llhttp/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright © 2018 Fedor Indutny + +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.11/site-packages/aiohttp-3.14.1.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/top_level.txt new file mode 100644 index 0000000..ee4ba4f --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp-3.14.1.dist-info/top_level.txt @@ -0,0 +1 @@ +aiohttp diff --git a/venv/lib/python3.11/site-packages/aiohttp/.hash/_cparser.pxd.hash b/venv/lib/python3.11/site-packages/aiohttp/.hash/_cparser.pxd.hash new file mode 100644 index 0000000..268a48a --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/.hash/_cparser.pxd.hash @@ -0,0 +1 @@ +9ef9f04e439a30021acccf8e501e87eba2e9b0d35e506de71716d248b4284043 /home/runner/work/aiohttp/aiohttp/aiohttp/_cparser.pxd diff --git a/venv/lib/python3.11/site-packages/aiohttp/.hash/_find_header.pxd.hash b/venv/lib/python3.11/site-packages/aiohttp/.hash/_find_header.pxd.hash new file mode 100644 index 0000000..f006c2d --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/.hash/_find_header.pxd.hash @@ -0,0 +1 @@ +d067f01423cddb3c442933b5fcc039b18ab651fcec1bc91c577693aafc25cf78 /home/runner/work/aiohttp/aiohttp/aiohttp/_find_header.pxd diff --git a/venv/lib/python3.11/site-packages/aiohttp/.hash/_http_parser.pyx.hash b/venv/lib/python3.11/site-packages/aiohttp/.hash/_http_parser.pyx.hash new file mode 100644 index 0000000..3531f2c --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/.hash/_http_parser.pyx.hash @@ -0,0 +1 @@ +98d2d47a729345990d9575dcf68c550de5d5277d60b2b2385d1ebd8eb0d6c4fc /home/runner/work/aiohttp/aiohttp/aiohttp/_http_parser.pyx diff --git a/venv/lib/python3.11/site-packages/aiohttp/.hash/_http_writer.pyx.hash b/venv/lib/python3.11/site-packages/aiohttp/.hash/_http_writer.pyx.hash new file mode 100644 index 0000000..846878a --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/.hash/_http_writer.pyx.hash @@ -0,0 +1 @@ +16686a44bbfeab2bcbd8126f570935dbc545ab060c6f56f5bc72cf0adddac170 /home/runner/work/aiohttp/aiohttp/aiohttp/_http_writer.pyx diff --git a/venv/lib/python3.11/site-packages/aiohttp/.hash/hdrs.py.hash b/venv/lib/python3.11/site-packages/aiohttp/.hash/hdrs.py.hash new file mode 100644 index 0000000..1e0a522 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/.hash/hdrs.py.hash @@ -0,0 +1 @@ +a46ad6c3a2faf8d26a2c6afc1a2210ce379a23f2799fce7b26a01f6ce5a40642 /home/runner/work/aiohttp/aiohttp/aiohttp/hdrs.py diff --git a/venv/lib/python3.11/site-packages/aiohttp/__init__.py b/venv/lib/python3.11/site-packages/aiohttp/__init__.py new file mode 100644 index 0000000..1b35d23 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/__init__.py @@ -0,0 +1,279 @@ +__version__ = "3.14.1" + +from typing import TYPE_CHECKING + +from . import hdrs as hdrs +from .client import ( + BaseConnector, + ClientConnectionError, + ClientConnectionResetError, + ClientConnectorCertificateError, + ClientConnectorDNSError, + ClientConnectorError, + ClientConnectorSSLError, + ClientError, + ClientHttpProxyError, + ClientOSError, + ClientPayloadError, + ClientProxyConnectionError, + ClientRequest, + ClientResponse, + ClientResponseError, + ClientSession, + ClientSSLError, + ClientTimeout, + ClientWebSocketResponse, + ClientWSTimeout, + ConnectionTimeoutError, + ContentTypeError, + Fingerprint, + InvalidURL, + InvalidUrlClientError, + InvalidUrlRedirectClientError, + NamedPipeConnector, + NonHttpUrlClientError, + NonHttpUrlRedirectClientError, + RedirectClientError, + RequestInfo, + ServerConnectionError, + ServerDisconnectedError, + ServerFingerprintMismatch, + ServerTimeoutError, + SocketTimeoutError, + TCPConnector, + TooManyRedirects, + UnixConnector, + WSMessageTypeError, + WSServerHandshakeError, + request, +) +from .client_middleware_digest_auth import DigestAuthMiddleware +from .client_middlewares import ClientHandlerType, ClientMiddlewareType +from .compression_utils import set_zlib_backend +from .connector import ( + AddrInfoType as AddrInfoType, + SocketFactoryType as SocketFactoryType, +) +from .cookiejar import CookieJar as CookieJar, DummyCookieJar as DummyCookieJar +from .formdata import FormData as FormData +from .helpers import BasicAuth, ChainMapProxy, ETag, encode_basic_auth +from .http import ( + HttpVersion as HttpVersion, + HttpVersion10 as HttpVersion10, + HttpVersion11 as HttpVersion11, + WebSocketError as WebSocketError, + WSCloseCode as WSCloseCode, + WSMessage as WSMessage, + WSMsgType as WSMsgType, +) +from .multipart import ( + BadContentDispositionHeader as BadContentDispositionHeader, + BadContentDispositionParam as BadContentDispositionParam, + BodyPartReader as BodyPartReader, + MultipartReader as MultipartReader, + MultipartWriter as MultipartWriter, + content_disposition_filename as content_disposition_filename, + parse_content_disposition as parse_content_disposition, +) +from .payload import ( + PAYLOAD_REGISTRY as PAYLOAD_REGISTRY, + AsyncIterablePayload as AsyncIterablePayload, + BufferedReaderPayload as BufferedReaderPayload, + BytesIOPayload as BytesIOPayload, + BytesPayload as BytesPayload, + IOBasePayload as IOBasePayload, + JsonPayload as JsonPayload, + Payload as Payload, + StringIOPayload as StringIOPayload, + StringPayload as StringPayload, + TextIOPayload as TextIOPayload, + get_payload as get_payload, + payload_type as payload_type, +) +from .payload_streamer import streamer as streamer +from .resolver import ( + AsyncResolver as AsyncResolver, + DefaultResolver as DefaultResolver, + ThreadedResolver as ThreadedResolver, +) +from .streams import ( + EMPTY_PAYLOAD as EMPTY_PAYLOAD, + DataQueue as DataQueue, + EofStream as EofStream, + FlowControlDataQueue as FlowControlDataQueue, + StreamReader as StreamReader, +) +from .tracing import ( + TraceConfig as TraceConfig, + TraceConnectionCreateEndParams as TraceConnectionCreateEndParams, + TraceConnectionCreateStartParams as TraceConnectionCreateStartParams, + TraceConnectionQueuedEndParams as TraceConnectionQueuedEndParams, + TraceConnectionQueuedStartParams as TraceConnectionQueuedStartParams, + TraceConnectionReuseconnParams as TraceConnectionReuseconnParams, + TraceDnsCacheHitParams as TraceDnsCacheHitParams, + TraceDnsCacheMissParams as TraceDnsCacheMissParams, + TraceDnsResolveHostEndParams as TraceDnsResolveHostEndParams, + TraceDnsResolveHostStartParams as TraceDnsResolveHostStartParams, + TraceRequestChunkSentParams as TraceRequestChunkSentParams, + TraceRequestEndParams as TraceRequestEndParams, + TraceRequestExceptionParams as TraceRequestExceptionParams, + TraceRequestHeadersSentParams as TraceRequestHeadersSentParams, + TraceRequestRedirectParams as TraceRequestRedirectParams, + TraceRequestStartParams as TraceRequestStartParams, + TraceResponseChunkReceivedParams as TraceResponseChunkReceivedParams, +) + +if TYPE_CHECKING: + # At runtime these are lazy-loaded at the bottom of the file. + from .worker import ( + GunicornUVLoopWebWorker as GunicornUVLoopWebWorker, + GunicornWebWorker as GunicornWebWorker, + ) + +__all__: tuple[str, ...] = ( + "hdrs", + # client + "AddrInfoType", + "BaseConnector", + "ClientConnectionError", + "ClientConnectionResetError", + "ClientConnectorCertificateError", + "ClientConnectorDNSError", + "ClientConnectorError", + "ClientConnectorSSLError", + "ClientError", + "ClientHttpProxyError", + "ClientOSError", + "ClientPayloadError", + "ClientProxyConnectionError", + "ClientResponse", + "ClientRequest", + "ClientResponseError", + "ClientSSLError", + "ClientSession", + "ClientTimeout", + "ClientWebSocketResponse", + "ClientWSTimeout", + "ConnectionTimeoutError", + "ContentTypeError", + "Fingerprint", + "FlowControlDataQueue", + "InvalidURL", + "InvalidUrlClientError", + "InvalidUrlRedirectClientError", + "NonHttpUrlClientError", + "NonHttpUrlRedirectClientError", + "RedirectClientError", + "RequestInfo", + "ServerConnectionError", + "ServerDisconnectedError", + "ServerFingerprintMismatch", + "ServerTimeoutError", + "SocketFactoryType", + "SocketTimeoutError", + "TCPConnector", + "TooManyRedirects", + "UnixConnector", + "NamedPipeConnector", + "WSServerHandshakeError", + "request", + # client_middleware + "ClientMiddlewareType", + "ClientHandlerType", + # cookiejar + "CookieJar", + "DummyCookieJar", + # formdata + "FormData", + # helpers + "BasicAuth", + "ChainMapProxy", + "DigestAuthMiddleware", + "ETag", + "encode_basic_auth", + "set_zlib_backend", + # http + "HttpVersion", + "HttpVersion10", + "HttpVersion11", + "WSMsgType", + "WSCloseCode", + "WSMessage", + "WebSocketError", + # multipart + "BadContentDispositionHeader", + "BadContentDispositionParam", + "BodyPartReader", + "MultipartReader", + "MultipartWriter", + "content_disposition_filename", + "parse_content_disposition", + # payload + "AsyncIterablePayload", + "BufferedReaderPayload", + "BytesIOPayload", + "BytesPayload", + "IOBasePayload", + "JsonPayload", + "PAYLOAD_REGISTRY", + "Payload", + "StringIOPayload", + "StringPayload", + "TextIOPayload", + "get_payload", + "payload_type", + # payload_streamer + "streamer", + # resolver + "AsyncResolver", + "DefaultResolver", + "ThreadedResolver", + # streams + "DataQueue", + "EMPTY_PAYLOAD", + "EofStream", + "StreamReader", + # tracing + "TraceConfig", + "TraceConnectionCreateEndParams", + "TraceConnectionCreateStartParams", + "TraceConnectionQueuedEndParams", + "TraceConnectionQueuedStartParams", + "TraceConnectionReuseconnParams", + "TraceDnsCacheHitParams", + "TraceDnsCacheMissParams", + "TraceDnsResolveHostEndParams", + "TraceDnsResolveHostStartParams", + "TraceRequestChunkSentParams", + "TraceRequestEndParams", + "TraceRequestExceptionParams", + "TraceRequestHeadersSentParams", + "TraceRequestRedirectParams", + "TraceRequestStartParams", + "TraceResponseChunkReceivedParams", + # workers (imported lazily with __getattr__) + "GunicornUVLoopWebWorker", + "GunicornWebWorker", + "WSMessageTypeError", +) + + +def __dir__() -> tuple[str, ...]: + return __all__ + ("__doc__",) + + +def __getattr__(name: str) -> object: + global GunicornUVLoopWebWorker, GunicornWebWorker + + # Importing gunicorn takes a long time (>100ms), so only import if actually needed. + if name in ("GunicornUVLoopWebWorker", "GunicornWebWorker"): + try: + from .worker import GunicornUVLoopWebWorker as guv, GunicornWebWorker as gw + except ImportError: + return None + + GunicornUVLoopWebWorker = guv # type: ignore[misc] + GunicornWebWorker = gw # type: ignore[misc] + return guv if name == "GunicornUVLoopWebWorker" else gw + + raise AttributeError(f"module {__name__} has no attribute {name}") diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a643a4f Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/_cookie_helpers.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/_cookie_helpers.cpython-311.pyc new file mode 100644 index 0000000..e343560 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/_cookie_helpers.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/abc.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/abc.cpython-311.pyc new file mode 100644 index 0000000..9d66f02 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/abc.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/base_protocol.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/base_protocol.cpython-311.pyc new file mode 100644 index 0000000..d76407c Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/base_protocol.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000..44c5246 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_exceptions.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_exceptions.cpython-311.pyc new file mode 100644 index 0000000..8c37c27 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_exceptions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_middleware_digest_auth.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_middleware_digest_auth.cpython-311.pyc new file mode 100644 index 0000000..97aab88 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_middleware_digest_auth.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_middlewares.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_middlewares.cpython-311.pyc new file mode 100644 index 0000000..64aeb95 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_middlewares.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_proto.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_proto.cpython-311.pyc new file mode 100644 index 0000000..6963eec Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_proto.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_reqrep.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_reqrep.cpython-311.pyc new file mode 100644 index 0000000..7b71d9c Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_reqrep.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_ws.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_ws.cpython-311.pyc new file mode 100644 index 0000000..990fdff Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/client_ws.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/compression_utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/compression_utils.cpython-311.pyc new file mode 100644 index 0000000..b8d1476 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/compression_utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/connector.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/connector.cpython-311.pyc new file mode 100644 index 0000000..fadc5bc Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/connector.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/cookiejar.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/cookiejar.cpython-311.pyc new file mode 100644 index 0000000..71788d2 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/cookiejar.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/formdata.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/formdata.cpython-311.pyc new file mode 100644 index 0000000..9bab728 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/formdata.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/hdrs.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/hdrs.cpython-311.pyc new file mode 100644 index 0000000..96dc60d Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/hdrs.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/helpers.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/helpers.cpython-311.pyc new file mode 100644 index 0000000..d012859 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/helpers.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http.cpython-311.pyc new file mode 100644 index 0000000..28e2bd1 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_exceptions.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_exceptions.cpython-311.pyc new file mode 100644 index 0000000..24726b7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_exceptions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_parser.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_parser.cpython-311.pyc new file mode 100644 index 0000000..117e946 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_parser.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_websocket.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_websocket.cpython-311.pyc new file mode 100644 index 0000000..2a3ab3c Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_websocket.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_writer.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_writer.cpython-311.pyc new file mode 100644 index 0000000..2aca0fd Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/http_writer.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/log.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/log.cpython-311.pyc new file mode 100644 index 0000000..8e4f00b Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/log.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/multipart.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/multipart.cpython-311.pyc new file mode 100644 index 0000000..c80d0f0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/multipart.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/payload.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/payload.cpython-311.pyc new file mode 100644 index 0000000..d8e8f1b Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/payload.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/payload_streamer.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/payload_streamer.cpython-311.pyc new file mode 100644 index 0000000..24c3cb5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/payload_streamer.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/pytest_plugin.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/pytest_plugin.cpython-311.pyc new file mode 100644 index 0000000..f4f5c80 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/pytest_plugin.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/resolver.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/resolver.cpython-311.pyc new file mode 100644 index 0000000..0786292 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/resolver.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/streams.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/streams.cpython-311.pyc new file mode 100644 index 0000000..0679b0f Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/streams.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/tcp_helpers.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/tcp_helpers.cpython-311.pyc new file mode 100644 index 0000000..17812aa Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/tcp_helpers.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/test_utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/test_utils.cpython-311.pyc new file mode 100644 index 0000000..c832eed Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/test_utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/tracing.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/tracing.cpython-311.pyc new file mode 100644 index 0000000..e0eaadf Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/tracing.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/typedefs.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/typedefs.cpython-311.pyc new file mode 100644 index 0000000..0dd1ccc Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/typedefs.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web.cpython-311.pyc new file mode 100644 index 0000000..d7e8fad Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_app.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_app.cpython-311.pyc new file mode 100644 index 0000000..13164d3 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_app.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_exceptions.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_exceptions.cpython-311.pyc new file mode 100644 index 0000000..4b22618 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_exceptions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_fileresponse.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_fileresponse.cpython-311.pyc new file mode 100644 index 0000000..f638eb7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_fileresponse.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_log.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_log.cpython-311.pyc new file mode 100644 index 0000000..a6a4eba Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_log.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_middlewares.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_middlewares.cpython-311.pyc new file mode 100644 index 0000000..004257d Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_middlewares.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_protocol.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_protocol.cpython-311.pyc new file mode 100644 index 0000000..4e0bdbf Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_protocol.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_request.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_request.cpython-311.pyc new file mode 100644 index 0000000..8467e14 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_request.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_response.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_response.cpython-311.pyc new file mode 100644 index 0000000..0c31fae Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_response.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_routedef.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_routedef.cpython-311.pyc new file mode 100644 index 0000000..6bc8c77 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_routedef.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_runner.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_runner.cpython-311.pyc new file mode 100644 index 0000000..5ad9414 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_runner.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_server.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_server.cpython-311.pyc new file mode 100644 index 0000000..5bb7cf3 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_server.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_urldispatcher.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_urldispatcher.cpython-311.pyc new file mode 100644 index 0000000..8560c71 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_urldispatcher.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_ws.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_ws.cpython-311.pyc new file mode 100644 index 0000000..337913b Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/web_ws.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/__pycache__/worker.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/worker.cpython-311.pyc new file mode 100644 index 0000000..1ebeca0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/__pycache__/worker.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_cookie_helpers.py b/venv/lib/python3.11/site-packages/aiohttp/_cookie_helpers.py new file mode 100644 index 0000000..00ca820 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_cookie_helpers.py @@ -0,0 +1,361 @@ +""" +Internal cookie handling helpers. + +This module contains internal utilities for cookie parsing and manipulation. +These are not part of the public API and may change without notice. +""" + +import re +from collections.abc import Sequence +from http.cookies import CookieError, Morsel +from typing import cast + +from .log import internal_logger + +__all__ = ( + "parse_set_cookie_headers", + "parse_cookie_header", + "preserve_morsel_with_coded_value", +) + +# Cookie parsing constants +# Allow more characters in cookie names to handle real-world cookies +# that don't strictly follow RFC standards (fixes #2683) +# RFC 6265 defines cookie-name token as per RFC 2616 Section 2.2, +# but many servers send cookies with characters like {} [] () etc. +# This makes the cookie parser more tolerant of real-world cookies +# while still providing some validation to catch obviously malformed names. +_COOKIE_NAME_RE = re.compile(r"^[!#$%&\'()*+\-./0-9:<=>?@A-Z\[\]^_`a-z{|}~]+$") +_COOKIE_KNOWN_ATTRS = frozenset( # AKA Morsel._reserved + ( + "path", + "domain", + "max-age", + "expires", + "secure", + "httponly", + "samesite", + "partitioned", + "version", + "comment", + ) +) +_COOKIE_BOOL_ATTRS = frozenset( # AKA Morsel._flags + ("secure", "httponly", "partitioned") +) + +# SimpleCookie's pattern for parsing cookies with relaxed validation +# Based on http.cookies pattern but extended to allow more characters in cookie names +# to handle real-world cookies (fixes #2683) +_COOKIE_PATTERN = re.compile( + r""" + \s* # Optional whitespace at start of cookie + (?P # Start of group 'key' + # aiohttp has extended to include [] for compatibility with real-world cookies + [\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\[\]]+ # 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 double-quoted string (properly closed) + | # or + "[^";]* # Unmatched opening quote (differs from SimpleCookie - issue #7993) + | # or + # Special case for "expires" attr - RFC 822, RFC 850, RFC 1036, RFC 1123 + (\w{3,6}day|\w{3}),\s # Day of the week or abbreviated day (with comma) + [\w\d\s-]{9,11}\s[\d:]{8}\s # Date and time in specific format + (GMT|[+-]\d{4}) # Timezone: GMT or RFC 2822 offset like -0000, +0100 + # NOTE: RFC 2822 timezone support is an aiohttp extension + # for issue #4493 - SimpleCookie does NOT support this + | # or + # ANSI C asctime() format: "Wed Jun 9 10:18:14 2021" + # NOTE: This is an aiohttp extension for issue #4327 - SimpleCookie does NOT support this format + \w{3}\s+\w{3}\s+[\s\d]\d\s+\d{2}:\d{2}:\d{2}\s+\d{4} + | # or + [\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=\[\]]* # 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.VERBOSE | re.ASCII, +) + + +def preserve_morsel_with_coded_value(cookie: Morsel[str]) -> Morsel[str]: + """ + Preserve a Morsel's coded_value exactly as received from the server. + + This function ensures that cookie encoding is preserved exactly as sent by + the server, which is critical for compatibility with old servers that have + strict requirements about cookie formats. + + This addresses the issue described in https://github.com/aio-libs/aiohttp/pull/1453 + where Python's SimpleCookie would re-encode cookies, breaking authentication + with certain servers. + + Args: + cookie: A Morsel object from SimpleCookie + + Returns: + A Morsel object with preserved coded_value + + """ + mrsl_val = cast("Morsel[str]", cookie.get(cookie.key, Morsel())) + # We use __setstate__ instead of the public set() API because it allows us to + # bypass validation and set already validated state. This is more stable than + # setting protected attributes directly and unlikely to change since it would + # break pickling. + try: + mrsl_val.__setstate__( # type: ignore[attr-defined] + { + "key": cookie.key, + "value": cookie.value, + "coded_value": cookie.coded_value, + } + ) + except CookieError: + return cookie + return mrsl_val + + +_unquote_sub = re.compile(r"\\(?:([0-3][0-7][0-7])|(.))").sub + + +def _unquote_replace(m: re.Match[str]) -> str: + """ + Replace function for _unquote_sub regex substitution. + + Handles escaped characters in cookie values: + - Octal sequences are converted to their character representation + - Other escaped characters are unescaped by removing the backslash + """ + if m[1]: + return chr(int(m[1], 8)) + return m[2] + + +def _unquote(value: str) -> str: + """ + Unquote a cookie value. + + Vendored from http.cookies._unquote to ensure compatibility. + + Note: The original implementation checked for None, but we've removed + that check since all callers already ensure the value is not None. + """ + # If there aren't any doublequotes, + # then there can't be any special characters. See RFC 2109. + if len(value) < 2: + return value + if value[0] != '"' or value[-1] != '"': + return value + + # We have to assume that we must decode this string. + # Down to work. + + # Remove the "s + value = value[1:-1] + + # Check for special sequences. Examples: + # \012 --> \n + # \" --> " + # + return _unquote_sub(_unquote_replace, value) + + +def parse_cookie_header(header: str) -> list[tuple[str, Morsel[str]]]: + """ + Parse a Cookie header according to RFC 6265 Section 5.4. + + Cookie headers contain only name-value pairs separated by semicolons. + There are no attributes in Cookie headers - even names that match + attribute names (like 'path' or 'secure') should be treated as cookies. + + This parser uses the same regex-based approach as parse_set_cookie_headers + to properly handle quoted values that may contain semicolons. When the + regex fails to match a malformed cookie, it falls back to simple parsing + to ensure subsequent cookies are not lost + https://github.com/aio-libs/aiohttp/issues/11632 + + Args: + header: The Cookie header value to parse + + Returns: + List of (name, Morsel) tuples for compatibility with SimpleCookie.update() + """ + if not header: + return [] + + cookies: list[tuple[str, Morsel[str]]] = [] + morsel: Morsel[str] + i = 0 + n = len(header) + + invalid_names = [] + while i < n: + # Use the same pattern as parse_set_cookie_headers to find cookies + match = _COOKIE_PATTERN.match(header, i) + if not match: + # Fallback for malformed cookies https://github.com/aio-libs/aiohttp/issues/11632 + # Find next semicolon to skip or attempt simple key=value parsing + next_semi = header.find(";", i) + eq_pos = header.find("=", i) + + # Try to extract key=value if '=' comes before ';' + if eq_pos != -1 and (next_semi == -1 or eq_pos < next_semi): + end_pos = next_semi if next_semi != -1 else n + key = header[i:eq_pos].strip() + value = header[eq_pos + 1 : end_pos].strip() + + # Validate the name (same as regex path) + if not _COOKIE_NAME_RE.match(key): + invalid_names.append(key) + else: + morsel = Morsel() + try: + morsel.__setstate__( # type: ignore[attr-defined] + { + "key": key, + "value": _unquote(value), + "coded_value": value, + } + ) + except CookieError: + pass + else: + cookies.append((key, morsel)) + + # Move to next cookie or end + i = next_semi + 1 if next_semi != -1 else n + continue + + key = match.group("key") + value = match.group("val") or "" + i = match.end(0) + + # Validate the name + if not key or not _COOKIE_NAME_RE.match(key): + invalid_names.append(key) + continue + + # Create new morsel + morsel = Morsel() + # Preserve the original value as coded_value (with quotes if present) + # We use __setstate__ instead of the public set() API because it allows us to + # bypass validation and set already validated state. This is more stable than + # setting protected attributes directly and unlikely to change since it would + # break pickling. + try: + morsel.__setstate__( # type: ignore[attr-defined] + {"key": key, "value": _unquote(value), "coded_value": value} + ) + except CookieError: + continue + + cookies.append((key, morsel)) + + if invalid_names: + internal_logger.debug( + "Cannot load cookie. Illegal cookie names: %r", invalid_names + ) + + return cookies + + +def parse_set_cookie_headers(headers: Sequence[str]) -> list[tuple[str, Morsel[str]]]: + """ + Parse cookie headers using a vendored version of SimpleCookie parsing. + + This implementation is based on SimpleCookie.__parse_string to ensure + compatibility with how SimpleCookie parses cookies, including handling + of malformed cookies with missing semicolons. + + This function is used for both Cookie and Set-Cookie headers in order to be + forgiving. Ideally we would have followed RFC 6265 Section 5.2 (for Cookie + headers) and RFC 6265 Section 4.2.1 (for Set-Cookie headers), but the + real world data makes it impossible since we need to be a bit more forgiving. + + NOTE: This implementation differs from SimpleCookie in handling unmatched quotes. + SimpleCookie will stop parsing when it encounters a cookie value with an unmatched + quote (e.g., 'cookie="value'), causing subsequent cookies to be silently dropped. + This implementation handles unmatched quotes more gracefully to prevent cookie loss. + See https://github.com/aio-libs/aiohttp/issues/7993 + """ + parsed_cookies: list[tuple[str, Morsel[str]]] = [] + + for header in headers: + if not header: + continue + + # Parse cookie string using SimpleCookie's algorithm + i = 0 + n = len(header) + current_morsel: Morsel[str] | None = None + morsel_seen = False + + while 0 <= i < n: + # Start looking for a cookie + match = _COOKIE_PATTERN.match(header, i) + if not match: + # No more cookies + break + + key, value = match.group("key"), match.group("val") + i = match.end(0) + lower_key = key.lower() + + if key[0] == "$": + if not morsel_seen: + # We ignore attributes which pertain to the cookie + # mechanism as a whole, such as "$Version". + continue + # Process as attribute + if current_morsel is not None: + attr_lower_key = lower_key[1:] + if attr_lower_key in _COOKIE_KNOWN_ATTRS: + current_morsel[attr_lower_key] = value or "" + elif lower_key in _COOKIE_KNOWN_ATTRS: + if not morsel_seen: + # Invalid cookie string - attribute before cookie + break + if lower_key in _COOKIE_BOOL_ATTRS: + # Boolean attribute with any value should be True + if current_morsel is not None and current_morsel.isReservedKey(key): + current_morsel[lower_key] = True + elif value is None: + # Invalid cookie string - non-boolean attribute without value + break + elif current_morsel is not None: + # Regular attribute with value + current_morsel[lower_key] = _unquote(value) + elif value is not None: + # This is a cookie name=value pair + # Validate the name + if key in _COOKIE_KNOWN_ATTRS or not _COOKIE_NAME_RE.match(key): + internal_logger.warning( + "Can not load cookies: Illegal cookie name %r", key + ) + current_morsel = None + else: + # Create new morsel + current_morsel = Morsel() + # Preserve the original value as coded_value (with quotes if present) + try: + current_morsel.__setstate__( # type: ignore[attr-defined] + { + "key": key, + "value": _unquote(value), + "coded_value": value, + } + ) + except CookieError: + current_morsel = None + else: + parsed_cookies.append((key, current_morsel)) + morsel_seen = True + else: + # Invalid cookie string - no value for non-attribute + break + + return parsed_cookies diff --git a/venv/lib/python3.11/site-packages/aiohttp/_cparser.pxd b/venv/lib/python3.11/site-packages/aiohttp/_cparser.pxd new file mode 100644 index 0000000..cc7ef58 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_cparser.pxd @@ -0,0 +1,159 @@ +from libc.stdint cimport int32_t, uint8_t, uint16_t, uint64_t + + +cdef extern from "llhttp.h": + + struct llhttp__internal_s: + int32_t _index + void* _span_pos0 + void* _span_cb0 + int32_t error + const char* reason + const char* error_pos + void* data + void* _current + uint64_t content_length + uint8_t type + uint8_t method + uint8_t http_major + uint8_t http_minor + uint8_t header_state + uint8_t lenient_flags + uint8_t upgrade + uint8_t finish + uint16_t flags + uint16_t status_code + void* settings + + ctypedef llhttp__internal_s llhttp__internal_t + ctypedef llhttp__internal_t llhttp_t + + ctypedef int (*llhttp_data_cb)(llhttp_t*, const char *at, size_t length) except -1 + ctypedef int (*llhttp_cb)(llhttp_t*) except -1 + + struct llhttp_settings_s: + llhttp_cb on_message_begin + llhttp_data_cb on_url + llhttp_data_cb on_status + llhttp_data_cb on_header_field + llhttp_data_cb on_header_value + llhttp_cb on_headers_complete + llhttp_data_cb on_body + llhttp_cb on_message_complete + llhttp_cb on_chunk_header + llhttp_cb on_chunk_complete + + llhttp_cb on_url_complete + llhttp_cb on_status_complete + llhttp_cb on_header_field_complete + llhttp_cb on_header_value_complete + + ctypedef llhttp_settings_s llhttp_settings_t + + enum llhttp_errno: + HPE_OK, + HPE_INTERNAL, + HPE_STRICT, + HPE_LF_EXPECTED, + HPE_UNEXPECTED_CONTENT_LENGTH, + HPE_CLOSED_CONNECTION, + HPE_INVALID_METHOD, + HPE_INVALID_URL, + HPE_INVALID_CONSTANT, + HPE_INVALID_VERSION, + HPE_INVALID_HEADER_TOKEN, + HPE_INVALID_CONTENT_LENGTH, + HPE_INVALID_CHUNK_SIZE, + HPE_INVALID_STATUS, + HPE_INVALID_EOF_STATE, + HPE_INVALID_TRANSFER_ENCODING, + HPE_CB_MESSAGE_BEGIN, + HPE_CB_HEADERS_COMPLETE, + HPE_CB_MESSAGE_COMPLETE, + HPE_CB_CHUNK_HEADER, + HPE_CB_CHUNK_COMPLETE, + HPE_PAUSED, + HPE_PAUSED_UPGRADE, + HPE_USER + + ctypedef llhttp_errno llhttp_errno_t + + enum llhttp_flags: + F_CHUNKED, + F_CONTENT_LENGTH + + enum llhttp_type: + HTTP_REQUEST, + HTTP_RESPONSE, + HTTP_BOTH + + enum llhttp_method: + HTTP_DELETE, + HTTP_GET, + HTTP_HEAD, + HTTP_POST, + HTTP_PUT, + HTTP_CONNECT, + HTTP_OPTIONS, + HTTP_TRACE, + HTTP_COPY, + HTTP_LOCK, + HTTP_MKCOL, + HTTP_MOVE, + HTTP_PROPFIND, + HTTP_PROPPATCH, + HTTP_SEARCH, + HTTP_UNLOCK, + HTTP_BIND, + HTTP_REBIND, + HTTP_UNBIND, + HTTP_ACL, + HTTP_REPORT, + HTTP_MKACTIVITY, + HTTP_CHECKOUT, + HTTP_MERGE, + HTTP_MSEARCH, + HTTP_NOTIFY, + HTTP_SUBSCRIBE, + HTTP_UNSUBSCRIBE, + HTTP_PATCH, + HTTP_PURGE, + HTTP_MKCALENDAR, + HTTP_LINK, + HTTP_UNLINK, + HTTP_SOURCE, + HTTP_PRI, + HTTP_DESCRIBE, + HTTP_ANNOUNCE, + HTTP_SETUP, + HTTP_PLAY, + HTTP_PAUSE, + HTTP_TEARDOWN, + HTTP_GET_PARAMETER, + HTTP_SET_PARAMETER, + HTTP_REDIRECT, + HTTP_RECORD, + HTTP_FLUSH + + ctypedef llhttp_method llhttp_method_t; + + void llhttp_settings_init(llhttp_settings_t* settings) + void llhttp_init(llhttp_t* parser, llhttp_type type, + const llhttp_settings_t* settings) + + llhttp_errno_t llhttp_execute(llhttp_t* parser, const char* data, size_t len) + + int llhttp_should_keep_alive(const llhttp_t* parser) + + void llhttp_resume(llhttp_t* parser) + void llhttp_resume_after_upgrade(llhttp_t* parser) + + llhttp_errno_t llhttp_get_errno(const llhttp_t* parser) + const char* llhttp_get_error_reason(const llhttp_t* parser) + const char* llhttp_get_error_pos(const llhttp_t* parser) + + const char* llhttp_method_name(llhttp_method_t method) + + void llhttp_set_lenient_headers(llhttp_t* parser, int enabled) + void llhttp_set_lenient_optional_cr_before_lf(llhttp_t* parser, int enabled) + void llhttp_set_lenient_spaces_after_chunk_size(llhttp_t* parser, int enabled) diff --git a/venv/lib/python3.11/site-packages/aiohttp/_find_header.pxd b/venv/lib/python3.11/site-packages/aiohttp/_find_header.pxd new file mode 100644 index 0000000..37a6c37 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_find_header.pxd @@ -0,0 +1,2 @@ +cdef extern from "_find_header.h": + int find_header(char *, int) diff --git a/venv/lib/python3.11/site-packages/aiohttp/_headers.pxi b/venv/lib/python3.11/site-packages/aiohttp/_headers.pxi new file mode 100644 index 0000000..3744721 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_headers.pxi @@ -0,0 +1,83 @@ +# The file is autogenerated from aiohttp/hdrs.py +# Run ./tools/gen.py to update it after the origin changing. + +from . import hdrs +cdef tuple headers = ( + hdrs.ACCEPT, + hdrs.ACCEPT_CHARSET, + hdrs.ACCEPT_ENCODING, + hdrs.ACCEPT_LANGUAGE, + hdrs.ACCEPT_RANGES, + hdrs.ACCESS_CONTROL_ALLOW_CREDENTIALS, + hdrs.ACCESS_CONTROL_ALLOW_HEADERS, + hdrs.ACCESS_CONTROL_ALLOW_METHODS, + hdrs.ACCESS_CONTROL_ALLOW_ORIGIN, + hdrs.ACCESS_CONTROL_EXPOSE_HEADERS, + hdrs.ACCESS_CONTROL_MAX_AGE, + hdrs.ACCESS_CONTROL_REQUEST_HEADERS, + hdrs.ACCESS_CONTROL_REQUEST_METHOD, + hdrs.AGE, + hdrs.ALLOW, + hdrs.AUTHORIZATION, + hdrs.CACHE_CONTROL, + hdrs.CONNECTION, + hdrs.CONTENT_DISPOSITION, + hdrs.CONTENT_ENCODING, + hdrs.CONTENT_LANGUAGE, + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_LOCATION, + hdrs.CONTENT_MD5, + hdrs.CONTENT_RANGE, + hdrs.CONTENT_TRANSFER_ENCODING, + hdrs.CONTENT_TYPE, + hdrs.COOKIE, + hdrs.DATE, + hdrs.DESTINATION, + hdrs.DIGEST, + hdrs.ETAG, + hdrs.EXPECT, + hdrs.EXPIRES, + hdrs.FORWARDED, + hdrs.FROM, + hdrs.HOST, + hdrs.IF_MATCH, + hdrs.IF_MODIFIED_SINCE, + hdrs.IF_NONE_MATCH, + hdrs.IF_RANGE, + hdrs.IF_UNMODIFIED_SINCE, + hdrs.KEEP_ALIVE, + hdrs.LAST_EVENT_ID, + hdrs.LAST_MODIFIED, + hdrs.LINK, + hdrs.LOCATION, + hdrs.MAX_FORWARDS, + hdrs.ORIGIN, + hdrs.PRAGMA, + hdrs.PROXY_AUTHENTICATE, + hdrs.PROXY_AUTHORIZATION, + hdrs.RANGE, + hdrs.REFERER, + hdrs.RETRY_AFTER, + hdrs.SEC_WEBSOCKET_ACCEPT, + hdrs.SEC_WEBSOCKET_EXTENSIONS, + hdrs.SEC_WEBSOCKET_KEY, + hdrs.SEC_WEBSOCKET_KEY1, + hdrs.SEC_WEBSOCKET_PROTOCOL, + hdrs.SEC_WEBSOCKET_VERSION, + hdrs.SERVER, + hdrs.SET_COOKIE, + hdrs.TE, + hdrs.TRAILER, + hdrs.TRANSFER_ENCODING, + hdrs.URI, + hdrs.UPGRADE, + hdrs.USER_AGENT, + hdrs.VARY, + hdrs.VIA, + hdrs.WWW_AUTHENTICATE, + hdrs.WANT_DIGEST, + hdrs.WARNING, + hdrs.X_FORWARDED_FOR, + hdrs.X_FORWARDED_HOST, + hdrs.X_FORWARDED_PROTO, +) diff --git a/venv/lib/python3.11/site-packages/aiohttp/_http_parser.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/aiohttp/_http_parser.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..a29e930 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_http_parser.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_http_parser.pyx b/venv/lib/python3.11/site-packages/aiohttp/_http_parser.pyx new file mode 100644 index 0000000..5ca1ccf --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_http_parser.pyx @@ -0,0 +1,965 @@ +# Based on https://github.com/MagicStack/httptools +# + +from cpython cimport ( + Py_buffer, + PyBUF_SIMPLE, + PyBuffer_Release, + PyBytes_AsString, + PyBytes_AsStringAndSize, + PyObject_GetBuffer, +) +from cpython.mem cimport PyMem_Free, PyMem_Malloc +from libc.limits cimport ULLONG_MAX +from libc.string cimport memcpy + +from multidict import CIMultiDict as _CIMultiDict, CIMultiDictProxy as _CIMultiDictProxy +from yarl import URL as _URL + +from aiohttp import hdrs +from aiohttp.helpers import DEBUG, set_exception + +from .http_exceptions import ( + BadHttpMessage, + BadHttpMethod, + BadStatusLine, + ContentLengthError, + InvalidHeader, + InvalidURLError, + LineTooLong, + PayloadEncodingError, + TransferEncodingError, +) +from .http_parser import DeflateBuffer as _DeflateBuffer +from .http_writer import ( + HttpVersion as _HttpVersion, + HttpVersion10 as _HttpVersion10, + HttpVersion11 as _HttpVersion11, +) +from .streams import EMPTY_PAYLOAD as _EMPTY_PAYLOAD, StreamReader as _StreamReader + +cimport cython + +from aiohttp cimport _cparser as cparser + +include "_headers.pxi" + +from aiohttp cimport _find_header + + +cdef frozenset ALLOWED_UPGRADES = frozenset({"websocket"}) +DEF DEFAULT_FREELIST_SIZE = 250 + +cdef extern from "Python.h": + int PyByteArray_Resize(object, Py_ssize_t) except -1 + Py_ssize_t PyByteArray_Size(object) except -1 + char* PyByteArray_AsString(object) + +__all__ = ('HttpRequestParser', 'HttpResponseParser', + 'RawRequestMessage', 'RawResponseMessage') + +cdef object URL = _URL +cdef object URL_build = URL.build +cdef object CIMultiDict = _CIMultiDict +cdef object CIMultiDictProxy = _CIMultiDictProxy +cdef object HttpVersion = _HttpVersion +cdef object HttpVersion10 = _HttpVersion10 +cdef object HttpVersion11 = _HttpVersion11 +cdef object SEC_WEBSOCKET_KEY1 = hdrs.SEC_WEBSOCKET_KEY1 +cdef object CONTENT_ENCODING = hdrs.CONTENT_ENCODING +cdef object EMPTY_PAYLOAD = _EMPTY_PAYLOAD +cdef object StreamReader = _StreamReader +cdef object DeflateBuffer = _DeflateBuffer +cdef tuple EMPTY_FEED_DATA_RESULT = ((), False, b"") + +# RFC 9110 singleton headers — duplicates are rejected in strict mode. +# In lax mode (response parser default), the check is skipped entirely +# since real-world servers (e.g. Google APIs, Werkzeug) commonly send +# duplicate headers like Content-Type or Server. +cdef frozenset SINGLETON_HEADERS = frozenset({ + hdrs.CONTENT_LENGTH, + hdrs.CONTENT_LOCATION, + hdrs.CONTENT_RANGE, + hdrs.CONTENT_TYPE, + hdrs.ETAG, + hdrs.HOST, + hdrs.MAX_FORWARDS, + hdrs.SERVER, + hdrs.TRANSFER_ENCODING, + hdrs.USER_AGENT, +}) + +cdef inline object extend(object buf, const char* at, size_t length): + cdef Py_ssize_t s + cdef char* ptr + s = PyByteArray_Size(buf) + PyByteArray_Resize(buf, s + length) + ptr = PyByteArray_AsString(buf) + memcpy(ptr + s, at, length) + + +DEF METHODS_COUNT = 46; + +cdef list _http_method = [] + +for i in range(METHODS_COUNT): + _http_method.append( + cparser.llhttp_method_name( i).decode('ascii')) + + +cdef inline str http_method_str(int i): + if i < METHODS_COUNT: + return _http_method[i] + else: + return "" + +cdef inline object find_header(bytes raw_header): + cdef Py_ssize_t size + cdef char *buf + cdef int idx + PyBytes_AsStringAndSize(raw_header, &buf, &size) + idx = _find_header.find_header(buf, size) + if idx == -1: + return raw_header.decode('utf-8', 'surrogateescape') + return headers[idx] + + +@cython.freelist(DEFAULT_FREELIST_SIZE) +cdef class RawRequestMessage: + cdef readonly str method + cdef readonly str path + cdef readonly object version # HttpVersion + cdef readonly object headers # CIMultiDict + cdef readonly object raw_headers # tuple + cdef readonly object should_close + cdef readonly object compression + cdef readonly object upgrade + cdef readonly object chunked + cdef readonly object url # yarl.URL + + def __init__(self, method, path, version, headers, raw_headers, + should_close, compression, upgrade, chunked, url): + self.method = method + self.path = path + self.version = version + self.headers = headers + self.raw_headers = raw_headers + self.should_close = should_close + self.compression = compression + self.upgrade = upgrade + self.chunked = chunked + self.url = url + + def __repr__(self): + info = [] + info.append(("method", self.method)) + info.append(("path", self.path)) + info.append(("version", self.version)) + info.append(("headers", self.headers)) + info.append(("raw_headers", self.raw_headers)) + info.append(("should_close", self.should_close)) + info.append(("compression", self.compression)) + info.append(("upgrade", self.upgrade)) + info.append(("chunked", self.chunked)) + info.append(("url", self.url)) + sinfo = ', '.join(name + '=' + repr(val) for name, val in info) + return '' + + def _replace(self, **dct): + cdef RawRequestMessage ret + ret = _new_request_message(self.method, + self.path, + self.version, + self.headers, + self.raw_headers, + self.should_close, + self.compression, + self.upgrade, + self.chunked, + self.url) + if "method" in dct: + ret.method = dct["method"] + if "path" in dct: + ret.path = dct["path"] + if "version" in dct: + ret.version = dct["version"] + if "headers" in dct: + ret.headers = dct["headers"] + if "raw_headers" in dct: + ret.raw_headers = dct["raw_headers"] + if "should_close" in dct: + ret.should_close = dct["should_close"] + if "compression" in dct: + ret.compression = dct["compression"] + if "upgrade" in dct: + ret.upgrade = dct["upgrade"] + if "chunked" in dct: + ret.chunked = dct["chunked"] + if "url" in dct: + ret.url = dct["url"] + return ret + +cdef _new_request_message(str method, + str path, + object version, + object headers, + object raw_headers, + bint should_close, + object compression, + bint upgrade, + bint chunked, + object url): + cdef RawRequestMessage ret + ret = RawRequestMessage.__new__(RawRequestMessage) + ret.method = method + ret.path = path + ret.version = version + ret.headers = headers + ret.raw_headers = raw_headers + ret.should_close = should_close + ret.compression = compression + ret.upgrade = upgrade + ret.chunked = chunked + ret.url = url + return ret + + +@cython.freelist(DEFAULT_FREELIST_SIZE) +cdef class RawResponseMessage: + cdef readonly object version # HttpVersion + cdef readonly int code + cdef readonly str reason + cdef readonly object headers # CIMultiDict + cdef readonly object raw_headers # tuple + cdef readonly object should_close + cdef readonly object compression + cdef readonly object upgrade + cdef readonly object chunked + + def __init__(self, version, code, reason, headers, raw_headers, + should_close, compression, upgrade, chunked): + self.version = version + self.code = code + self.reason = reason + self.headers = headers + self.raw_headers = raw_headers + self.should_close = should_close + self.compression = compression + self.upgrade = upgrade + self.chunked = chunked + + def __repr__(self): + info = [] + info.append(("version", self.version)) + info.append(("code", self.code)) + info.append(("reason", self.reason)) + info.append(("headers", self.headers)) + info.append(("raw_headers", self.raw_headers)) + info.append(("should_close", self.should_close)) + info.append(("compression", self.compression)) + info.append(("upgrade", self.upgrade)) + info.append(("chunked", self.chunked)) + sinfo = ', '.join(name + '=' + repr(val) for name, val in info) + return '' + + +cdef _new_response_message(object version, + int code, + str reason, + object headers, + object raw_headers, + bint should_close, + object compression, + bint upgrade, + bint chunked): + cdef RawResponseMessage ret + ret = RawResponseMessage.__new__(RawResponseMessage) + ret.version = version + ret.code = code + ret.reason = reason + ret.headers = headers + ret.raw_headers = raw_headers + ret.should_close = should_close + ret.compression = compression + ret.upgrade = upgrade + ret.chunked = chunked + return ret + + +@cython.internal +cdef class HttpParser: + + cdef: + cparser.llhttp_t* _cparser + cparser.llhttp_settings_t* _csettings + + bytes _raw_name + object _name + bytes _raw_value + bint _has_value + int _header_name_size + + readonly object protocol + object _loop + object _timer + + size_t _max_line_size + size_t _max_field_size + size_t _max_headers + bint _response_with_body + bint _read_until_eof + bint _lax + + bytes _tail + bint _started + object _url + bytearray _buf + str _path + str _reason + list _headers + set _seen_singletons + list _raw_headers + bint _upgraded + list _messages + bint _more_data_available + bint _paused + Py_ssize_t _msg_in_flight + Py_ssize_t _max_msg_queue_size + bint _eof_pending + object _payload + unsigned long long _content_length_expected + bint _payload_error + object _payload_exception + object _last_error + bint _auto_decompress + int _limit + + str _content_encoding + + Py_buffer py_buf + + def __cinit__(self): + self._cparser = \ + PyMem_Malloc(sizeof(cparser.llhttp_t)) + if self._cparser is NULL: + raise MemoryError() + + self._csettings = \ + PyMem_Malloc(sizeof(cparser.llhttp_settings_t)) + if self._csettings is NULL: + raise MemoryError() + + def __dealloc__(self): + PyMem_Free(self._cparser) + PyMem_Free(self._csettings) + + cdef _init( + self, cparser.llhttp_type mode, + object protocol, object loop, int limit, + object timer=None, + size_t max_line_size=8190, size_t max_headers=128, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True, + Py_ssize_t max_msg_queue_size=0, + ): + cparser.llhttp_settings_init(self._csettings) + cparser.llhttp_init(self._cparser, mode, self._csettings) + self._cparser.data = self + self._cparser.content_length = 0 + self._content_length_expected = 0 + + self.protocol = protocol + self._loop = loop + self._timer = timer + + self._buf = bytearray() + self._more_data_available = False + self._paused = False + self._msg_in_flight = 0 + self._max_msg_queue_size = max_msg_queue_size + self._eof_pending = False + self._payload = None + self._payload_error = 0 + self._payload_exception = payload_exception + self._messages = [] + + self._raw_name = b"" + self._raw_value = b"" + self._tail = b"" + self._has_value = False + self._header_name_size = 0 + + self._max_line_size = max_line_size + self._max_headers = max_headers + self._max_field_size = max_field_size + self._response_with_body = response_with_body + self._read_until_eof = read_until_eof + self._upgraded = False + self._auto_decompress = auto_decompress + self._content_encoding = None + self._lax = False + self._seen_singletons = set() + + self._csettings.on_url = cb_on_url + self._csettings.on_status = cb_on_status + self._csettings.on_header_field = cb_on_header_field + self._csettings.on_header_value = cb_on_header_value + self._csettings.on_headers_complete = cb_on_headers_complete + self._csettings.on_body = cb_on_body + self._csettings.on_message_begin = cb_on_message_begin + self._csettings.on_message_complete = cb_on_message_complete + self._csettings.on_chunk_header = cb_on_chunk_header + self._csettings.on_chunk_complete = cb_on_chunk_complete + + self._last_error = None + self._limit = limit + + cdef _process_header(self): + cdef str value + if self._raw_name != b"": + name = find_header(self._raw_name) + value = self._raw_value.decode('utf-8', 'surrogateescape') + + # reject null bytes in header values - matches the Python parser + # check at http_parser.py. llhttp in lenient mode doesn't reject + # these itself, so we need to catch them here. + # ref: RFC 9110 section 5.5 (CTL chars forbidden in field values) + if "\x00" in value: + raise InvalidHeader(self._raw_value) + + if not self._lax and name in SINGLETON_HEADERS: + if name in self._seen_singletons: + raise BadHttpMessage(f"Duplicate '{name}' header found.") + self._seen_singletons.add(name) + self._headers.append((name, value)) + if len(self._headers) > self._max_headers: + raise BadHttpMessage("Too many headers received") + + if name is CONTENT_ENCODING: + self._content_encoding = value + + self._has_value = False + self._header_name_size = 0 + self._raw_headers.append((self._raw_name, self._raw_value)) + self._raw_name = b"" + self._raw_value = b"" + + cdef _on_header_field(self, char* at, size_t length): + if self._has_value: + self._process_header() + + if self._raw_name == b"": + self._raw_name = at[:length] + else: + self._raw_name += at[:length] + + cdef _on_header_value(self, char* at, size_t length): + if self._raw_value == b"": + self._raw_value = at[:length] + else: + self._raw_value += at[:length] + self._has_value = True + + cdef _on_headers_complete(self): + cdef str h_upg + cdef str enc + + self._process_header() + + http_version = self.http_version() + should_close = not cparser.llhttp_should_keep_alive(self._cparser) + upgrade = self._cparser.upgrade + chunked = self._cparser.flags & cparser.F_CHUNKED + + raw_headers = tuple(self._raw_headers) + headers = CIMultiDictProxy(CIMultiDict(self._headers)) + + if self._cparser.type == cparser.HTTP_REQUEST: + if http_version == HttpVersion11 and hdrs.HOST not in headers: + raise BadHttpMessage("Missing 'Host' header in request.") + h_upg = headers.get("upgrade", "") + if (upgrade and h_upg.isascii() and h_upg.lower() in ALLOWED_UPGRADES) or self._cparser.method == cparser.HTTP_CONNECT: + self._upgraded = True + else: + if upgrade and self._cparser.status_code == 101: + self._upgraded = True + + # do not support old websocket spec + if SEC_WEBSOCKET_KEY1 in headers: + raise InvalidHeader(SEC_WEBSOCKET_KEY1) + + encoding = None + enc = self._content_encoding + if enc is not None: + self._content_encoding = None + if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}: + encoding = enc + + if self._cparser.type == cparser.HTTP_REQUEST: + method = http_method_str(self._cparser.method) + msg = _new_request_message( + method, self._path, + http_version, headers, raw_headers, + should_close, encoding, upgrade, chunked, self._url) + else: + msg = _new_response_message( + http_version, self._cparser.status_code, self._reason, + headers, raw_headers, should_close, encoding, + upgrade, chunked) + + if ( + self._response_with_body + and ( + ULLONG_MAX > self._cparser.content_length > 0 or chunked or + self._cparser.method == cparser.HTTP_CONNECT or + (self._cparser.status_code >= 199 and + self._cparser.content_length == 0 and + self._read_until_eof) + ) + ): + payload = StreamReader( + self.protocol, timer=self._timer, loop=self._loop, + limit=self._limit) + else: + payload = EMPTY_PAYLOAD + + self._payload = payload + self._content_length_expected = self._cparser.content_length + if encoding is not None and self._auto_decompress: + self._payload = DeflateBuffer(payload, encoding, max_decompress_size=self._limit) + + self._messages.append((msg, payload)) + + cdef _on_message_complete(self): + self._payload.feed_eof() + self._payload = None + + cdef _on_chunk_header(self): + self._payload.begin_http_chunk_receiving() + + cdef _on_chunk_complete(self): + self._payload.end_http_chunk_receiving() + + cdef object _on_status_complete(self): + pass + + cdef inline http_version(self): + cdef cparser.llhttp_t* parser = self._cparser + + if parser.http_major == 1: + if parser.http_minor == 0: + return HttpVersion10 + elif parser.http_minor == 1: + return HttpVersion11 + + return HttpVersion(parser.http_major, parser.http_minor) + + ### Public API ### + + def pause_reading(self): + assert self._payload is not None + self._paused = True + + def message_consumed(self): + # Protocol drained a queued message; free a slot for parsing. + if self._msg_in_flight > 0: + self._msg_in_flight -= 1 + + def feed_eof(self): + cdef bytes desc + + if self._payload is not None: + if self._cparser.flags & cparser.F_CHUNKED: + raise TransferEncodingError( + "Not enough data to satisfy transfer length header.") + elif self._cparser.flags & cparser.F_CONTENT_LENGTH: + received = self._content_length_expected - self._cparser.content_length + raise ContentLengthError( + f"Not enough data to satisfy content length header " + f"(received {received} of {self._content_length_expected} bytes).") + elif cparser.llhttp_get_errno(self._cparser) != cparser.HPE_OK: + desc = cparser.llhttp_get_error_reason(self._cparser) + raise PayloadEncodingError(desc.decode('latin-1')) + else: + self._eof_pending = True + while self._more_data_available: + if self._paused: + self._paused = False + return # Will resume via feed_data(b"") later + self._more_data_available = self._payload.feed_data(b"", 0) + self._payload.feed_eof() + self._payload = None + self._more_data_available = False + self._eof_pending = False + elif self._started: + self._on_headers_complete() + if self._messages: + return self._messages[-1][0] + + def feed_data(self, incoming_data): + cdef: + size_t data_len + size_t nb + char* base + cdef cparser.llhttp_errno_t errno + cdef bytes data + + # Proactor loop sends bytearray. + # Ensure cython sees `data` as bytes + if type(incoming_data) is not bytes: + data = bytes(incoming_data) + else: + data = incoming_data + + if self._tail: + data, self._tail = self._tail + data, b"" + + if self._more_data_available: + result = cb_on_body(self._cparser, b"", 0) + if result is cparser.HPE_PAUSED: + self._tail = data + return EMPTY_FEED_DATA_RESULT + + if self._eof_pending: + self._payload.feed_eof() + self._payload = None + self._eof_pending = False + # We can't have new messages here, otherwise we wouldn't have + # received EOF. + return EMPTY_FEED_DATA_RESULT + + PyObject_GetBuffer(data, &self.py_buf, PyBUF_SIMPLE) + # Cache buffer pointer before PyBuffer_Release to avoid use-after-release. + base = self.py_buf.buf + data_len = self.py_buf.len + + errno = cparser.llhttp_execute( + self._cparser, + base, + data_len) + + if errno is cparser.HPE_PAUSED_UPGRADE: + cparser.llhttp_resume_after_upgrade(self._cparser) + nb = cparser.llhttp_get_error_pos(self._cparser) - base + elif errno is cparser.HPE_PAUSED: + cparser.llhttp_resume(self._cparser) + pos = cparser.llhttp_get_error_pos(self._cparser) - base + self._tail = data[pos:] + + PyBuffer_Release(&self.py_buf) + + if errno not in (cparser.HPE_OK, cparser.HPE_PAUSED, cparser.HPE_PAUSED_UPGRADE): + if self._payload_error == 0: + if self._last_error is not None: + ex = self._last_error + self._last_error = None + else: + after = cparser.llhttp_get_error_pos(self._cparser) + before = data[:after - base] + after_b = after.split(b"\r\n", 1)[0] + before = before.rsplit(b"\r\n", 1)[-1] + data = before + after_b + pointer = " " * (len(repr(before))-1) + "^" + ex = parser_error_from_errno(self._cparser, data, pointer) + self._payload = None + raise ex + + if self._messages: + messages = self._messages + self._messages = [] + else: + messages = () + + if self._upgraded: + return messages, True, data[nb:] + if not messages: # Shortcut to reduce Python overhead + return EMPTY_FEED_DATA_RESULT + return messages, False, b"" + + def set_upgraded(self, val): + self._upgraded = val + + +cdef class HttpRequestParser(HttpParser): + + def __init__( + self, protocol, loop, int limit, timer=None, + size_t max_line_size=8190, size_t max_headers=128, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True, Py_ssize_t max_msg_queue_size=0, + ): + self._init(cparser.HTTP_REQUEST, protocol, loop, limit, timer, + max_line_size, max_headers, max_field_size, + payload_exception, response_with_body, read_until_eof, + auto_decompress, max_msg_queue_size) + + cdef object _on_status_complete(self): + cdef int idx1, idx2 + if not self._buf: + return + self._path = self._buf.decode('utf-8', 'surrogateescape') + try: + idx3 = len(self._path) + if self._cparser.method == cparser.HTTP_CONNECT: + # authority-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 + self._url = URL.build(authority=self._path, encoded=True) + elif idx3 > 1 and self._path[0] == '/': + # origin-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 + idx1 = self._path.find("?") + if idx1 == -1: + query = "" + idx2 = self._path.find("#") + if idx2 == -1: + path = self._path + fragment = "" + else: + path = self._path[0: idx2] + fragment = self._path[idx2+1:] + + else: + path = self._path[0:idx1] + idx1 += 1 + idx2 = self._path.find("#", idx1+1) + if idx2 == -1: + query = self._path[idx1:] + fragment = "" + else: + query = self._path[idx1: idx2] + fragment = self._path[idx2+1:] + + self._url = URL.build( + path=path, + query_string=query, + fragment=fragment, + encoded=True, + ) + else: + # absolute-form for proxy maybe, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 + self._url = URL(self._path, encoded=True) + finally: + PyByteArray_Resize(self._buf, 0) + + +cdef class HttpResponseParser(HttpParser): + + def __init__( + self, protocol, loop, int limit, timer=None, + size_t max_line_size=8190, size_t max_headers=128, + size_t max_field_size=8190, payload_exception=None, + bint response_with_body=True, bint read_until_eof=False, + bint auto_decompress=True + ): + self._init(cparser.HTTP_RESPONSE, protocol, loop, limit, timer, + max_line_size, max_headers, max_field_size, + payload_exception, response_with_body, read_until_eof, + auto_decompress) + # Use strict parsing on dev mode, so users are warned about broken servers. + if not DEBUG: + cparser.llhttp_set_lenient_headers(self._cparser, 1) + cparser.llhttp_set_lenient_optional_cr_before_lf(self._cparser, 1) + cparser.llhttp_set_lenient_spaces_after_chunk_size(self._cparser, 1) + self._lax = True + + cdef object _on_status_complete(self): + if self._buf: + self._reason = self._buf.decode('utf-8', 'surrogateescape') + PyByteArray_Resize(self._buf, 0) + else: + self._reason = self._reason or '' + +cdef int cb_on_message_begin(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + + pyparser._started = True + pyparser._headers = [] + pyparser._seen_singletons = set() + pyparser._raw_headers = [] + PyByteArray_Resize(pyparser._buf, 0) + pyparser._path = None + pyparser._reason = None + return 0 + + +cdef int cb_on_url(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + try: + if len(pyparser._buf) + length > pyparser._max_line_size: + status = pyparser._buf + at[:length] + raise LineTooLong(status[:100] + b"...", pyparser._max_line_size) + extend(pyparser._buf, at, length) + except BaseException as ex: + pyparser._last_error = ex + return -1 + else: + return 0 + + +cdef int cb_on_status(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + try: + if len(pyparser._buf) + length > pyparser._max_line_size: + reason = pyparser._buf + at[:length] + raise LineTooLong(reason[:100] + b"...", pyparser._max_line_size) + extend(pyparser._buf, at, length) + except BaseException as ex: + pyparser._last_error = ex + return -1 + else: + return 0 + + +cdef int cb_on_header_field(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + cdef Py_ssize_t size + try: + pyparser._on_status_complete() + size = len(pyparser._raw_name) + length + if size > pyparser._max_field_size: + name = pyparser._raw_name + at[:length] + raise LineTooLong(name[:100] + b"...", pyparser._max_field_size) + pyparser._header_name_size = size + pyparser._on_header_field(at, length) + except BaseException as ex: + pyparser._last_error = ex + return -1 + else: + return 0 + + +cdef int cb_on_header_value(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + cdef Py_ssize_t size + try: + size = len(pyparser._raw_value) + length + if pyparser._header_name_size + size > pyparser._max_field_size: + value = pyparser._raw_value + at[:length] + raise LineTooLong(value[:100] + b"...", pyparser._max_field_size) + pyparser._on_header_value(at, length) + except BaseException as ex: + pyparser._last_error = ex + return -1 + else: + return 0 + + +cdef int cb_on_headers_complete(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + try: + pyparser._on_status_complete() + pyparser._on_headers_complete() + except BaseException as exc: + pyparser._last_error = exc + return -1 + else: + if pyparser._upgraded or pyparser._cparser.method == cparser.HTTP_CONNECT: + return 2 + if not pyparser._response_with_body: + return 1 + return 0 + + +cdef int cb_on_body(cparser.llhttp_t* parser, + const char *at, size_t length) except -1: + cdef HttpParser pyparser = parser.data + cdef bytes body = at[:length] + while body or pyparser._more_data_available: + try: + pyparser._more_data_available = pyparser._payload.feed_data(body, length) + except BaseException as underlying_exc: + reraised_exc = underlying_exc + if pyparser._payload_exception is not None: + reraised_exc = pyparser._payload_exception(str(underlying_exc)) + + set_exception(pyparser._payload, reraised_exc, underlying_exc) + + pyparser._payload_error = 1 + pyparser._paused = False + return -1 + body = b"" + length = 0 + + if pyparser._paused: + pyparser._paused = False + return cparser.HPE_PAUSED + pyparser._paused = False + return 0 + + +cdef int cb_on_message_complete(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + try: + pyparser._started = False + pyparser._on_message_complete() + except BaseException as exc: + pyparser._last_error = exc + return -1 + else: + if pyparser._max_msg_queue_size: + pyparser._msg_in_flight += 1 + if pyparser._msg_in_flight >= pyparser._max_msg_queue_size: + # Queue full: pause llhttp between messages. feed_data() buffers + # the remainder as tail; resumes once the queue drains. + return cparser.HPE_PAUSED + return 0 + + +cdef int cb_on_chunk_header(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + try: + pyparser._on_chunk_header() + except BaseException as exc: + pyparser._last_error = exc + return -1 + else: + return 0 + + +cdef int cb_on_chunk_complete(cparser.llhttp_t* parser) except -1: + cdef HttpParser pyparser = parser.data + try: + pyparser._on_chunk_complete() + except BaseException as exc: + pyparser._last_error = exc + return -1 + else: + return 0 + + +cdef parser_error_from_errno(cparser.llhttp_t* parser, data, pointer): + cdef cparser.llhttp_errno_t errno = cparser.llhttp_get_errno(parser) + cdef bytes desc = cparser.llhttp_get_error_reason(parser) + + err_msg = "{}:\n\n {!r}\n {}".format(desc.decode("latin-1"), data, pointer) + + if errno in {cparser.HPE_CB_MESSAGE_BEGIN, + cparser.HPE_CB_HEADERS_COMPLETE, + cparser.HPE_CB_MESSAGE_COMPLETE, + cparser.HPE_CB_CHUNK_HEADER, + cparser.HPE_CB_CHUNK_COMPLETE, + cparser.HPE_INVALID_HEADER_TOKEN, + cparser.HPE_INVALID_CONTENT_LENGTH, + cparser.HPE_INVALID_CHUNK_SIZE, + cparser.HPE_INVALID_EOF_STATE, + cparser.HPE_INVALID_TRANSFER_ENCODING}: + return BadHttpMessage(err_msg) + elif errno == cparser.HPE_INVALID_METHOD: + if data.startswith(b"\x16\x03"): + return BadHttpMethod(error="Received HTTPS traffic on an HTTP port") + return BadHttpMethod(error=err_msg) + elif errno in {cparser.HPE_INVALID_STATUS, + cparser.HPE_INVALID_VERSION, + cparser.HPE_INVALID_CONSTANT}: + return BadStatusLine(error=f"Bad status line:\n {err_msg}") + elif errno == cparser.HPE_INVALID_URL: + return InvalidURLError(err_msg) + + return BadHttpMessage(err_msg) diff --git a/venv/lib/python3.11/site-packages/aiohttp/_http_writer.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/aiohttp/_http_writer.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..2443b5f Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_http_writer.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_http_writer.pyx b/venv/lib/python3.11/site-packages/aiohttp/_http_writer.pyx new file mode 100644 index 0000000..7859ebd --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_http_writer.pyx @@ -0,0 +1,164 @@ +from cpython.bytes cimport PyBytes_FromStringAndSize +from cpython.exc cimport PyErr_NoMemory +from cpython.mem cimport PyMem_Free, PyMem_Malloc, PyMem_Realloc +from cpython.object cimport PyObject_Str +from libc.stdint cimport uint8_t, uint64_t +from libc.string cimport memcpy + +from multidict import istr + +DEF BUF_SIZE = 16 * 1024 # 16KiB + +cdef object _istr = istr + + +# ----------------- writer --------------------------- + +cdef struct Writer: + char *buf + Py_ssize_t size + Py_ssize_t pos + bint heap_allocated + +cdef inline void _init_writer(Writer* writer, char *buf): + writer.buf = buf + writer.size = BUF_SIZE + writer.pos = 0 + writer.heap_allocated = 0 + + +cdef inline void _release_writer(Writer* writer): + if writer.heap_allocated: + PyMem_Free(writer.buf) + + +cdef inline int _write_byte(Writer* writer, uint8_t ch): + cdef char * buf + cdef Py_ssize_t size + + if writer.pos == writer.size: + # reallocate + size = writer.size + BUF_SIZE + if not writer.heap_allocated: + buf = PyMem_Malloc(size) + if buf == NULL: + PyErr_NoMemory() + return -1 + memcpy(buf, writer.buf, writer.size) + else: + buf = PyMem_Realloc(writer.buf, size) + if buf == NULL: + PyErr_NoMemory() + return -1 + writer.buf = buf + writer.size = size + writer.heap_allocated = 1 + writer.buf[writer.pos] = ch + writer.pos += 1 + return 0 + + +cdef inline int _write_utf8(Writer* writer, Py_UCS4 symbol): + cdef uint64_t utf = symbol + + if utf < 0x80: + return _write_byte(writer, utf) + elif utf < 0x800: + if _write_byte(writer, (0xc0 | (utf >> 6))) < 0: + return -1 + return _write_byte(writer, (0x80 | (utf & 0x3f))) + elif 0xD800 <= utf <= 0xDFFF: + # surogate pair, ignored + return 0 + elif utf < 0x10000: + if _write_byte(writer, (0xe0 | (utf >> 12))) < 0: + return -1 + if _write_byte(writer, (0x80 | ((utf >> 6) & 0x3f))) < 0: + return -1 + return _write_byte(writer, (0x80 | (utf & 0x3f))) + elif utf > 0x10FFFF: + # symbol is too large + return 0 + else: + if _write_byte(writer, (0xf0 | (utf >> 18))) < 0: + return -1 + if _write_byte(writer, + (0x80 | ((utf >> 12) & 0x3f))) < 0: + return -1 + if _write_byte(writer, + (0x80 | ((utf >> 6) & 0x3f))) < 0: + return -1 + return _write_byte(writer, (0x80 | (utf & 0x3f))) + + +cdef inline int _write_str(Writer* writer, str s): + cdef Py_UCS4 ch + for ch in s: + if _write_utf8(writer, ch) < 0: + return -1 + + +cdef inline int _write_str_raise_on_nlcr(Writer* writer, object s): + cdef Py_UCS4 ch + cdef str out_str + if type(s) is str: + out_str = s + elif type(s) is _istr: + out_str = PyObject_Str(s) + elif not isinstance(s, str): + raise TypeError("Cannot serialize non-str key {!r}".format(s)) + else: + out_str = str(s) + + for ch in out_str: + # https://www.rfc-editor.org/info/rfc9110/#section-5.5-5 + # https://www.rfc-editor.org/info/rfc9112/#section-4-3 + if (ch < 0x20 and ch != 0x09) or ch == 0x7F: + raise ValueError( + "Forbidden control character detected in headers. " + "Potential header injection attack." + ) + if _write_utf8(writer, ch) < 0: + return -1 + + +# --------------- _serialize_headers ---------------------- + +def _serialize_headers(str status_line, headers): + cdef Writer writer + cdef object key + cdef object val + cdef char buf[BUF_SIZE] + + _init_writer(&writer, buf) + + try: + if _write_str_raise_on_nlcr(&writer, status_line) < 0: + raise + if _write_byte(&writer, b'\r') < 0: + raise + if _write_byte(&writer, b'\n') < 0: + raise + + for key, val in headers.items(): + if _write_str_raise_on_nlcr(&writer, key) < 0: + raise + if _write_byte(&writer, b':') < 0: + raise + if _write_byte(&writer, b' ') < 0: + raise + if _write_str_raise_on_nlcr(&writer, val) < 0: + raise + if _write_byte(&writer, b'\r') < 0: + raise + if _write_byte(&writer, b'\n') < 0: + raise + + if _write_byte(&writer, b'\r') < 0: + raise + if _write_byte(&writer, b'\n') < 0: + raise + + return PyBytes_FromStringAndSize(writer.buf, writer.pos) + finally: + _release_writer(&writer) diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/.hash/mask.pxd.hash b/venv/lib/python3.11/site-packages/aiohttp/_websocket/.hash/mask.pxd.hash new file mode 100644 index 0000000..eadfed3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/.hash/mask.pxd.hash @@ -0,0 +1 @@ +b01999d409b29bd916e067bc963d5f2d9ee63cfc9ae0bccb769910131417bf93 /home/runner/work/aiohttp/aiohttp/aiohttp/_websocket/mask.pxd diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/.hash/mask.pyx.hash b/venv/lib/python3.11/site-packages/aiohttp/_websocket/.hash/mask.pyx.hash new file mode 100644 index 0000000..5cd7ae6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/.hash/mask.pyx.hash @@ -0,0 +1 @@ +0478ceb55d0ed30ef1a7da742cd003449bc69a07cf9fdb06789bd2b347cbfffe /home/runner/work/aiohttp/aiohttp/aiohttp/_websocket/mask.pyx diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/.hash/reader_c.pxd.hash b/venv/lib/python3.11/site-packages/aiohttp/_websocket/.hash/reader_c.pxd.hash new file mode 100644 index 0000000..3356452 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/.hash/reader_c.pxd.hash @@ -0,0 +1 @@ +97e3831a92693b1e05c69b02b644722139a646f065468f26bfceea36079065ba /home/runner/work/aiohttp/aiohttp/aiohttp/_websocket/reader_c.pxd diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/__init__.py b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__init__.py new file mode 100644 index 0000000..836257c --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__init__.py @@ -0,0 +1 @@ +"""WebSocket protocol versions 13 and 8.""" diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..b5f2b44 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/helpers.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/helpers.cpython-311.pyc new file mode 100644 index 0000000..37d09ae Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/helpers.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/models.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..ab457f9 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/models.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/reader.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/reader.cpython-311.pyc new file mode 100644 index 0000000..d85df16 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/reader.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/reader_c.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/reader_c.cpython-311.pyc new file mode 100644 index 0000000..3f11cce Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/reader_c.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/reader_py.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/reader_py.cpython-311.pyc new file mode 100644 index 0000000..b48c1ba Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/reader_py.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/writer.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/writer.cpython-311.pyc new file mode 100644 index 0000000..59316a0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_websocket/__pycache__/writer.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/helpers.py b/venv/lib/python3.11/site-packages/aiohttp/_websocket/helpers.py new file mode 100644 index 0000000..6a42a00 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/helpers.py @@ -0,0 +1,148 @@ +"""Helpers for WebSocket protocol versions 13 and 8.""" + +import functools +import re +from re import Pattern +from struct import Struct +from typing import TYPE_CHECKING, Final + +from ..helpers import NO_EXTENSIONS +from .models import WSHandshakeError + +UNPACK_LEN3 = Struct("!Q").unpack_from +UNPACK_CLOSE_CODE = Struct("!H").unpack +PACK_LEN1 = Struct("!BB").pack +PACK_LEN2 = Struct("!BBH").pack +PACK_LEN3 = Struct("!BBQ").pack +PACK_CLOSE_CODE = Struct("!H").pack +PACK_RANDBITS = Struct("!L").pack +MSG_SIZE: Final[int] = 2**14 +MASK_LEN: Final[int] = 4 + +WS_KEY: Final[bytes] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11" + + +# Used by _websocket_mask_python +@functools.lru_cache +def _xor_table() -> list[bytes]: + return [bytes(a ^ b for a in range(256)) for b in range(256)] + + +def _websocket_mask_python(mask: bytes, data: bytearray) -> None: + """Websocket masking function. + + `mask` is a `bytes` object of length 4; `data` is a `bytearray` + object of any length. The contents of `data` are masked with `mask`, + as specified in section 5.3 of RFC 6455. + + Note that this function mutates the `data` argument. + + This pure-python implementation may be replaced by an optimized + version when available. + + """ + assert isinstance(data, bytearray), data + assert len(mask) == 4, mask + + if data: + _XOR_TABLE = _xor_table() + a, b, c, d = (_XOR_TABLE[n] for n in mask) + data[::4] = data[::4].translate(a) + data[1::4] = data[1::4].translate(b) + data[2::4] = data[2::4].translate(c) + data[3::4] = data[3::4].translate(d) + + +if TYPE_CHECKING or NO_EXTENSIONS: # pragma: no cover + websocket_mask = _websocket_mask_python +else: + try: + from .mask import _websocket_mask_cython # type: ignore[import-not-found] + + websocket_mask = _websocket_mask_cython + except ImportError: # pragma: no cover + websocket_mask = _websocket_mask_python + + +_WS_EXT_RE: Final[Pattern[str]] = re.compile( + r"^(?:;\s*(?:" + r"(server_no_context_takeover)|" + r"(client_no_context_takeover)|" + r"(server_max_window_bits(?:=(\d+))?)|" + r"(client_max_window_bits(?:=(\d+))?)))*$" +) + +_WS_EXT_RE_SPLIT: Final[Pattern[str]] = re.compile(r"permessage-deflate([^,]+)?") + + +def ws_ext_parse(extstr: str | None, isserver: bool = False) -> tuple[int, bool]: + if not extstr: + return 0, False + + compress = 0 + notakeover = False + for ext in _WS_EXT_RE_SPLIT.finditer(extstr): + defext = ext.group(1) + # Return compress = 15 when get `permessage-deflate` + if not defext: + compress = 15 + break + match = _WS_EXT_RE.match(defext) + if match: + compress = 15 + if isserver: + # Server never fail to detect compress handshake. + # Server does not need to send max wbit to client + if match.group(4): + compress = int(match.group(4)) + # Group3 must match if group4 matches + # Compress wbit 8 does not support in zlib + # If compress level not support, + # CONTINUE to next extension + if compress > 15 or compress < 9: + compress = 0 + continue + if match.group(1): + notakeover = True + # Ignore regex group 5 & 6 for client_max_window_bits + break + else: + if match.group(6): + compress = int(match.group(6)) + # Group5 must match if group6 matches + # Compress wbit 8 does not support in zlib + # If compress level not support, + # FAIL the parse progress + if compress > 15 or compress < 9: + raise WSHandshakeError("Invalid window size") + if match.group(2): + notakeover = True + # Ignore regex group 5 & 6 for client_max_window_bits + break + # Return Fail if client side and not match + elif not isserver: + raise WSHandshakeError("Extension for deflate not supported" + ext.group(1)) + + return compress, notakeover + + +def ws_ext_gen( + compress: int = 15, isserver: bool = False, server_notakeover: bool = False +) -> str: + # client_notakeover=False not used for server + # compress wbit 8 does not support in zlib + if compress < 9 or compress > 15: + raise ValueError( + "Compress wbits must between 9 and 15, zlib does not support wbits=8" + ) + enabledext = ["permessage-deflate"] + if not isserver: + enabledext.append("client_max_window_bits") + + if compress < 15: + enabledext.append("server_max_window_bits=" + str(compress)) + if server_notakeover: + enabledext.append("server_no_context_takeover") + # if client_notakeover: + # enabledext.append('client_no_context_takeover') + return "; ".join(enabledext) diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/mask.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/aiohttp/_websocket/mask.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..5092599 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_websocket/mask.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/mask.pxd b/venv/lib/python3.11/site-packages/aiohttp/_websocket/mask.pxd new file mode 100644 index 0000000..90983de --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/mask.pxd @@ -0,0 +1,3 @@ +"""Cython declarations for websocket masking.""" + +cpdef void _websocket_mask_cython(bytes mask, bytearray data) diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/mask.pyx b/venv/lib/python3.11/site-packages/aiohttp/_websocket/mask.pyx new file mode 100644 index 0000000..2d956c8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/mask.pyx @@ -0,0 +1,48 @@ +from cpython cimport PyBytes_AsString + + +#from cpython cimport PyByteArray_AsString # cython still not exports that +cdef extern from "Python.h": + char* PyByteArray_AsString(bytearray ba) except NULL + +from libc.stdint cimport uint32_t, uint64_t, uintmax_t + + +cpdef void _websocket_mask_cython(bytes mask, bytearray data): + """Note, this function mutates its `data` argument + """ + cdef: + Py_ssize_t data_len, i + # bit operations on signed integers are implementation-specific + unsigned char * in_buf + const unsigned char * mask_buf + uint32_t uint32_msk + uint64_t uint64_msk + + assert len(mask) == 4 + + data_len = len(data) + in_buf = PyByteArray_AsString(data) + mask_buf = PyBytes_AsString(mask) + uint32_msk = (mask_buf)[0] + + # TODO: align in_data ptr to achieve even faster speeds + # does it need in python ?! malloc() always aligns to sizeof(long) bytes + + if sizeof(size_t) >= 8: + uint64_msk = uint32_msk + uint64_msk = (uint64_msk << 32) | uint32_msk + + while data_len >= 8: + (in_buf)[0] ^= uint64_msk + in_buf += 8 + data_len -= 8 + + + while data_len >= 4: + (in_buf)[0] ^= uint32_msk + in_buf += 4 + data_len -= 4 + + for i in range(0, data_len): + in_buf[i] ^= mask_buf[i] diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/models.py b/venv/lib/python3.11/site-packages/aiohttp/_websocket/models.py new file mode 100644 index 0000000..3cdaa92 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/models.py @@ -0,0 +1,107 @@ +"""Models for WebSocket protocol versions 13 and 8.""" + +import json +from collections.abc import Callable +from enum import IntEnum +from typing import Any, Final, NamedTuple, cast + +WS_DEFLATE_TRAILING: Final[bytes] = bytes([0x00, 0x00, 0xFF, 0xFF]) + + +class WSCloseCode(IntEnum): + OK = 1000 + GOING_AWAY = 1001 + PROTOCOL_ERROR = 1002 + UNSUPPORTED_DATA = 1003 + ABNORMAL_CLOSURE = 1006 + INVALID_TEXT = 1007 + POLICY_VIOLATION = 1008 + MESSAGE_TOO_BIG = 1009 + MANDATORY_EXTENSION = 1010 + INTERNAL_ERROR = 1011 + SERVICE_RESTART = 1012 + TRY_AGAIN_LATER = 1013 + BAD_GATEWAY = 1014 + + +class WSMsgType(IntEnum): + # websocket spec types + CONTINUATION = 0x0 + TEXT = 0x1 + BINARY = 0x2 + PING = 0x9 + PONG = 0xA + CLOSE = 0x8 + + # aiohttp specific types + CLOSING = 0x100 + CLOSED = 0x101 + ERROR = 0x102 + + text = TEXT + binary = BINARY + ping = PING + pong = PONG + close = CLOSE + closing = CLOSING + closed = CLOSED + error = ERROR + + +class WSMessage(NamedTuple): + type: WSMsgType + # To type correctly, this would need some kind of tagged union for each type. + data: Any + extra: str | None + + def json(self, *, loads: Callable[[Any], Any] = json.loads) -> Any: + """Return parsed JSON data. + + .. versionadded:: 0.22 + """ + return loads(self.data) + + +class WSMessageTextBytes(NamedTuple): + """WebSocket TEXT message with raw bytes (no UTF-8 decoding).""" + + type: WSMsgType + # To type correctly, this would need some kind of tagged union for each type. + # In 4.0, we use a union of message types to properly type data, but in 3.x + # we keep it as Any to avoid a breaking change. + data: Any + extra: str | None + + def json(self, *, loads: Callable[[Any], Any] = json.loads) -> Any: + """Return parsed JSON data.""" + return loads(self.data) + + +# Type aliases for message types based on decode_text setting +# When decode_text=True, TEXT messages have str data (WSMessage) +# When decode_text=False, TEXT messages have bytes data (WSMessageTextBytes) +WSMessageDecodeText = WSMessage +WSMessageNoDecodeText = WSMessage | WSMessageTextBytes + + +# Constructing the tuple directly to avoid the overhead of +# the lambda and arg processing since NamedTuples are constructed +# with a run time built lambda +# https://github.com/python/cpython/blob/d83fcf8371f2f33c7797bc8f5423a8bca8c46e5c/Lib/collections/__init__.py#L441 +WS_CLOSED_MESSAGE = tuple.__new__(WSMessage, (WSMsgType.CLOSED, None, None)) +WS_CLOSING_MESSAGE = tuple.__new__(WSMessage, (WSMsgType.CLOSING, None, None)) + + +class WebSocketError(Exception): + """WebSocket protocol parser error.""" + + def __init__(self, code: int, message: str) -> None: + self.code = code + super().__init__(code, message) + + def __str__(self) -> str: + return cast(str, self.args[1]) + + +class WSHandshakeError(Exception): + """WebSocket protocol handshake error.""" diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader.py b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader.py new file mode 100644 index 0000000..23f3226 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader.py @@ -0,0 +1,31 @@ +"""Reader for WebSocket protocol versions 13 and 8.""" + +from typing import TYPE_CHECKING + +from ..helpers import NO_EXTENSIONS + +if TYPE_CHECKING or NO_EXTENSIONS: # pragma: no cover + from .reader_py import ( + WebSocketDataQueue as WebSocketDataQueuePython, + WebSocketReader as WebSocketReaderPython, + ) + + WebSocketReader = WebSocketReaderPython + WebSocketDataQueue = WebSocketDataQueuePython +else: + try: + from .reader_c import ( # type: ignore[import-not-found] + WebSocketDataQueue as WebSocketDataQueueCython, + WebSocketReader as WebSocketReaderCython, + ) + + WebSocketReader = WebSocketReaderCython + WebSocketDataQueue = WebSocketDataQueueCython + except ImportError: # pragma: no cover + from .reader_py import ( + WebSocketDataQueue as WebSocketDataQueuePython, + WebSocketReader as WebSocketReaderPython, + ) + + WebSocketReader = WebSocketReaderPython + WebSocketDataQueue = WebSocketDataQueuePython diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_c.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_c.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..b5b9fa5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_c.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_c.pxd b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_c.pxd new file mode 100644 index 0000000..5aa067f --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_c.pxd @@ -0,0 +1,112 @@ +import cython + +from .mask cimport _websocket_mask_cython as websocket_mask + + +cdef unsigned int READ_HEADER +cdef unsigned int READ_PAYLOAD_LENGTH +cdef unsigned int READ_PAYLOAD_MASK +cdef unsigned int READ_PAYLOAD + +cdef int OP_CODE_NOT_SET +cdef int OP_CODE_CONTINUATION +cdef int OP_CODE_TEXT +cdef int OP_CODE_BINARY +cdef int OP_CODE_CLOSE +cdef int OP_CODE_PING +cdef int OP_CODE_PONG + +cdef int COMPRESSED_NOT_SET +cdef int COMPRESSED_FALSE +cdef int COMPRESSED_TRUE + +cdef object UNPACK_LEN3 +cdef object UNPACK_CLOSE_CODE +cdef object TUPLE_NEW + +cdef object WSMsgType +cdef object WSMessage +cdef object WSMessageTextBytes + +cdef object WS_MSG_TYPE_TEXT +cdef object WS_MSG_TYPE_BINARY + +cdef set ALLOWED_CLOSE_CODES +cdef set MESSAGE_TYPES_WITH_CONTENT + +cdef tuple EMPTY_FRAME +cdef tuple EMPTY_FRAME_ERROR + +cdef class WebSocketDataQueue: + + cdef unsigned int _size + cdef public object _protocol + cdef unsigned int _limit + cdef object _loop + cdef bint _eof + cdef object _waiter + cdef object _exception + cdef public object _buffer + cdef object _get_buffer + cdef object _put_buffer + + cdef void _release_waiter(self) + + cpdef void feed_data(self, object data, unsigned int size) + + @cython.locals(size="unsigned int") + cdef _read_from_buffer(self) + +cdef class WebSocketReader: + + cdef WebSocketDataQueue queue + cdef unsigned int _max_msg_size + cdef bint _decode_text + + cdef Exception _exc + cdef bytearray _partial + cdef unsigned int _state + + cdef int _opcode + cdef bint _frame_fin + cdef int _frame_opcode + cdef list _payload_fragments + cdef Py_ssize_t _frame_payload_len + + cdef bytes _tail + cdef bint _has_mask + cdef bytes _frame_mask + cdef Py_ssize_t _payload_bytes_to_read + cdef unsigned int _payload_len_flag + cdef int _compressed + cdef object _decompressobj + cdef bint _compress + + cpdef tuple feed_data(self, object data) + + @cython.locals( + is_continuation=bint, + fin=bint, + has_partial=bint, + payload_merged=bytes, + ) + cpdef void _handle_frame(self, bint fin, int opcode, object payload, int compressed) except * + + @cython.locals( + start_pos=Py_ssize_t, + data_len=Py_ssize_t, + length=Py_ssize_t, + chunk_size=Py_ssize_t, + chunk_len=Py_ssize_t, + data_len=Py_ssize_t, + data_cstr="const unsigned char *", + first_byte="unsigned char", + second_byte="unsigned char", + f_start_pos=Py_ssize_t, + f_end_pos=Py_ssize_t, + has_mask=bint, + fin=bint, + had_fragments=Py_ssize_t, + payload_bytearray=bytearray, + ) + cpdef void _feed_data(self, bytes data) except * diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_c.py b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_c.py new file mode 100644 index 0000000..784fb08 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_c.py @@ -0,0 +1,509 @@ +"""Reader for WebSocket protocol versions 13 and 8.""" + +import asyncio +import builtins +from collections import deque +from typing import Final + +from ..base_protocol import BaseProtocol +from ..compression_utils import ZLibDecompressor +from ..helpers import _EXC_SENTINEL, set_exception +from ..streams import EofStream +from .helpers import UNPACK_CLOSE_CODE, UNPACK_LEN3, websocket_mask +from .models import ( + WS_DEFLATE_TRAILING, + WebSocketError, + WSCloseCode, + WSMessage, + WSMessageTextBytes, + WSMsgType, +) + +ALLOWED_CLOSE_CODES: Final[set[int]] = {int(i) for i in WSCloseCode} + +# States for the reader, used to parse the WebSocket frame +# integer values are used so they can be cythonized +READ_HEADER = 1 +READ_PAYLOAD_LENGTH = 2 +READ_PAYLOAD_MASK = 3 +READ_PAYLOAD = 4 + +WS_MSG_TYPE_BINARY = WSMsgType.BINARY +WS_MSG_TYPE_TEXT = WSMsgType.TEXT + +# WSMsgType values unpacked so they can by cythonized to ints +OP_CODE_NOT_SET = -1 +OP_CODE_CONTINUATION = WSMsgType.CONTINUATION.value +OP_CODE_TEXT = WSMsgType.TEXT.value +OP_CODE_BINARY = WSMsgType.BINARY.value +OP_CODE_CLOSE = WSMsgType.CLOSE.value +OP_CODE_PING = WSMsgType.PING.value +OP_CODE_PONG = WSMsgType.PONG.value + +EMPTY_FRAME_ERROR = (True, b"") +EMPTY_FRAME = (False, b"") + +COMPRESSED_NOT_SET = -1 +COMPRESSED_FALSE = 0 +COMPRESSED_TRUE = 1 + +TUPLE_NEW = tuple.__new__ + +cython_int = int # Typed to int in Python, but cython with use a signed int in the pxd + + +class WebSocketDataQueue: + """WebSocketDataQueue resumes and pauses an underlying stream. + + It is a destination for WebSocket data. + """ + + def __init__( + self, protocol: BaseProtocol, limit: int, *, loop: asyncio.AbstractEventLoop + ) -> None: + self._size = 0 + self._protocol = protocol + self._limit = limit * 2 + self._loop = loop + self._eof = False + self._waiter: asyncio.Future[None] | None = None + self._exception: BaseException | None = None + self._buffer: deque[tuple[WSMessage | WSMessageTextBytes, int]] = deque() + self._get_buffer = self._buffer.popleft + self._put_buffer = self._buffer.append + + def is_eof(self) -> bool: + return self._eof + + def exception(self) -> BaseException | None: + return self._exception + + def set_exception( + self, + exc: BaseException, + exc_cause: builtins.BaseException = _EXC_SENTINEL, + ) -> None: + self._eof = True + self._exception = exc + if (waiter := self._waiter) is not None: + self._waiter = None + set_exception(waiter, exc, exc_cause) + + def _release_waiter(self) -> None: + if (waiter := self._waiter) is None: + return + self._waiter = None + if not waiter.done(): + waiter.set_result(None) + + def feed_eof(self) -> None: + self._eof = True + self._release_waiter() + self._exception = None # Break cyclic references + + def feed_data( + self, data: "WSMessage | WSMessageTextBytes", size: "cython_int" + ) -> None: + self._size += size + self._put_buffer((data, size)) + self._release_waiter() + if self._size > self._limit and not self._protocol._reading_paused: + self._protocol.pause_reading() + + async def read(self) -> WSMessage | WSMessageTextBytes: + if not self._buffer and not self._eof: + assert not self._waiter + self._waiter = self._loop.create_future() + try: + await self._waiter + except (asyncio.CancelledError, asyncio.TimeoutError): + self._waiter = None + raise + return self._read_from_buffer() + + def _read_from_buffer(self) -> WSMessage | WSMessageTextBytes: + if self._buffer: + data, size = self._get_buffer() + self._size -= size + if self._size < self._limit and self._protocol._reading_paused: + self._protocol.resume_reading() + return data + if self._exception is not None: + raise self._exception + raise EofStream + + +class WebSocketReader: + def __init__( + self, + queue: WebSocketDataQueue, + max_msg_size: int, + compress: bool = True, + decode_text: bool = True, + ) -> None: + self.queue = queue + self._max_msg_size = max_msg_size + self._decode_text = decode_text + + self._exc: Exception | None = None + self._partial = bytearray() + self._state = READ_HEADER + + self._opcode: int = OP_CODE_NOT_SET + self._frame_fin = False + self._frame_opcode: int = OP_CODE_NOT_SET + self._payload_fragments: list[bytes] = [] + self._frame_payload_len = 0 + + self._tail: bytes = b"" + self._has_mask = False + self._frame_mask: bytes | None = None + self._payload_bytes_to_read = 0 + self._payload_len_flag = 0 + self._compressed: int = COMPRESSED_NOT_SET + self._decompressobj: ZLibDecompressor | None = None + self._compress = compress + + def feed_eof(self) -> None: + self.queue.feed_eof() + + # data can be bytearray on Windows because proactor event loop uses bytearray + # and asyncio types this to Union[bytes, bytearray, memoryview] so we need + # coerce data to bytes if it is not + def feed_data(self, data: bytes | bytearray | memoryview) -> tuple[bool, bytes]: + if type(data) is not bytes: + data = bytes(data) + + if self._exc is not None: + return True, data + + try: + self._feed_data(data) + except Exception as exc: + self._exc = exc + set_exception(self.queue, exc) + return EMPTY_FRAME_ERROR + + return EMPTY_FRAME + + def _handle_frame( + self, + fin: bool, + opcode: int | cython_int, # Union intended: Cython pxd uses C int + payload: bytes | bytearray, + compressed: int | cython_int, # Union intended: Cython pxd uses C int + ) -> None: + msg: WSMessage + if opcode in {OP_CODE_TEXT, OP_CODE_BINARY, OP_CODE_CONTINUATION}: + # Validate continuation frames before processing + if opcode == OP_CODE_CONTINUATION and self._opcode == OP_CODE_NOT_SET: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Continuation frame for non started message", + ) + + # load text/binary + if not fin: + # got partial frame payload + if opcode != OP_CODE_CONTINUATION: + self._opcode = opcode + self._partial += payload + return + + has_partial = bool(self._partial) + if opcode == OP_CODE_CONTINUATION: + opcode = self._opcode + self._opcode = OP_CODE_NOT_SET + # previous frame was non finished + # we should get continuation opcode + elif has_partial: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "The opcode in non-fin frame is expected " + f"to be zero, got {opcode!r}", + ) + + assembled_payload: bytes | bytearray + if has_partial: + assembled_payload = self._partial + payload + self._partial.clear() + else: + assembled_payload = payload + + # Decompress process must to be done after all packets + # received. + if compressed: + if not self._decompressobj: + self._decompressobj = ZLibDecompressor(suppress_deflate_header=True) + # XXX: It's possible that the zlib backend (isal is known to + # do this, maybe others too?) will return max_length bytes, + # but internally buffer more data such that the payload is + # >max_length, so we return one extra byte and if we're able + # to do that, then the message is too big. + payload_merged = self._decompressobj.decompress_sync( + assembled_payload + WS_DEFLATE_TRAILING, + ( + self._max_msg_size + 1 + if self._max_msg_size + else self._max_msg_size + ), + ) + if self._max_msg_size and len(payload_merged) > self._max_msg_size: + raise WebSocketError( + WSCloseCode.MESSAGE_TOO_BIG, + f"Decompressed message exceeds size limit {self._max_msg_size}", + ) + elif type(assembled_payload) is bytes: + payload_merged = assembled_payload + else: + payload_merged = bytes(assembled_payload) + + if opcode == OP_CODE_TEXT: + if self._decode_text: + try: + text = payload_merged.decode("utf-8") + except UnicodeDecodeError as exc: + raise WebSocketError( + WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message" + ) from exc + + # XXX: The Text and Binary messages here can be a performance + # bottleneck, so we use tuple.__new__ to improve performance. + # This is not type safe, but many tests should fail in + # test_client_ws_functional.py if this is wrong. + self.queue.feed_data( + TUPLE_NEW(WSMessage, (WS_MSG_TYPE_TEXT, text, "")), + len(payload_merged), + ) + else: + # Return raw bytes for TEXT messages when decode_text=False + self.queue.feed_data( + TUPLE_NEW( + WSMessageTextBytes, (WS_MSG_TYPE_TEXT, payload_merged, "") + ), + len(payload_merged), + ) + else: + self.queue.feed_data( + TUPLE_NEW(WSMessage, (WS_MSG_TYPE_BINARY, payload_merged, "")), + len(payload_merged), + ) + elif opcode == OP_CODE_CLOSE: + if len(payload) >= 2: + close_code = UNPACK_CLOSE_CODE(payload[:2])[0] + if close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + f"Invalid close code: {close_code}", + ) + try: + close_message = payload[2:].decode("utf-8") + except UnicodeDecodeError as exc: + raise WebSocketError( + WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message" + ) from exc + msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, close_code, close_message)) + elif payload: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + f"Invalid close frame: {fin} {opcode} {payload!r}", + ) + else: + msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, 0, "")) + + self.queue.feed_data(msg, 0) + elif opcode == OP_CODE_PING: + msg = TUPLE_NEW(WSMessage, (WSMsgType.PING, payload, "")) + self.queue.feed_data(msg, len(payload)) + elif opcode == OP_CODE_PONG: + msg = TUPLE_NEW(WSMessage, (WSMsgType.PONG, payload, "")) + self.queue.feed_data(msg, len(payload)) + else: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, f"Unexpected opcode={opcode!r}" + ) + + def _feed_data(self, data: bytes) -> None: + """Return the next frame from the socket.""" + if self._tail: + data, self._tail = self._tail + data, b"" + + start_pos: int = 0 + data_len = len(data) + data_cstr = data + + while True: + # read header + if self._state == READ_HEADER: + if data_len - start_pos < 2: + break + first_byte = data_cstr[start_pos] + second_byte = data_cstr[start_pos + 1] + start_pos += 2 + + fin = (first_byte >> 7) & 1 + rsv1 = (first_byte >> 6) & 1 + rsv2 = (first_byte >> 5) & 1 + rsv3 = (first_byte >> 4) & 1 + opcode = first_byte & 0xF + + # frame-fin = %x0 ; more frames of this message follow + # / %x1 ; final frame of this message + # frame-rsv1 = %x0 ; + # 1 bit, MUST be 0 unless negotiated otherwise + # frame-rsv2 = %x0 ; + # 1 bit, MUST be 0 unless negotiated otherwise + # frame-rsv3 = %x0 ; + # 1 bit, MUST be 0 unless negotiated otherwise + # + # Remove rsv1 from this test for deflate development + if rsv2 or rsv3 or (rsv1 and not self._compress): + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Received frame with non-zero reserved bits", + ) + + if opcode not in { + OP_CODE_CONTINUATION, + OP_CODE_TEXT, + OP_CODE_BINARY, + OP_CODE_CLOSE, + OP_CODE_PING, + OP_CODE_PONG, + }: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + f"Unexpected opcode={opcode!r}", + ) + + if opcode > 0x7 and fin == 0: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Received fragmented control frame", + ) + + has_mask = (second_byte >> 7) & 1 + length = second_byte & 0x7F + + # Control frames MUST have a payload + # length of 125 bytes or less + if opcode > 0x7 and length > 125: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Control frame payload cannot be larger than 125 bytes", + ) + + # Set compress status if last package is FIN + # OR set compress status if this is first fragment + # Raise error if not first fragment with rsv1 = 0x1 + if self._frame_fin or self._compressed == COMPRESSED_NOT_SET: + self._compressed = COMPRESSED_TRUE if rsv1 else COMPRESSED_FALSE + elif rsv1: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Received frame with non-zero reserved bits", + ) + + self._frame_fin = bool(fin) + self._frame_opcode = opcode + self._has_mask = bool(has_mask) + self._payload_len_flag = length + self._state = READ_PAYLOAD_LENGTH + + # read payload length + if self._state == READ_PAYLOAD_LENGTH: + len_flag = self._payload_len_flag + if len_flag == 126: + if data_len - start_pos < 2: + break + first_byte = data_cstr[start_pos] + second_byte = data_cstr[start_pos + 1] + start_pos += 2 + self._payload_bytes_to_read = first_byte << 8 | second_byte + elif len_flag > 126: + if data_len - start_pos < 8: + break + self._payload_bytes_to_read = UNPACK_LEN3(data, start_pos)[0] + start_pos += 8 + else: + self._payload_bytes_to_read = len_flag + + # Reject oversized data frames before buffering any payload + # bytes. Control frames are capped at 125 bytes (checked in + # READ_HEADER) so only text/binary/continuation need this. + if self._max_msg_size and self._frame_opcode in { + OP_CODE_TEXT, + OP_CODE_BINARY, + OP_CODE_CONTINUATION, + }: + projected_size = self._payload_bytes_to_read + len(self._partial) + if projected_size >= self._max_msg_size: + raise WebSocketError( + WSCloseCode.MESSAGE_TOO_BIG, + f"Message size {projected_size} " + f"exceeds limit {self._max_msg_size}", + ) + + self._state = READ_PAYLOAD_MASK if self._has_mask else READ_PAYLOAD + + # read payload mask + if self._state == READ_PAYLOAD_MASK: + if data_len - start_pos < 4: + break + self._frame_mask = data_cstr[start_pos : start_pos + 4] + start_pos += 4 + self._state = READ_PAYLOAD + + if self._state == READ_PAYLOAD: + chunk_len = data_len - start_pos + if self._payload_bytes_to_read >= chunk_len: + f_end_pos = data_len + self._payload_bytes_to_read -= chunk_len + else: + f_end_pos = start_pos + self._payload_bytes_to_read + self._payload_bytes_to_read = 0 + + had_fragments = self._frame_payload_len + self._frame_payload_len += f_end_pos - start_pos + f_start_pos = start_pos + start_pos = f_end_pos + + if self._payload_bytes_to_read != 0: + # If we don't have a complete frame, we need to save the + # data for the next call to feed_data. + self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos]) + break + + payload: bytes | bytearray + if had_fragments: + # We have to join the payload fragments get the payload + self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos]) + if self._has_mask: + assert self._frame_mask is not None + payload_bytearray = bytearray(b"".join(self._payload_fragments)) + websocket_mask(self._frame_mask, payload_bytearray) + payload = payload_bytearray + else: + payload = b"".join(self._payload_fragments) + self._payload_fragments.clear() + elif self._has_mask: + assert self._frame_mask is not None + payload_bytearray = data_cstr[f_start_pos:f_end_pos] # type: ignore[assignment] + if type(payload_bytearray) is not bytearray: # pragma: no branch + # Cython will do the conversion for us + # but we need to do it for Python and we + # will always get here in Python + payload_bytearray = bytearray(payload_bytearray) + websocket_mask(self._frame_mask, payload_bytearray) + payload = payload_bytearray + else: + payload = data_cstr[f_start_pos:f_end_pos] + + self._handle_frame( + self._frame_fin, self._frame_opcode, payload, self._compressed + ) + self._frame_payload_len = 0 + self._state = READ_HEADER + + # XXX: Cython needs slices to be bounded, so we can't omit the slice end here. + self._tail = data_cstr[start_pos:data_len] if start_pos < data_len else b"" diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_py.py b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_py.py new file mode 100644 index 0000000..784fb08 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/reader_py.py @@ -0,0 +1,509 @@ +"""Reader for WebSocket protocol versions 13 and 8.""" + +import asyncio +import builtins +from collections import deque +from typing import Final + +from ..base_protocol import BaseProtocol +from ..compression_utils import ZLibDecompressor +from ..helpers import _EXC_SENTINEL, set_exception +from ..streams import EofStream +from .helpers import UNPACK_CLOSE_CODE, UNPACK_LEN3, websocket_mask +from .models import ( + WS_DEFLATE_TRAILING, + WebSocketError, + WSCloseCode, + WSMessage, + WSMessageTextBytes, + WSMsgType, +) + +ALLOWED_CLOSE_CODES: Final[set[int]] = {int(i) for i in WSCloseCode} + +# States for the reader, used to parse the WebSocket frame +# integer values are used so they can be cythonized +READ_HEADER = 1 +READ_PAYLOAD_LENGTH = 2 +READ_PAYLOAD_MASK = 3 +READ_PAYLOAD = 4 + +WS_MSG_TYPE_BINARY = WSMsgType.BINARY +WS_MSG_TYPE_TEXT = WSMsgType.TEXT + +# WSMsgType values unpacked so they can by cythonized to ints +OP_CODE_NOT_SET = -1 +OP_CODE_CONTINUATION = WSMsgType.CONTINUATION.value +OP_CODE_TEXT = WSMsgType.TEXT.value +OP_CODE_BINARY = WSMsgType.BINARY.value +OP_CODE_CLOSE = WSMsgType.CLOSE.value +OP_CODE_PING = WSMsgType.PING.value +OP_CODE_PONG = WSMsgType.PONG.value + +EMPTY_FRAME_ERROR = (True, b"") +EMPTY_FRAME = (False, b"") + +COMPRESSED_NOT_SET = -1 +COMPRESSED_FALSE = 0 +COMPRESSED_TRUE = 1 + +TUPLE_NEW = tuple.__new__ + +cython_int = int # Typed to int in Python, but cython with use a signed int in the pxd + + +class WebSocketDataQueue: + """WebSocketDataQueue resumes and pauses an underlying stream. + + It is a destination for WebSocket data. + """ + + def __init__( + self, protocol: BaseProtocol, limit: int, *, loop: asyncio.AbstractEventLoop + ) -> None: + self._size = 0 + self._protocol = protocol + self._limit = limit * 2 + self._loop = loop + self._eof = False + self._waiter: asyncio.Future[None] | None = None + self._exception: BaseException | None = None + self._buffer: deque[tuple[WSMessage | WSMessageTextBytes, int]] = deque() + self._get_buffer = self._buffer.popleft + self._put_buffer = self._buffer.append + + def is_eof(self) -> bool: + return self._eof + + def exception(self) -> BaseException | None: + return self._exception + + def set_exception( + self, + exc: BaseException, + exc_cause: builtins.BaseException = _EXC_SENTINEL, + ) -> None: + self._eof = True + self._exception = exc + if (waiter := self._waiter) is not None: + self._waiter = None + set_exception(waiter, exc, exc_cause) + + def _release_waiter(self) -> None: + if (waiter := self._waiter) is None: + return + self._waiter = None + if not waiter.done(): + waiter.set_result(None) + + def feed_eof(self) -> None: + self._eof = True + self._release_waiter() + self._exception = None # Break cyclic references + + def feed_data( + self, data: "WSMessage | WSMessageTextBytes", size: "cython_int" + ) -> None: + self._size += size + self._put_buffer((data, size)) + self._release_waiter() + if self._size > self._limit and not self._protocol._reading_paused: + self._protocol.pause_reading() + + async def read(self) -> WSMessage | WSMessageTextBytes: + if not self._buffer and not self._eof: + assert not self._waiter + self._waiter = self._loop.create_future() + try: + await self._waiter + except (asyncio.CancelledError, asyncio.TimeoutError): + self._waiter = None + raise + return self._read_from_buffer() + + def _read_from_buffer(self) -> WSMessage | WSMessageTextBytes: + if self._buffer: + data, size = self._get_buffer() + self._size -= size + if self._size < self._limit and self._protocol._reading_paused: + self._protocol.resume_reading() + return data + if self._exception is not None: + raise self._exception + raise EofStream + + +class WebSocketReader: + def __init__( + self, + queue: WebSocketDataQueue, + max_msg_size: int, + compress: bool = True, + decode_text: bool = True, + ) -> None: + self.queue = queue + self._max_msg_size = max_msg_size + self._decode_text = decode_text + + self._exc: Exception | None = None + self._partial = bytearray() + self._state = READ_HEADER + + self._opcode: int = OP_CODE_NOT_SET + self._frame_fin = False + self._frame_opcode: int = OP_CODE_NOT_SET + self._payload_fragments: list[bytes] = [] + self._frame_payload_len = 0 + + self._tail: bytes = b"" + self._has_mask = False + self._frame_mask: bytes | None = None + self._payload_bytes_to_read = 0 + self._payload_len_flag = 0 + self._compressed: int = COMPRESSED_NOT_SET + self._decompressobj: ZLibDecompressor | None = None + self._compress = compress + + def feed_eof(self) -> None: + self.queue.feed_eof() + + # data can be bytearray on Windows because proactor event loop uses bytearray + # and asyncio types this to Union[bytes, bytearray, memoryview] so we need + # coerce data to bytes if it is not + def feed_data(self, data: bytes | bytearray | memoryview) -> tuple[bool, bytes]: + if type(data) is not bytes: + data = bytes(data) + + if self._exc is not None: + return True, data + + try: + self._feed_data(data) + except Exception as exc: + self._exc = exc + set_exception(self.queue, exc) + return EMPTY_FRAME_ERROR + + return EMPTY_FRAME + + def _handle_frame( + self, + fin: bool, + opcode: int | cython_int, # Union intended: Cython pxd uses C int + payload: bytes | bytearray, + compressed: int | cython_int, # Union intended: Cython pxd uses C int + ) -> None: + msg: WSMessage + if opcode in {OP_CODE_TEXT, OP_CODE_BINARY, OP_CODE_CONTINUATION}: + # Validate continuation frames before processing + if opcode == OP_CODE_CONTINUATION and self._opcode == OP_CODE_NOT_SET: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Continuation frame for non started message", + ) + + # load text/binary + if not fin: + # got partial frame payload + if opcode != OP_CODE_CONTINUATION: + self._opcode = opcode + self._partial += payload + return + + has_partial = bool(self._partial) + if opcode == OP_CODE_CONTINUATION: + opcode = self._opcode + self._opcode = OP_CODE_NOT_SET + # previous frame was non finished + # we should get continuation opcode + elif has_partial: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "The opcode in non-fin frame is expected " + f"to be zero, got {opcode!r}", + ) + + assembled_payload: bytes | bytearray + if has_partial: + assembled_payload = self._partial + payload + self._partial.clear() + else: + assembled_payload = payload + + # Decompress process must to be done after all packets + # received. + if compressed: + if not self._decompressobj: + self._decompressobj = ZLibDecompressor(suppress_deflate_header=True) + # XXX: It's possible that the zlib backend (isal is known to + # do this, maybe others too?) will return max_length bytes, + # but internally buffer more data such that the payload is + # >max_length, so we return one extra byte and if we're able + # to do that, then the message is too big. + payload_merged = self._decompressobj.decompress_sync( + assembled_payload + WS_DEFLATE_TRAILING, + ( + self._max_msg_size + 1 + if self._max_msg_size + else self._max_msg_size + ), + ) + if self._max_msg_size and len(payload_merged) > self._max_msg_size: + raise WebSocketError( + WSCloseCode.MESSAGE_TOO_BIG, + f"Decompressed message exceeds size limit {self._max_msg_size}", + ) + elif type(assembled_payload) is bytes: + payload_merged = assembled_payload + else: + payload_merged = bytes(assembled_payload) + + if opcode == OP_CODE_TEXT: + if self._decode_text: + try: + text = payload_merged.decode("utf-8") + except UnicodeDecodeError as exc: + raise WebSocketError( + WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message" + ) from exc + + # XXX: The Text and Binary messages here can be a performance + # bottleneck, so we use tuple.__new__ to improve performance. + # This is not type safe, but many tests should fail in + # test_client_ws_functional.py if this is wrong. + self.queue.feed_data( + TUPLE_NEW(WSMessage, (WS_MSG_TYPE_TEXT, text, "")), + len(payload_merged), + ) + else: + # Return raw bytes for TEXT messages when decode_text=False + self.queue.feed_data( + TUPLE_NEW( + WSMessageTextBytes, (WS_MSG_TYPE_TEXT, payload_merged, "") + ), + len(payload_merged), + ) + else: + self.queue.feed_data( + TUPLE_NEW(WSMessage, (WS_MSG_TYPE_BINARY, payload_merged, "")), + len(payload_merged), + ) + elif opcode == OP_CODE_CLOSE: + if len(payload) >= 2: + close_code = UNPACK_CLOSE_CODE(payload[:2])[0] + if close_code < 3000 and close_code not in ALLOWED_CLOSE_CODES: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + f"Invalid close code: {close_code}", + ) + try: + close_message = payload[2:].decode("utf-8") + except UnicodeDecodeError as exc: + raise WebSocketError( + WSCloseCode.INVALID_TEXT, "Invalid UTF-8 text message" + ) from exc + msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, close_code, close_message)) + elif payload: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + f"Invalid close frame: {fin} {opcode} {payload!r}", + ) + else: + msg = TUPLE_NEW(WSMessage, (WSMsgType.CLOSE, 0, "")) + + self.queue.feed_data(msg, 0) + elif opcode == OP_CODE_PING: + msg = TUPLE_NEW(WSMessage, (WSMsgType.PING, payload, "")) + self.queue.feed_data(msg, len(payload)) + elif opcode == OP_CODE_PONG: + msg = TUPLE_NEW(WSMessage, (WSMsgType.PONG, payload, "")) + self.queue.feed_data(msg, len(payload)) + else: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, f"Unexpected opcode={opcode!r}" + ) + + def _feed_data(self, data: bytes) -> None: + """Return the next frame from the socket.""" + if self._tail: + data, self._tail = self._tail + data, b"" + + start_pos: int = 0 + data_len = len(data) + data_cstr = data + + while True: + # read header + if self._state == READ_HEADER: + if data_len - start_pos < 2: + break + first_byte = data_cstr[start_pos] + second_byte = data_cstr[start_pos + 1] + start_pos += 2 + + fin = (first_byte >> 7) & 1 + rsv1 = (first_byte >> 6) & 1 + rsv2 = (first_byte >> 5) & 1 + rsv3 = (first_byte >> 4) & 1 + opcode = first_byte & 0xF + + # frame-fin = %x0 ; more frames of this message follow + # / %x1 ; final frame of this message + # frame-rsv1 = %x0 ; + # 1 bit, MUST be 0 unless negotiated otherwise + # frame-rsv2 = %x0 ; + # 1 bit, MUST be 0 unless negotiated otherwise + # frame-rsv3 = %x0 ; + # 1 bit, MUST be 0 unless negotiated otherwise + # + # Remove rsv1 from this test for deflate development + if rsv2 or rsv3 or (rsv1 and not self._compress): + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Received frame with non-zero reserved bits", + ) + + if opcode not in { + OP_CODE_CONTINUATION, + OP_CODE_TEXT, + OP_CODE_BINARY, + OP_CODE_CLOSE, + OP_CODE_PING, + OP_CODE_PONG, + }: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + f"Unexpected opcode={opcode!r}", + ) + + if opcode > 0x7 and fin == 0: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Received fragmented control frame", + ) + + has_mask = (second_byte >> 7) & 1 + length = second_byte & 0x7F + + # Control frames MUST have a payload + # length of 125 bytes or less + if opcode > 0x7 and length > 125: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Control frame payload cannot be larger than 125 bytes", + ) + + # Set compress status if last package is FIN + # OR set compress status if this is first fragment + # Raise error if not first fragment with rsv1 = 0x1 + if self._frame_fin or self._compressed == COMPRESSED_NOT_SET: + self._compressed = COMPRESSED_TRUE if rsv1 else COMPRESSED_FALSE + elif rsv1: + raise WebSocketError( + WSCloseCode.PROTOCOL_ERROR, + "Received frame with non-zero reserved bits", + ) + + self._frame_fin = bool(fin) + self._frame_opcode = opcode + self._has_mask = bool(has_mask) + self._payload_len_flag = length + self._state = READ_PAYLOAD_LENGTH + + # read payload length + if self._state == READ_PAYLOAD_LENGTH: + len_flag = self._payload_len_flag + if len_flag == 126: + if data_len - start_pos < 2: + break + first_byte = data_cstr[start_pos] + second_byte = data_cstr[start_pos + 1] + start_pos += 2 + self._payload_bytes_to_read = first_byte << 8 | second_byte + elif len_flag > 126: + if data_len - start_pos < 8: + break + self._payload_bytes_to_read = UNPACK_LEN3(data, start_pos)[0] + start_pos += 8 + else: + self._payload_bytes_to_read = len_flag + + # Reject oversized data frames before buffering any payload + # bytes. Control frames are capped at 125 bytes (checked in + # READ_HEADER) so only text/binary/continuation need this. + if self._max_msg_size and self._frame_opcode in { + OP_CODE_TEXT, + OP_CODE_BINARY, + OP_CODE_CONTINUATION, + }: + projected_size = self._payload_bytes_to_read + len(self._partial) + if projected_size >= self._max_msg_size: + raise WebSocketError( + WSCloseCode.MESSAGE_TOO_BIG, + f"Message size {projected_size} " + f"exceeds limit {self._max_msg_size}", + ) + + self._state = READ_PAYLOAD_MASK if self._has_mask else READ_PAYLOAD + + # read payload mask + if self._state == READ_PAYLOAD_MASK: + if data_len - start_pos < 4: + break + self._frame_mask = data_cstr[start_pos : start_pos + 4] + start_pos += 4 + self._state = READ_PAYLOAD + + if self._state == READ_PAYLOAD: + chunk_len = data_len - start_pos + if self._payload_bytes_to_read >= chunk_len: + f_end_pos = data_len + self._payload_bytes_to_read -= chunk_len + else: + f_end_pos = start_pos + self._payload_bytes_to_read + self._payload_bytes_to_read = 0 + + had_fragments = self._frame_payload_len + self._frame_payload_len += f_end_pos - start_pos + f_start_pos = start_pos + start_pos = f_end_pos + + if self._payload_bytes_to_read != 0: + # If we don't have a complete frame, we need to save the + # data for the next call to feed_data. + self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos]) + break + + payload: bytes | bytearray + if had_fragments: + # We have to join the payload fragments get the payload + self._payload_fragments.append(data_cstr[f_start_pos:f_end_pos]) + if self._has_mask: + assert self._frame_mask is not None + payload_bytearray = bytearray(b"".join(self._payload_fragments)) + websocket_mask(self._frame_mask, payload_bytearray) + payload = payload_bytearray + else: + payload = b"".join(self._payload_fragments) + self._payload_fragments.clear() + elif self._has_mask: + assert self._frame_mask is not None + payload_bytearray = data_cstr[f_start_pos:f_end_pos] # type: ignore[assignment] + if type(payload_bytearray) is not bytearray: # pragma: no branch + # Cython will do the conversion for us + # but we need to do it for Python and we + # will always get here in Python + payload_bytearray = bytearray(payload_bytearray) + websocket_mask(self._frame_mask, payload_bytearray) + payload = payload_bytearray + else: + payload = data_cstr[f_start_pos:f_end_pos] + + self._handle_frame( + self._frame_fin, self._frame_opcode, payload, self._compressed + ) + self._frame_payload_len = 0 + self._state = READ_HEADER + + # XXX: Cython needs slices to be bounded, so we can't omit the slice end here. + self._tail = data_cstr[start_pos:data_len] if start_pos < data_len else b"" diff --git a/venv/lib/python3.11/site-packages/aiohttp/_websocket/writer.py b/venv/lib/python3.11/site-packages/aiohttp/_websocket/writer.py new file mode 100644 index 0000000..7daf4fc --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/_websocket/writer.py @@ -0,0 +1,261 @@ +"""WebSocket protocol versions 13 and 8.""" + +import asyncio +import random +import sys +from functools import partial +from typing import Final, Optional, Set + +from ..base_protocol import BaseProtocol +from ..client_exceptions import ClientConnectionResetError +from ..compression_utils import ZLibBackend, ZLibCompressor +from ..helpers import DEFAULT_CHUNK_SIZE +from .helpers import ( + MASK_LEN, + MSG_SIZE, + PACK_CLOSE_CODE, + PACK_LEN1, + PACK_LEN2, + PACK_LEN3, + PACK_RANDBITS, + websocket_mask, +) +from .models import WS_DEFLATE_TRAILING, WSMsgType + +# WebSocket opcode boundary: opcodes 0-7 are data frames, 8-15 are control frames +# Control frames (ping, pong, close) are never compressed +WS_CONTROL_FRAME_OPCODE: Final[int] = 8 + +# For websockets, keeping latency low is extremely important as implementations +# generally expect to be able to send and receive messages quickly. We use a +# larger chunk size to reduce the number of executor calls and avoid task +# creation overhead, since both are significant sources of latency when chunks +# are small. A size of 16KiB was chosen as a balance between avoiding task +# overhead and not blocking the event loop too long with synchronous compression. + +WEBSOCKET_MAX_SYNC_CHUNK_SIZE = 16 * 1024 + + +class WebSocketWriter: + """WebSocket writer. + + The writer is responsible for sending messages to the client. It is + created by the protocol when a connection is established. The writer + should avoid implementing any application logic and should only be + concerned with the low-level details of the WebSocket protocol. + """ + + def __init__( + self, + protocol: BaseProtocol, + transport: asyncio.Transport, + *, + use_mask: bool = False, + limit: int = DEFAULT_CHUNK_SIZE, + random: random.Random = random.Random(), + compress: int = 0, + notakeover: bool = False, + ) -> None: + """Initialize a WebSocket writer.""" + self.protocol = protocol + self.transport = transport + self.use_mask = use_mask + self.get_random_bits = partial(random.getrandbits, 32) + self.compress = compress + self.notakeover = notakeover + self._closing = False + self._limit = limit + self._output_size = 0 + self._compressobj: Optional[ZLibCompressor] = None + self._send_lock = asyncio.Lock() + self._background_tasks: Set[asyncio.Task[None]] = set() + + async def send_frame( + self, message: bytes, opcode: int, compress: int | None = None + ) -> None: + """Send a frame over the websocket with message as its payload.""" + if self._closing and not (opcode & WSMsgType.CLOSE): + raise ClientConnectionResetError("Cannot write to closing transport") + + if not (compress or self.compress) or opcode >= WS_CONTROL_FRAME_OPCODE: + # Non-compressed frames don't need lock or shield + self._write_websocket_frame(message, opcode, 0) + elif len(message) <= WEBSOCKET_MAX_SYNC_CHUNK_SIZE: + # Small compressed payloads - compress synchronously in event loop + # We need the lock even though sync compression has no await points. + # This prevents small frames from interleaving with large frames that + # compress in the executor, avoiding compressor state corruption. + async with self._send_lock: + self._send_compressed_frame_sync(message, opcode, compress) + else: + # Large compressed frames need shield to prevent corruption + # For large compressed frames, the entire compress+send + # operation must be atomic. If cancelled after compression but + # before send, the compressor state would be advanced but data + # not sent, corrupting subsequent frames. + # Create a task to shield from cancellation + # The lock is acquired inside the shielded task so the entire + # operation (lock + compress + send) completes atomically. + # Use eager_start on Python 3.12+ to avoid scheduling overhead + loop = asyncio.get_running_loop() + coro = self._send_compressed_frame_async_locked(message, opcode, compress) + if sys.version_info >= (3, 12): + send_task = asyncio.Task(coro, loop=loop, eager_start=True) + else: + send_task = loop.create_task(coro) + # Keep a strong reference to prevent garbage collection + self._background_tasks.add(send_task) + send_task.add_done_callback(self._background_tasks.discard) + await asyncio.shield(send_task) + + # It is safe to return control to the event loop when using compression + # after this point as we have already sent or buffered all the data. + # Once we have written output_size up to the limit, we call the + # drain helper which waits for the transport to be ready to accept + # more data. This is a flow control mechanism to prevent the buffer + # from growing too large. The drain helper will return right away + # if the writer is not paused. + if self._output_size > self._limit: + self._output_size = 0 + if self.protocol._paused: + await self.protocol._drain_helper() + + def _write_websocket_frame(self, message: bytes, opcode: int, rsv: int) -> None: + """ + Write a websocket frame to the transport. + + This method handles frame header construction, masking, and writing to transport. + It does not handle compression or flow control - those are the responsibility + of the caller. + """ + msg_length = len(message) + + use_mask = self.use_mask + mask_bit = 0x80 if use_mask else 0 + + # Depending on the message length, the header is assembled differently. + # The first byte is reserved for the opcode and the RSV bits. + first_byte = 0x80 | rsv | opcode + if msg_length < 126: + header = PACK_LEN1(first_byte, msg_length | mask_bit) + header_len = 2 + elif msg_length < 65536: + header = PACK_LEN2(first_byte, 126 | mask_bit, msg_length) + header_len = 4 + else: + header = PACK_LEN3(first_byte, 127 | mask_bit, msg_length) + header_len = 10 + + if self.transport.is_closing(): + raise ClientConnectionResetError("Cannot write to closing transport") + + # https://datatracker.ietf.org/doc/html/rfc6455#section-5.3 + # If we are using a mask, we need to generate it randomly + # and apply it to the message before sending it. A mask is + # a 32-bit value that is applied to the message using a + # bitwise XOR operation. It is used to prevent certain types + # of attacks on the websocket protocol. The mask is only used + # when aiohttp is acting as a client. Servers do not use a mask. + if use_mask: + mask = PACK_RANDBITS(self.get_random_bits()) + message_arr = bytearray(message) + websocket_mask(mask, message_arr) + self.transport.write(header + mask + message_arr) + self._output_size += MASK_LEN + elif msg_length > MSG_SIZE: + self.transport.write(header) + self.transport.write(message) + else: + self.transport.write(header + message) + + self._output_size += header_len + msg_length + + def _get_compressor(self, compress: int | None) -> ZLibCompressor: + """Get or create a compressor object for the given compression level.""" + if compress: + # Do not set self._compress if compressing is for this frame + return ZLibCompressor( + level=ZLibBackend.Z_BEST_SPEED, + wbits=-compress, + max_sync_chunk_size=WEBSOCKET_MAX_SYNC_CHUNK_SIZE, + ) + if not self._compressobj: + self._compressobj = ZLibCompressor( + level=ZLibBackend.Z_BEST_SPEED, + wbits=-self.compress, + max_sync_chunk_size=WEBSOCKET_MAX_SYNC_CHUNK_SIZE, + ) + return self._compressobj + + def _send_compressed_frame_sync( + self, message: bytes, opcode: int, compress: int | None + ) -> None: + """ + Synchronous send for small compressed frames. + + This is used for small compressed payloads that compress synchronously in the event loop. + Since there are no await points, this is inherently cancellation-safe. + """ + # RSV are the reserved bits in the frame header. They are used to + # indicate that the frame is using an extension. + # https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 + compressobj = self._get_compressor(compress) + # (0x40) RSV1 is set for compressed frames + # https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.3.1 + self._write_websocket_frame( + ( + compressobj.compress_sync(message) + + compressobj.flush( + ZLibBackend.Z_FULL_FLUSH + if self.notakeover + else ZLibBackend.Z_SYNC_FLUSH + ) + ).removesuffix(WS_DEFLATE_TRAILING), + opcode, + 0x40, + ) + + async def _send_compressed_frame_async_locked( + self, message: bytes, opcode: int, compress: int | None + ) -> None: + """ + Async send for large compressed frames with lock. + + Acquires the lock and compresses large payloads asynchronously in + the executor. The lock is held for the entire operation to ensure + the compressor state is not corrupted by concurrent sends. + + MUST be run shielded from cancellation. If cancelled after + compression but before sending, the compressor state would be + advanced but data not sent, corrupting subsequent frames. + """ + async with self._send_lock: + # RSV are the reserved bits in the frame header. They are used to + # indicate that the frame is using an extension. + # https://datatracker.ietf.org/doc/html/rfc6455#section-5.2 + compressobj = self._get_compressor(compress) + # (0x40) RSV1 is set for compressed frames + # https://datatracker.ietf.org/doc/html/rfc7692#section-7.2.3.1 + self._write_websocket_frame( + ( + await compressobj.compress(message) + + compressobj.flush( + ZLibBackend.Z_FULL_FLUSH + if self.notakeover + else ZLibBackend.Z_SYNC_FLUSH + ) + ).removesuffix(WS_DEFLATE_TRAILING), + opcode, + 0x40, + ) + + async def close(self, code: int = 1000, message: bytes | str = b"") -> None: + """Close the websocket, sending the specified code and message.""" + if isinstance(message, str): + message = message.encode("utf-8") + try: + await self.send_frame( + PACK_CLOSE_CODE(code) + message, opcode=WSMsgType.CLOSE + ) + finally: + self._closing = True diff --git a/venv/lib/python3.11/site-packages/aiohttp/abc.py b/venv/lib/python3.11/site-packages/aiohttp/abc.py new file mode 100644 index 0000000..b672873 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/abc.py @@ -0,0 +1,270 @@ +import asyncio +import logging +import socket +from abc import ABC, abstractmethod +from collections.abc import Awaitable, Callable, Generator, Iterable, Sequence, Sized +from http.cookies import BaseCookie, Morsel, SimpleCookie +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, TypedDict + +from multidict import CIMultiDict +from yarl import URL + +from ._cookie_helpers import parse_set_cookie_headers +from .typedefs import LooseCookies + +if TYPE_CHECKING: + from .web_app import Application + from .web_exceptions import HTTPException + from .web_request import BaseRequest, Request + from .web_response import StreamResponse +else: + BaseRequest = Request = Application = StreamResponse = Any + HTTPException = Any + + +class AbstractRouter(ABC): + def __init__(self) -> None: + self._frozen = False + + def post_init(self, app: Application) -> None: + """Post init stage. + + Not an abstract method for sake of backward compatibility, + but if the router wants to be aware of the application + it can override this. + """ + + @property + def frozen(self) -> bool: + return self._frozen + + def freeze(self) -> None: + """Freeze router.""" + self._frozen = True + + @abstractmethod + async def resolve(self, request: Request) -> "AbstractMatchInfo": + """Return MATCH_INFO for given request""" + + +class AbstractMatchInfo(ABC): + + __slots__ = () + + @property # pragma: no branch + @abstractmethod + def handler(self) -> Callable[[Request], Awaitable[StreamResponse]]: + """Execute matched request handler""" + + @property + @abstractmethod + def expect_handler( + self, + ) -> Callable[[Request], Awaitable[StreamResponse | None]]: + """Expect handler for 100-continue processing""" + + @property # pragma: no branch + @abstractmethod + def http_exception(self) -> HTTPException | None: + """HTTPException instance raised on router's resolving, or None""" + + @abstractmethod # pragma: no branch + def get_info(self) -> dict[str, Any]: + """Return a dict with additional info useful for introspection""" + + @property # pragma: no branch + @abstractmethod + def apps(self) -> tuple[Application, ...]: + """Stack of nested applications. + + Top level application is left-most element. + + """ + + @abstractmethod + def add_app(self, app: Application) -> None: + """Add application to the nested apps stack.""" + + @abstractmethod + def freeze(self) -> None: + """Freeze the match info. + + The method is called after route resolution. + + After the call .add_app() is forbidden. + + """ + + +class AbstractView(ABC): + """Abstract class based view.""" + + def __init__(self, request: Request) -> None: + self._request = request + + @property + def request(self) -> Request: + """Request instance.""" + return self._request + + @abstractmethod + def __await__(self) -> Generator[None, None, StreamResponse]: + """Execute the view handler.""" + + +class ResolveResult(TypedDict): + """Resolve result. + + This is the result returned from an AbstractResolver's + resolve method. + + :param hostname: The hostname that was provided. + :param host: The IP address that was resolved. + :param port: The port that was resolved. + :param family: The address family that was resolved. + :param proto: The protocol that was resolved. + :param flags: The flags that were resolved. + """ + + hostname: str + host: str + port: int + family: int + proto: int + flags: int + + +class AbstractResolver(ABC): + """Abstract DNS resolver.""" + + @abstractmethod + async def resolve( + self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET + ) -> list[ResolveResult]: + """Return IP address for given hostname""" + + @abstractmethod + async def close(self) -> None: + """Release resolver""" + + +if TYPE_CHECKING: + IterableBase = Iterable[Morsel[str]] +else: + IterableBase = Iterable + + +ClearCookiePredicate = Callable[["Morsel[str]"], bool] + + +class AbstractCookieJar(Sized, IterableBase): + """Abstract Cookie Jar.""" + + def __init__(self, *, loop: asyncio.AbstractEventLoop | None = None) -> None: + self._loop = loop or asyncio.get_running_loop() + + @property + @abstractmethod + def unsafe(self) -> bool: + """Return True if cookies can be used with IP addresses.""" + + @property + @abstractmethod + def quote_cookie(self) -> bool: + """Return True if cookies should be quoted.""" + + @property + @abstractmethod + def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]: + """Return the cookies stored in this jar.""" + + @property + @abstractmethod + def host_only_cookies(self) -> frozenset[tuple[str, str]]: + """Return the host-only cookies stored in this jar.""" + + @abstractmethod + def clear(self, predicate: ClearCookiePredicate | None = None) -> None: + """Clear all cookies if no predicate is passed.""" + + @abstractmethod + def clear_domain(self, domain: str) -> None: + """Clear all cookies for domain and all subdomains.""" + + @abstractmethod + def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None: + """Update cookies.""" + + def update_cookies_from_headers( + self, headers: Sequence[str], response_url: URL + ) -> None: + """Update cookies from raw Set-Cookie headers.""" + if headers and (cookies_to_update := parse_set_cookie_headers(headers)): + self.update_cookies(cookies_to_update, response_url) + + @abstractmethod + def filter_cookies(self, request_url: URL) -> "BaseCookie[str]": + """Return the jar's cookies filtered by their attributes.""" + + +class AbstractStreamWriter(ABC): + """Abstract stream writer.""" + + buffer_size: int = 0 + output_size: int = 0 + length: int | None = 0 + + @abstractmethod + async def write(self, chunk: bytes | bytearray | memoryview) -> None: + """Write chunk into stream.""" + + @abstractmethod + async def write_eof(self, chunk: bytes = b"") -> None: + """Write last chunk.""" + + @abstractmethod + async def drain(self) -> None: + """Flush the write buffer.""" + + @abstractmethod + def enable_compression( + self, encoding: str = "deflate", strategy: int | None = None + ) -> None: + """Enable HTTP body compression""" + + @abstractmethod + def enable_chunking(self) -> None: + """Enable HTTP chunked mode""" + + @abstractmethod + async def write_headers( + self, status_line: str, headers: "CIMultiDict[str]" + ) -> None: + """Write HTTP headers""" + + def send_headers(self) -> None: + """Force sending buffered headers if not already sent. + + Required only if write_headers() buffers headers instead of sending immediately. + For backwards compatibility, this method does nothing by default. + """ + + +class AbstractAccessLogger(ABC): + """Abstract writer to access log.""" + + __slots__ = ("logger", "log_format") + + def __init__(self, logger: logging.Logger, log_format: str) -> None: + self.logger = logger + self.log_format = log_format + + @abstractmethod + def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None: + """Emit log to logger.""" + + @property + def enabled(self) -> bool: + """Check if logger is enabled.""" + return True diff --git a/venv/lib/python3.11/site-packages/aiohttp/base_protocol.py b/venv/lib/python3.11/site-packages/aiohttp/base_protocol.py new file mode 100644 index 0000000..df3f8c0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/base_protocol.py @@ -0,0 +1,140 @@ +import asyncio +from typing import TYPE_CHECKING, Any, cast + +from .client_exceptions import ClientConnectionResetError +from .helpers import set_exception +from .tcp_helpers import tcp_nodelay + +if TYPE_CHECKING: + from .http_parser import HttpParser + +# Raised by transport.pause_reading()/resume_reading() when the transport +# does not support flow control; safe to ignore. +# NOTE: Catch these with a plain try/except/pass, never contextlib.suppress(): +# pause/resume run on the hot read path and suppress() is ~6x slower than +# try/except here (it builds a context manager and unpacks this tuple per call). +PAUSE_RESUME_READING_ERRORS = (AttributeError, NotImplementedError, RuntimeError) + + +class BaseProtocol(asyncio.Protocol): + __slots__ = ( + "_loop", + "_paused", + "_parser", + "_drain_waiter", + "_connection_lost", + "_reading_paused", + "_upgraded", + "transport", + ) + + def __init__( + self, loop: asyncio.AbstractEventLoop, parser: "HttpParser[Any] | None" = None + ) -> None: + self._loop: asyncio.AbstractEventLoop = loop + self._paused = False + self._drain_waiter: asyncio.Future[None] | None = None + self._reading_paused = False + self._parser = parser + self._upgraded = False + + self.transport: asyncio.Transport | None = None + + @property + def connected(self) -> bool: + """Return True if the connection is open.""" + return self.transport is not None + + @property + def writing_paused(self) -> bool: + return self._paused + + def pause_writing(self) -> None: + assert not self._paused + self._paused = True + + def resume_writing(self) -> None: + assert self._paused + self._paused = False + + waiter = self._drain_waiter + if waiter is not None: + self._drain_waiter = None + if not waiter.done(): + waiter.set_result(None) + + def pause_reading(self) -> None: + self._reading_paused = True + # Parser shouldn't be paused on websockets. + if not self._upgraded: + assert self._parser is not None + self._parser.pause_reading() + if self.transport is not None: + try: + self.transport.pause_reading() + except PAUSE_RESUME_READING_ERRORS: + # Transport lacks flow control; nothing to pause. Intentionally + # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). + pass + + def _reading_paused_for_msg_queue(self) -> bool: + """Keep the transport paused for protocol-specific reasons (overridden).""" + return False + + def resume_reading(self, resume_parser: bool = True) -> None: + self._reading_paused = False + + # This will resume parsing any unprocessed data from the last pause. + if not self._upgraded and resume_parser: + self.data_received(b"") + + # Reading may have been paused again in the above call if there was a lot of + # compressed data still pending. + if ( + not self._reading_paused + and not self._reading_paused_for_msg_queue() + and self.transport is not None + ): + try: + self.transport.resume_reading() + except PAUSE_RESUME_READING_ERRORS: + # Transport lacks flow control; nothing to resume. Intentionally + # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). + pass + self._reading_paused = False + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + tr = cast(asyncio.Transport, transport) + tcp_nodelay(tr, True) + self.transport = tr + + def connection_lost(self, exc: BaseException | None) -> None: + # Wake up the writer if currently paused. + self.transport = None + if not self._paused: + return + waiter = self._drain_waiter + if waiter is None: + return + self._drain_waiter = None + if waiter.done(): + return + if exc is None: + waiter.set_result(None) + else: + set_exception( + waiter, + ConnectionError("Connection lost"), + exc, + ) + + async def _drain_helper(self) -> None: + if self.transport is None: + raise ClientConnectionResetError("Connection lost") + if not self._paused: + return + waiter = self._drain_waiter + if waiter is None: + waiter = self._loop.create_future() + self._drain_waiter = waiter + await waiter diff --git a/venv/lib/python3.11/site-packages/aiohttp/client.py b/venv/lib/python3.11/site-packages/aiohttp/client.py new file mode 100644 index 0000000..d9d8dfd --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/client.py @@ -0,0 +1,1808 @@ +"""HTTP Client for asyncio.""" + +import asyncio +import base64 +import hashlib +import json +import os +import sys +import traceback +import warnings +from collections.abc import ( + Awaitable, + Callable, + Coroutine, + Generator, + Iterable, + Sequence, +) +from contextlib import suppress +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Final, + Generic, + Literal, + TypedDict, + TypeVar, + overload, +) + +import attr +from multidict import CIMultiDict, MultiDict, MultiDictProxy, istr +from yarl import URL + +from . import hdrs, http, payload +from ._websocket.reader import WebSocketDataQueue +from .abc import AbstractCookieJar +from .client_exceptions import ( + ClientConnectionError, + ClientConnectionResetError, + ClientConnectorCertificateError, + ClientConnectorDNSError, + ClientConnectorError, + ClientConnectorSSLError, + ClientError, + ClientHttpProxyError, + ClientOSError, + ClientPayloadError, + ClientProxyConnectionError, + ClientResponseError, + ClientSSLError, + ConnectionTimeoutError, + ContentTypeError, + InvalidURL, + InvalidUrlClientError, + InvalidUrlRedirectClientError, + NonHttpUrlClientError, + NonHttpUrlRedirectClientError, + RedirectClientError, + ServerConnectionError, + ServerDisconnectedError, + ServerFingerprintMismatch, + ServerTimeoutError, + SocketTimeoutError, + TooManyRedirects, + WSMessageTypeError, + WSServerHandshakeError, +) +from .client_middlewares import ClientMiddlewareType, build_client_middlewares +from .client_reqrep import ( + ClientRequest as ClientRequest, + ClientResponse as ClientResponse, + Fingerprint as Fingerprint, + RequestInfo as RequestInfo, + _merge_ssl_params, +) +from .client_ws import ( + DEFAULT_WS_CLIENT_TIMEOUT, + ClientWebSocketResponse as ClientWebSocketResponse, + ClientWSTimeout as ClientWSTimeout, +) +from .connector import ( + HTTP_AND_EMPTY_SCHEMA_SET, + BaseConnector as BaseConnector, + NamedPipeConnector as NamedPipeConnector, + TCPConnector as TCPConnector, + UnixConnector as UnixConnector, +) +from .cookiejar import CookieJar +from .helpers import ( + _SENTINEL, + DEBUG, + DEFAULT_CHUNK_SIZE, + EMPTY_BODY_METHODS, + BasicAuth, + TimeoutHandle, + basicauth_from_netrc, + get_env_proxy_for_url, + netrc_from_env, + sentinel, + strip_auth_from_url, +) +from .http import WS_KEY, HttpVersion, WebSocketReader, WebSocketWriter +from .http_websocket import WSHandshakeError, ws_ext_gen, ws_ext_parse +from .tracing import Trace, TraceConfig +from .typedefs import ( + JSONBytesEncoder, + JSONEncoder, + LooseCookies, + LooseHeaders, + Query, + StrOrURL, +) + +__all__ = ( + # client_exceptions + "ClientConnectionError", + "ClientConnectionResetError", + "ClientConnectorCertificateError", + "ClientConnectorDNSError", + "ClientConnectorError", + "ClientConnectorSSLError", + "ClientError", + "ClientHttpProxyError", + "ClientOSError", + "ClientPayloadError", + "ClientProxyConnectionError", + "ClientResponseError", + "ClientSSLError", + "ConnectionTimeoutError", + "ContentTypeError", + "InvalidURL", + "InvalidUrlClientError", + "RedirectClientError", + "NonHttpUrlClientError", + "InvalidUrlRedirectClientError", + "NonHttpUrlRedirectClientError", + "ServerConnectionError", + "ServerDisconnectedError", + "ServerFingerprintMismatch", + "ServerTimeoutError", + "SocketTimeoutError", + "TooManyRedirects", + "WSServerHandshakeError", + # client_reqrep + "ClientRequest", + "ClientResponse", + "Fingerprint", + "RequestInfo", + # connector + "BaseConnector", + "TCPConnector", + "UnixConnector", + "NamedPipeConnector", + # client_ws + "ClientWebSocketResponse", + # client + "ClientSession", + "ClientTimeout", + "ClientWSTimeout", + "request", + "WSMessageTypeError", +) + + +if TYPE_CHECKING: + from ssl import SSLContext +else: + SSLContext = Any + +if sys.version_info >= (3, 11) and TYPE_CHECKING: + from typing import Unpack + + +class _RequestOptions(TypedDict, total=False): + params: Query + data: Any + json: Any + cookies: LooseCookies | None + headers: LooseHeaders | None + skip_auto_headers: Iterable[str] | None + auth: BasicAuth | None + allow_redirects: bool + max_redirects: int + compress: str | bool | None + chunked: bool | None + expect100: bool + raise_for_status: None | bool | Callable[[ClientResponse], Awaitable[None]] + read_until_eof: bool + proxy: StrOrURL | None + proxy_auth: BasicAuth | None + timeout: "ClientTimeout | _SENTINEL | None" + ssl: SSLContext | bool | Fingerprint + server_hostname: str | None + proxy_headers: LooseHeaders | None + trace_request_ctx: object + read_bufsize: int | None + auto_decompress: bool | None + max_line_size: int | None + max_field_size: int | None + max_headers: int | None + middlewares: Sequence[ClientMiddlewareType] | None + + +class _WSConnectOptions(TypedDict, total=False): + method: str + protocols: Iterable[str] + timeout: "ClientWSTimeout | _SENTINEL" + receive_timeout: float | None + autoclose: bool + autoping: bool + heartbeat: float | None + auth: BasicAuth | None + origin: str | None + params: Query + headers: LooseHeaders | None + proxy: StrOrURL | None + proxy_auth: BasicAuth | None + ssl: SSLContext | bool | Fingerprint + verify_ssl: bool | None + fingerprint: bytes | None + ssl_context: SSLContext | None + server_hostname: str | None + proxy_headers: LooseHeaders | None + compress: int + max_msg_size: int + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class ClientTimeout: + total: float | None = None + connect: float | None = None + sock_read: float | None = None + sock_connect: float | None = None + ceil_threshold: float = 5 + + # pool_queue_timeout: Optional[float] = None + # dns_resolution_timeout: Optional[float] = None + # socket_connect_timeout: Optional[float] = None + # connection_acquiring_timeout: Optional[float] = None + # new_connection_timeout: Optional[float] = None + # http_header_timeout: Optional[float] = None + # response_body_timeout: Optional[float] = None + + # to create a timeout specific for a single request, either + # - create a completely new one to overwrite the default + # - or use http://www.attrs.org/en/stable/api.html#attr.evolve + # to overwrite the defaults + + +# 5 Minute default read timeout +DEFAULT_TIMEOUT: Final[ClientTimeout] = ClientTimeout(total=5 * 60, sock_connect=30) + +# https://www.rfc-editor.org/rfc/rfc9110#section-9.2.2 +IDEMPOTENT_METHODS = frozenset({"GET", "HEAD", "OPTIONS", "TRACE", "PUT", "DELETE"}) + +_RetType_co = TypeVar( + "_RetType_co", + bound="ClientResponse | ClientWebSocketResponse[bool]", + covariant=True, +) +_CharsetResolver = Callable[[ClientResponse, bytes], str] + + +class ClientSession: + """First-class interface for making HTTP requests.""" + + ATTRS = frozenset( + [ + "_base_url", + "_base_url_origin", + "_source_traceback", + "_connector", + "_loop", + "_cookie_jar", + "_connector_owner", + "_default_auth", + "_version", + "_json_serialize", + "_json_serialize_bytes", + "_requote_redirect_url", + "_timeout", + "_raise_for_status", + "_auto_decompress", + "_trust_env", + "_default_headers", + "_skip_auto_headers", + "_request_class", + "_response_class", + "_ws_response_class", + "_trace_configs", + "_read_bufsize", + "_max_line_size", + "_max_field_size", + "_max_headers", + "_resolve_charset", + "_default_proxy", + "_default_proxy_auth", + "_retry_connection", + "_middlewares", + "requote_redirect_url", + ] + ) + + _source_traceback: traceback.StackSummary | None = None + _connector: BaseConnector | None = None + + def __init__( + self, + base_url: StrOrURL | None = None, + *, + connector: BaseConnector | None = None, + loop: asyncio.AbstractEventLoop | None = None, + cookies: LooseCookies | None = None, + headers: LooseHeaders | None = None, + proxy: StrOrURL | None = None, + proxy_auth: BasicAuth | None = None, + skip_auto_headers: Iterable[str] | None = None, + auth: BasicAuth | None = None, + json_serialize: JSONEncoder = json.dumps, + json_serialize_bytes: JSONBytesEncoder | None = None, + request_class: type[ClientRequest] = ClientRequest, + response_class: type[ClientResponse] = ClientResponse, + ws_response_class: type[ClientWebSocketResponse] = ClientWebSocketResponse, + version: HttpVersion = http.HttpVersion11, + cookie_jar: AbstractCookieJar | None = None, + connector_owner: bool = True, + raise_for_status: bool | Callable[[ClientResponse], Awaitable[None]] = False, + read_timeout: float | _SENTINEL = sentinel, + conn_timeout: float | None = None, + timeout: object | ClientTimeout = sentinel, + auto_decompress: bool = True, + trust_env: bool = False, + requote_redirect_url: bool = True, + trace_configs: list[TraceConfig] | None = None, + read_bufsize: int = DEFAULT_CHUNK_SIZE, + max_line_size: int = 8190, + max_field_size: int = 8190, + max_headers: int = 128, + fallback_charset_resolver: _CharsetResolver = lambda r, b: "utf-8", + middlewares: Sequence[ClientMiddlewareType] = (), + ssl_shutdown_timeout: _SENTINEL | None | float = sentinel, + ) -> None: + # We initialise _connector to None immediately, as it's referenced in __del__() + # and could cause issues if an exception occurs during initialisation. + self._connector: BaseConnector | None = None + + if loop is None: + if connector is not None: + loop = connector._loop + + loop = loop or asyncio.get_running_loop() + + if base_url is None or isinstance(base_url, URL): + self._base_url: URL | None = base_url + self._base_url_origin = None if base_url is None else base_url.origin() + else: + self._base_url = URL(base_url) + self._base_url_origin = self._base_url.origin() + assert self._base_url.absolute, "Only absolute URLs are supported" + if self._base_url is not None and not self._base_url.path.endswith("/"): + raise ValueError("base_url must have a trailing '/'") + + if timeout is sentinel or timeout is None: + self._timeout = DEFAULT_TIMEOUT + if read_timeout is not sentinel: + warnings.warn( + "read_timeout is deprecated, use timeout argument instead", + DeprecationWarning, + stacklevel=2, + ) + self._timeout = attr.evolve(self._timeout, total=read_timeout) + if conn_timeout is not None: + self._timeout = attr.evolve(self._timeout, connect=conn_timeout) + warnings.warn( + "conn_timeout is deprecated, use timeout argument instead", + DeprecationWarning, + stacklevel=2, + ) + else: + if not isinstance(timeout, ClientTimeout): + raise ValueError( + f"timeout parameter cannot be of {type(timeout)} type, " + "please use 'timeout=ClientTimeout(...)'", + ) + self._timeout = timeout + if read_timeout is not sentinel: + raise ValueError( + "read_timeout and timeout parameters " + "conflict, please setup " + "timeout.read" + ) + if conn_timeout is not None: + raise ValueError( + "conn_timeout and timeout parameters " + "conflict, please setup " + "timeout.connect" + ) + + if ssl_shutdown_timeout is not sentinel: + warnings.warn( + "The ssl_shutdown_timeout parameter is deprecated and will be removed in aiohttp 4.0", + DeprecationWarning, + stacklevel=2, + ) + + if connector is None: + connector = TCPConnector( + loop=loop, ssl_shutdown_timeout=ssl_shutdown_timeout + ) + + if connector._loop is not loop: + raise RuntimeError("Session and connector has to use same event loop") + + self._loop = loop + + if loop.get_debug(): + self._source_traceback = traceback.extract_stack(sys._getframe(1)) + + if cookie_jar is None: + cookie_jar = CookieJar(loop=loop) + self._cookie_jar = cookie_jar + + if cookies: + self._cookie_jar.update_cookies(cookies) + + if auth is not None: + warnings.warn( + "The 'auth' parameter is deprecated and will be removed in v4;" + " pass headers={'Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=2, + ) + if proxy_auth is not None: + warnings.warn( + "The 'proxy_auth' parameter is deprecated and will be removed in v4;" + " pass proxy_headers={'Proxy-Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=2, + ) + self._connector = connector + self._connector_owner = connector_owner + self._default_auth = auth + self._version = version + self._json_serialize = json_serialize + self._json_serialize_bytes = json_serialize_bytes + self._raise_for_status = raise_for_status + self._auto_decompress = auto_decompress + self._trust_env = trust_env + self._requote_redirect_url = requote_redirect_url + self._read_bufsize = read_bufsize + self._max_line_size = max_line_size + self._max_field_size = max_field_size + self._max_headers = max_headers + + # Convert to list of tuples + if headers: + real_headers: CIMultiDict[str] = CIMultiDict(headers) + else: + real_headers = CIMultiDict() + self._default_headers: CIMultiDict[str] = real_headers + if skip_auto_headers is not None: + self._skip_auto_headers = frozenset(istr(i) for i in skip_auto_headers) + else: + self._skip_auto_headers = frozenset() + + self._request_class = request_class + self._response_class = response_class + self._ws_response_class = ws_response_class + + self._trace_configs = trace_configs or [] + for trace_config in self._trace_configs: + trace_config.freeze() + + self._resolve_charset = fallback_charset_resolver + + self._default_proxy = proxy + self._default_proxy_auth = proxy_auth + self._retry_connection: bool = True + self._middlewares = middlewares + + def __init_subclass__(cls: type["ClientSession"]) -> None: + warnings.warn( + f"Inheritance class {cls.__name__} from ClientSession is discouraged", + DeprecationWarning, + stacklevel=2, + ) + + if DEBUG: + + def __setattr__(self, name: str, val: Any) -> None: + if name not in self.ATTRS: + warnings.warn( + f"Setting custom ClientSession.{name} attribute is discouraged", + DeprecationWarning, + stacklevel=2, + ) + super().__setattr__(name, val) + + def __del__(self, _warnings: Any = warnings) -> None: + if not self.closed: + kwargs = {"source": self} + _warnings.warn( + f"Unclosed client session {self!r}", ResourceWarning, **kwargs + ) + context = {"client_session": self, "message": "Unclosed client session"} + if self._source_traceback is not None: + context["source_traceback"] = self._source_traceback + self._loop.call_exception_handler(context) + + if sys.version_info >= (3, 11) and TYPE_CHECKING: + + def request( + self, + method: str, + url: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> "_RequestContextManager": ... + + else: + + def request( + self, method: str, url: StrOrURL, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP request.""" + return _RequestContextManager(self._request(method, url, **kwargs)) + + def _build_url(self, str_or_url: StrOrURL) -> URL: + url = URL(str_or_url) + if self._base_url and not url.absolute: + return self._base_url.join(url) + return url + + async def _request( + self, + method: str, + str_or_url: StrOrURL, + *, + params: Query = None, + data: Any = None, + json: Any = None, + cookies: LooseCookies | None = None, + headers: LooseHeaders | None = None, + skip_auto_headers: Iterable[str] | None = None, + auth: BasicAuth | None = None, + allow_redirects: bool = True, + max_redirects: int = 10, + compress: str | bool | None = None, + chunked: bool | None = None, + expect100: bool = False, + raise_for_status: ( + None | bool | Callable[[ClientResponse], Awaitable[None]] + ) = None, + read_until_eof: bool = True, + proxy: StrOrURL | None = None, + proxy_auth: BasicAuth | None = None, + timeout: ClientTimeout | _SENTINEL = sentinel, + verify_ssl: bool | None = None, + fingerprint: bytes | None = None, + ssl_context: SSLContext | None = None, + ssl: SSLContext | bool | Fingerprint = True, + server_hostname: str | None = None, + proxy_headers: LooseHeaders | None = None, + trace_request_ctx: object = None, + read_bufsize: int | None = None, + auto_decompress: bool | None = None, + max_line_size: int | None = None, + max_field_size: int | None = None, + max_headers: int | None = None, + middlewares: Sequence[ClientMiddlewareType] | None = None, + ) -> ClientResponse: + + # NOTE: timeout clamps existing connect and read timeouts. We cannot + # set the default to None because we need to detect if the user wants + # to use the existing timeouts by setting timeout to None. + + if self.closed: + raise RuntimeError("Session is closed") + + ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint) + + if auth is not None: + warnings.warn( + "The 'auth' parameter is deprecated and will be removed in v4;" + " pass headers={'Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=3, + ) + if proxy_auth is not None: + warnings.warn( + "The 'proxy_auth' parameter is deprecated and will be removed in v4;" + " pass proxy_headers={'Proxy-Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=3, + ) + + if data is not None and json is not None: + raise ValueError( + "data and json parameters can not be used at the same time" + ) + elif json is not None: + if self._json_serialize_bytes is not None: + data = payload.JsonBytesPayload(json, dumps=self._json_serialize_bytes) + else: + data = payload.JsonPayload(json, dumps=self._json_serialize) + + if not isinstance(chunked, bool) and chunked is not None: + warnings.warn("Chunk size is deprecated #1615", DeprecationWarning) + + redirects = 0 + history: list[ClientResponse] = [] + version = self._version + params = params or {} + + # Merge with default headers and transform to CIMultiDict + headers = self._prepare_headers(headers) + + try: + url = self._build_url(str_or_url) + except ValueError as e: + raise InvalidUrlClientError(str_or_url) from e + + assert self._connector is not None + if url.scheme not in self._connector.allowed_protocol_schema_set: + raise NonHttpUrlClientError(url) + + skip_headers: Iterable[istr] | None + if skip_auto_headers is not None: + skip_headers = { + istr(i) for i in skip_auto_headers + } | self._skip_auto_headers + elif self._skip_auto_headers: + skip_headers = self._skip_auto_headers + else: + skip_headers = None + + if proxy is None: + proxy = self._default_proxy + if proxy_auth is None: + proxy_auth = self._default_proxy_auth + + if proxy is None: + proxy_headers = None + else: + proxy_headers = self._prepare_headers(proxy_headers) + try: + proxy = URL(proxy) + except ValueError as e: + raise InvalidURL(proxy) from e + + if timeout is sentinel: + real_timeout: ClientTimeout = self._timeout + else: + if not isinstance(timeout, ClientTimeout): + real_timeout = ClientTimeout(total=timeout) + else: + real_timeout = timeout + # timeout is cumulative for all request operations + # (request, redirects, responses, data consuming) + tm = TimeoutHandle( + self._loop, real_timeout.total, ceil_threshold=real_timeout.ceil_threshold + ) + handle = tm.start() + + if read_bufsize is None: + read_bufsize = self._read_bufsize + + if auto_decompress is None: + auto_decompress = self._auto_decompress + + if max_line_size is None: + max_line_size = self._max_line_size + + if max_field_size is None: + max_field_size = self._max_field_size + + if max_headers is None: + max_headers = self._max_headers + + traces = [ + Trace( + self, + trace_config, + trace_config.trace_config_ctx(trace_request_ctx=trace_request_ctx), + ) + for trace_config in self._trace_configs + ] + + for trace in traces: + await trace.send_request_start(method, url.update_query(params), headers) + + timer = tm.timer() + try: + with timer: + # https://www.rfc-editor.org/rfc/rfc9112.html#name-retrying-requests + retry_persistent_connection = ( + self._retry_connection and method in IDEMPOTENT_METHODS + ) + while True: + url, auth_from_url = strip_auth_from_url(url) + if not url.raw_host: + # NOTE: Bail early, otherwise, causes `InvalidURL` through + # NOTE: `self._request_class()` below. + err_exc_cls = ( + InvalidUrlRedirectClientError + if redirects + else InvalidUrlClientError + ) + raise err_exc_cls(url) + # If `auth` was passed for an already authenticated URL, + # disallow only if this is the initial URL; this is to avoid issues + # with sketchy redirects that are not the caller's responsibility + if not history and (auth and auth_from_url): + raise ValueError( + "Cannot combine AUTH argument with " + "credentials encoded in URL" + ) + + # Override the auth with the one from the URL only if we + # have no auth, or if we got an auth from a redirect URL + if auth is None or (history and auth_from_url is not None): + auth = auth_from_url + + if ( + auth is None + and self._default_auth + and ( + not self._base_url or self._base_url_origin == url.origin() + ) + ): + auth = self._default_auth + + # Try netrc if auth is still None and trust_env is enabled. + if auth is None and self._trust_env and url.host is not None: + auth = await self._loop.run_in_executor( + None, self._get_netrc_auth, url.host + ) + + # It would be confusing if we support explicit + # Authorization header with auth argument + if ( + headers is not None + and auth is not None + and hdrs.AUTHORIZATION in headers + ): + raise ValueError( + "Cannot combine AUTHORIZATION header " + "with AUTH argument or credentials " + "encoded in URL" + ) + + all_cookies = self._cookie_jar.filter_cookies(url) + + if cookies is not None: + tmp_cookie_jar = CookieJar( + unsafe=self._cookie_jar.unsafe, + quote_cookie=self._cookie_jar.quote_cookie, + ) + tmp_cookie_jar.update_cookies(cookies) + req_cookies = tmp_cookie_jar.filter_cookies(url) + if req_cookies: + all_cookies.load(req_cookies) + + proxy_: URL | None = None + if proxy is not None: + proxy_ = URL(proxy) + elif self._trust_env: + with suppress(LookupError): + proxy_, proxy_auth = await asyncio.to_thread( + get_env_proxy_for_url, url + ) + + req = self._request_class( + method, + url, + params=params, + headers=headers, + skip_auto_headers=skip_headers, + data=data, + cookies=all_cookies, + auth=auth, + version=version, + compress=compress, + chunked=chunked, + expect100=expect100, + loop=self._loop, + response_class=self._response_class, + proxy=proxy_, + proxy_auth=proxy_auth, + timer=timer, + session=self, + ssl=ssl if ssl is not None else True, + server_hostname=server_hostname, + proxy_headers=proxy_headers, + traces=traces, + trust_env=self.trust_env, + ) + + async def _connect_and_send_request( + req: ClientRequest, + ) -> ClientResponse: + # connection timeout + assert self._connector is not None + try: + conn = await self._connector.connect( + req, traces=traces, timeout=real_timeout + ) + except asyncio.TimeoutError as exc: + raise ConnectionTimeoutError( + f"Connection timeout to host {req.url}" + ) from exc + + assert conn.protocol is not None + conn.protocol.set_response_params( + timer=timer, + skip_payload=req.method in EMPTY_BODY_METHODS, + read_until_eof=read_until_eof, + auto_decompress=auto_decompress, + read_timeout=real_timeout.sock_read, + read_bufsize=read_bufsize, + timeout_ceil_threshold=self._connector._timeout_ceil_threshold, + max_line_size=max_line_size, + max_field_size=max_field_size, + max_headers=max_headers, + ) + try: + resp = await req.send(conn) + try: + await resp.start(conn) + except BaseException: + resp.close() + raise + except BaseException: + conn.close() + raise + return resp + + # Apply middleware (if any) - per-request middleware overrides session middleware + effective_middlewares = ( + self._middlewares if middlewares is None else middlewares + ) + + if effective_middlewares: + handler = build_client_middlewares( + _connect_and_send_request, effective_middlewares + ) + else: + handler = _connect_and_send_request + + try: + resp = await handler(req) + # Client connector errors should not be retried + except ( + ConnectionTimeoutError, + ClientConnectorError, + ClientConnectorCertificateError, + ClientConnectorSSLError, + ): + raise + except (ClientOSError, ServerDisconnectedError): + if retry_persistent_connection: + retry_persistent_connection = False + continue + raise + except ClientError: + raise + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise ClientOSError(*exc.args) from exc + + # Update cookies from raw headers to preserve duplicates + if resp._raw_cookie_headers: + self._cookie_jar.update_cookies_from_headers( + resp._raw_cookie_headers, resp.url + ) + + # redirects + if resp.status in (301, 302, 303, 307, 308) and allow_redirects: + + for trace in traces: + await trace.send_request_redirect( + method, url.update_query(params), headers, resp + ) + + redirects += 1 + history.append(resp) + if max_redirects and redirects >= max_redirects: + if req._body is not None: + await req._body.close() + resp.close() + raise TooManyRedirects( + history[0].request_info, tuple(history) + ) + + # For 301 and 302, mimic IE, now changed in RFC + # https://github.com/kennethreitz/requests/pull/269 + if (resp.status == 303 and resp.method != hdrs.METH_HEAD) or ( + resp.status in (301, 302) and resp.method == hdrs.METH_POST + ): + method = hdrs.METH_GET + data = None + if headers.get(hdrs.CONTENT_LENGTH): + headers.pop(hdrs.CONTENT_LENGTH) + else: + # For 307/308, always preserve the request body + # For 301/302 with non-POST methods, preserve the request body + # https://www.rfc-editor.org/rfc/rfc9110#section-15.4.3-3.1 + # Use the existing payload to avoid recreating it from + # a potentially consumed file. + # + # If the payload is already consumed and cannot be replayed, + # fail fast instead of silently sending an empty body. + if req._body is not None and req._body.consumed: + resp.close() + raise ClientPayloadError( + "Cannot follow redirect with a consumed request " + "body. Use bytes, a seekable file-like object, " + "or set allow_redirects=False." + ) + data = req._body + + r_url = resp.headers.get(hdrs.LOCATION) or resp.headers.get( + hdrs.URI + ) + if r_url is None: + # see github.com/aio-libs/aiohttp/issues/2022 + break + else: + # reading from correct redirection + # response is forbidden + resp.release() + + try: + parsed_redirect_url = URL( + r_url, encoded=not self._requote_redirect_url + ) + except ValueError as e: + if req._body is not None: + await req._body.close() + resp.close() + raise InvalidUrlRedirectClientError( + r_url, + "Server attempted redirecting to a location that does not look like a URL", + ) from e + + scheme = parsed_redirect_url.scheme + if scheme not in HTTP_AND_EMPTY_SCHEMA_SET: + if req._body is not None: + await req._body.close() + resp.close() + raise NonHttpUrlRedirectClientError(r_url) + elif not scheme: + parsed_redirect_url = url.join(parsed_redirect_url) + + try: + redirect_origin = parsed_redirect_url.origin() + except ValueError as origin_val_err: + if req._body is not None: + await req._body.close() + resp.close() + raise InvalidUrlRedirectClientError( + parsed_redirect_url, + "Invalid redirect URL origin", + ) from origin_val_err + + if url.origin() != redirect_origin: + auth = None + cookies = None + headers.pop(hdrs.AUTHORIZATION, None) + headers.pop(hdrs.COOKIE, None) + headers.pop(hdrs.PROXY_AUTHORIZATION, None) + + url = parsed_redirect_url + params = {} + resp.release() + continue + + break + + if req._body is not None: + await req._body.close() + # check response status + if raise_for_status is None: + raise_for_status = self._raise_for_status + + if raise_for_status is None: + pass + elif callable(raise_for_status): + await raise_for_status(resp) + elif raise_for_status: + resp.raise_for_status() + + # register connection + if handle is not None: + if resp.connection is not None: + resp.connection.add_callback(handle.cancel) + else: + handle.cancel() + + resp._history = tuple(history) + + for trace in traces: + await trace.send_request_end( + method, url.update_query(params), headers, resp + ) + return resp + + except BaseException as e: + # cleanup timer + tm.close() + if handle: + handle.cancel() + handle = None + + for trace in traces: + await trace.send_request_exception( + method, url.update_query(params), headers, e + ) + raise + + if sys.version_info >= (3, 11) and TYPE_CHECKING: + + @overload + def ws_connect( + self, + url: StrOrURL, + *, + decode_text: Literal[True] = ..., + **kwargs: Unpack[_WSConnectOptions], + ) -> "_BaseRequestContextManager[ClientWebSocketResponse[Literal[True]]]": ... + + @overload + def ws_connect( + self, + url: StrOrURL, + *, + decode_text: Literal[False], + **kwargs: Unpack[_WSConnectOptions], + ) -> "_BaseRequestContextManager[ClientWebSocketResponse[Literal[False]]]": ... + + @overload + def ws_connect( + self, + url: StrOrURL, + *, + decode_text: bool = ..., + **kwargs: Unpack[_WSConnectOptions], + ) -> "_BaseRequestContextManager[ClientWebSocketResponse[bool]]": ... + + def ws_connect( + self, + url: StrOrURL, + *, + method: str = hdrs.METH_GET, + protocols: Iterable[str] = (), + timeout: ClientWSTimeout | _SENTINEL = sentinel, + receive_timeout: float | None = None, + autoclose: bool = True, + autoping: bool = True, + heartbeat: float | None = None, + auth: BasicAuth | None = None, + origin: str | None = None, + params: Query = None, + headers: LooseHeaders | None = None, + proxy: StrOrURL | None = None, + proxy_auth: BasicAuth | None = None, + ssl: SSLContext | bool | Fingerprint = True, + verify_ssl: bool | None = None, + fingerprint: bytes | None = None, + ssl_context: SSLContext | None = None, + server_hostname: str | None = None, + proxy_headers: LooseHeaders | None = None, + compress: int = 0, + max_msg_size: int = 4 * 1024 * 1024, + decode_text: bool = True, + ) -> "_BaseRequestContextManager[ClientWebSocketResponse[bool]]": + """Initiate websocket connection.""" + return _WSRequestContextManager( + self._ws_connect( + url, + method=method, + protocols=protocols, + timeout=timeout, + receive_timeout=receive_timeout, + autoclose=autoclose, + autoping=autoping, + heartbeat=heartbeat, + auth=auth, + origin=origin, + params=params, + headers=headers, + proxy=proxy, + proxy_auth=proxy_auth, + ssl=ssl, + verify_ssl=verify_ssl, + fingerprint=fingerprint, + ssl_context=ssl_context, + server_hostname=server_hostname, + proxy_headers=proxy_headers, + compress=compress, + max_msg_size=max_msg_size, + decode_text=decode_text, + ) + ) + + if sys.version_info >= (3, 11) and TYPE_CHECKING: + + @overload + async def _ws_connect( + self, + url: StrOrURL, + *, + decode_text: Literal[True] = ..., + **kwargs: Unpack[_WSConnectOptions], + ) -> "ClientWebSocketResponse[Literal[True]]": ... + + @overload + async def _ws_connect( + self, + url: StrOrURL, + *, + decode_text: Literal[False], + **kwargs: Unpack[_WSConnectOptions], + ) -> "ClientWebSocketResponse[Literal[False]]": ... + + @overload + async def _ws_connect( + self, + url: StrOrURL, + *, + decode_text: bool = ..., + **kwargs: Unpack[_WSConnectOptions], + ) -> "ClientWebSocketResponse[bool]": ... + + async def _ws_connect( + self, + url: StrOrURL, + *, + method: str = hdrs.METH_GET, + protocols: Iterable[str] = (), + timeout: ClientWSTimeout | _SENTINEL = sentinel, + receive_timeout: float | None = None, + autoclose: bool = True, + autoping: bool = True, + heartbeat: float | None = None, + auth: BasicAuth | None = None, + origin: str | None = None, + params: Query = None, + headers: LooseHeaders | None = None, + proxy: StrOrURL | None = None, + proxy_auth: BasicAuth | None = None, + ssl: SSLContext | bool | Fingerprint = True, + verify_ssl: bool | None = None, + fingerprint: bytes | None = None, + ssl_context: SSLContext | None = None, + server_hostname: str | None = None, + proxy_headers: LooseHeaders | None = None, + compress: int = 0, + max_msg_size: int = 4 * 1024 * 1024, + decode_text: bool = True, + ) -> "ClientWebSocketResponse[bool]": + if auth is not None: + warnings.warn( + "The 'auth' parameter is deprecated and will be removed in v4;" + " pass headers={'Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=3, + ) + if proxy_auth is not None: + warnings.warn( + "The 'proxy_auth' parameter is deprecated and will be removed in v4;" + " pass proxy_headers={'Proxy-Authorization': " + "aiohttp.encode_basic_auth(login, password)} instead", + DeprecationWarning, + stacklevel=3, + ) + if timeout is not sentinel: + if isinstance(timeout, ClientWSTimeout): + ws_timeout = timeout + else: + warnings.warn( + "parameter 'timeout' of type 'float' " + "is deprecated, please use " + "'timeout=ClientWSTimeout(ws_close=...)'", + DeprecationWarning, + stacklevel=2, + ) + ws_timeout = ClientWSTimeout(ws_close=timeout) + else: + ws_timeout = DEFAULT_WS_CLIENT_TIMEOUT + if receive_timeout is not None: + warnings.warn( + "float parameter 'receive_timeout' " + "is deprecated, please use parameter " + "'timeout=ClientWSTimeout(ws_receive=...)'", + DeprecationWarning, + stacklevel=2, + ) + ws_timeout = attr.evolve(ws_timeout, ws_receive=receive_timeout) + + if headers is None: + real_headers: CIMultiDict[str] = CIMultiDict() + else: + real_headers = CIMultiDict(headers) + + default_headers = { + hdrs.UPGRADE: "websocket", + hdrs.CONNECTION: "Upgrade", + hdrs.SEC_WEBSOCKET_VERSION: "13", + } + + for key, value in default_headers.items(): + real_headers.setdefault(key, value) + + sec_key = base64.b64encode(os.urandom(16)) + real_headers[hdrs.SEC_WEBSOCKET_KEY] = sec_key.decode() + + if protocols: + real_headers[hdrs.SEC_WEBSOCKET_PROTOCOL] = ",".join(protocols) + if origin is not None: + real_headers[hdrs.ORIGIN] = origin + if compress: + extstr = ws_ext_gen(compress=compress) + real_headers[hdrs.SEC_WEBSOCKET_EXTENSIONS] = extstr + + # For the sake of backward compatibility, if user passes in None, convert it to True + if ssl is None: + warnings.warn( + "ssl=None is deprecated, please use ssl=True", + DeprecationWarning, + stacklevel=2, + ) + ssl = True + ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint) + + # send request + resp = await self.request( + method, + url, + params=params, + headers=real_headers, + read_until_eof=False, + auth=auth, + proxy=proxy, + proxy_auth=proxy_auth, + ssl=ssl, + server_hostname=server_hostname, + proxy_headers=proxy_headers, + ) + + try: + # check handshake + if resp.status != 101: + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message="Invalid response status", + status=resp.status, + headers=resp.headers, + ) + + if resp.headers.get(hdrs.UPGRADE, "").lower() != "websocket": + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message="Invalid upgrade header", + status=resp.status, + headers=resp.headers, + ) + + if resp.headers.get(hdrs.CONNECTION, "").lower() != "upgrade": + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message="Invalid connection header", + status=resp.status, + headers=resp.headers, + ) + + # key calculation + r_key = resp.headers.get(hdrs.SEC_WEBSOCKET_ACCEPT, "") + match = base64.b64encode(hashlib.sha1(sec_key + WS_KEY).digest()).decode() + if r_key != match: + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message="Invalid challenge response", + status=resp.status, + headers=resp.headers, + ) + + # websocket protocol + protocol = None + if protocols and hdrs.SEC_WEBSOCKET_PROTOCOL in resp.headers: + resp_protocols = [ + proto.strip() + for proto in resp.headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",") + ] + + for proto in resp_protocols: + if proto in protocols: + protocol = proto + break + + # websocket compress + notakeover = False + if compress: + compress_hdrs = resp.headers.get(hdrs.SEC_WEBSOCKET_EXTENSIONS) + if compress_hdrs: + try: + compress, notakeover = ws_ext_parse(compress_hdrs) + except WSHandshakeError as exc: + raise WSServerHandshakeError( + resp.request_info, + resp.history, + message=exc.args[0], + status=resp.status, + headers=resp.headers, + ) from exc + else: + compress = 0 + notakeover = False + + conn = resp.connection + assert conn is not None + conn_proto = conn.protocol + assert conn_proto is not None + + # For WS connection the read_timeout must be either receive_timeout or greater + # None == no timeout, i.e. infinite timeout, so None is the max timeout possible + if ws_timeout.ws_receive is None: + # Reset regardless + conn_proto.read_timeout = None + elif conn_proto.read_timeout is not None: + conn_proto.read_timeout = max( + ws_timeout.ws_receive, conn_proto.read_timeout + ) + + transport = conn.transport + assert transport is not None + reader = WebSocketDataQueue(conn_proto, DEFAULT_CHUNK_SIZE, loop=self._loop) + writer = WebSocketWriter( + conn_proto, + transport, + use_mask=True, + compress=compress, + notakeover=notakeover, + ) + except BaseException: + resp.close() + raise + else: + ws_resp = self._ws_response_class( + reader, + writer, + protocol, + resp, + ws_timeout, + autoclose, + autoping, + self._loop, + heartbeat=heartbeat, + compress=compress, + client_notakeover=notakeover, + ) + parser = WebSocketReader(reader, max_msg_size, decode_text=decode_text) + cb = None if heartbeat is None else ws_resp._on_data_received + conn_proto.set_parser(parser, reader, data_received_cb=cb) + return ws_resp + + def _prepare_headers(self, headers: LooseHeaders | None) -> "CIMultiDict[str]": + """Add default headers and transform it to CIMultiDict""" + # Convert headers to MultiDict + result = CIMultiDict(self._default_headers) + if headers: + if not isinstance(headers, (MultiDictProxy, MultiDict)): + headers = CIMultiDict(headers) + added_names: set[str] = set() + for key, value in headers.items(): + if key in added_names: + result.add(key, value) + else: + result[key] = value + added_names.add(key) + return result + + def _get_netrc_auth(self, host: str) -> BasicAuth | None: + """ + Get auth from netrc for the given host. + + This method is designed to be called in an executor to avoid + blocking I/O in the event loop. + """ + netrc_obj = netrc_from_env() + try: + return basicauth_from_netrc(netrc_obj, host) + except LookupError: + return None + + if sys.version_info >= (3, 11) and TYPE_CHECKING: + + def get( + self, + url: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> "_RequestContextManager": ... + + def options( + self, + url: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> "_RequestContextManager": ... + + def head( + self, + url: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> "_RequestContextManager": ... + + def post( + self, + url: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> "_RequestContextManager": ... + + def put( + self, + url: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> "_RequestContextManager": ... + + def patch( + self, + url: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> "_RequestContextManager": ... + + def delete( + self, + url: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> "_RequestContextManager": ... + + else: + + def get( + self, url: StrOrURL, *, allow_redirects: bool = True, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP GET request.""" + return _RequestContextManager( + self._request( + hdrs.METH_GET, url, allow_redirects=allow_redirects, **kwargs + ) + ) + + def options( + self, url: StrOrURL, *, allow_redirects: bool = True, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP OPTIONS request.""" + return _RequestContextManager( + self._request( + hdrs.METH_OPTIONS, url, allow_redirects=allow_redirects, **kwargs + ) + ) + + def head( + self, url: StrOrURL, *, allow_redirects: bool = False, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP HEAD request.""" + return _RequestContextManager( + self._request( + hdrs.METH_HEAD, url, allow_redirects=allow_redirects, **kwargs + ) + ) + + def post( + self, url: StrOrURL, *, data: Any = None, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP POST request.""" + return _RequestContextManager( + self._request(hdrs.METH_POST, url, data=data, **kwargs) + ) + + def put( + self, url: StrOrURL, *, data: Any = None, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP PUT request.""" + return _RequestContextManager( + self._request(hdrs.METH_PUT, url, data=data, **kwargs) + ) + + def patch( + self, url: StrOrURL, *, data: Any = None, **kwargs: Any + ) -> "_RequestContextManager": + """Perform HTTP PATCH request.""" + return _RequestContextManager( + self._request(hdrs.METH_PATCH, url, data=data, **kwargs) + ) + + def delete(self, url: StrOrURL, **kwargs: Any) -> "_RequestContextManager": + """Perform HTTP DELETE request.""" + return _RequestContextManager( + self._request(hdrs.METH_DELETE, url, **kwargs) + ) + + async def close(self) -> None: + """Close underlying connector. + + Release all acquired resources. + """ + if not self.closed: + if self._connector is not None and self._connector_owner: + await self._connector.close() + self._connector = None + + @property + def closed(self) -> bool: + """Is client session closed. + + A readonly property. + """ + return self._connector is None or self._connector.closed + + @property + def connector(self) -> BaseConnector | None: + """Connector instance used for the session.""" + return self._connector + + @property + def cookie_jar(self) -> AbstractCookieJar: + """The session cookies.""" + return self._cookie_jar + + @property + def version(self) -> tuple[int, int]: + """The session HTTP protocol version.""" + return self._version + + @property + def requote_redirect_url(self) -> bool: + """Do URL requoting on redirection handling.""" + return self._requote_redirect_url + + @requote_redirect_url.setter + def requote_redirect_url(self, val: bool) -> None: + """Do URL requoting on redirection handling.""" + warnings.warn( + "session.requote_redirect_url modification is deprecated #2778", + DeprecationWarning, + stacklevel=2, + ) + self._requote_redirect_url = val + + @property + def loop(self) -> asyncio.AbstractEventLoop: + """Session's loop.""" + warnings.warn( + "client.loop property is deprecated", DeprecationWarning, stacklevel=2 + ) + return self._loop + + @property + def timeout(self) -> ClientTimeout: + """Timeout for the session.""" + return self._timeout + + @property + def headers(self) -> "CIMultiDict[str]": + """The default headers of the client session.""" + return self._default_headers + + @property + def skip_auto_headers(self) -> frozenset[istr]: + """Headers for which autogeneration should be skipped""" + return self._skip_auto_headers + + @property + def auth(self) -> BasicAuth | None: + """An object that represents HTTP Basic Authorization""" + return self._default_auth + + @property + def json_serialize(self) -> JSONEncoder: + """Json serializer callable""" + return self._json_serialize + + @property + def connector_owner(self) -> bool: + """Should connector be closed on session closing""" + return self._connector_owner + + @property + def raise_for_status( + self, + ) -> bool | Callable[[ClientResponse], Awaitable[None]]: + """Should `ClientResponse.raise_for_status()` be called for each response.""" + return self._raise_for_status + + @property + def auto_decompress(self) -> bool: + """Should the body response be automatically decompressed.""" + return self._auto_decompress + + @property + def trust_env(self) -> bool: + """ + Should proxies information from environment or netrc be trusted. + + Information is from HTTP_PROXY / HTTPS_PROXY environment variables + or ~/.netrc file if present. + """ + return self._trust_env + + @property + def trace_configs(self) -> list[TraceConfig]: + """A list of TraceConfig instances used for client tracing""" + return self._trace_configs + + def detach(self) -> None: + """Detach connector from session without closing the former. + + Session is switched to closed state anyway. + """ + self._connector = None + + def __enter__(self) -> None: + raise TypeError("Use async with instead") + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + # __exit__ should exist in pair with __enter__ but never executed + pass # pragma: no cover + + async def __aenter__(self) -> "ClientSession": + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() + + +class _BaseRequestContextManager( + Coroutine[Any, Any, _RetType_co], Generic[_RetType_co] +): + + __slots__ = ("_coro", "_resp") + + def __init__(self, coro: Coroutine[asyncio.Future[Any], None, _RetType_co]) -> None: + self._coro: Coroutine[asyncio.Future[Any], None, _RetType_co] = coro + + def send(self, arg: None) -> asyncio.Future[Any]: + return self._coro.send(arg) + + def throw(self, *args: Any, **kwargs: Any) -> asyncio.Future[Any]: + return self._coro.throw(*args, **kwargs) + + def close(self) -> None: + return self._coro.close() + + def __await__(self) -> Generator[Any, None, _RetType_co]: + ret = self._coro.__await__() + return ret + + def __iter__(self) -> Generator[Any, None, _RetType_co]: + return self.__await__() + + async def __aenter__(self) -> _RetType_co: + self._resp: _RetType_co = await self._coro + return await self._resp.__aenter__() # type: ignore[return-value] + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + await self._resp.__aexit__(exc_type, exc, tb) + + +_RequestContextManager = _BaseRequestContextManager[ClientResponse] +_WSRequestContextManager = _BaseRequestContextManager[ClientWebSocketResponse[bool]] + + +class _SessionRequestContextManager: + + __slots__ = ("_coro", "_resp", "_session") + + def __init__( + self, + coro: Coroutine[asyncio.Future[Any], None, ClientResponse], + session: ClientSession, + ) -> None: + self._coro = coro + self._resp: ClientResponse | None = None + self._session = session + + async def __aenter__(self) -> ClientResponse: + try: + self._resp = await self._coro + except BaseException: + await self._session.close() + raise + else: + return self._resp + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + assert self._resp is not None + self._resp.close() + await self._session.close() + + +if sys.version_info >= (3, 11) and TYPE_CHECKING: + + def request( + method: str, + url: StrOrURL, + *, + version: HttpVersion = http.HttpVersion11, + connector: BaseConnector | None = None, + loop: asyncio.AbstractEventLoop | None = None, + **kwargs: Unpack[_RequestOptions], + ) -> _SessionRequestContextManager: ... + +else: + + def request( + method: str, + url: StrOrURL, + *, + version: HttpVersion = http.HttpVersion11, + connector: BaseConnector | None = None, + loop: asyncio.AbstractEventLoop | None = None, + **kwargs: Any, + ) -> _SessionRequestContextManager: + """Constructs and sends a request. + + Returns response object. + method - HTTP method + url - request url + params - (optional) Dictionary or bytes to be sent in the query + string of the new request + data - (optional) Dictionary, bytes, or file-like object to + send in the body of the request + json - (optional) Any json compatible python object + headers - (optional) Dictionary of HTTP Headers to send with + the request + cookies - (optional) Dict object to send with the request + auth - (optional) BasicAuth named tuple represent HTTP Basic Auth + auth - aiohttp.helpers.BasicAuth + allow_redirects - (optional) If set to False, do not follow + redirects + version - Request HTTP version. + compress - Set to True if request has to be compressed + with deflate encoding. + chunked - Set to chunk size for chunked transfer encoding. + expect100 - Expect 100-continue response from server. + connector - BaseConnector sub-class instance to support + connection pooling. + read_until_eof - Read response until eof if response + does not have Content-Length header. + loop - Optional event loop. + timeout - Optional ClientTimeout settings structure, 5min + total timeout by default. + Usage:: + >>> import aiohttp + >>> async with aiohttp.request('GET', 'http://python.org/') as resp: + ... print(resp) + ... data = await resp.read() + + """ + connector_owner = False + if connector is None: + connector_owner = True + connector = TCPConnector(loop=loop, force_close=True) + + session = ClientSession( + loop=loop, + cookies=kwargs.pop("cookies", None), + version=version, + timeout=kwargs.pop("timeout", sentinel), + connector=connector, + connector_owner=connector_owner, + ) + + return _SessionRequestContextManager( + session._request(method, url, **kwargs), + session, + ) diff --git a/venv/lib/python3.11/site-packages/aiohttp/client_exceptions.py b/venv/lib/python3.11/site-packages/aiohttp/client_exceptions.py new file mode 100644 index 0000000..e3e503b --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/client_exceptions.py @@ -0,0 +1,426 @@ +"""HTTP related errors.""" + +import asyncio +import warnings +from typing import TYPE_CHECKING, Union + +from multidict import MultiMapping + +from .typedefs import StrOrURL + +if TYPE_CHECKING: + import ssl + + SSLContext = ssl.SSLContext +else: + try: + import ssl + + SSLContext = ssl.SSLContext + except ImportError: # pragma: no cover + ssl = SSLContext = None # type: ignore[assignment] + +if TYPE_CHECKING: + from .client_reqrep import ClientResponse, ConnectionKey, Fingerprint, RequestInfo + from .http_parser import RawResponseMessage +else: + RequestInfo = ClientResponse = ConnectionKey = RawResponseMessage = None + +__all__ = ( + "ClientError", + "ClientConnectionError", + "ClientConnectionResetError", + "ClientOSError", + "ClientConnectorError", + "ClientProxyConnectionError", + "ClientSSLError", + "ClientConnectorDNSError", + "ClientConnectorSSLError", + "ClientConnectorCertificateError", + "ConnectionTimeoutError", + "SocketTimeoutError", + "ServerConnectionError", + "ServerTimeoutError", + "ServerDisconnectedError", + "ServerFingerprintMismatch", + "ClientResponseError", + "ClientHttpProxyError", + "WSServerHandshakeError", + "ContentTypeError", + "ClientPayloadError", + "InvalidURL", + "InvalidUrlClientError", + "RedirectClientError", + "NonHttpUrlClientError", + "InvalidUrlRedirectClientError", + "NonHttpUrlRedirectClientError", + "WSMessageTypeError", +) + + +class ClientError(Exception): + """Base class for client connection errors.""" + + +class ClientResponseError(ClientError): + """Base class for exceptions that occur after getting a response. + + request_info: An instance of RequestInfo. + history: A sequence of responses, if redirects occurred. + status: HTTP status code. + message: Error message. + headers: Response headers. + """ + + def __init__( + self, + request_info: RequestInfo, + history: tuple[ClientResponse, ...], + *, + code: int | None = None, + status: int | None = None, + message: str = "", + headers: MultiMapping[str] | None = None, + ) -> None: + self.request_info = request_info + if code is not None: + if status is not None: + raise ValueError( + "Both code and status arguments are provided; " + "code is deprecated, use status instead" + ) + warnings.warn( + "code argument is deprecated, use status instead", + DeprecationWarning, + stacklevel=2, + ) + if status is not None: + self.status = status + elif code is not None: + self.status = code + else: + self.status = 0 + self.message = message + self.headers = headers + self.history = history + self.args = (request_info, history) + + def __str__(self) -> str: + return f"{self.status}, message={self.message!r}, url={str(self.request_info.real_url)!r}" + + def __repr__(self) -> str: + args = f"{self.request_info!r}, {self.history!r}" + if self.status != 0: + args += f", status={self.status!r}" + if self.message != "": + args += f", message={self.message!r}" + if self.headers is not None: + args += f", headers={self.headers!r}" + return f"{type(self).__name__}({args})" + + @property + def code(self) -> int: + warnings.warn( + "code property is deprecated, use status instead", + DeprecationWarning, + stacklevel=2, + ) + return self.status + + @code.setter + def code(self, value: int) -> None: + warnings.warn( + "code property is deprecated, use status instead", + DeprecationWarning, + stacklevel=2, + ) + self.status = value + + +class ContentTypeError(ClientResponseError): + """ContentType found is not valid.""" + + +class WSServerHandshakeError(ClientResponseError): + """websocket server handshake error.""" + + +class ClientHttpProxyError(ClientResponseError): + """HTTP proxy error. + + Raised in :class:`aiohttp.connector.TCPConnector` if + proxy responds with status other than ``200 OK`` + on ``CONNECT`` request. + """ + + +class TooManyRedirects(ClientResponseError): + """Client was redirected too many times.""" + + +class ClientConnectionError(ClientError): + """Base class for client socket errors.""" + + +class ClientConnectionResetError(ClientConnectionError, ConnectionResetError): + """ConnectionResetError""" + + +class ClientOSError(ClientConnectionError, OSError): + """OSError error.""" + + +class ClientConnectorError(ClientOSError): + """Client connector error. + + Raised in :class:`aiohttp.connector.TCPConnector` if + a connection can not be established. + """ + + def __init__(self, connection_key: ConnectionKey, os_error: OSError) -> None: + self._conn_key = connection_key + self._os_error = os_error + super().__init__(os_error.errno, os_error.strerror) + self.args = (connection_key, os_error) + + @property + def os_error(self) -> OSError: + return self._os_error + + @property + def host(self) -> str: + return self._conn_key.host + + @property + def port(self) -> int | None: + return self._conn_key.port + + @property + def ssl(self) -> Union[SSLContext, bool, "Fingerprint"]: + return self._conn_key.ssl + + def __str__(self) -> str: + return "Cannot connect to host {0.host}:{0.port} ssl:{1} [{2}]".format( + self, "default" if self.ssl is True else self.ssl, self.strerror + ) + + # OSError.__reduce__ does too much black magick + __reduce__ = BaseException.__reduce__ + + +class ClientConnectorDNSError(ClientConnectorError): + """DNS resolution failed during client connection. + + Raised in :class:`aiohttp.connector.TCPConnector` if + DNS resolution fails. + """ + + +class ClientProxyConnectionError(ClientConnectorError): + """Proxy connection error. + + Raised in :class:`aiohttp.connector.TCPConnector` if + connection to proxy can not be established. + """ + + +class UnixClientConnectorError(ClientConnectorError): + """Unix connector error. + + Raised in :py:class:`aiohttp.connector.UnixConnector` + if connection to unix socket can not be established. + """ + + def __init__( + self, path: str, connection_key: ConnectionKey, os_error: OSError + ) -> None: + self._path = path + super().__init__(connection_key, os_error) + + @property + def path(self) -> str: + return self._path + + def __str__(self) -> str: + return "Cannot connect to unix socket {0.path} ssl:{1} [{2}]".format( + self, "default" if self.ssl is True else self.ssl, self.strerror + ) + + +class ServerConnectionError(ClientConnectionError): + """Server connection errors.""" + + +class ServerDisconnectedError(ServerConnectionError): + """Server disconnected.""" + + def __init__(self, message: RawResponseMessage | str | None = None) -> None: + if message is None: + message = "Server disconnected" + + self.args = (message,) + self.message = message + + +class ServerTimeoutError(ServerConnectionError, asyncio.TimeoutError): + """Server timeout error.""" + + +class ConnectionTimeoutError(ServerTimeoutError): + """Connection timeout error.""" + + +class SocketTimeoutError(ServerTimeoutError): + """Socket timeout error.""" + + +class ServerFingerprintMismatch(ServerConnectionError): + """SSL certificate does not match expected fingerprint.""" + + def __init__(self, expected: bytes, got: bytes, host: str, port: int) -> None: + self.expected = expected + self.got = got + self.host = host + self.port = port + self.args = (expected, got, host, port) + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} expected={self.expected!r} got={self.got!r} host={self.host!r} port={self.port!r}>" + + +class ClientPayloadError(ClientError): + """Response payload error.""" + + +class InvalidURL(ClientError, ValueError): + """Invalid URL. + + URL used for fetching is malformed, e.g. it doesn't contains host + part. + """ + + # Derive from ValueError for backward compatibility + + def __init__(self, url: StrOrURL, description: str | None = None) -> None: + # The type of url is not yarl.URL because the exception can be raised + # on URL(url) call + self._url = url + self._description = description + + if description: + super().__init__(url, description) + else: + super().__init__(url) + + @property + def url(self) -> StrOrURL: + return self._url + + @property + def description(self) -> "str | None": + return self._description + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self}>" + + def __str__(self) -> str: + if self._description: + return f"{self._url} - {self._description}" + return str(self._url) + + +class InvalidUrlClientError(InvalidURL): + """Invalid URL client error.""" + + +class RedirectClientError(ClientError): + """Client redirect error.""" + + +class NonHttpUrlClientError(ClientError): + """Non http URL client error.""" + + +class InvalidUrlRedirectClientError(InvalidUrlClientError, RedirectClientError): + """Invalid URL redirect client error.""" + + +class NonHttpUrlRedirectClientError(NonHttpUrlClientError, RedirectClientError): + """Non http URL redirect client error.""" + + +class ClientSSLError(ClientConnectorError): + """Base error for ssl.*Errors.""" + + +if ssl is not None: + cert_errors = (ssl.CertificateError,) + cert_errors_bases = ( + ClientSSLError, + ssl.CertificateError, + ) + + ssl_errors = (ssl.SSLError,) + ssl_error_bases = (ClientSSLError, ssl.SSLError) +else: # pragma: no cover + cert_errors = tuple() + cert_errors_bases = ( + ClientSSLError, + ValueError, + ) + + ssl_errors = tuple() + ssl_error_bases = (ClientSSLError,) + + +class ClientConnectorSSLError(*ssl_error_bases): # type: ignore[misc] + """Response ssl error.""" + + +class ClientConnectorCertificateError(*cert_errors_bases): # type: ignore[misc] + """Response certificate error.""" + + _conn_key: ConnectionKey + + def __init__( + # TODO: If we require ssl in future, this can become ssl.CertificateError + self, + connection_key: ConnectionKey, + certificate_error: Exception, + ) -> None: + if isinstance(certificate_error, cert_errors + (OSError,)): + # ssl.CertificateError has errno and strerror, so we should be fine + os_error = certificate_error + else: + os_error = OSError() + + super().__init__(connection_key, os_error) + self._certificate_error = certificate_error + self.args = (connection_key, certificate_error) + + @property + def certificate_error(self) -> Exception: + return self._certificate_error + + @property + def host(self) -> str: + return self._conn_key.host + + @property + def port(self) -> int | None: + return self._conn_key.port + + @property + def ssl(self) -> bool: + return self._conn_key.is_ssl + + def __str__(self) -> str: + return ( + f"Cannot connect to host {self.host}:{self.port} ssl:{self.ssl} " + f"[{self.certificate_error.__class__.__name__}: " + f"{self.certificate_error.args}]" + ) + + +class WSMessageTypeError(TypeError): + """WebSocket message type is not valid.""" diff --git a/venv/lib/python3.11/site-packages/aiohttp/client_middleware_digest_auth.py b/venv/lib/python3.11/site-packages/aiohttp/client_middleware_digest_auth.py new file mode 100644 index 0000000..6c3e37f --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/client_middleware_digest_auth.py @@ -0,0 +1,494 @@ +""" +Digest authentication middleware for aiohttp client. + +This middleware implements HTTP Digest Authentication according to RFC 7616, +providing a more secure alternative to Basic Authentication. It supports all +standard hash algorithms including MD5, SHA, SHA-256, SHA-512 and their session +variants, as well as both 'auth' and 'auth-int' quality of protection (qop) options. +""" + +import hashlib +import os +import re +import sys +import time +from collections.abc import Callable +from typing import Final, Literal, TypedDict + +from yarl import URL + +from . import hdrs +from .client_exceptions import ClientError +from .client_middlewares import ClientHandlerType +from .client_reqrep import ClientRequest, ClientResponse +from .payload import Payload + + +class DigestAuthChallenge(TypedDict, total=False): + realm: str + nonce: str + qop: str + algorithm: str + opaque: str + domain: str + stale: str + + +DigestFunctions: dict[str, Callable[[bytes], "hashlib._Hash"]] = { + "MD5": hashlib.md5, + "MD5-SESS": hashlib.md5, + "SHA": hashlib.sha1, + "SHA-SESS": hashlib.sha1, + "SHA256": hashlib.sha256, + "SHA256-SESS": hashlib.sha256, + "SHA-256": hashlib.sha256, + "SHA-256-SESS": hashlib.sha256, + "SHA512": hashlib.sha512, + "SHA512-SESS": hashlib.sha512, + "SHA-512": hashlib.sha512, + "SHA-512-SESS": hashlib.sha512, +} + + +# Compile the regex pattern once at module level for performance +_HEADER_PAIRS_PATTERN = re.compile( + r'(?:^|\s|,\s*)(\w+)\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))' + if sys.version_info < (3, 11) + else r'(?:^|\s|,\s*)((?>\w+))\s*=\s*(?:"((?:[^"\\]|\\.)*)"|([^\s,]+))' + # +------------|--------|--|-|-|--|----|------|----|--||-----|-> Match valid start/sep + # +--------|--|-|-|--|----|------|----|--||-----|-> alphanumeric key (atomic + # | | | | | | | | || | group reduces backtracking) + # +--|-|-|--|----|------|----|--||-----|-> maybe whitespace + # | | | | | | | || | + # +-|-|--|----|------|----|--||-----|-> = (delimiter) + # +-|--|----|------|----|--||-----|-> maybe whitespace + # | | | | | || | + # +--|----|------|----|--||-----|-> group quoted or unquoted + # | | | | || | + # +----|------|----|--||-----|-> if quoted... + # +------|----|--||-----|-> anything but " or \ + # +----|--||-----|-> escaped characters allowed + # +--||-----|-> or can be empty string + # || | + # +|-----|-> if unquoted... + # +-----|-> anything but , or + # +-> at least one char req'd +) + + +# RFC 7616: Challenge parameters to extract +CHALLENGE_FIELDS: Final[ + tuple[ + Literal["realm", "nonce", "qop", "algorithm", "opaque", "domain", "stale"], ... + ] +] = ( + "realm", + "nonce", + "qop", + "algorithm", + "opaque", + "domain", + "stale", +) + +# Supported digest authentication algorithms +# Use a tuple of sorted keys for predictable documentation and error messages +SUPPORTED_ALGORITHMS: Final[tuple[str, ...]] = tuple(sorted(DigestFunctions.keys())) + +# RFC 7616: Fields that require quoting in the Digest auth header +# These fields must be enclosed in double quotes in the Authorization header. +# Algorithm, qop, and nc are never quoted per RFC specifications. +# This frozen set is used by the template-based header construction to +# automatically determine which fields need quotes. +QUOTED_AUTH_FIELDS: Final[frozenset[str]] = frozenset( + {"username", "realm", "nonce", "uri", "response", "opaque", "cnonce"} +) + + +def escape_quotes(value: str) -> str: + """Escape double quotes for HTTP header values.""" + return value.replace('"', '\\"') + + +def unescape_quotes(value: str) -> str: + """Unescape double quotes in HTTP header values.""" + return value.replace('\\"', '"') + + +def parse_header_pairs(header: str) -> dict[str, str]: + """ + Parse key-value pairs from WWW-Authenticate or similar HTTP headers. + + This function handles the complex format of WWW-Authenticate header values, + supporting both quoted and unquoted values, proper handling of commas in + quoted values, and whitespace variations per RFC 7616. + + Examples of supported formats: + - key1="value1", key2=value2 + - key1 = "value1" , key2="value, with, commas" + - key1=value1,key2="value2" + - realm="example.com", nonce="12345", qop="auth" + + Args: + header: The header value string to parse + + Returns: + Dictionary mapping parameter names to their values + """ + return { + stripped_key: unescape_quotes(quoted_val) if quoted_val else unquoted_val + for key, quoted_val, unquoted_val in _HEADER_PAIRS_PATTERN.findall(header) + if (stripped_key := key.strip()) + } + + +class DigestAuthMiddleware: + """ + HTTP digest authentication middleware for aiohttp client. + + This middleware intercepts 401 Unauthorized responses containing a Digest + authentication challenge, calculates the appropriate digest credentials, + and automatically retries the request with the proper Authorization header. + + Features: + - Handles all aspects of Digest authentication handshake automatically + - Supports all standard hash algorithms: + - MD5, MD5-SESS + - SHA, SHA-SESS + - SHA256, SHA256-SESS, SHA-256, SHA-256-SESS + - SHA512, SHA512-SESS, SHA-512, SHA-512-SESS + - Supports 'auth' and 'auth-int' quality of protection modes + - Properly handles quoted strings and parameter parsing + - Includes replay attack protection with client nonce count tracking + - Supports preemptive authentication per RFC 7616 Section 3.6 + + Origin scoping: + The credentials are scoped to the origin of the first request the + middleware handles. A request to a different origin is passed through + untouched, so it never receives a digest response computed from those + credentials, unless that origin falls within a protection space the + anchor origin advertised through the RFC 7616 ``domain`` directive. Make + the first request through the middleware against the intended origin, as + the anchor is pinned to it and not reset for the life of the instance. + + Standards compliance: + - RFC 7616: HTTP Digest Access Authentication (primary reference) + - RFC 2617: HTTP Authentication (deprecated by RFC 7616) + - RFC 1945: Section 11.1 (username restrictions) + + Implementation notes: + The core digest calculation is inspired by the implementation in + https://github.com/requests/requests/blob/v2.18.4/requests/auth.py + with added support for modern digest auth features and error handling. + """ + + def __init__( + self, + login: str, + password: str, + preemptive: bool = True, + ) -> None: + if login is None: + raise ValueError("None is not allowed as login value") + + if password is None: + raise ValueError("None is not allowed as password value") + + if ":" in login: + raise ValueError('A ":" is not allowed in username (RFC 1945#section-11.1)') + + self._login_str: Final[str] = login + self._login_bytes: Final[bytes] = login.encode("utf-8") + self._password_bytes: Final[bytes] = password.encode("utf-8") + + self._last_nonce_bytes = b"" + self._nonce_count = 0 + self._challenge: DigestAuthChallenge = {} + self._preemptive: bool = preemptive + # Set of URLs defining the protection space + self._protection_space: list[str] = [] + # Origin the credentials are scoped to; set on the first request. + self._origin: URL | None = None + + async def _encode(self, method: str, url: URL, body: Payload | Literal[b""]) -> str: + """ + Build digest authorization header for the current challenge. + + Args: + method: The HTTP method (GET, POST, etc.) + url: The request URL + body: The request body (used for qop=auth-int) + + Returns: + A fully formatted Digest authorization header string + + Raises: + ClientError: If the challenge is missing required parameters or + contains unsupported values + + """ + challenge = self._challenge + if "realm" not in challenge: + raise ClientError( + "Malformed Digest auth challenge: Missing 'realm' parameter" + ) + + if "nonce" not in challenge: + raise ClientError( + "Malformed Digest auth challenge: Missing 'nonce' parameter" + ) + + # Empty realm values are allowed per RFC 7616 (SHOULD, not MUST, contain host name) + realm = challenge["realm"] + nonce = challenge["nonce"] + + # Empty nonce values are not allowed as they are security-critical for replay protection + if not nonce: + raise ClientError( + "Security issue: Digest auth challenge contains empty 'nonce' value" + ) + + qop_raw = challenge.get("qop", "") + # Preserve original algorithm case for response while using uppercase for processing + algorithm_original = challenge.get("algorithm", "MD5") + algorithm = algorithm_original.upper() + opaque = challenge.get("opaque", "") + + # Convert string values to bytes once + nonce_bytes = nonce.encode("utf-8") + realm_bytes = realm.encode("utf-8") + # Use the encoded request-target (raw_path_qs) since that is what is + # transmitted on the wire and what the server signs against. Using the + # decoded form would cause digest verification to fail when the path + # or query string contains percent-encoded reserved characters. + path = URL(url).raw_path_qs + + # Process QoP + qop = "" + qop_bytes = b"" + if qop_raw: + valid_qops = {"auth", "auth-int"}.intersection( + {q.strip() for q in qop_raw.split(",") if q.strip()} + ) + if not valid_qops: + raise ClientError( + f"Digest auth error: Unsupported Quality of Protection (qop) value(s): {qop_raw}" + ) + + qop = "auth-int" if "auth-int" in valid_qops else "auth" + qop_bytes = qop.encode("utf-8") + + if algorithm not in DigestFunctions: + raise ClientError( + f"Digest auth error: Unsupported hash algorithm: {algorithm}. " + f"Supported algorithms: {', '.join(SUPPORTED_ALGORITHMS)}" + ) + hash_fn: Final = DigestFunctions[algorithm] + + def H(x: bytes) -> bytes: + """RFC 7616 Section 3: Hash function H(data) = hex(hash(data)).""" + return hash_fn(x).hexdigest().encode() + + def KD(s: bytes, d: bytes) -> bytes: + """RFC 7616 Section 3: KD(secret, data) = H(concat(secret, ":", data)).""" + return H(b":".join((s, d))) + + # Calculate A1 and A2 + A1 = b":".join((self._login_bytes, realm_bytes, self._password_bytes)) + A2 = f"{method.upper()}:{path}".encode() + if qop == "auth-int": + if isinstance(body, Payload): # will always be empty bytes unless Payload + entity_bytes = await body.as_bytes() # Get bytes from Payload + else: + entity_bytes = body + entity_hash = H(entity_bytes) + A2 = b":".join((A2, entity_hash)) + + HA1 = H(A1) + HA2 = H(A2) + + # Nonce count handling + if nonce_bytes == self._last_nonce_bytes: + self._nonce_count += 1 + else: + self._nonce_count = 1 + + self._last_nonce_bytes = nonce_bytes + ncvalue = f"{self._nonce_count:08x}" + ncvalue_bytes = ncvalue.encode("utf-8") + + # Generate client nonce + cnonce = hashlib.sha1( + b"".join( + [ + str(self._nonce_count).encode("utf-8"), + nonce_bytes, + time.ctime().encode("utf-8"), + os.urandom(8), + ] + ) + ).hexdigest()[:16] + cnonce_bytes = cnonce.encode("utf-8") + + # Special handling for session-based algorithms + if algorithm.upper().endswith("-SESS"): + HA1 = H(b":".join((HA1, nonce_bytes, cnonce_bytes))) + + # Calculate the response digest + if qop: + noncebit = b":".join( + (nonce_bytes, ncvalue_bytes, cnonce_bytes, qop_bytes, HA2) + ) + response_digest = KD(HA1, noncebit) + else: + response_digest = KD(HA1, b":".join((nonce_bytes, HA2))) + + # Define a dict mapping of header fields to their values + # Group fields into always-present, optional, and qop-dependent + header_fields = { + # Always present fields + "username": escape_quotes(self._login_str), + "realm": escape_quotes(realm), + "nonce": escape_quotes(nonce), + "uri": path, + "response": response_digest.decode(), + "algorithm": algorithm_original, + } + + # Optional fields + if opaque: + header_fields["opaque"] = escape_quotes(opaque) + + # QoP-dependent fields + if qop: + header_fields["qop"] = qop + header_fields["nc"] = ncvalue + header_fields["cnonce"] = cnonce + + # Build header using templates for each field type + pairs: list[str] = [] + for field, value in header_fields.items(): + if field in QUOTED_AUTH_FIELDS: + pairs.append(f'{field}="{value}"') + else: + pairs.append(f"{field}={value}") + + return f"Digest {', '.join(pairs)}" + + def _in_protection_space(self, url: URL) -> bool: + """ + Check if the given URL is within the current protection space. + + According to RFC 7616, a URI is in the protection space if any URI + in the protection space is a prefix of it (after both have been made absolute). + """ + request_str = str(url) + for space_str in self._protection_space: + # Check if request starts with space URL + if not request_str.startswith(space_str): + continue + # Exact match or space ends with / (proper directory prefix) + if len(request_str) == len(space_str) or space_str[-1] == "/": + return True + # Check next char is / to ensure proper path boundary + if request_str[len(space_str)] == "/": + return True + return False + + def _authenticate(self, response: ClientResponse) -> bool: + """ + Takes the given response and tries digest-auth, if needed. + + Returns true if the original request must be resent. + """ + if response.status != 401: + return False + + auth_header = response.headers.get("www-authenticate", "") + if not auth_header: + return False # No authentication header present + + method, sep, headers = auth_header.partition(" ") + if not sep: + # No space found in www-authenticate header + return False # Malformed auth header, missing scheme separator + + if method.lower() != "digest": + # Not a digest auth challenge (could be Basic, Bearer, etc.) + return False + + if not headers: + # We have a digest scheme but no parameters + return False # Malformed digest header, missing parameters + + # We have a digest auth header with content + if not (header_pairs := parse_header_pairs(headers)): + # Failed to parse any key-value pairs + return False # Malformed digest header, no valid parameters + + # Extract challenge parameters + self._challenge = {} + for field in CHALLENGE_FIELDS: + if (value := header_pairs.get(field)) is not None: + self._challenge[field] = value + + # Update protection space based on domain parameter or default to origin + origin = response.url.origin() + + if domain := self._challenge.get("domain"): + # Parse space-separated list of URIs + self._protection_space = [] + for uri in domain.split(): + # Remove quotes if present + uri = uri.strip('"') + if uri.startswith("/"): + # Path-absolute, relative to origin + self._protection_space.append(str(origin.join(URL(uri)))) + else: + # Absolute URI + self._protection_space.append(str(URL(uri))) + else: + # No domain specified, protection space is entire origin + self._protection_space = [str(origin)] + + # Return True only if we found at least one challenge parameter + return bool(self._challenge) + + async def __call__( + self, request: ClientRequest, handler: ClientHandlerType + ) -> ClientResponse: + """Run the digest auth middleware.""" + # Credentials are scoped to the first request's origin. Other origins + # pass through untouched unless a challenge from the anchor origin + # advertised them via RFC 7616 domain; mirrors aiohttp stripping + # Authorization on cross-origin redirects. + origin = request.url.origin() + if self._origin is None: + self._origin = origin + elif origin != self._origin and not self._in_protection_space(request.url): + return await handler(request) + + response = None + for retry_count in range(2): + # Apply authorization header if: + # 1. This is a retry after 401 (retry_count > 0), OR + # 2. Preemptive auth is enabled AND we have a challenge AND the URL is in protection space + if retry_count > 0 or ( + self._preemptive + and self._challenge + and self._in_protection_space(request.url) + ): + request.headers[hdrs.AUTHORIZATION] = await self._encode( + request.method, request.url, request.body + ) + + # Send the request + response = await handler(request) + + # Check if we need to authenticate + if not self._authenticate(response): + break + + # At this point, response is guaranteed to be defined + assert response is not None + return response diff --git a/venv/lib/python3.11/site-packages/aiohttp/client_middlewares.py b/venv/lib/python3.11/site-packages/aiohttp/client_middlewares.py new file mode 100644 index 0000000..3ca2cb2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/client_middlewares.py @@ -0,0 +1,55 @@ +"""Client middleware support.""" + +from collections.abc import Awaitable, Callable, Sequence + +from .client_reqrep import ClientRequest, ClientResponse + +__all__ = ("ClientMiddlewareType", "ClientHandlerType", "build_client_middlewares") + +# Type alias for client request handlers - functions that process requests and return responses +ClientHandlerType = Callable[[ClientRequest], Awaitable[ClientResponse]] + +# Type for client middleware - similar to server but uses ClientRequest/ClientResponse +ClientMiddlewareType = Callable[ + [ClientRequest, ClientHandlerType], Awaitable[ClientResponse] +] + + +def build_client_middlewares( + handler: ClientHandlerType, + middlewares: Sequence[ClientMiddlewareType], +) -> ClientHandlerType: + """ + Apply middlewares to request handler. + + The middlewares are applied in reverse order, so the first middleware + in the list wraps all subsequent middlewares and the handler. + + This implementation avoids using partial/update_wrapper to minimize overhead + and doesn't cache to avoid holding references to stateful middleware. + """ + # Optimize for single middleware case + if len(middlewares) == 1: + middleware = middlewares[0] + + async def single_middleware_handler(req: ClientRequest) -> ClientResponse: + return await middleware(req, handler) + + return single_middleware_handler + + # Build the chain for multiple middlewares + current_handler = handler + + for middleware in reversed(middlewares): + # Create a new closure that captures the current state + def make_wrapper( + mw: ClientMiddlewareType, next_h: ClientHandlerType + ) -> ClientHandlerType: + async def wrapped(req: ClientRequest) -> ClientResponse: + return await mw(req, next_h) + + return wrapped + + current_handler = make_wrapper(middleware, current_handler) + + return current_handler diff --git a/venv/lib/python3.11/site-packages/aiohttp/client_proto.py b/venv/lib/python3.11/site-packages/aiohttp/client_proto.py new file mode 100644 index 0000000..a0b8512 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/client_proto.py @@ -0,0 +1,370 @@ +import asyncio +from contextlib import suppress +from typing import Any, Callable + +from .base_protocol import BaseProtocol +from .client_exceptions import ( + ClientConnectionError, + ClientOSError, + ClientPayloadError, + ServerDisconnectedError, + SocketTimeoutError, +) +from .helpers import ( + _EXC_SENTINEL, + DEFAULT_CHUNK_SIZE, + EMPTY_BODY_STATUS_CODES, + BaseTimerContext, + set_exception, + set_result, +) +from .http import HttpResponseParser, RawResponseMessage +from .http_exceptions import HttpProcessingError +from .streams import EMPTY_PAYLOAD, DataQueue, StreamReader + + +class ResponseHandler(BaseProtocol, DataQueue[tuple[RawResponseMessage, StreamReader]]): + """Helper class to adapt between Protocol and StreamReader.""" + + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + BaseProtocol.__init__(self, loop=loop, parser=None) + DataQueue.__init__(self, loop) + + self._should_close = False + + self._payload: StreamReader | None = None + self._skip_payload = False + self._payload_parser = None + self._data_received_cb: Callable[[], None] | None = None + + self._timer = None + self._tail = b"" + + self._read_timeout: float | None = None + self._read_timeout_handle: asyncio.TimerHandle | None = None + + self._timeout_ceil_threshold: float | None = 5 + + self._closed: None | asyncio.Future[None] = None + self._connection_lost_called = False + + @property + def closed(self) -> None | asyncio.Future[None]: + """Future that is set when the connection is closed. + + This property returns a Future that will be completed when the connection + is closed. The Future is created lazily on first access to avoid creating + futures that will never be awaited. + + Returns: + - A Future[None] if the connection is still open or was closed after + this property was accessed + - None if connection_lost() was already called before this property + was ever accessed (indicating no one is waiting for the closure) + """ + if self._closed is None and not self._connection_lost_called: + self._closed = self._loop.create_future() + return self._closed + + @property + def upgraded(self) -> bool: + return self._upgraded + + @property + def should_close(self) -> bool: + return bool( + self._should_close + or (self._payload is not None and not self._payload.is_eof()) + or self._upgraded + or self._exception is not None + or self._payload_parser is not None + or self._buffer + or self._tail + ) + + def force_close(self) -> None: + self._should_close = True + + def close(self) -> None: + self._exception = None # Break cyclic references + transport = self.transport + if transport is not None: + transport.close() + self.transport = None + self._payload = None + self._drop_timeout() + + def abort(self) -> None: + self._exception = None # Break cyclic references + transport = self.transport + if transport is not None: + transport.abort() + self.transport = None + self._payload = None + self._drop_timeout() + + def is_connected(self) -> bool: + return self.transport is not None and not self.transport.is_closing() + + def connection_lost(self, exc: BaseException | None) -> None: + self._connection_lost_called = True + self._drop_timeout() + + original_connection_error = exc + reraised_exc = original_connection_error + + connection_closed_cleanly = original_connection_error is None + + if self._closed is not None: + # If someone is waiting for the closed future, + # we should set it to None or an exception. If + # self._closed is None, it means that + # connection_lost() was called already + # or nobody is waiting for it. + if connection_closed_cleanly: + set_result(self._closed, None) + else: + assert original_connection_error is not None + set_exception( + self._closed, + ClientConnectionError( + f"Connection lost: {original_connection_error !s}", + ), + original_connection_error, + ) + + if self._payload_parser is not None: + with suppress(Exception): # FIXME: log this somehow? + self._payload_parser.feed_eof() + + uncompleted = None + if self._parser is not None: + try: + uncompleted = self._parser.feed_eof() + except Exception as underlying_exc: + if self._payload is not None: + client_payload_exc_msg = ( + f"Response payload is not completed: {underlying_exc !r}" + ) + if not connection_closed_cleanly: + client_payload_exc_msg = ( + f"{client_payload_exc_msg !s}. " + f"{original_connection_error !r}" + ) + set_exception( + self._payload, + ClientPayloadError(client_payload_exc_msg), + underlying_exc, + ) + + if not self.is_eof(): + if isinstance(original_connection_error, OSError): + reraised_exc = ClientOSError(*original_connection_error.args) + if connection_closed_cleanly: + reraised_exc = ServerDisconnectedError(uncompleted) + # assigns self._should_close to True as side effect, + # we do it anyway below + underlying_non_eof_exc = ( + _EXC_SENTINEL + if connection_closed_cleanly + else original_connection_error + ) + assert underlying_non_eof_exc is not None + assert reraised_exc is not None + self.set_exception(reraised_exc, underlying_non_eof_exc) + + self._should_close = True + self._parser = None + self._payload = None + self._payload_parser = None + self._reading_paused = False + + super().connection_lost(reraised_exc) + + def eof_received(self) -> None: + # should call parser.feed_eof() most likely + self._drop_timeout() + + def pause_reading(self) -> None: + super().pause_reading() + self._drop_timeout() + + def resume_reading(self, resume_parser: bool = True) -> None: + super().resume_reading(resume_parser) + self._reschedule_timeout() + + def set_exception( + self, + exc: BaseException, + exc_cause: BaseException = _EXC_SENTINEL, + ) -> None: + self._should_close = True + self._drop_timeout() + super().set_exception(exc, exc_cause) + + def set_parser( + self, + parser: Any, + payload: Any, + data_received_cb: Callable[[], None] | None = None, + ) -> None: + # TODO: actual types are: + # parser: WebSocketReader + # payload: WebSocketDataQueue + # but they are not generi enough + # Need an ABC for both types + self._payload = payload + self._payload_parser = parser + self._data_received_cb = data_received_cb + + self._drop_timeout() + + if self._tail: + data, self._tail = self._tail, b"" + self.data_received(data) + + def set_response_params( + self, + *, + timer: BaseTimerContext | None = None, + skip_payload: bool = False, + read_until_eof: bool = False, + auto_decompress: bool = True, + read_timeout: float | None = None, + read_bufsize: int = DEFAULT_CHUNK_SIZE, + timeout_ceil_threshold: float = 5, + max_line_size: int = 8190, + max_field_size: int = 8190, + max_headers: int = 128, + ) -> None: + self._skip_payload = skip_payload + + self._read_timeout = read_timeout + + self._timeout_ceil_threshold = timeout_ceil_threshold + + self._parser = HttpResponseParser( + self, + self._loop, + read_bufsize, + timer=timer, + payload_exception=ClientPayloadError, + response_with_body=not skip_payload, + read_until_eof=read_until_eof, + auto_decompress=auto_decompress, + max_line_size=max_line_size, + max_field_size=max_field_size, + max_headers=max_headers, + ) + + if self._tail: + data, self._tail = self._tail, b"" + self.data_received(data) + + def _drop_timeout(self) -> None: + if self._read_timeout_handle is not None: + self._read_timeout_handle.cancel() + self._read_timeout_handle = None + + def _reschedule_timeout(self) -> None: + timeout = self._read_timeout + if self._read_timeout_handle is not None: + self._read_timeout_handle.cancel() + + if timeout: + self._read_timeout_handle = self._loop.call_later( + timeout, self._on_read_timeout + ) + else: + self._read_timeout_handle = None + + def start_timeout(self) -> None: + self._reschedule_timeout() + + @property + def read_timeout(self) -> float | None: + return self._read_timeout + + @read_timeout.setter + def read_timeout(self, read_timeout: float | None) -> None: + self._read_timeout = read_timeout + + def _on_read_timeout(self) -> None: + exc = SocketTimeoutError("Timeout on reading data from socket") + self.set_exception(exc) + if self._payload is not None: + set_exception(self._payload, exc) + + def data_received(self, data: bytes) -> None: + # If no data, then we are resuming decompression. We haven't received + # data from the socket, so we can avoid the reschedule overhead. + if data: + self._reschedule_timeout() + + # custom payload parser - currently always WebSocketReader + if self._payload_parser is not None: + if self._data_received_cb is not None: + self._data_received_cb() + eof, tail = self._payload_parser.feed_data(data) + if eof: + self._payload = None + self._payload_parser = None + + if tail: + self.data_received(tail) + return + + if self._upgraded or self._parser is None: + # i.e. websocket connection, websocket parser is not set yet + self._tail += data + return + + # parse http messages + try: + messages, upgraded, tail = self._parser.feed_data(data) + except BaseException as underlying_exc: + if self.transport is not None: + # connection.release() could be called BEFORE + # data_received(), the transport is already + # closed in this case + self.transport.close() + if not isinstance(underlying_exc, Exception): + raise + # should_close is True after the call + if isinstance(underlying_exc, HttpProcessingError): + exc = HttpProcessingError( + code=underlying_exc.code, + message=underlying_exc.message, + headers=underlying_exc.headers, + ) + else: + exc = HttpProcessingError() + self.set_exception(exc, underlying_exc) + return + + self._upgraded = upgraded + + payload: StreamReader | None = None + for message, payload in messages: + if message.should_close: + self._should_close = True + + self._payload = payload + + if self._skip_payload or message.code in EMPTY_BODY_STATUS_CODES: + self.feed_data((message, EMPTY_PAYLOAD), 0) + else: + self.feed_data((message, payload), 0) + + if payload is not None: + # new message(s) was processed + # register timeout handler unsubscribing + # either on end-of-stream or immediately for + # EMPTY_PAYLOAD + if payload is not EMPTY_PAYLOAD: + payload.on_eof(self._drop_timeout) + else: + self._drop_timeout() + + if upgraded and tail: + self.data_received(tail) diff --git a/venv/lib/python3.11/site-packages/aiohttp/client_reqrep.py b/venv/lib/python3.11/site-packages/aiohttp/client_reqrep.py new file mode 100644 index 0000000..c77213e --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/client_reqrep.py @@ -0,0 +1,1557 @@ +import asyncio +import codecs +import contextlib +import functools +import io +import re +import sys +import traceback +import warnings +from collections.abc import Callable, Iterable, Mapping +from hashlib import md5, sha1, sha256 +from http.cookies import Morsel, SimpleCookie +from types import MappingProxyType, TracebackType +from typing import TYPE_CHECKING, Any, Literal, NamedTuple, Optional, Union + +import attr +from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy +from yarl import URL + +from . import hdrs, helpers, http, multipart, payload +from ._cookie_helpers import ( + parse_cookie_header, + parse_set_cookie_headers, + preserve_morsel_with_coded_value, +) +from .abc import AbstractStreamWriter +from .client_exceptions import ( + ClientConnectionError, + ClientOSError, + ClientResponseError, + ContentTypeError, + InvalidURL, + ServerFingerprintMismatch, +) +from .compression_utils import HAS_BROTLI, HAS_ZSTD +from .formdata import FormData +from .helpers import ( + _SENTINEL, + BaseTimerContext, + BasicAuth, + HeadersMixin, + TimerNoop, + _basic_auth_no_warn, + noop, + reify, + sentinel, + set_exception, + set_result, +) +from .http import ( + SERVER_SOFTWARE, + HttpVersion, + HttpVersion10, + HttpVersion11, + StreamWriter, +) +from .streams import StreamReader +from .typedefs import ( + DEFAULT_JSON_DECODER, + JSONDecoder, + LooseCookies, + LooseHeaders, + Query, + RawHeaders, +) + +if TYPE_CHECKING: + import ssl + from ssl import SSLContext +else: + try: + import ssl + from ssl import SSLContext + except ImportError: # pragma: no cover + ssl = None # type: ignore[assignment] + SSLContext = object # type: ignore[misc,assignment] + + +__all__ = ("ClientRequest", "ClientResponse", "RequestInfo", "Fingerprint") + + +if TYPE_CHECKING: + from .client import ClientSession + from .connector import Connection + from .tracing import Trace + + +_CONNECTION_CLOSED_EXCEPTION = ClientConnectionError("Connection closed") +_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") +json_re = re.compile(r"^application/(?:[\w.+-]+?\+)?json") +_DIGITS_RE = re.compile(r"\d+", re.ASCII) + + +def _gen_default_accept_encoding() -> str: + encodings = [ + "gzip", + "deflate", + ] + if HAS_BROTLI: + encodings.append("br") + if HAS_ZSTD: + encodings.append("zstd") + return ", ".join(encodings) + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class ContentDisposition: + type: str | None + parameters: "MappingProxyType[str, str]" + filename: str | None + + +class _RequestInfo(NamedTuple): + url: URL + method: str + headers: "CIMultiDictProxy[str]" + real_url: URL + + +class RequestInfo(_RequestInfo): + + def __new__( + cls, + url: URL, + method: str, + headers: "CIMultiDictProxy[str]", + real_url: URL | _SENTINEL = sentinel, + ) -> "RequestInfo": + """Create a new RequestInfo instance. + + For backwards compatibility, the real_url parameter is optional. + """ + return tuple.__new__( + cls, (url, method, headers, url if real_url is sentinel else real_url) + ) + + +class Fingerprint: + HASHFUNC_BY_DIGESTLEN = { + 16: md5, + 20: sha1, + 32: sha256, + } + + def __init__(self, fingerprint: bytes) -> None: + digestlen = len(fingerprint) + hashfunc = self.HASHFUNC_BY_DIGESTLEN.get(digestlen) + if not hashfunc: + raise ValueError("fingerprint has invalid length") + elif hashfunc is md5 or hashfunc is sha1: + raise ValueError("md5 and sha1 are insecure and not supported. Use sha256.") + self._hashfunc = hashfunc + self._fingerprint = fingerprint + + @property + def fingerprint(self) -> bytes: + return self._fingerprint + + def check(self, transport: asyncio.Transport) -> None: + if not transport.get_extra_info("sslcontext"): + return + sslobj = transport.get_extra_info("ssl_object") + cert = sslobj.getpeercert(binary_form=True) + got = self._hashfunc(cert).digest() + if got != self._fingerprint: + host, port, *_ = transport.get_extra_info("peername") + raise ServerFingerprintMismatch(self._fingerprint, got, host, port) + + +if ssl is not None: + SSL_ALLOWED_TYPES = (ssl.SSLContext, bool, Fingerprint, type(None)) +else: # pragma: no cover + SSL_ALLOWED_TYPES = (bool, type(None)) + + +def _merge_ssl_params( + ssl: Union["SSLContext", bool, Fingerprint], + verify_ssl: bool | None, + ssl_context: Optional["SSLContext"], + fingerprint: bytes | None, +) -> Union["SSLContext", bool, Fingerprint]: + if ssl is None: + ssl = True # Double check for backwards compatibility + if verify_ssl is not None and not verify_ssl: + warnings.warn( + "verify_ssl is deprecated, use ssl=False instead", + DeprecationWarning, + stacklevel=3, + ) + if ssl is not True: + raise ValueError( + "verify_ssl, ssl_context, fingerprint and ssl " + "parameters are mutually exclusive" + ) + else: + ssl = False + if ssl_context is not None: + warnings.warn( + "ssl_context is deprecated, use ssl=context instead", + DeprecationWarning, + stacklevel=3, + ) + if ssl is not True: + raise ValueError( + "verify_ssl, ssl_context, fingerprint and ssl " + "parameters are mutually exclusive" + ) + else: + ssl = ssl_context + if fingerprint is not None: + warnings.warn( + "fingerprint is deprecated, use ssl=Fingerprint(fingerprint) instead", + DeprecationWarning, + stacklevel=3, + ) + if ssl is not True: + raise ValueError( + "verify_ssl, ssl_context, fingerprint and ssl " + "parameters are mutually exclusive" + ) + else: + ssl = Fingerprint(fingerprint) + if not isinstance(ssl, SSL_ALLOWED_TYPES): + raise TypeError( + "ssl should be SSLContext, bool, Fingerprint or None, " + f"got {ssl!r} instead." + ) + return ssl + + +_SSL_SCHEMES = frozenset(("https", "wss")) + + +# ConnectionKey is a NamedTuple because it is used as a key in a dict +# and a set in the connector. Since a NamedTuple is a tuple it uses +# the fast native tuple __hash__ and __eq__ implementation in CPython. +class ConnectionKey(NamedTuple): + # the key should contain an information about used proxy / TLS + # to prevent reusing wrong connections from a pool + host: str + port: int | None + is_ssl: bool + ssl: SSLContext | bool | Fingerprint + proxy: URL | None + proxy_auth: BasicAuth | None + proxy_headers_hash: int | None # hash(CIMultiDict) + server_hostname: str | None = None + + +def _is_expected_content_type( + response_content_type: str, expected_content_type: str +) -> bool: + if expected_content_type == "application/json": + return json_re.match(response_content_type) is not None + return expected_content_type in response_content_type + + +def _warn_if_unclosed_payload(payload: payload.Payload, stacklevel: int = 2) -> None: + """Warn if the payload is not closed. + + Callers must check that the body is a Payload before calling this method. + + Args: + payload: The payload to check + stacklevel: Stack level for the warning (default 2 for direct callers) + """ + if not payload.autoclose and not payload.consumed: + warnings.warn( + "The previous request body contains unclosed resources. " + "Use await request.update_body() instead of setting request.body " + "directly to properly close resources and avoid leaks.", + ResourceWarning, + stacklevel=stacklevel, + ) + + +class ClientResponse(HeadersMixin): + + # Some of these attributes are None when created, + # but will be set by the start() method. + # As the end user will likely never see the None values, we cheat the types below. + # from the Status-Line of the response + version: HttpVersion | None = None # HTTP-Version + status: int = None # type: ignore[assignment] # Status-Code + reason: str | None = None # Reason-Phrase + + content: StreamReader = None # type: ignore[assignment] # Payload stream + _body: bytes | None = None + _headers: CIMultiDictProxy[str] = None # type: ignore[assignment] + _history: tuple["ClientResponse", ...] = () + _raw_headers: RawHeaders = None # type: ignore[assignment] + + _connection: Optional["Connection"] = None # current connection + _cookies: SimpleCookie | None = None + _raw_cookie_headers: tuple[str, ...] | None = None + _continue: Optional["asyncio.Future[bool]"] = None + _source_traceback: traceback.StackSummary | None = None + _session: Optional["ClientSession"] = None + # set up by ClientRequest after ClientResponse object creation + # post-init stage allows to not change ctor signature + _closed = True # to allow __del__ for non-initialized properly response + _released = False + _in_context = False + + _resolve_charset: Callable[["ClientResponse", bytes], str] = lambda *_: "utf-8" + + __writer: Optional["asyncio.Task[None]"] = None + _stream_writer: Optional[AbstractStreamWriter] = None + _output_size: int = 0 + _upload_complete: Optional[asyncio.Future[None]] = None + + def __init__( + self, + method: str, + url: URL, + *, + writer: "asyncio.Task[None] | None", + continue100: Optional["asyncio.Future[bool]"], + timer: BaseTimerContext, + request_info: RequestInfo, + traces: list["Trace"], + loop: asyncio.AbstractEventLoop, + session: "ClientSession", + stream_writer: AbstractStreamWriter, + ) -> None: + # URL forbids subclasses, so a simple type check is enough. + assert type(url) is URL + + self.method = method + + self._real_url = url + self._url = url.with_fragment(None) if url.raw_fragment else url + if writer is None: # Request already sent + self._output_size = stream_writer.output_size + else: + self._stream_writer = stream_writer + self._writer = writer + if continue100 is not None: + self._continue = continue100 + self._request_info = request_info + self._timer = timer if timer is not None else TimerNoop() + self._cache: dict[str, Any] = {} + self._traces = traces + self._loop = loop + # Save reference to _resolve_charset, so that get_encoding() will still + # work after the response has finished reading the body. + # TODO: Fix session=None in tests (see ClientRequest.__init__). + if session is not None: + # store a reference to session #1985 + self._session = session + self._resolve_charset = session._resolve_charset + if loop.get_debug(): + self._source_traceback = traceback.extract_stack(sys._getframe(1)) + + def __reset_writer(self, _: object = None) -> None: + self.__writer = None + if self._stream_writer is not None: + self._output_size = self._stream_writer.output_size + self._stream_writer = None + if self._upload_complete is not None and not self._upload_complete.done(): + self._upload_complete.set_result(None) + + @property + def _writer(self) -> Optional["asyncio.Task[None]"]: + """The writer task for streaming data. + + _writer is only provided for backwards compatibility + for subclasses that may need to access it. + """ + return self.__writer + + @_writer.setter + def _writer(self, writer: Optional["asyncio.Task[None]"]) -> None: + """Set the writer task for streaming data.""" + if self.__writer is not None: + self.__writer.remove_done_callback(self.__reset_writer) + self.__writer = writer + if writer is None: + return + if writer.done(): + # The writer is already done, so we can clear it immediately. + self.__reset_writer() + else: + writer.add_done_callback(self.__reset_writer) + + @property + def output_size(self) -> int: + """Number of bytes sent for this request.""" + if self._stream_writer is not None: + return self._stream_writer.output_size + return self._output_size + + @property + def upload_complete(self) -> "asyncio.Future[None]": + """Future set when the request body has been fully sent. + + Already done when the request had no body or was written eagerly. + """ + if self._upload_complete is None: + self._upload_complete = self._loop.create_future() + if self._stream_writer is None: # upload already finished + self._upload_complete.set_result(None) + return self._upload_complete + + @property + def cookies(self) -> SimpleCookie: + if self._cookies is None: + if self._raw_cookie_headers is not None: + # Parse cookies for response.cookies (SimpleCookie for backward compatibility) + cookies = SimpleCookie() + # Use parse_set_cookie_headers for more lenient parsing that handles + # malformed cookies better than SimpleCookie.load + cookies.update(parse_set_cookie_headers(self._raw_cookie_headers)) + self._cookies = cookies + else: + self._cookies = SimpleCookie() + return self._cookies + + @cookies.setter + def cookies(self, cookies: SimpleCookie) -> None: + self._cookies = cookies + # Generate raw cookie headers from the SimpleCookie + if cookies: + self._raw_cookie_headers = tuple( + morsel.OutputString() for morsel in cookies.values() + ) + else: + self._raw_cookie_headers = None + + @reify + def url(self) -> URL: + return self._url + + @reify + def url_obj(self) -> URL: + warnings.warn("Deprecated, use .url #1654", DeprecationWarning, stacklevel=2) + return self._url + + @reify + def real_url(self) -> URL: + return self._real_url + + @reify + def host(self) -> str: + assert self._url.host is not None + return self._url.host + + @reify + def headers(self) -> "CIMultiDictProxy[str]": + return self._headers + + @reify + def raw_headers(self) -> RawHeaders: + return self._raw_headers + + @reify + def request_info(self) -> RequestInfo: + return self._request_info + + @reify + def content_disposition(self) -> ContentDisposition | None: + raw = self._headers.get(hdrs.CONTENT_DISPOSITION) + if raw is None: + return None + disposition_type, params_dct = multipart.parse_content_disposition(raw) + params = MappingProxyType(params_dct) + filename = multipart.content_disposition_filename(params) + return ContentDisposition(disposition_type, params, filename) + + def __del__(self, _warnings: Any = warnings) -> None: + if self._closed: + return + + if self._connection is not None: + self._connection.release() + self._cleanup_writer() + + if self._loop.get_debug(): + kwargs = {"source": self} + _warnings.warn(f"Unclosed response {self!r}", ResourceWarning, **kwargs) + context = {"client_response": self, "message": "Unclosed response"} + if self._source_traceback: + context["source_traceback"] = self._source_traceback + self._loop.call_exception_handler(context) + + def __repr__(self) -> str: + out = io.StringIO() + ascii_encodable_url = str(self.url) + if self.reason: + ascii_encodable_reason = self.reason.encode( + "ascii", "backslashreplace" + ).decode("ascii") + else: + ascii_encodable_reason = "None" + print( + f"", + file=out, + ) + print(self.headers, file=out) + return out.getvalue() + + @property + def connection(self) -> Optional["Connection"]: + return self._connection + + @reify + def history(self) -> tuple["ClientResponse", ...]: + """A sequence of of responses, if redirects occurred.""" + return self._history + + @reify + def links(self) -> "MultiDictProxy[MultiDictProxy[str | URL]]": + links_str = ", ".join(self.headers.getall("link", [])) + + if not links_str: + return MultiDictProxy(MultiDict()) + + links: MultiDict[MultiDictProxy[str | URL]] = MultiDict() + + for val in re.split(r",(?=\s*<)", links_str): + match = re.match(r"\s*<(.*)>(.*)", val) + if match is None: # pragma: no cover + # the check exists to suppress mypy error + continue + url, params_str = match.groups() + params = params_str.split(";")[1:] + + link: MultiDict[str | URL] = MultiDict() + + for param in params: + match = re.match(r"^\s*(\S*)\s*=\s*(['\"]?)(.*?)(\2)\s*$", param, re.M) + if match is None: # pragma: no cover + # the check exists to suppress mypy error + continue + key, _, value, _ = match.groups() + + link.add(key, value) + + key = link.get("rel", url) + + link.add("url", self.url.join(URL(url))) + + links.add(str(key), MultiDictProxy(link)) + + return MultiDictProxy(links) + + async def start(self, connection: "Connection") -> "ClientResponse": + """Start response processing.""" + self._closed = False + self._protocol = connection.protocol + self._connection = connection + + with self._timer: + while True: + # read response + try: + protocol = self._protocol + message, payload = await protocol.read() # type: ignore[union-attr] + except http.HttpProcessingError as exc: + raise ClientResponseError( + self.request_info, + self.history, + status=exc.code, + message=exc.message, + headers=exc.headers, + ) from exc + + if message.code < 100 or message.code > 199 or message.code == 101: + break + + if self._continue is not None: + set_result(self._continue, True) + self._continue = None + + # payload eof handler + payload.on_eof(self._response_eof) + + # response status + self.version = message.version + self.status = message.code + self.reason = message.reason + + # headers + self._headers = message.headers # type is CIMultiDictProxy + self._raw_headers = message.raw_headers # type is Tuple[bytes, bytes] + + # payload + self.content = payload + + # cookies + if cookie_hdrs := self.headers.getall(hdrs.SET_COOKIE, ()): + # Store raw cookie headers for CookieJar + self._raw_cookie_headers = tuple(cookie_hdrs) + return self + + def _response_eof(self) -> None: + if self._closed: + return + + # protocol could be None because connection could be detached + protocol = self._connection and self._connection.protocol + if protocol is not None and protocol.upgraded: + return + + self._closed = True + self._cleanup_writer() + self._release_connection() + + @property + def closed(self) -> bool: + return self._closed + + def close(self) -> None: + if not self._released: + self._notify_content() + + self._closed = True + if self._loop is None or self._loop.is_closed(): + return + + self._cleanup_writer() + if self._connection is not None: + self._connection.close() + self._connection = None + + def release(self) -> Any: + if not self._released: + self._notify_content() + + self._closed = True + + self._cleanup_writer() + self._release_connection() + return noop() + + @property + def ok(self) -> bool: + """Returns ``True`` if ``status`` is less than ``400``, ``False`` if not. + + This is **not** a check for ``200 OK`` but a check that the response + status is under 400. + """ + return 400 > self.status + + def raise_for_status(self) -> None: + if not self.ok: + # reason should always be not None for a started response + assert self.reason is not None + + # If we're in a context we can rely on __aexit__() to release as the + # exception propagates. + if not self._in_context: + self.release() + + raise ClientResponseError( + self.request_info, + self.history, + status=self.status, + message=self.reason, + headers=self.headers, + ) + + def _release_connection(self) -> None: + if self._connection is not None: + if self.__writer is None: + self._connection.release() + self._connection = None + else: + self.__writer.add_done_callback(lambda f: self._release_connection()) + + async def _wait_released(self) -> None: + if self.__writer is not None: + try: + await self.__writer + except asyncio.CancelledError: + if ( + sys.version_info >= (3, 11) + and (task := asyncio.current_task()) + and task.cancelling() + ): + raise + self._release_connection() + + def _cleanup_writer(self) -> None: + if self.__writer is not None: + self.__writer.cancel() + if self._stream_writer is not None: + self._output_size = self._stream_writer.output_size + self._stream_writer = None + self._session = None + + def _notify_content(self) -> None: + content = self.content + if content and content.exception() is None: + set_exception(content, _CONNECTION_CLOSED_EXCEPTION) + self._released = True + + async def wait_for_close(self) -> None: + if self.__writer is not None: + try: + await self.__writer + except asyncio.CancelledError: + if ( + sys.version_info >= (3, 11) + and (task := asyncio.current_task()) + and task.cancelling() + ): + raise + self.release() + + async def read(self) -> bytes: + """Read response payload.""" + if self._body is None: + try: + self._body = await self.content.read() + for trace in self._traces: + await trace.send_response_chunk_received( + self.method, self.url, self._body + ) + except BaseException: + self.close() + raise + elif self._released: # Response explicitly released + raise ClientConnectionError("Connection closed") + + protocol = self._connection and self._connection.protocol + if protocol is None or not protocol.upgraded: + await self._wait_released() # Underlying connection released + return self._body + + def get_encoding(self) -> str: + ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower() + mimetype = helpers.parse_mimetype(ctype) + + encoding = mimetype.parameters.get("charset") + if encoding: + with contextlib.suppress(LookupError, ValueError): + return codecs.lookup(encoding).name + + if mimetype.type == "application" and ( + mimetype.subtype == "json" or mimetype.subtype == "rdap" + ): + # RFC 7159 states that the default encoding is UTF-8. + # RFC 7483 defines application/rdap+json + return "utf-8" + + if self._body is None: + raise RuntimeError( + "Cannot compute fallback encoding of a not yet read body" + ) + + return self._resolve_charset(self, self._body) + + async def text(self, encoding: str | None = None, errors: str = "strict") -> str: + """Read response payload and decode.""" + if self._body is None: + await self.read() + + if encoding is None: + encoding = self.get_encoding() + + return self._body.decode(encoding, errors=errors) # type: ignore[union-attr] + + async def json( + self, + *, + encoding: str | None = None, + loads: JSONDecoder = DEFAULT_JSON_DECODER, + content_type: str | None = "application/json", + ) -> Any: + """Read and decodes JSON response.""" + if self._body is None: + await self.read() + + if content_type: + ctype = self.headers.get(hdrs.CONTENT_TYPE, "").lower() + if not _is_expected_content_type(ctype, content_type): + raise ContentTypeError( + self.request_info, + self.history, + status=self.status, + message=( + "Attempt to decode JSON with unexpected mimetype: %s" % ctype + ), + headers=self.headers, + ) + + stripped = self._body.strip() # type: ignore[union-attr] + if not stripped: + return None + + if encoding is None: + encoding = self.get_encoding() + + return loads(stripped.decode(encoding)) + + async def __aenter__(self) -> "ClientResponse": + self._in_context = True + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._in_context = False + # similar to _RequestContextManager, we do not need to check + # for exceptions, response object can close connection + # if state is broken + self.release() + await self.wait_for_close() + + +class ClientRequest: + GET_METHODS = { + hdrs.METH_GET, + hdrs.METH_HEAD, + hdrs.METH_OPTIONS, + hdrs.METH_TRACE, + } + POST_METHODS = {hdrs.METH_PATCH, hdrs.METH_POST, hdrs.METH_PUT} + ALL_METHODS = GET_METHODS.union(POST_METHODS).union({hdrs.METH_DELETE}) + + DEFAULT_HEADERS = { + hdrs.ACCEPT: "*/*", + hdrs.ACCEPT_ENCODING: _gen_default_accept_encoding(), + } + + # Type of body depends on PAYLOAD_REGISTRY, which is dynamic. + _body: None | payload.Payload = None + auth = None + response = None + + __writer: Optional["asyncio.Task[None]"] = None # async task for streaming data + + # These class defaults help create_autospec() work correctly. + # If autospec is improved in future, maybe these can be removed. + url = URL() + method = "GET" + + _continue = None # waiter future for '100 Continue' response + + _skip_auto_headers: Optional["CIMultiDict[None]"] = None + + # N.B. + # Adding __del__ method with self._writer closing doesn't make sense + # because _writer is instance method, thus it keeps a reference to self. + # Until writer has finished finalizer will not be called. + + def __init__( + self, + method: str, + url: URL, + *, + params: Query = None, + headers: LooseHeaders | None = None, + skip_auto_headers: Iterable[str] | None = None, + data: Any = None, + cookies: LooseCookies | None = None, + auth: BasicAuth | None = None, + version: http.HttpVersion = http.HttpVersion11, + compress: str | bool | None = None, + chunked: bool | None = None, + expect100: bool = False, + loop: asyncio.AbstractEventLoop | None = None, + response_class: type["ClientResponse"] | None = None, + proxy: URL | None = None, + proxy_auth: BasicAuth | None = None, + timer: BaseTimerContext | None = None, + session: Optional["ClientSession"] = None, + ssl: SSLContext | bool | Fingerprint = True, + proxy_headers: LooseHeaders | None = None, + traces: list["Trace"] | None = None, + trust_env: bool = False, + server_hostname: str | None = None, + ): + if loop is None: + loop = asyncio.get_event_loop() + if match := _CONTAINS_CONTROL_CHAR_RE.search(method): + raise ValueError( + f"Method cannot contain non-token characters {method!r} " + f"(found at least {match.group()!r})" + ) + # URL forbids subclasses, so a simple type check is enough. + assert type(url) is URL, url + if proxy is not None: + assert type(proxy) is URL, proxy + # FIXME: session is None in tests only, need to fix tests + # assert session is not None + if TYPE_CHECKING: + assert session is not None + self._session = session + if params: + url = url.extend_query(params) + self.original_url = url + self.url = url.with_fragment(None) if url.raw_fragment else url + self.method = method.upper() + self.chunked = chunked + self.compress = compress + self.loop = loop + self.length = None + if response_class is None: + real_response_class = ClientResponse + else: + real_response_class = response_class + self.response_class: type[ClientResponse] = real_response_class + self._timer = timer if timer is not None else TimerNoop() + self._ssl = ssl if ssl is not None else True + self.server_hostname = server_hostname + + if loop.get_debug(): + self._source_traceback = traceback.extract_stack(sys._getframe(1)) + + self.update_version(version) + self.update_host(url) + self.update_headers(headers) + self.update_auto_headers(skip_auto_headers) + self.update_cookies(cookies) + self.update_content_encoding(data) + self.update_auth(auth, trust_env) + self.update_proxy(proxy, proxy_auth, proxy_headers) + + self.update_body_from_data(data) + if data is not None or self.method not in self.GET_METHODS: + self.update_transfer_encoding() + self.update_expect_continue(expect100) + self._traces = [] if traces is None else traces + + def __reset_writer(self, _: object = None) -> None: + self.__writer = None + + def _get_content_length(self) -> int | None: + """Extract and validate Content-Length header value. + + Returns parsed Content-Length value or None if not set. + Raises ValueError if header exists but cannot be parsed as an integer. + """ + if hdrs.CONTENT_LENGTH not in self.headers: + return None + + content_length_hdr = self.headers[hdrs.CONTENT_LENGTH] + if not _DIGITS_RE.fullmatch(content_length_hdr): + raise ValueError(f"Invalid Content-Length header: {content_length_hdr!r}") + return int(content_length_hdr) + + @property + def skip_auto_headers(self) -> CIMultiDict[None]: + return self._skip_auto_headers or CIMultiDict() + + @property + def _writer(self) -> Optional["asyncio.Task[None]"]: + return self.__writer + + @_writer.setter + def _writer(self, writer: "asyncio.Task[None]") -> None: + if self.__writer is not None: + self.__writer.remove_done_callback(self.__reset_writer) + self.__writer = writer + writer.add_done_callback(self.__reset_writer) + + def is_ssl(self) -> bool: + return self.url.scheme in _SSL_SCHEMES + + @property + def ssl(self) -> Union["SSLContext", bool, Fingerprint]: + return self._ssl + + @property + def connection_key(self) -> ConnectionKey: + if proxy_headers := self.proxy_headers: + h: int | None = hash(tuple(proxy_headers.items())) + else: + h = None + url = self.url + return tuple.__new__( + ConnectionKey, + ( + url.raw_host or "", + url.port, + url.scheme in _SSL_SCHEMES, + self._ssl, + self.proxy, + self.proxy_auth, + h, + self.server_hostname, + ), + ) + + @property + def host(self) -> str: + ret = self.url.raw_host + assert ret is not None + return ret + + @property + def port(self) -> int | None: + return self.url.port + + @property + def body(self) -> payload.Payload | Literal[b""]: + """Request body.""" + # empty body is represented as bytes for backwards compatibility + return self._body or b"" + + @body.setter + def body(self, value: Any) -> None: + """Set request body with warning for non-autoclose payloads. + + WARNING: This setter must be called from within an event loop and is not + thread-safe. Setting body outside of an event loop may raise RuntimeError + when closing file-based payloads. + + DEPRECATED: Direct assignment to body is deprecated and will be removed + in a future version. Use await update_body() instead for proper resource + management. + """ + # Close existing payload if present + if self._body is not None: + # Warn if the payload needs manual closing + # stacklevel=3: user code -> body setter -> _warn_if_unclosed_payload + _warn_if_unclosed_payload(self._body, stacklevel=3) + # NOTE: In the future, when we remove sync close support, + # this setter will need to be removed and only the async + # update_body() method will be available. For now, we call + # _close() for backwards compatibility. + self._body._close() + self._update_body(value) + + @property + def request_info(self) -> RequestInfo: + headers: CIMultiDictProxy[str] = CIMultiDictProxy(self.headers) + # These are created on every request, so we use a NamedTuple + # for performance reasons. We don't use the RequestInfo.__new__ + # method because it has a different signature which is provided + # for backwards compatibility only. + return tuple.__new__( + RequestInfo, (self.url, self.method, headers, self.original_url) + ) + + @property + def session(self) -> "ClientSession": + """Return the ClientSession instance. + + This property provides access to the ClientSession that initiated + this request, allowing middleware to make additional requests + using the same session. + """ + return self._session + + def update_host(self, url: URL) -> None: + """Update destination host, port and connection type (ssl).""" + # get host/port + if not url.raw_host: + raise InvalidURL(url) + + # basic auth info + if url.raw_user or url.raw_password: + self.auth = _basic_auth_no_warn(url.user or "", url.password or "") + + def update_version(self, version: http.HttpVersion | str) -> None: + """Convert request version to two elements tuple. + + parser HTTP version '1.1' => (1, 1) + """ + if isinstance(version, str): + v = [part.strip() for part in version.split(".", 1)] + try: + version = http.HttpVersion(int(v[0]), int(v[1])) + except ValueError: + raise ValueError( + f"Can not parse http version number: {version}" + ) from None + self.version = version + + def update_headers(self, headers: LooseHeaders | None) -> None: + """Update request headers.""" + self.headers: CIMultiDict[str] = CIMultiDict() + + # Build the host header + host = self.url.host_port_subcomponent + + # host_port_subcomponent is None when the URL is a relative URL. + # but we know we do not have a relative URL here. + assert host is not None + self.headers[hdrs.HOST] = host + + if not headers: + return + + if isinstance(headers, (dict, MultiDictProxy, MultiDict)): + headers = headers.items() + + for key, value in headers: # type: ignore[str-unpack] + # A special case for Host header + if key in hdrs.HOST_ALL: + self.headers[key] = value + else: + self.headers.add(key, value) + + def update_auto_headers(self, skip_auto_headers: Iterable[str] | None) -> None: + if skip_auto_headers is not None: + self._skip_auto_headers = CIMultiDict( + (hdr, None) for hdr in sorted(skip_auto_headers) + ) + used_headers = self.headers.copy() + used_headers.extend(self._skip_auto_headers) # type: ignore[arg-type] + else: + # Fast path when there are no headers to skip + # which is the most common case. + used_headers = self.headers + + for hdr, val in self.DEFAULT_HEADERS.items(): + if hdr not in used_headers: + self.headers[hdr] = val + + if hdrs.USER_AGENT not in used_headers: + self.headers[hdrs.USER_AGENT] = SERVER_SOFTWARE + + def update_cookies(self, cookies: LooseCookies | None) -> None: + """Update request cookies header.""" + if not cookies: + return + + c = SimpleCookie() + if hdrs.COOKIE in self.headers: + # parse_cookie_header for RFC 6265 compliant Cookie header parsing + c.update(parse_cookie_header(self.headers.get(hdrs.COOKIE, ""))) + del self.headers[hdrs.COOKIE] + + if isinstance(cookies, Mapping): + iter_cookies = cookies.items() + else: + iter_cookies = cookies # type: ignore[assignment] + for name, value in iter_cookies: + if isinstance(value, Morsel): + # Use helper to preserve coded_value exactly as sent by server + c[name] = preserve_morsel_with_coded_value(value) + else: + c[name] = value # type: ignore[assignment] + + self.headers[hdrs.COOKIE] = c.output(header="", sep=";").strip() + + def update_content_encoding(self, data: Any) -> None: + """Set request content encoding.""" + if not data: + # Don't compress an empty body. + self.compress = None + return + + if self.headers.get(hdrs.CONTENT_ENCODING): + if self.compress: + raise ValueError( + "compress can not be set if Content-Encoding header is set" + ) + elif self.compress: + if not isinstance(self.compress, str): + self.compress = "deflate" + self.headers[hdrs.CONTENT_ENCODING] = self.compress + self.chunked = True # enable chunked, no need to deal with length + + def update_transfer_encoding(self) -> None: + """Analyze transfer-encoding header.""" + te = self.headers.get(hdrs.TRANSFER_ENCODING, "").lower() + + if "chunked" in te: + if self.chunked: + raise ValueError( + "chunked can not be set " + 'if "Transfer-Encoding: chunked" header is set' + ) + + elif self.chunked: + if hdrs.CONTENT_LENGTH in self.headers: + raise ValueError( + "chunked can not be set if Content-Length header is set" + ) + + self.headers[hdrs.TRANSFER_ENCODING] = "chunked" + + def update_auth(self, auth: BasicAuth | None, trust_env: bool = False) -> None: + """Set basic auth.""" + if auth is None: + auth = self.auth + if auth is None: + return + + if not isinstance(auth, helpers.BasicAuth): + raise TypeError("BasicAuth() tuple is required instead") + + self.headers[hdrs.AUTHORIZATION] = auth.encode() + + def update_body_from_data(self, body: Any, _stacklevel: int = 3) -> None: + """Update request body from data.""" + if self._body is not None: + _warn_if_unclosed_payload(self._body, stacklevel=_stacklevel) + + if body is None: + self._body = None + # Set Content-Length to 0 when body is None for methods that expect a body + if ( + self.method not in self.GET_METHODS + and not self.chunked + and hdrs.CONTENT_LENGTH not in self.headers + ): + self.headers[hdrs.CONTENT_LENGTH] = "0" + return + + # FormData + maybe_payload = body() if isinstance(body, FormData) else body + + try: + body_payload = payload.PAYLOAD_REGISTRY.get(maybe_payload, disposition=None) + except payload.LookupError: + body_payload = FormData(maybe_payload)() # type: ignore[arg-type] + + self._body = body_payload + # enable chunked encoding if needed + if not self.chunked and hdrs.CONTENT_LENGTH not in self.headers: + if (size := body_payload.size) is not None: + self.headers[hdrs.CONTENT_LENGTH] = str(size) + else: + self.chunked = True + + # copy payload headers + assert body_payload.headers + headers = self.headers + skip_headers = self._skip_auto_headers + for key, value in body_payload.headers.items(): + if key in headers or (skip_headers is not None and key in skip_headers): + continue + headers[key] = value + + def _update_body(self, body: Any) -> None: + """Update request body after its already been set.""" + # Remove existing Content-Length header since body is changing + if hdrs.CONTENT_LENGTH in self.headers: + del self.headers[hdrs.CONTENT_LENGTH] + + # Remove existing Transfer-Encoding header to avoid conflicts + if self.chunked and hdrs.TRANSFER_ENCODING in self.headers: + del self.headers[hdrs.TRANSFER_ENCODING] + + # Now update the body using the existing method + # Called from _update_body, add 1 to stacklevel from caller + self.update_body_from_data(body, _stacklevel=4) + + # Update transfer encoding headers if needed (same logic as __init__) + if body is not None or self.method not in self.GET_METHODS: + self.update_transfer_encoding() + + async def update_body(self, body: Any) -> None: + """ + Update request body and close previous payload if needed. + + This method safely updates the request body by first closing any existing + payload to prevent resource leaks, then setting the new body. + + IMPORTANT: Always use this method instead of setting request.body directly. + Direct assignment to request.body will leak resources if the previous body + contains file handles, streams, or other resources that need cleanup. + + Args: + body: The new body content. Can be: + - bytes/bytearray: Raw binary data + - str: Text data (will be encoded using charset from Content-Type) + - FormData: Form data that will be encoded as multipart/form-data + - Payload: A pre-configured payload object + - AsyncIterable: An async iterable of bytes chunks + - File-like object: Will be read and sent as binary data + - None: Clears the body + + Usage: + # CORRECT: Use update_body + await request.update_body(b"new request data") + + # WRONG: Don't set body directly + # request.body = b"new request data" # This will leak resources! + + # Update with form data + form_data = FormData() + form_data.add_field('field', 'value') + await request.update_body(form_data) + + # Clear body + await request.update_body(None) + + Note: + This method is async because it may need to close file handles or + other resources associated with the previous payload. Always await + this method to ensure proper cleanup. + + Warning: + Setting request.body directly is highly discouraged and can lead to: + - Resource leaks (unclosed file handles, streams) + - Memory leaks (unreleased buffers) + - Unexpected behavior with streaming payloads + + It is not recommended to change the payload type in middleware. If the + body was already set (e.g., as bytes), it's best to keep the same type + rather than converting it (e.g., to str) as this may result in unexpected + behavior. + + See Also: + - update_body_from_data: Synchronous body update without cleanup + - body property: Direct body access (STRONGLY DISCOURAGED) + + """ + # Close existing payload if it exists and needs closing + if self._body is not None: + await self._body.close() + self._update_body(body) + + def update_expect_continue(self, expect: bool = False) -> None: + if expect: + self.headers[hdrs.EXPECT] = "100-continue" + elif ( + hdrs.EXPECT in self.headers + and self.headers[hdrs.EXPECT].lower() == "100-continue" + ): + expect = True + + if expect: + self._continue = self.loop.create_future() + + def update_proxy( + self, + proxy: URL | None, + proxy_auth: BasicAuth | None, + proxy_headers: LooseHeaders | None, + ) -> None: + self.proxy = proxy + if proxy is None: + self.proxy_auth = None + self.proxy_headers = None + return + + if proxy_auth and not isinstance(proxy_auth, helpers.BasicAuth): + raise ValueError("proxy_auth must be None or BasicAuth() tuple") + self.proxy_auth = proxy_auth + + if proxy_headers is not None and not isinstance( + proxy_headers, (MultiDict, MultiDictProxy) + ): + proxy_headers = CIMultiDict(proxy_headers) + self.proxy_headers = proxy_headers + + async def write_bytes( + self, + writer: AbstractStreamWriter, + conn: "Connection", + content_length: int | None = None, + ) -> None: + """ + Write the request body to the connection stream. + + This method handles writing different types of request bodies: + 1. Payload objects (using their specialized write_with_length method) + 2. Bytes/bytearray objects + 3. Iterable body content + + Args: + writer: The stream writer to write the body to + conn: The connection being used for this request + content_length: Optional maximum number of bytes to write from the body + (None means write the entire body) + + The method properly handles: + - Waiting for 100-Continue responses if required + - Content length constraints for chunked encoding + - Error handling for network issues, cancellation, and other exceptions + - Signaling EOF and timeout management + + Raises: + ClientOSError: When there's an OS-level error writing the body + ClientConnectionError: When there's a general connection error + asyncio.CancelledError: When the operation is cancelled + + """ + # 100 response + if self._continue is not None: + # Force headers to be sent before waiting for 100-continue + writer.send_headers() + await writer.drain() + await self._continue + + protocol = conn.protocol + assert protocol is not None + try: + # This should be a rare case but the + # self._body can be set to None while + # the task is being started or we wait above + # for the 100-continue response. + # The more likely case is we have an empty + # payload, but 100-continue is still expected. + if self._body is not None: + await self._body.write_with_length(writer, content_length) + except OSError as underlying_exc: + reraised_exc = underlying_exc + + # Distinguish between timeout and other OS errors for better error reporting + exc_is_not_timeout = underlying_exc.errno is not None or not isinstance( + underlying_exc, asyncio.TimeoutError + ) + if exc_is_not_timeout: + reraised_exc = ClientOSError( + underlying_exc.errno, + f"Can not write request body for {self.url !s}", + ) + + set_exception(protocol, reraised_exc, underlying_exc) + except asyncio.CancelledError: + # Body hasn't been fully sent, so connection can't be reused + conn.close() + raise + except Exception as underlying_exc: + set_exception( + protocol, + ClientConnectionError( + "Failed to send bytes into the underlying connection " + f"{conn !s}: {underlying_exc!r}", + ), + underlying_exc, + ) + else: + # Successfully wrote the body, signal EOF and start response timeout + await writer.write_eof() + protocol.start_timeout() + + async def send(self, conn: "Connection") -> "ClientResponse": + # Specify request target: + # - CONNECT request must send authority form URI + # - not CONNECT proxy must send absolute form URI + # - most common is origin form URI + if self.method == hdrs.METH_CONNECT: + connect_host = self.url.host_subcomponent + assert connect_host is not None + path = f"{connect_host}:{self.url.port}" + elif self.proxy and not self.is_ssl(): + path = str(self.url) + else: + path = self.url.raw_path_qs + + protocol = conn.protocol + assert protocol is not None + writer = StreamWriter( + protocol, + self.loop, + on_chunk_sent=( + functools.partial(self._on_chunk_request_sent, self.method, self.url) + if self._traces + else None + ), + on_headers_sent=( + functools.partial(self._on_headers_request_sent, self.method, self.url) + if self._traces + else None + ), + ) + + if self.compress: + writer.enable_compression(self.compress) # type: ignore[arg-type] + + if self.chunked is not None: + writer.enable_chunking() + + # set default content-type + if ( + self.method in self.POST_METHODS + and ( + self._skip_auto_headers is None + or hdrs.CONTENT_TYPE not in self._skip_auto_headers + ) + and hdrs.CONTENT_TYPE not in self.headers + ): + self.headers[hdrs.CONTENT_TYPE] = "application/octet-stream" + + v = self.version + if hdrs.CONNECTION not in self.headers: + if conn._connector.force_close: + if v == HttpVersion11: + self.headers[hdrs.CONNECTION] = "close" + elif v == HttpVersion10: + self.headers[hdrs.CONNECTION] = "keep-alive" + + # status + headers + status_line = f"{self.method} {path} HTTP/{v.major}.{v.minor}" + + # Buffer headers for potential coalescing with body + await writer.write_headers(status_line, self.headers) + + task: asyncio.Task[None] | None + if self._body or self._continue is not None or protocol.writing_paused: + coro = self.write_bytes(writer, conn, self._get_content_length()) + if sys.version_info >= (3, 12): + # Optimization for Python 3.12, try to write + # bytes immediately to avoid having to schedule + # the task on the event loop. + task = asyncio.Task(coro, loop=self.loop, eager_start=True) + else: + task = self.loop.create_task(coro) + if task.done(): + task = None + else: + self._writer = task + else: + # We have nothing to write because + # - there is no body + # - the protocol does not have writing paused + # - we are not waiting for a 100-continue response + protocol.start_timeout() + writer.set_eof() + task = None + response_class = self.response_class + assert response_class is not None + self.response = response_class( + self.method, + self.original_url, + writer=task, + continue100=self._continue, + timer=self._timer, + request_info=self.request_info, + traces=self._traces, + loop=self.loop, + session=self._session, + stream_writer=writer, + ) + return self.response + + async def close(self) -> None: + if self.__writer is not None: + try: + await self.__writer + except asyncio.CancelledError: + if ( + sys.version_info >= (3, 11) + and (task := asyncio.current_task()) + and task.cancelling() + ): + raise + + def terminate(self) -> None: + if self.__writer is not None: + if not self.loop.is_closed(): + self.__writer.cancel() + self.__writer.remove_done_callback(self.__reset_writer) + self.__writer = None + + async def _on_chunk_request_sent(self, method: str, url: URL, chunk: bytes) -> None: + for trace in self._traces: + await trace.send_request_chunk_sent(method, url, chunk) + + async def _on_headers_request_sent( + self, method: str, url: URL, headers: "CIMultiDict[str]" + ) -> None: + for trace in self._traces: + await trace.send_request_headers(method, url, headers) diff --git a/venv/lib/python3.11/site-packages/aiohttp/client_ws.py b/venv/lib/python3.11/site-packages/aiohttp/client_ws.py new file mode 100644 index 0000000..02f560f --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/client_ws.py @@ -0,0 +1,560 @@ +"""WebSocket client for asyncio.""" + +import asyncio +import sys +from collections.abc import Callable +from types import TracebackType +from typing import Any, Generic, Literal, Optional, cast, overload + +import attr + +from ._websocket.reader import WebSocketDataQueue +from .client_exceptions import ClientError, ServerTimeoutError, WSMessageTypeError +from .client_reqrep import ClientResponse +from .helpers import calculate_timeout_when, set_result +from .http import ( + WS_CLOSED_MESSAGE, + WS_CLOSING_MESSAGE, + WebSocketError, + WSCloseCode, + WSMessage, + WSMessageDecodeText, + WSMessageNoDecodeText, + WSMsgType, +) +from .http_websocket import _INTERNAL_RECEIVE_TYPES, WebSocketWriter +from .streams import EofStream +from .typedefs import ( + DEFAULT_JSON_DECODER, + DEFAULT_JSON_ENCODER, + JSONBytesEncoder, + JSONDecoder, + JSONEncoder, +) + +if sys.version_info >= (3, 13): + from typing import TypeVar +else: + from typing_extensions import TypeVar + +if sys.version_info >= (3, 11): + import asyncio as async_timeout + from typing import Self +else: + import async_timeout + from typing_extensions import Self + +# TypeVar for whether text messages are decoded to str (True) or kept as bytes (False) +# Covariant because it only affects return types, not input types +_DecodeText = TypeVar("_DecodeText", bound=bool, covariant=True, default=Literal[True]) + + +@attr.s(frozen=True, slots=True) +class ClientWSTimeout: + ws_receive = attr.ib(type=Optional[float], default=None) + ws_close = attr.ib(type=Optional[float], default=None) + + +DEFAULT_WS_CLIENT_TIMEOUT = ClientWSTimeout(ws_receive=None, ws_close=10.0) + + +class ClientWebSocketResponse(Generic[_DecodeText]): + def __init__( + self, + reader: WebSocketDataQueue, + writer: WebSocketWriter, + protocol: str | None, + response: ClientResponse, + timeout: ClientWSTimeout, + autoclose: bool, + autoping: bool, + loop: asyncio.AbstractEventLoop, + *, + heartbeat: float | None = None, + compress: int = 0, + client_notakeover: bool = False, + ) -> None: + self._response = response + self._conn = response.connection + + self._writer = writer + self._reader = reader + self._protocol = protocol + self._closed = False + self._closing = False + self._close_code: int | None = None + self._timeout = timeout + self._autoclose = autoclose + self._autoping = autoping + self._heartbeat = heartbeat + self._heartbeat_cb: asyncio.TimerHandle | None = None + self._heartbeat_when: float = 0.0 + if heartbeat is not None: + self._pong_heartbeat = heartbeat / 2.0 + self._pong_response_cb: asyncio.TimerHandle | None = None + self._loop = loop + self._waiting: bool = False + self._close_wait: asyncio.Future[None] | None = None + self._exception: BaseException | None = None + self._compress = compress + self._client_notakeover = client_notakeover + self._ping_task: asyncio.Task[None] | None = None + self._need_heartbeat_reset = False + self._heartbeat_reset_handle: asyncio.Handle | None = None + + self._reset_heartbeat() + + def _cancel_heartbeat(self) -> None: + self._cancel_pong_response_cb() + if self._heartbeat_reset_handle is not None: + self._heartbeat_reset_handle.cancel() + self._heartbeat_reset_handle = None + self._need_heartbeat_reset = False + if self._heartbeat_cb is not None: + self._heartbeat_cb.cancel() + self._heartbeat_cb = None + if self._ping_task is not None: + self._ping_task.cancel() + self._ping_task = None + + def _cancel_pong_response_cb(self) -> None: + if self._pong_response_cb is not None: + self._pong_response_cb.cancel() + self._pong_response_cb = None + + def _on_data_received(self) -> None: + if self._heartbeat is None or self._need_heartbeat_reset: + return + loop = self._loop + assert loop is not None + # Coalesce multiple chunks received in the same loop tick into a single + # heartbeat reset. Resetting immediately per chunk increases timer churn. + self._need_heartbeat_reset = True + self._heartbeat_reset_handle = loop.call_soon(self._flush_heartbeat_reset) + + def _flush_heartbeat_reset(self) -> None: + self._heartbeat_reset_handle = None + if not self._need_heartbeat_reset: + return + self._reset_heartbeat() + self._need_heartbeat_reset = False + + def _reset_heartbeat(self) -> None: + if self._heartbeat is None: + return + self._cancel_pong_response_cb() + loop = self._loop + assert loop is not None + conn = self._conn + timeout_ceil_threshold = ( + conn._connector._timeout_ceil_threshold if conn is not None else 5 + ) + now = loop.time() + when = calculate_timeout_when(now, self._heartbeat, timeout_ceil_threshold) + self._heartbeat_when = when + if self._heartbeat_cb is None: + # We do not cancel the previous heartbeat_cb here because + # it generates a significant amount of TimerHandle churn + # which causes asyncio to rebuild the heap frequently. + # Instead _send_heartbeat() will reschedule the next + # heartbeat if it fires too early. + self._heartbeat_cb = loop.call_at(when, self._send_heartbeat) + + def _send_heartbeat(self) -> None: + self._heartbeat_cb = None + + # If heartbeat reset is pending (data is being received), skip sending + # the ping and let the reset callback handle rescheduling the heartbeat. + if self._need_heartbeat_reset: + return + + loop = self._loop + now = loop.time() + if now < self._heartbeat_when: + # Heartbeat fired too early, reschedule + self._heartbeat_cb = loop.call_at( + self._heartbeat_when, self._send_heartbeat + ) + return + + conn = self._conn + timeout_ceil_threshold = ( + conn._connector._timeout_ceil_threshold if conn is not None else 5 + ) + when = calculate_timeout_when(now, self._pong_heartbeat, timeout_ceil_threshold) + self._cancel_pong_response_cb() + self._pong_response_cb = loop.call_at(when, self._pong_not_received) + + coro = self._writer.send_frame(b"", WSMsgType.PING) + if sys.version_info >= (3, 12): + # Optimization for Python 3.12, try to send the ping + # immediately to avoid having to schedule + # the task on the event loop. + ping_task = asyncio.Task(coro, loop=loop, eager_start=True) + else: + ping_task = loop.create_task(coro) + + if not ping_task.done(): + self._ping_task = ping_task + ping_task.add_done_callback(self._ping_task_done) + else: + self._ping_task_done(ping_task) + + def _ping_task_done(self, task: "asyncio.Task[None]") -> None: + """Callback for when the ping task completes.""" + if not task.cancelled() and (exc := task.exception()): + self._handle_ping_pong_exception(exc) + self._ping_task = None + + def _pong_not_received(self) -> None: + self._handle_ping_pong_exception( + ServerTimeoutError(f"No PONG received after {self._pong_heartbeat} seconds") + ) + + def _handle_ping_pong_exception(self, exc: BaseException) -> None: + """Handle exceptions raised during ping/pong processing.""" + if self._closed: + return + self._set_closed() + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = exc + self._response.close() + if self._waiting and not self._closing: + self._reader.feed_data(WSMessage(WSMsgType.ERROR, exc, None), 0) + + def _set_closed(self) -> None: + """Set the connection to closed. + + Cancel any heartbeat timers and set the closed flag. + """ + self._closed = True + self._cancel_heartbeat() + + def _set_closing(self) -> None: + """Set the connection to closing. + + Cancel any heartbeat timers and set the closing flag. + """ + self._closing = True + self._cancel_heartbeat() + + @property + def closed(self) -> bool: + return self._closed + + @property + def close_code(self) -> int | None: + return self._close_code + + @property + def protocol(self) -> str | None: + return self._protocol + + @property + def compress(self) -> int: + return self._compress + + @property + def client_notakeover(self) -> bool: + return self._client_notakeover + + def get_extra_info(self, name: str, default: Any = None) -> Any: + """extra info from connection transport""" + conn = self._response.connection + if conn is None: + return default + transport = conn.transport + if transport is None: + return default + return transport.get_extra_info(name, default) + + def exception(self) -> BaseException | None: + return self._exception + + async def ping(self, message: bytes = b"") -> None: + await self._writer.send_frame(message, WSMsgType.PING) + + async def pong(self, message: bytes = b"") -> None: + await self._writer.send_frame(message, WSMsgType.PONG) + + async def send_frame( + self, message: bytes, opcode: WSMsgType, compress: int | None = None + ) -> None: + """Send a frame over the websocket.""" + await self._writer.send_frame(message, opcode, compress) + + async def send_str(self, data: str, compress: int | None = None) -> None: + if not isinstance(data, str): + raise TypeError("data argument must be str (%r)" % type(data)) + await self._writer.send_frame( + data.encode("utf-8"), WSMsgType.TEXT, compress=compress + ) + + async def send_bytes(self, data: bytes, compress: int | None = None) -> None: + if not isinstance(data, (bytes, bytearray, memoryview)): + raise TypeError("data argument must be byte-ish (%r)" % type(data)) + await self._writer.send_frame(data, WSMsgType.BINARY, compress=compress) + + async def send_json( + self, + data: Any, + compress: int | None = None, + *, + dumps: JSONEncoder = DEFAULT_JSON_ENCODER, + ) -> None: + await self.send_str(dumps(data), compress=compress) + + async def send_json_bytes( + self, + data: Any, + compress: int | None = None, + *, + dumps: JSONBytesEncoder, + ) -> None: + """Send JSON data using a bytes-returning encoder as a binary frame. + + Use this when your JSON encoder (like orjson) returns bytes + instead of str, avoiding the encode/decode overhead. + """ + await self.send_bytes(dumps(data), compress=compress) + + async def close(self, *, code: int = WSCloseCode.OK, message: bytes = b"") -> bool: + # we need to break `receive()` cycle first, + # `close()` may be called from different task + if self._waiting and not self._closing: + assert self._loop is not None + self._close_wait = self._loop.create_future() + self._set_closing() + self._reader.feed_data(WS_CLOSING_MESSAGE, 0) + await self._close_wait + + if self._closed: + return False + + self._set_closed() + try: + await self._writer.close(code, message) + except asyncio.CancelledError: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._response.close() + raise + except Exception as exc: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = exc + self._response.close() + return True + + if self._close_code: + self._response.close() + return True + + while True: + try: + async with async_timeout.timeout(self._timeout.ws_close): + msg = await self._reader.read() + except asyncio.CancelledError: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._response.close() + raise + except Exception as exc: + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + self._exception = exc + self._response.close() + return True + + if msg.type is WSMsgType.CLOSE: + self._close_code = msg.data + self._response.close() + return True + + @overload + async def receive( + self: "ClientWebSocketResponse[Literal[True]]", timeout: float | None = None + ) -> WSMessageDecodeText: ... + + @overload + async def receive( + self: "ClientWebSocketResponse[Literal[False]]", timeout: float | None = None + ) -> WSMessageNoDecodeText: ... + + @overload + async def receive( + self: "ClientWebSocketResponse[_DecodeText]", timeout: float | None = None + ) -> WSMessageDecodeText | WSMessageNoDecodeText: ... + + async def receive( + self, timeout: float | None = None + ) -> WSMessageDecodeText | WSMessageNoDecodeText: + receive_timeout = timeout or self._timeout.ws_receive + + while True: + if self._waiting: + raise RuntimeError("Concurrent call to receive() is not allowed") + + if self._closed: + return WS_CLOSED_MESSAGE + elif self._closing: + await self.close() + return WS_CLOSED_MESSAGE + + try: + self._waiting = True + try: + if receive_timeout: + # Entering the context manager and creating + # Timeout() object can take almost 50% of the + # run time in this loop so we avoid it if + # there is no read timeout. + async with async_timeout.timeout(receive_timeout): + msg = await self._reader.read() + else: + msg = await self._reader.read() + finally: + self._waiting = False + if self._close_wait: + set_result(self._close_wait, None) + except (asyncio.CancelledError, asyncio.TimeoutError): + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + raise + except EofStream: + self._close_code = WSCloseCode.OK + await self.close() + return WSMessage(WSMsgType.CLOSED, None, None) + except ClientError: + # Likely ServerDisconnectedError when connection is lost + self._set_closed() + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + return WS_CLOSED_MESSAGE + except WebSocketError as exc: + self._close_code = exc.code + await self.close(code=exc.code) + return WSMessage(WSMsgType.ERROR, exc, None) + except Exception as exc: + self._exception = exc + self._set_closing() + self._close_code = WSCloseCode.ABNORMAL_CLOSURE + await self.close() + return WSMessage(WSMsgType.ERROR, exc, None) + + if msg.type not in _INTERNAL_RECEIVE_TYPES: + # If its not a close/closing/ping/pong message + # we can return it immediately + return msg + + if msg.type is WSMsgType.CLOSE: + self._set_closing() + self._close_code = msg.data + if not self._closed and self._autoclose: + await self.close() + elif msg.type is WSMsgType.CLOSING: + self._set_closing() + elif msg.type is WSMsgType.PING and self._autoping: + await self.pong(msg.data) + continue + elif msg.type is WSMsgType.PONG and self._autoping: + continue + + return msg + + @overload + async def receive_str( + self: "ClientWebSocketResponse[Literal[True]]", *, timeout: float | None = None + ) -> str: ... + + @overload + async def receive_str( + self: "ClientWebSocketResponse[Literal[False]]", *, timeout: float | None = None + ) -> bytes: ... + + @overload + async def receive_str( + self: "ClientWebSocketResponse[_DecodeText]", *, timeout: float | None = None + ) -> str | bytes: ... + + async def receive_str(self, *, timeout: float | None = None) -> str | bytes: + """Receive TEXT message. + + Returns str when decode_text=True (default), bytes when decode_text=False. + """ + msg = await self.receive(timeout) + if msg.type is not WSMsgType.TEXT: + raise WSMessageTypeError( + f"Received message {msg.type}:{msg.data!r} is not WSMsgType.TEXT" + ) + return cast(str, msg.data) + + async def receive_bytes(self, *, timeout: float | None = None) -> bytes: + msg = await self.receive(timeout) + if msg.type is not WSMsgType.BINARY: + raise WSMessageTypeError( + f"Received message {msg.type}:{msg.data!r} is not WSMsgType.BINARY" + ) + return cast(bytes, msg.data) + + @overload + async def receive_json( + self: "ClientWebSocketResponse[Literal[True]]", + *, + loads: JSONDecoder = ..., + timeout: float | None = None, + ) -> Any: ... + + @overload + async def receive_json( + self: "ClientWebSocketResponse[Literal[False]]", + *, + loads: Callable[[bytes], Any] = ..., + timeout: float | None = None, + ) -> Any: ... + + @overload + async def receive_json( + self: "ClientWebSocketResponse[_DecodeText]", + *, + loads: JSONDecoder | Callable[[bytes], Any] = ..., + timeout: float | None = None, + ) -> Any: ... + + async def receive_json( + self, + *, + loads: JSONDecoder | Callable[[bytes], Any] = DEFAULT_JSON_DECODER, + timeout: float | None = None, + ) -> Any: + data = await self.receive_str(timeout=timeout) + return loads(data) # type: ignore[arg-type] + + def __aiter__(self) -> Self: + return self + + @overload + async def __anext__( + self: "ClientWebSocketResponse[Literal[True]]", + ) -> WSMessageDecodeText: ... + + @overload + async def __anext__( + self: "ClientWebSocketResponse[Literal[False]]", + ) -> WSMessageNoDecodeText: ... + + @overload + async def __anext__( + self: "ClientWebSocketResponse[_DecodeText]", + ) -> WSMessageDecodeText | WSMessageNoDecodeText: ... + + async def __anext__(self) -> WSMessageDecodeText | WSMessageNoDecodeText: + msg = await self.receive() + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED): + raise StopAsyncIteration + return msg + + async def __aenter__(self) -> Self: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.close() diff --git a/venv/lib/python3.11/site-packages/aiohttp/compression_utils.py b/venv/lib/python3.11/site-packages/aiohttp/compression_utils.py new file mode 100644 index 0000000..9313d21 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/compression_utils.py @@ -0,0 +1,447 @@ +import asyncio +import sys +import zlib +from abc import ABC, abstractmethod +from concurrent.futures import Executor +from typing import Any, Final, Protocol, TypedDict, cast + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + from typing import Union + + Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] + +try: + try: + import brotlicffi as brotli + except ImportError: + import brotli + + HAS_BROTLI = True +except ImportError: # pragma: no cover + HAS_BROTLI = False + +try: + if sys.version_info >= (3, 14): + from compression.zstd import ZstdDecompressor # noqa: I900 + else: # TODO(PY314): Remove mentions of backports.zstd across codebase + from backports.zstd import ZstdDecompressor + + HAS_ZSTD = True +except ImportError: + HAS_ZSTD = False + + +MAX_SYNC_CHUNK_SIZE = 4096 + +# Unlimited decompression constants - different libraries use different conventions +ZLIB_MAX_LENGTH_UNLIMITED = 0 # zlib uses 0 to mean unlimited +ZSTD_MAX_LENGTH_UNLIMITED = -1 # zstd uses -1 to mean unlimited + + +class ZLibCompressObjProtocol(Protocol): + def compress(self, data: Buffer) -> bytes: ... + def flush(self, mode: int = ..., /) -> bytes: ... + + +class ZLibDecompressObjProtocol(Protocol): + def decompress(self, data: Buffer, max_length: int = ...) -> bytes: ... + def flush(self, length: int = ..., /) -> bytes: ... + + @property + def eof(self) -> bool: ... + + @property + def unconsumed_tail(self) -> bytes: ... + + @property + def unused_data(self) -> bytes: ... + + +class ZLibBackendProtocol(Protocol): + MAX_WBITS: int + Z_FULL_FLUSH: int + Z_SYNC_FLUSH: int + Z_BEST_SPEED: int + Z_FINISH: int + + def compressobj( + self, + level: int = ..., + method: int = ..., + wbits: int = ..., + memLevel: int = ..., + strategy: int = ..., + zdict: Buffer | None = ..., + ) -> ZLibCompressObjProtocol: ... + def decompressobj( + self, wbits: int = ..., zdict: Buffer = ... + ) -> ZLibDecompressObjProtocol: ... + + def compress( + self, data: Buffer, /, level: int = ..., wbits: int = ... + ) -> bytes: ... + def decompress( + self, data: Buffer, /, wbits: int = ..., bufsize: int = ... + ) -> bytes: ... + + +class CompressObjArgs(TypedDict, total=False): + wbits: int + strategy: int + level: int + + +class ZLibBackendWrapper: + def __init__(self, _zlib_backend: ZLibBackendProtocol): + self._zlib_backend: ZLibBackendProtocol = _zlib_backend + + @property + def name(self) -> str: + return getattr(self._zlib_backend, "__name__", "undefined") + + @property + def MAX_WBITS(self) -> int: + return self._zlib_backend.MAX_WBITS + + @property + def Z_FULL_FLUSH(self) -> int: + return self._zlib_backend.Z_FULL_FLUSH + + @property + def Z_SYNC_FLUSH(self) -> int: + return self._zlib_backend.Z_SYNC_FLUSH + + @property + def Z_BEST_SPEED(self) -> int: + return self._zlib_backend.Z_BEST_SPEED + + @property + def Z_FINISH(self) -> int: + return self._zlib_backend.Z_FINISH + + def compressobj(self, *args: Any, **kwargs: Any) -> ZLibCompressObjProtocol: + return self._zlib_backend.compressobj(*args, **kwargs) + + def decompressobj(self, *args: Any, **kwargs: Any) -> ZLibDecompressObjProtocol: + return self._zlib_backend.decompressobj(*args, **kwargs) + + def compress(self, data: Buffer, *args: Any, **kwargs: Any) -> bytes: + return self._zlib_backend.compress(data, *args, **kwargs) + + def decompress(self, data: Buffer, *args: Any, **kwargs: Any) -> bytes: + return self._zlib_backend.decompress(data, *args, **kwargs) + + # Everything not explicitly listed in the Protocol we just pass through + def __getattr__(self, attrname: str) -> Any: + return getattr(self._zlib_backend, attrname) + + +ZLibBackend: ZLibBackendWrapper = ZLibBackendWrapper(zlib) + + +def set_zlib_backend(new_zlib_backend: ZLibBackendProtocol) -> None: + ZLibBackend._zlib_backend = new_zlib_backend + + +def encoding_to_mode( + encoding: str | None = None, + suppress_deflate_header: bool = False, +) -> int: + if encoding == "gzip": + return 16 + ZLibBackend.MAX_WBITS + + return -ZLibBackend.MAX_WBITS if suppress_deflate_header else ZLibBackend.MAX_WBITS + + +class DecompressionBaseHandler(ABC): + def __init__( + self, + executor: Executor | None = None, + max_sync_chunk_size: int | None = MAX_SYNC_CHUNK_SIZE, + ): + """Base class for decompression handlers.""" + self._executor = executor + self._max_sync_chunk_size = max_sync_chunk_size + + @abstractmethod + def decompress_sync( + self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED + ) -> bytes: + """Decompress the given data.""" + + async def decompress( + self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED + ) -> bytes: + """Decompress the given data.""" + if ( + self._max_sync_chunk_size is not None + and len(data) > self._max_sync_chunk_size + ): + return await asyncio.get_event_loop().run_in_executor( + self._executor, self.decompress_sync, data, max_length + ) + return self.decompress_sync(data, max_length) + + @property + @abstractmethod + def data_available(self) -> bool: + """Return True if more output is available by passing b"".""" + + +class ZLibCompressor: + def __init__( + self, + encoding: str | None = None, + suppress_deflate_header: bool = False, + level: int | None = None, + wbits: int | None = None, + strategy: int | None = None, + executor: Executor | None = None, + max_sync_chunk_size: int | None = MAX_SYNC_CHUNK_SIZE, + ): + self._executor = executor + self._max_sync_chunk_size = max_sync_chunk_size + self._mode = ( + encoding_to_mode(encoding, suppress_deflate_header) + if wbits is None + else wbits + ) + self._zlib_backend: Final = ZLibBackendWrapper(ZLibBackend._zlib_backend) + + kwargs: CompressObjArgs = {} + kwargs["wbits"] = self._mode + if strategy is not None: + kwargs["strategy"] = strategy + if level is not None: + kwargs["level"] = level + self._compressor = self._zlib_backend.compressobj(**kwargs) + + def compress_sync(self, data: Buffer) -> bytes: + return self._compressor.compress(data) + + async def compress(self, data: Buffer) -> bytes: + """Compress the data and returned the compressed bytes. + + Note that flush() must be called after the last call to compress() + + If the data size is large than the max_sync_chunk_size, the compression + will be done in the executor. Otherwise, the compression will be done + in the event loop. + + **WARNING: This method is NOT cancellation-safe when used with flush().** + If this operation is cancelled, the compressor state may be corrupted. + The connection MUST be closed after cancellation to avoid data corruption + in subsequent compress operations. + + For cancellation-safe compression (e.g., WebSocket), the caller MUST wrap + compress() + flush() + send operations in a shield and lock to ensure atomicity. + """ + # For large payloads, offload compression to executor to avoid blocking event loop + should_use_executor = ( + self._max_sync_chunk_size is not None + and len(data) > self._max_sync_chunk_size + ) + if should_use_executor: + return await asyncio.get_running_loop().run_in_executor( + self._executor, self._compressor.compress, data + ) + return self.compress_sync(data) + + def flush(self, mode: int | None = None) -> bytes: + """Flush the compressor synchronously. + + **WARNING: This method is NOT cancellation-safe when called after compress().** + The flush() operation accesses shared compressor state. If compress() was + cancelled, calling flush() may result in corrupted data. The connection MUST + be closed after compress() cancellation. + + For cancellation-safe compression (e.g., WebSocket), the caller MUST wrap + compress() + flush() + send operations in a shield and lock to ensure atomicity. + """ + return self._compressor.flush( + mode if mode is not None else self._zlib_backend.Z_FINISH + ) + + +class ZLibDecompressor(DecompressionBaseHandler): + def __init__( + self, + encoding: str | None = None, + suppress_deflate_header: bool = False, + executor: Executor | None = None, + max_sync_chunk_size: int | None = MAX_SYNC_CHUNK_SIZE, + ): + super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size) + self._mode = encoding_to_mode(encoding, suppress_deflate_header) + self._zlib_backend: Final = ZLibBackendWrapper(ZLibBackend._zlib_backend) + self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode) + self._last_empty = False + self._pending_unused_data: bytes | None = None + + def decompress_sync( + self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED + ) -> bytes: + if self._pending_unused_data is not None: + data = self._pending_unused_data + bytes(data) + self._pending_unused_data = None + result = self._decompressor.decompress( + self._decompressor.unconsumed_tail + data, max_length + ) + # Only way to know that isal has no further data is checking we get no output + self._last_empty = result == b"" + + # Handle concatenated gzip/deflate streams (multi-member). + # After a member ends, unused_data holds the start of the next member. + # Create a fresh decompressor for each subsequent member. + while self._decompressor.eof and self._decompressor.unused_data: + unused = self._decompressor.unused_data + self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode) + if max_length != ZLIB_MAX_LENGTH_UNLIMITED: + max_length -= len(result) + if max_length <= 0: + self._pending_unused_data = unused + break + chunk = self._decompressor.decompress(unused, max_length) + self._last_empty = chunk == b"" + result += chunk + + # Member ended exactly at chunk boundary — no unused_data, but the + # next feed_data() call would fail on the spent decompressor. + # Only reset for gzip; deflate's feed_eof() relies on eof=True to + # confirm the stream is complete. + if self._decompressor.eof and self._mode > self._zlib_backend.MAX_WBITS: + self._decompressor = self._zlib_backend.decompressobj(wbits=self._mode) + + return result + + def flush(self, length: int = 0) -> bytes: + return ( + self._decompressor.flush(length) + if length > 0 + else self._decompressor.flush() + ) + + @property + def data_available(self) -> bool: + return ( + bool(self._decompressor.unconsumed_tail) + or not self._last_empty + or self._pending_unused_data is not None + ) + + @property + def eof(self) -> bool: + return self._decompressor.eof + + +class BrotliDecompressor(DecompressionBaseHandler): + # Supports both 'brotlipy' and 'Brotli' packages + # since they share an import name. The top branches + # are for 'brotlipy' and bottom branches for 'Brotli' + def __init__( + self, + executor: Executor | None = None, + max_sync_chunk_size: int | None = MAX_SYNC_CHUNK_SIZE, + ) -> None: + """Decompress data using the Brotli library.""" + if not HAS_BROTLI: + raise RuntimeError( + "The brotli decompression is not available. " + "Please install `Brotli` module" + ) + self._obj = brotli.Decompressor() + self._last_empty = False + super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size) + + def decompress_sync( + self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED + ) -> bytes: + """Decompress the given data.""" + if hasattr(self._obj, "decompress"): + if max_length == ZLIB_MAX_LENGTH_UNLIMITED: + result = cast(bytes, self._obj.decompress(data)) + else: + result = cast(bytes, self._obj.decompress(data, max_length)) + else: + if max_length == ZLIB_MAX_LENGTH_UNLIMITED: + result = cast(bytes, self._obj.process(data)) + else: + result = cast(bytes, self._obj.process(data, max_length)) + # Only way to know that brotli has no further data is checking we get no output + self._last_empty = result == b"" + return result + + def flush(self) -> bytes: + """Flush the decompressor.""" + if hasattr(self._obj, "flush"): + return cast(bytes, self._obj.flush()) + return b"" + + @property + def data_available(self) -> bool: + return not self._obj.is_finished() and not self._last_empty + + +class ZSTDDecompressor(DecompressionBaseHandler): + def __init__( + self, + executor: Executor | None = None, + max_sync_chunk_size: int | None = MAX_SYNC_CHUNK_SIZE, + ) -> None: + if not HAS_ZSTD: + raise RuntimeError( + "The zstd decompression is not available. " + "Please install `backports.zstd` module" + ) + self._obj = ZstdDecompressor() + self._pending_unused_data: bytes | None = None + super().__init__(executor=executor, max_sync_chunk_size=max_sync_chunk_size) + + def decompress_sync( + self, data: Buffer, max_length: int = ZLIB_MAX_LENGTH_UNLIMITED + ) -> bytes: + # zstd uses -1 for unlimited, while zlib uses 0 for unlimited + # Convert the zlib convention (0=unlimited) to zstd convention (-1=unlimited) + zstd_max_length = ( + ZSTD_MAX_LENGTH_UNLIMITED + if max_length == ZLIB_MAX_LENGTH_UNLIMITED + else max_length + ) + if self._pending_unused_data is not None: + data = self._pending_unused_data + data + self._pending_unused_data = None + result = self._obj.decompress(data, zstd_max_length) + + # Handle multi-frame zstd streams. + # https://datatracker.ietf.org/doc/html/rfc8878#section-3.1.1 + # ZstdDecompressor handles one frame only. When a frame ends, + # eof becomes True and any trailing data goes to unused_data. + # We create a fresh decompressor to continue with the next frame. + while self._obj.eof and self._obj.unused_data: + unused_data = self._obj.unused_data + self._obj = ZstdDecompressor() + if zstd_max_length != ZSTD_MAX_LENGTH_UNLIMITED: + zstd_max_length -= len(result) + if zstd_max_length <= 0: + self._pending_unused_data = unused_data + break + result += self._obj.decompress(unused_data, zstd_max_length) + + # Frame ended exactly at chunk boundary — no unused_data, but the + # next feed_data() call would fail on the spent decompressor. + # Prepare a fresh one for the next chunk. + if self._obj.eof: + self._obj = ZstdDecompressor() + + return result + + def flush(self) -> bytes: + return b"" + + @property + def data_available(self) -> bool: + return ( + not self._obj.needs_input and not self._obj.eof + ) or self._pending_unused_data is not None diff --git a/venv/lib/python3.11/site-packages/aiohttp/connector.py b/venv/lib/python3.11/site-packages/aiohttp/connector.py new file mode 100644 index 0000000..b7aa7b7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/connector.py @@ -0,0 +1,1851 @@ +import asyncio +import functools +import random +import socket +import sys +import traceback +import warnings +from collections import OrderedDict, defaultdict, deque +from collections.abc import Awaitable, Callable, Iterator, Sequence +from contextlib import suppress +from http import HTTPStatus +from itertools import chain, cycle, islice +from time import monotonic +from types import TracebackType +from typing import TYPE_CHECKING, Any, Literal, Optional, Union, cast + +import aiohappyeyeballs +from aiohappyeyeballs import AddrInfoType, SocketFactoryType + +from . import hdrs, helpers +from .abc import AbstractResolver, ResolveResult +from .client_exceptions import ( + ClientConnectionError, + ClientConnectorCertificateError, + ClientConnectorDNSError, + ClientConnectorError, + ClientConnectorSSLError, + ClientHttpProxyError, + ClientProxyConnectionError, + InvalidUrlClientError, + ServerFingerprintMismatch, + UnixClientConnectorError, + cert_errors, + ssl_errors, +) +from .client_proto import ResponseHandler +from .client_reqrep import ClientRequest, Fingerprint, _merge_ssl_params +from .helpers import ( + _SENTINEL, + ceil_timeout, + is_canonical_ipv4_address, + is_ip_address, + noop, + sentinel, + set_exception, + set_result, +) +from .log import client_logger +from .resolver import DefaultResolver + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] + +if TYPE_CHECKING: + import ssl + + SSLContext = ssl.SSLContext +else: + try: + import ssl + + SSLContext = ssl.SSLContext + except ImportError: # pragma: no cover + ssl = None # type: ignore[assignment] + SSLContext = object # type: ignore[misc,assignment] + +EMPTY_SCHEMA_SET = frozenset({""}) +HTTP_SCHEMA_SET = frozenset({"http", "https"}) +WS_SCHEMA_SET = frozenset({"ws", "wss"}) + +HTTP_AND_EMPTY_SCHEMA_SET = HTTP_SCHEMA_SET | EMPTY_SCHEMA_SET +HIGH_LEVEL_SCHEMA_SET = HTTP_AND_EMPTY_SCHEMA_SET | WS_SCHEMA_SET + +NEEDS_CLEANUP_CLOSED = (3, 13, 0) <= sys.version_info < ( + 3, + 13, + 1, +) or sys.version_info < (3, 12, 8) +# Cleanup closed is no longer needed after https://github.com/python/cpython/pull/118960 +# which first appeared in Python 3.12.8 and 3.13.1 + + +__all__ = ( + "BaseConnector", + "TCPConnector", + "UnixConnector", + "NamedPipeConnector", + "AddrInfoType", + "SocketFactoryType", +) + + +if TYPE_CHECKING: + from .client import ClientTimeout + from .client_reqrep import ConnectionKey + from .tracing import Trace + + +class _DeprecationWaiter: + __slots__ = ("_awaitable", "_awaited") + + def __init__(self, awaitable: Awaitable[Any]) -> None: + self._awaitable = awaitable + self._awaited = False + + def __await__(self) -> Any: + self._awaited = True + return self._awaitable.__await__() + + def __del__(self) -> None: + if not self._awaited: + warnings.warn( + "Connector.close() is a coroutine, " + "please use await connector.close()", + DeprecationWarning, + ) + + +async def _wait_for_close(waiters: list[Awaitable[object]]) -> None: + """Wait for all waiters to finish closing.""" + results = await asyncio.gather(*waiters, return_exceptions=True) + for res in results: + if isinstance(res, Exception): + client_logger.debug("Error while closing connector: %r", res) + + +class Connection: + + _source_traceback = None + + def __init__( + self, + connector: "BaseConnector", + key: "ConnectionKey", + protocol: ResponseHandler, + loop: asyncio.AbstractEventLoop, + ) -> None: + self._key = key + self._connector = connector + self._loop = loop + self._protocol: ResponseHandler | None = protocol + self._callbacks: list[Callable[[], None]] = [] + + if loop.get_debug(): + self._source_traceback = traceback.extract_stack(sys._getframe(1)) + + def __repr__(self) -> str: + return f"Connection<{self._key}>" + + def __del__(self, _warnings: Any = warnings) -> None: + if self._protocol is not None: + kwargs = {"source": self} + _warnings.warn(f"Unclosed connection {self!r}", ResourceWarning, **kwargs) + if self._loop.is_closed(): + return + + self._connector._release(self._key, self._protocol, should_close=True) + + context = {"client_connection": self, "message": "Unclosed connection"} + if self._source_traceback is not None: + context["source_traceback"] = self._source_traceback + self._loop.call_exception_handler(context) + + def __bool__(self) -> Literal[True]: + """Force subclasses to not be falsy, to make checks simpler.""" + return True + + @property + def loop(self) -> asyncio.AbstractEventLoop: + warnings.warn( + "connector.loop property is deprecated", DeprecationWarning, stacklevel=2 + ) + return self._loop + + @property + def transport(self) -> asyncio.Transport | None: + if self._protocol is None: + return None + return self._protocol.transport + + @property + def protocol(self) -> ResponseHandler | None: + return self._protocol + + def add_callback(self, callback: Callable[[], None]) -> None: + if callback is not None: + self._callbacks.append(callback) + + def _notify_release(self) -> None: + callbacks, self._callbacks = self._callbacks[:], [] + + for cb in callbacks: + with suppress(Exception): + cb() + + def close(self) -> None: + self._notify_release() + + if self._protocol is not None: + self._connector._release(self._key, self._protocol, should_close=True) + self._protocol = None + + def release(self) -> None: + self._notify_release() + + if self._protocol is not None: + self._connector._release(self._key, self._protocol) + self._protocol = None + + @property + def closed(self) -> bool: + return self._protocol is None or not self._protocol.is_connected() + + +class _ConnectTunnelConnection(Connection): + """Special connection wrapper for CONNECT tunnels that must never be pooled. + + This connection wraps the proxy connection that will be upgraded with TLS. + It must never be released to the pool because: + 1. Its 'closed' future will never complete, causing session.close() to hang + 2. It represents an intermediate state, not a reusable connection + 3. The real connection (with TLS) will be created separately + """ + + def release(self) -> None: + """Do nothing - don't pool or close the connection. + + These connections are an intermediate state during the CONNECT tunnel + setup and will be cleaned up naturally after the TLS upgrade. If they + were to be pooled, they would never be properly closed, causing + session.close() to wait forever for their 'closed' future. + """ + + +class _TransportPlaceholder: + """placeholder for BaseConnector.connect function""" + + __slots__ = ("closed", "transport") + + def __init__(self, closed_future: asyncio.Future[Exception | None]) -> None: + """Initialize a placeholder for a transport.""" + self.closed = closed_future + self.transport = None + + def close(self) -> None: + """Close the placeholder.""" + + def abort(self) -> None: + """Abort the placeholder (does nothing).""" + + +class BaseConnector: + """Base connector class. + + keepalive_timeout - (optional) Keep-alive timeout. + force_close - Set to True to force close and do reconnect + after each request (and between redirects). + limit - The total number of simultaneous connections. + limit_per_host - Number of simultaneous connections to one host. + enable_cleanup_closed - Enables clean-up closed ssl transports. + Disabled by default. + timeout_ceil_threshold - Trigger ceiling of timeout values when + it's above timeout_ceil_threshold. + loop - Optional event loop. + """ + + _closed = True # prevent AttributeError in __del__ if ctor was failed + _source_traceback = None + + # abort transport after 2 seconds (cleanup broken connections) + _cleanup_closed_period = 2.0 + + allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET + + def __init__( + self, + *, + keepalive_timeout: object | None | float = sentinel, + force_close: bool = False, + limit: int = 100, + limit_per_host: int = 0, + enable_cleanup_closed: bool = False, + loop: asyncio.AbstractEventLoop | None = None, + timeout_ceil_threshold: float = 5, + ) -> None: + + if force_close: + if keepalive_timeout is not None and keepalive_timeout is not sentinel: + raise ValueError( + "keepalive_timeout cannot be set if force_close is True" + ) + else: + if keepalive_timeout is sentinel: + keepalive_timeout = 15.0 + + loop = loop or asyncio.get_running_loop() + self._timeout_ceil_threshold = timeout_ceil_threshold + + self._closed = False + if loop.get_debug(): + self._source_traceback = traceback.extract_stack(sys._getframe(1)) + + # Connection pool of reusable connections. + # We use a deque to store connections because it has O(1) popleft() + # and O(1) append() operations to implement a FIFO queue. + self._conns: defaultdict[ + ConnectionKey, deque[tuple[ResponseHandler, float]] + ] = defaultdict(deque) + self._limit = limit + self._limit_per_host = limit_per_host + self._acquired: set[ResponseHandler] = set() + self._acquired_per_host: defaultdict[ConnectionKey, set[ResponseHandler]] = ( + defaultdict(set) + ) + self._keepalive_timeout = cast(float, keepalive_timeout) + self._force_close = force_close + + # {host_key: FIFO list of waiters} + # The FIFO is implemented with an OrderedDict with None keys because + # python does not have an ordered set. + self._waiters: defaultdict[ + ConnectionKey, OrderedDict[asyncio.Future[None], None] + ] = defaultdict(OrderedDict) + + self._loop = loop + self._factory = functools.partial(ResponseHandler, loop=loop) + + # start keep-alive connection cleanup task + self._cleanup_handle: asyncio.TimerHandle | None = None + + # start cleanup closed transports task + self._cleanup_closed_handle: asyncio.TimerHandle | None = None + + if enable_cleanup_closed and not NEEDS_CLEANUP_CLOSED: + warnings.warn( + "enable_cleanup_closed ignored because " + "https://github.com/python/cpython/pull/118960 is fixed " + f"in Python version {sys.version_info}", + DeprecationWarning, + stacklevel=2, + ) + enable_cleanup_closed = False + + self._cleanup_closed_disabled = not enable_cleanup_closed + self._cleanup_closed_transports: list[asyncio.Transport | None] = [] + self._placeholder_future: asyncio.Future[Exception | None] = ( + loop.create_future() + ) + self._placeholder_future.set_result(None) + self._cleanup_closed() + + def __del__(self, _warnings: Any = warnings) -> None: + if self._closed: + return + if not self._conns: + return + + conns = [repr(c) for c in self._conns.values()] + + self._close() + + kwargs = {"source": self} + _warnings.warn(f"Unclosed connector {self!r}", ResourceWarning, **kwargs) + context = { + "connector": self, + "connections": conns, + "message": "Unclosed connector", + } + if self._source_traceback is not None: + context["source_traceback"] = self._source_traceback + self._loop.call_exception_handler(context) + + def __enter__(self) -> "BaseConnector": + warnings.warn( + '"with Connector():" is deprecated, ' + 'use "async with Connector():" instead', + DeprecationWarning, + ) + return self + + def __exit__(self, *exc: Any) -> None: + self._close() + + async def __aenter__(self) -> "BaseConnector": + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None = None, + exc_value: BaseException | None = None, + exc_traceback: TracebackType | None = None, + ) -> None: + await self.close() + + @property + def force_close(self) -> bool: + """Ultimately close connection on releasing if True.""" + return self._force_close + + @property + def limit(self) -> int: + """The total number for simultaneous connections. + + If limit is 0 the connector has no limit. + The default limit size is 100. + """ + return self._limit + + @property + def limit_per_host(self) -> int: + """The limit for simultaneous connections to the same endpoint. + + Endpoints are the same if they are have equal + (host, port, is_ssl) triple. + """ + return self._limit_per_host + + def _cleanup(self) -> None: + """Cleanup unused transports.""" + if self._cleanup_handle: + self._cleanup_handle.cancel() + # _cleanup_handle should be unset, otherwise _release() will not + # recreate it ever! + self._cleanup_handle = None + + now = monotonic() + timeout = self._keepalive_timeout + + if self._conns: + connections = defaultdict(deque) + deadline = now - timeout + for key, conns in self._conns.items(): + alive: deque[tuple[ResponseHandler, float]] = deque() + for proto, use_time in conns: + if proto.is_connected() and use_time - deadline >= 0: + alive.append((proto, use_time)) + continue + transport = proto.transport + proto.close() + if not self._cleanup_closed_disabled and key.is_ssl: + self._cleanup_closed_transports.append(transport) + + if alive: + connections[key] = alive + + self._conns = connections + + if self._conns: + self._cleanup_handle = helpers.weakref_handle( + self, + "_cleanup", + timeout, + self._loop, + timeout_ceil_threshold=self._timeout_ceil_threshold, + ) + + def _cleanup_closed(self) -> None: + """Double confirmation for transport close. + + Some broken ssl servers may leave socket open without proper close. + """ + if self._cleanup_closed_handle: + self._cleanup_closed_handle.cancel() + + for transport in self._cleanup_closed_transports: + if transport is not None: + transport.abort() + + self._cleanup_closed_transports = [] + + if not self._cleanup_closed_disabled: + self._cleanup_closed_handle = helpers.weakref_handle( + self, + "_cleanup_closed", + self._cleanup_closed_period, + self._loop, + timeout_ceil_threshold=self._timeout_ceil_threshold, + ) + + def close(self, *, abort_ssl: bool = False) -> Awaitable[None]: + """Close all opened transports. + + :param abort_ssl: If True, SSL connections will be aborted immediately + without performing the shutdown handshake. This provides + faster cleanup at the cost of less graceful disconnection. + """ + if not (waiters := self._close(abort_ssl=abort_ssl)): + # If there are no connections to close, we can return a noop + # awaitable to avoid scheduling a task on the event loop. + return _DeprecationWaiter(noop()) + coro = _wait_for_close(waiters) + if sys.version_info >= (3, 12): + # Optimization for Python 3.12, try to close connections + # immediately to avoid having to schedule the task on the event loop. + task = asyncio.Task(coro, loop=self._loop, eager_start=True) + else: + task = self._loop.create_task(coro) + return _DeprecationWaiter(task) + + def _close(self, *, abort_ssl: bool = False) -> list[Awaitable[object]]: + waiters: list[Awaitable[object]] = [] + + if self._closed: + return waiters + + self._closed = True + + try: + if self._loop.is_closed(): + return waiters + + # cancel cleanup task + if self._cleanup_handle: + self._cleanup_handle.cancel() + + # cancel cleanup close task + if self._cleanup_closed_handle: + self._cleanup_closed_handle.cancel() + + for data in self._conns.values(): + for proto, _ in data: + if ( + abort_ssl + and proto.transport + and proto.transport.get_extra_info("sslcontext") is not None + ): + proto.abort() + else: + proto.close() + if closed := proto.closed: + waiters.append(closed) + + for proto in self._acquired: + if ( + abort_ssl + and proto.transport + and proto.transport.get_extra_info("sslcontext") is not None + ): + proto.abort() + else: + proto.close() + if closed := proto.closed: + waiters.append(closed) + + for transport in self._cleanup_closed_transports: + if transport is not None: + transport.abort() + + return waiters + + finally: + self._conns.clear() + self._acquired.clear() + for keyed_waiters in self._waiters.values(): + for keyed_waiter in keyed_waiters: + keyed_waiter.cancel() + self._waiters.clear() + self._cleanup_handle = None + self._cleanup_closed_transports.clear() + self._cleanup_closed_handle = None + + @property + def closed(self) -> bool: + """Is connector closed. + + A readonly property. + """ + return self._closed + + def _available_connections(self, key: "ConnectionKey") -> int: + """ + Return number of available connections. + + The limit, limit_per_host and the connection key are taken into account. + + If it returns less than 1 means that there are no connections + available. + """ + # check total available connections + # If there are no limits, this will always return 1 + total_remain = 1 + + if self._limit and (total_remain := self._limit - len(self._acquired)) <= 0: + return total_remain + + # check limit per host + if host_remain := self._limit_per_host: + if acquired := self._acquired_per_host.get(key): + host_remain -= len(acquired) + if total_remain > host_remain: + return host_remain + + return total_remain + + def _update_proxy_auth_header_and_build_proxy_req( + self, req: ClientRequest + ) -> ClientRequest: + """Set Proxy-Authorization header for non-SSL proxy requests and builds the proxy request for SSL proxy requests.""" + url = req.proxy + assert url is not None + headers: dict[str, str] = {} + if req.proxy_headers is not None: + headers = req.proxy_headers # type: ignore[assignment] + headers[hdrs.HOST] = req.headers[hdrs.HOST] + proxy_req = ClientRequest( + hdrs.METH_GET, + url, + headers=headers, + auth=req.proxy_auth, + loop=self._loop, + ssl=req.ssl, + ) + auth = proxy_req.headers.pop(hdrs.AUTHORIZATION, None) + if auth is not None: + if not req.is_ssl(): + req.headers[hdrs.PROXY_AUTHORIZATION] = auth + else: + proxy_req.headers[hdrs.PROXY_AUTHORIZATION] = auth + return proxy_req + + async def connect( + self, req: ClientRequest, traces: list["Trace"], timeout: "ClientTimeout" + ) -> Connection: + """Get from pool or create new connection.""" + key = req.connection_key + if (conn := await self._get(key, traces)) is not None: + # If we do not have to wait and we can get a connection from the pool + # we can avoid the timeout ceil logic and directly return the connection + if req.proxy: + self._update_proxy_auth_header_and_build_proxy_req(req) + return conn + + async with ceil_timeout(timeout.connect, timeout.ceil_threshold): + if self._available_connections(key) <= 0: + await self._wait_for_available_connection(key, traces) + if (conn := await self._get(key, traces)) is not None: + if req.proxy: + self._update_proxy_auth_header_and_build_proxy_req(req) + return conn + + placeholder = cast( + ResponseHandler, _TransportPlaceholder(self._placeholder_future) + ) + self._acquired.add(placeholder) + if self._limit_per_host: + self._acquired_per_host[key].add(placeholder) + + try: + # Traces are done inside the try block to ensure that the + # that the placeholder is still cleaned up if an exception + # is raised. + if traces: + for trace in traces: + await trace.send_connection_create_start() + proto = await self._create_connection(req, traces, timeout) + if traces: + for trace in traces: + await trace.send_connection_create_end() + except BaseException: + self._release_acquired(key, placeholder) + raise + else: + if self._closed: + proto.close() + raise ClientConnectionError("Connector is closed.") + + # The connection was successfully created, drop the placeholder + # and add the real connection to the acquired set. There should + # be no awaits after the proto is added to the acquired set + # to ensure that the connection is not left in the acquired set + # on cancellation. + self._acquired.remove(placeholder) + self._acquired.add(proto) + if self._limit_per_host: + acquired_per_host = self._acquired_per_host[key] + acquired_per_host.remove(placeholder) + acquired_per_host.add(proto) + return Connection(self, key, proto, self._loop) + + async def _wait_for_available_connection( + self, key: "ConnectionKey", traces: list["Trace"] + ) -> None: + """Wait for an available connection slot.""" + # We loop here because there is a race between + # the connection limit check and the connection + # being acquired. If the connection is acquired + # between the check and the await statement, we + # need to loop again to check if the connection + # slot is still available. + attempts = 0 + while True: + fut: asyncio.Future[None] = self._loop.create_future() + keyed_waiters = self._waiters[key] + keyed_waiters[fut] = None + if attempts: + # If we have waited before, we need to move the waiter + # to the front of the queue as otherwise we might get + # starved and hit the timeout. + keyed_waiters.move_to_end(fut, last=False) + + try: + # Traces happen in the try block to ensure that the + # the waiter is still cleaned up if an exception is raised. + if traces: + for trace in traces: + await trace.send_connection_queued_start() + await fut + if traces: + for trace in traces: + await trace.send_connection_queued_end() + finally: + # pop the waiter from the queue if its still + # there and not already removed by _release_waiter + keyed_waiters.pop(fut, None) + if not self._waiters.get(key, True): + del self._waiters[key] + + if self._available_connections(key) > 0: + break + attempts += 1 + + async def _get( + self, key: "ConnectionKey", traces: list["Trace"] + ) -> Connection | None: + """Get next reusable connection for the key or None. + + The connection will be marked as acquired. + """ + if (conns := self._conns.get(key)) is None: + return None + + t1 = monotonic() + while conns: + proto, t0 = conns.popleft() + # We will we reuse the connection if its connected and + # the keepalive timeout has not been exceeded + if proto.is_connected() and t1 - t0 <= self._keepalive_timeout: + if not conns: + # The very last connection was reclaimed: drop the key + del self._conns[key] + self._acquired.add(proto) + if self._limit_per_host: + self._acquired_per_host[key].add(proto) + if traces: + for trace in traces: + try: + await trace.send_connection_reuseconn() + except BaseException: + self._release_acquired(key, proto) + raise + return Connection(self, key, proto, self._loop) + + # Connection cannot be reused, close it + transport = proto.transport + proto.close() + # only for SSL transports + if not self._cleanup_closed_disabled and key.is_ssl: + self._cleanup_closed_transports.append(transport) + + # No more connections: drop the key + del self._conns[key] + return None + + def _release_waiter(self) -> None: + """ + Iterates over all waiters until one to be released is found. + + The one to be released is not finished and + belongs to a host that has available connections. + """ + if not self._waiters: + return + + # Having the dict keys ordered this avoids to iterate + # at the same order at each call. + queues = list(self._waiters) + random.shuffle(queues) + + for key in queues: + if self._available_connections(key) < 1: + continue + + waiters = self._waiters[key] + while waiters: + waiter, _ = waiters.popitem(last=False) + if not waiter.done(): + waiter.set_result(None) + return + + def _release_acquired(self, key: "ConnectionKey", proto: ResponseHandler) -> None: + """Release acquired connection.""" + if self._closed: + # acquired connection is already released on connector closing + return + + self._acquired.discard(proto) + if self._limit_per_host and (conns := self._acquired_per_host.get(key)): + conns.discard(proto) + if not conns: + del self._acquired_per_host[key] + self._release_waiter() + + def _release( + self, + key: "ConnectionKey", + protocol: ResponseHandler, + *, + should_close: bool = False, + ) -> None: + if self._closed: + # acquired connection is already released on connector closing + return + + self._release_acquired(key, protocol) + + if self._force_close or should_close or protocol.should_close: + transport = protocol.transport + protocol.close() + + if key.is_ssl and not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(transport) + return + + self._conns[key].append((protocol, monotonic())) + + if self._cleanup_handle is None: + self._cleanup_handle = helpers.weakref_handle( + self, + "_cleanup", + self._keepalive_timeout, + self._loop, + timeout_ceil_threshold=self._timeout_ceil_threshold, + ) + + async def _create_connection( + self, req: ClientRequest, traces: list["Trace"], timeout: "ClientTimeout" + ) -> ResponseHandler: + raise NotImplementedError() + + +class _DNSCacheTable: + def __init__(self, ttl: float | None = None, max_size: int = 1000) -> None: + self._addrs_rr: OrderedDict[ + tuple[str, int], tuple[Iterator[ResolveResult], int] + ] = OrderedDict() + self._timestamps: dict[tuple[str, int], float] = {} + self._ttl = ttl + self._max_size = max_size + + def __contains__(self, host: object) -> bool: + return host in self._addrs_rr + + def add(self, key: tuple[str, int], addrs: list[ResolveResult]) -> None: + if key in self._addrs_rr: + self._addrs_rr.move_to_end(key) + + self._addrs_rr[key] = (cycle(addrs), len(addrs)) + + if self._ttl is not None: + self._timestamps[key] = monotonic() + + if len(self._addrs_rr) > self._max_size: + oldest_key, _ = self._addrs_rr.popitem(last=False) + self._timestamps.pop(oldest_key, None) + + def remove(self, key: tuple[str, int]) -> None: + self._addrs_rr.pop(key, None) + self._timestamps.pop(key, None) + + def clear(self) -> None: + self._addrs_rr.clear() + self._timestamps.clear() + + def next_addrs(self, key: tuple[str, int]) -> list[ResolveResult]: + loop, length = self._addrs_rr[key] + addrs = list(islice(loop, length)) + # Consume one more element to shift internal state of `cycle` + next(loop) + self._addrs_rr.move_to_end(key) + return addrs + + def expired(self, key: tuple[str, int]) -> bool: + if self._ttl is None: + return False + + return self._timestamps[key] + self._ttl < monotonic() + + +def _make_ssl_context(verified: bool) -> SSLContext: + """Create SSL context. + + This method is not async-friendly and should be called from a thread + because it will load certificates from disk and do other blocking I/O. + """ + if ssl is None: + # No ssl support + return None + if verified: + sslcontext = ssl.create_default_context() + else: + sslcontext = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) + sslcontext.options |= ssl.OP_NO_SSLv2 + sslcontext.options |= ssl.OP_NO_SSLv3 + sslcontext.check_hostname = False + sslcontext.verify_mode = ssl.CERT_NONE + sslcontext.options |= ssl.OP_NO_COMPRESSION + sslcontext.set_default_verify_paths() + sslcontext.set_alpn_protocols(("http/1.1",)) + return sslcontext + + +# The default SSLContext objects are created at import time +# since they do blocking I/O to load certificates from disk, +# and imports should always be done before the event loop starts +# or in a thread. +_SSL_CONTEXT_VERIFIED = _make_ssl_context(True) +_SSL_CONTEXT_UNVERIFIED = _make_ssl_context(False) + + +class TCPConnector(BaseConnector): + """TCP connector. + + verify_ssl - Set to True to check ssl certifications. + fingerprint - Pass the binary sha256 + digest of the expected certificate in DER format to verify + that the certificate the server presents matches. See also + https://en.wikipedia.org/wiki/HTTP_Public_Key_Pinning + resolver - Enable DNS lookups and use this + resolver + use_dns_cache - Use memory cache for DNS lookups. + ttl_dns_cache - Max seconds having cached a DNS entry, None forever. + family - socket address family + local_addr - local tuple of (host, port) to bind socket to + + keepalive_timeout - (optional) Keep-alive timeout. + force_close - Set to True to force close and do reconnect + after each request (and between redirects). + limit - The total number of simultaneous connections. + limit_per_host - Number of simultaneous connections to one host. + enable_cleanup_closed - Enables clean-up closed ssl transports. + Disabled by default. + happy_eyeballs_delay - This is the “Connection Attempt Delay” + as defined in RFC 8305. To disable + the happy eyeballs algorithm, set to None. + interleave - “First Address Family Count” as defined in RFC 8305 + loop - Optional event loop. + socket_factory - A SocketFactoryType function that, if supplied, + will be used to create sockets given an + AddrInfoType. + ssl_shutdown_timeout - DEPRECATED. Will be removed in aiohttp 4.0. + Grace period for SSL shutdown handshake on TLS + connections. Default is 0 seconds (immediate abort). + This parameter allowed for a clean SSL shutdown by + notifying the remote peer of connection closure, + while avoiding excessive delays during connector cleanup. + Note: Only takes effect on Python 3.11+. + """ + + allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"tcp"}) + + def __init__( + self, + *, + verify_ssl: bool = True, + fingerprint: bytes | None = None, + use_dns_cache: bool = True, + ttl_dns_cache: int | None = 10, + dns_cache_max_size: int = 1000, + family: socket.AddressFamily = socket.AddressFamily.AF_UNSPEC, + ssl_context: SSLContext | None = None, + ssl: bool | Fingerprint | SSLContext = True, + local_addr: tuple[str, int] | None = None, + resolver: AbstractResolver | None = None, + keepalive_timeout: None | float | object = sentinel, + force_close: bool = False, + limit: int = 100, + limit_per_host: int = 0, + enable_cleanup_closed: bool = False, + loop: asyncio.AbstractEventLoop | None = None, + timeout_ceil_threshold: float = 5, + happy_eyeballs_delay: float | None = 0.25, + interleave: int | None = None, + socket_factory: SocketFactoryType | None = None, + ssl_shutdown_timeout: _SENTINEL | None | float = sentinel, + ): + super().__init__( + keepalive_timeout=keepalive_timeout, + force_close=force_close, + limit=limit, + limit_per_host=limit_per_host, + enable_cleanup_closed=enable_cleanup_closed, + loop=loop, + timeout_ceil_threshold=timeout_ceil_threshold, + ) + + self._ssl = _merge_ssl_params(ssl, verify_ssl, ssl_context, fingerprint) + + self._resolver: AbstractResolver + if resolver is None: + self._resolver = DefaultResolver(loop=self._loop) + self._resolver_owner = True + else: + self._resolver = resolver + self._resolver_owner = False + + self._use_dns_cache = use_dns_cache + self._cached_hosts = _DNSCacheTable( + ttl=ttl_dns_cache, max_size=dns_cache_max_size + ) + self._throttle_dns_futures: dict[tuple[str, int], set[asyncio.Future[None]]] = ( + {} + ) + self._family = family + self._local_addr_infos = aiohappyeyeballs.addr_to_addr_infos(local_addr) + self._happy_eyeballs_delay = happy_eyeballs_delay + self._interleave = interleave + self._resolve_host_tasks: set[asyncio.Task[list[ResolveResult]]] = set() + self._socket_factory = socket_factory + self._ssl_shutdown_timeout: float | None + # Handle ssl_shutdown_timeout with warning for Python < 3.11 + if ssl_shutdown_timeout is sentinel: + self._ssl_shutdown_timeout = 0 + else: + # Deprecation warning for ssl_shutdown_timeout parameter + warnings.warn( + "The ssl_shutdown_timeout parameter is deprecated and will be removed in aiohttp 4.0", + DeprecationWarning, + stacklevel=2, + ) + if ( + sys.version_info < (3, 11) + and ssl_shutdown_timeout is not None + and ssl_shutdown_timeout != 0 + ): + warnings.warn( + f"ssl_shutdown_timeout={ssl_shutdown_timeout} is ignored on Python < 3.11; " + "only ssl_shutdown_timeout=0 is supported. The timeout will be ignored.", + RuntimeWarning, + stacklevel=2, + ) + self._ssl_shutdown_timeout = ssl_shutdown_timeout + + def _close(self, *, abort_ssl: bool = False) -> list[Awaitable[object]]: + """Close all ongoing DNS calls.""" + for fut in chain.from_iterable(self._throttle_dns_futures.values()): + fut.cancel() + + waiters = super()._close(abort_ssl=abort_ssl) + + for t in self._resolve_host_tasks: + t.cancel() + waiters.append(t) + + return waiters + + async def close(self, *, abort_ssl: bool = False) -> None: + """ + Close all opened transports. + + :param abort_ssl: If True, SSL connections will be aborted immediately + without performing the shutdown handshake. If False (default), + the behavior is determined by ssl_shutdown_timeout: + - If ssl_shutdown_timeout=0: connections are aborted + - If ssl_shutdown_timeout>0: graceful shutdown is performed + """ + # Use abort_ssl param if explicitly set, otherwise use ssl_shutdown_timeout default + await super().close(abort_ssl=abort_ssl or self._ssl_shutdown_timeout == 0) + if self._resolver_owner: + await self._resolver.close() + + @property + def family(self) -> int: + """Socket family like AF_INET.""" + return self._family + + @property + def use_dns_cache(self) -> bool: + """True if local DNS caching is enabled.""" + return self._use_dns_cache + + def clear_dns_cache(self, host: str | None = None, port: int | None = None) -> None: + """Remove specified host/port or clear all dns local cache.""" + if host is not None and port is not None: + self._cached_hosts.remove((host, port)) + elif host is not None or port is not None: + raise ValueError("either both host and port or none of them are allowed") + else: + self._cached_hosts.clear() + + async def _resolve_host( + self, host: str, port: int, traces: Sequence["Trace"] | None = None + ) -> list[ResolveResult]: + """Resolve host and return list of addresses.""" + if is_ip_address(host): + # Reject legacy numeric IPv4 forms (e.g. 2130706433, 127.1) that + # socket would map onto an address, slipping past a connector-level + # policy that only sees the raw host. + if ":" not in host and not is_canonical_ipv4_address(host): + raise InvalidUrlClientError(host, "is not a canonical IPv4 address") + return [ + { + "hostname": host, + "host": host, + "port": port, + "family": self._family, + "proto": 0, + "flags": 0, + } + ] + + if not self._use_dns_cache: + + if traces: + for trace in traces: + await trace.send_dns_resolvehost_start(host) + + if self._closed: + raise ClientConnectionError("Connector is closed") + + res = await self._resolver.resolve(host, port, family=self._family) + + if traces: + for trace in traces: + await trace.send_dns_resolvehost_end(host) + + return res + + key = (host, port) + if key in self._cached_hosts and not self._cached_hosts.expired(key): + # get result early, before any await (#4014) + result = self._cached_hosts.next_addrs(key) + + if traces: + for trace in traces: + await trace.send_dns_cache_hit(host) + return result + + futures: set[asyncio.Future[None]] + # + # If multiple connectors are resolving the same host, we wait + # for the first one to resolve and then use the result for all of them. + # We use a throttle to ensure that we only resolve the host once + # and then use the result for all the waiters. + # + if key in self._throttle_dns_futures: + # get futures early, before any await (#4014) + futures = self._throttle_dns_futures[key] + future: asyncio.Future[None] = self._loop.create_future() + futures.add(future) + if traces: + for trace in traces: + await trace.send_dns_cache_hit(host) + try: + await future + finally: + futures.discard(future) + return self._cached_hosts.next_addrs(key) + + # update dict early, before any await (#4014) + self._throttle_dns_futures[key] = futures = set() + # In this case we need to create a task to ensure that we can shield + # the task from cancellation as cancelling this lookup should not cancel + # the underlying lookup or else the cancel event will get broadcast to + # all the waiters across all connections. + # + coro = self._resolve_host_with_throttle(key, host, port, futures, traces) + loop = asyncio.get_running_loop() + if sys.version_info >= (3, 12): + # Optimization for Python 3.12, try to send immediately + resolved_host_task = asyncio.Task(coro, loop=loop, eager_start=True) + else: + resolved_host_task = loop.create_task(coro) + + if not resolved_host_task.done(): + self._resolve_host_tasks.add(resolved_host_task) + resolved_host_task.add_done_callback(self._resolve_host_tasks.discard) + + try: + return await asyncio.shield(resolved_host_task) + except asyncio.CancelledError: + + def drop_exception(fut: "asyncio.Future[list[ResolveResult]]") -> None: + with suppress(Exception, asyncio.CancelledError): + fut.result() + + resolved_host_task.add_done_callback(drop_exception) + raise + + async def _resolve_host_with_throttle( + self, + key: tuple[str, int], + host: str, + port: int, + futures: set["asyncio.Future[None]"], + traces: Sequence["Trace"] | None, + ) -> list[ResolveResult]: + """Resolve host and set result for all waiters. + + This method must be run in a task and shielded from cancellation + to avoid cancelling the underlying lookup. + """ + try: + if traces: + for trace in traces: + await trace.send_dns_cache_miss(host) + + for trace in traces: + await trace.send_dns_resolvehost_start(host) + + addrs = await self._resolver.resolve(host, port, family=self._family) + if traces: + for trace in traces: + await trace.send_dns_resolvehost_end(host) + + self._cached_hosts.add(key, addrs) + for fut in futures: + set_result(fut, None) + except BaseException as e: + # any DNS exception is set for the waiters to raise the same exception. + # This coro is always run in task that is shielded from cancellation so + # we should never be propagating cancellation here. + for fut in futures: + set_exception(fut, e) + raise + finally: + self._throttle_dns_futures.pop(key) + + return self._cached_hosts.next_addrs(key) + + async def _create_connection( + self, req: ClientRequest, traces: list["Trace"], timeout: "ClientTimeout" + ) -> ResponseHandler: + """Create connection. + + Has same keyword arguments as BaseEventLoop.create_connection. + """ + if req.proxy: + _, proto = await self._create_proxy_connection(req, traces, timeout) + else: + _, proto = await self._create_direct_connection(req, traces, timeout) + + return proto + + def _get_ssl_context(self, req: ClientRequest) -> SSLContext | None: + """Logic to get the correct SSL context + + 0. if req.ssl is false, return None + + 1. if ssl_context is specified in req, use it + 2. if _ssl_context is specified in self, use it + 3. otherwise: + 1. if verify_ssl is not specified in req, use self.ssl_context + (will generate a default context according to self.verify_ssl) + 2. if verify_ssl is True in req, generate a default SSL context + 3. if verify_ssl is False in req, generate a SSL context that + won't verify + """ + if not req.is_ssl(): + return None + + if ssl is None: # pragma: no cover + raise RuntimeError("SSL is not supported.") + sslcontext = req.ssl + if isinstance(sslcontext, ssl.SSLContext): + return sslcontext + if sslcontext is not True: + # not verified or fingerprinted + return _SSL_CONTEXT_UNVERIFIED + sslcontext = self._ssl + if isinstance(sslcontext, ssl.SSLContext): + return sslcontext + if sslcontext is not True: + # not verified or fingerprinted + return _SSL_CONTEXT_UNVERIFIED + return _SSL_CONTEXT_VERIFIED + + def _get_fingerprint(self, req: ClientRequest) -> Optional["Fingerprint"]: + ret = req.ssl + if isinstance(ret, Fingerprint): + return ret + ret = self._ssl + if isinstance(ret, Fingerprint): + return ret + return None + + async def _wrap_create_connection( + self, + *args: Any, + addr_infos: list[AddrInfoType], + req: ClientRequest, + timeout: "ClientTimeout", + client_error: type[Exception] = ClientConnectorError, + **kwargs: Any, + ) -> tuple[asyncio.Transport, ResponseHandler]: + try: + async with ceil_timeout( + timeout.sock_connect, ceil_threshold=timeout.ceil_threshold + ): + sock = await aiohappyeyeballs.start_connection( + addr_infos=addr_infos, + local_addr_infos=self._local_addr_infos, + happy_eyeballs_delay=self._happy_eyeballs_delay, + interleave=self._interleave, + loop=self._loop, + socket_factory=self._socket_factory, + ) + # Add ssl_shutdown_timeout for Python 3.11+ when SSL is used + if ( + kwargs.get("ssl") + and self._ssl_shutdown_timeout + and sys.version_info >= (3, 11) + ): + kwargs["ssl_shutdown_timeout"] = self._ssl_shutdown_timeout + return await self._loop.create_connection(*args, **kwargs, sock=sock) + except cert_errors as exc: + raise ClientConnectorCertificateError(req.connection_key, exc) from exc + except ssl_errors as exc: + raise ClientConnectorSSLError(req.connection_key, exc) from exc + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise client_error(req.connection_key, exc) from exc + + async def _wrap_existing_connection( + self, + *args: Any, + req: ClientRequest, + timeout: "ClientTimeout", + client_error: type[Exception] = ClientConnectorError, + **kwargs: Any, + ) -> tuple[asyncio.Transport, ResponseHandler]: + try: + async with ceil_timeout( + timeout.sock_connect, ceil_threshold=timeout.ceil_threshold + ): + return await self._loop.create_connection(*args, **kwargs) + except cert_errors as exc: + raise ClientConnectorCertificateError(req.connection_key, exc) from exc + except ssl_errors as exc: + raise ClientConnectorSSLError(req.connection_key, exc) from exc + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise client_error(req.connection_key, exc) from exc + + def _fail_on_no_start_tls(self, req: "ClientRequest") -> None: + """Raise a :py:exc:`RuntimeError` on missing ``start_tls()``. + + It is necessary for TLS-in-TLS so that it is possible to + send HTTPS queries through HTTPS proxies. + + This doesn't affect regular HTTP requests, though. + """ + if not req.is_ssl(): + return + + proxy_url = req.proxy + assert proxy_url is not None + if proxy_url.scheme != "https": + return + + self._check_loop_for_start_tls() + + def _check_loop_for_start_tls(self) -> None: + try: + self._loop.start_tls + except AttributeError as attr_exc: + raise RuntimeError( + "An HTTPS request is being sent through an HTTPS proxy. " + "This needs support for TLS in TLS but it is not implemented " + "in your runtime for the stdlib asyncio.\n\n" + "Please upgrade to Python 3.11 or higher. For more details, " + "please see:\n" + "* https://bugs.python.org/issue37179\n" + "* https://github.com/python/cpython/pull/28073\n" + "* https://docs.aiohttp.org/en/stable/" + "client_advanced.html#proxy-support\n" + "* https://github.com/aio-libs/aiohttp/discussions/6044\n", + ) from attr_exc + + def _loop_supports_start_tls(self) -> bool: + try: + self._check_loop_for_start_tls() + except RuntimeError: + return False + else: + return True + + def _warn_about_tls_in_tls( + self, + underlying_transport: asyncio.Transport, + req: ClientRequest, + ) -> None: + """Issue a warning if the requested URL has HTTPS scheme.""" + if req.request_info.url.scheme != "https": + return + + # TLS-in-TLS only applies when the proxy itself is HTTPS. + # When the proxy is HTTP, start_tls upgrades a plain TCP connection, + # which is standard TLS and works on all event loops and Python versions. + if req.proxy is None or req.proxy.scheme != "https": + return + + # Check if uvloop is being used, which supports TLS in TLS, + # otherwise assume that asyncio's native transport is being used. + if type(underlying_transport).__module__.startswith("uvloop"): + return + + # Support in asyncio was added in Python 3.11 (bpo-44011) + asyncio_supports_tls_in_tls = sys.version_info >= (3, 11) or getattr( + underlying_transport, + "_start_tls_compatible", + False, + ) + + if asyncio_supports_tls_in_tls: + return + + warnings.warn( + "An HTTPS request is being sent through an HTTPS proxy. " + "This support for TLS in TLS is known to be disabled " + "in the stdlib asyncio (Python <3.11). This is why you'll probably see " + "an error in the log below.\n\n" + "It is possible to enable it via monkeypatching. " + "For more details, see:\n" + "* https://bugs.python.org/issue37179\n" + "* https://github.com/python/cpython/pull/28073\n\n" + "You can temporarily patch this as follows:\n" + "* https://docs.aiohttp.org/en/stable/client_advanced.html#proxy-support\n" + "* https://github.com/aio-libs/aiohttp/discussions/6044\n", + RuntimeWarning, + source=self, + # Why `4`? At least 3 of the calls in the stack originate + # from the methods in this class. + stacklevel=3, + ) + + async def _start_tls_connection( + self, + underlying_transport: asyncio.Transport, + req: ClientRequest, + timeout: "ClientTimeout", + client_error: type[Exception] = ClientConnectorError, + ) -> tuple[asyncio.BaseTransport, ResponseHandler]: + """Wrap the raw TCP transport with TLS.""" + tls_proto = self._factory() # Create a brand new proto for TLS + sslcontext = self._get_ssl_context(req) + if TYPE_CHECKING: + # _start_tls_connection is unreachable in the current code path + # if sslcontext is None. + assert sslcontext is not None + + try: + async with ceil_timeout( + timeout.sock_connect, ceil_threshold=timeout.ceil_threshold + ): + try: + # ssl_shutdown_timeout is only available in Python 3.11+ + if sys.version_info >= (3, 11) and self._ssl_shutdown_timeout: + tls_transport = await self._loop.start_tls( + underlying_transport, + tls_proto, + sslcontext, + server_hostname=req.server_hostname or req.host, + ssl_handshake_timeout=timeout.total or None, + ssl_shutdown_timeout=self._ssl_shutdown_timeout, + ) + else: + tls_transport = await self._loop.start_tls( + underlying_transport, + tls_proto, + sslcontext, + server_hostname=req.server_hostname or req.host, + ssl_handshake_timeout=timeout.total or None, + ) + except BaseException: + # We need to close the underlying transport since + # `start_tls()` probably failed before it had a + # chance to do this: + if self._ssl_shutdown_timeout == 0: + underlying_transport.abort() + else: + underlying_transport.close() + raise + if isinstance(tls_transport, asyncio.Transport): + fingerprint = self._get_fingerprint(req) + if fingerprint: + try: + fingerprint.check(tls_transport) + except ServerFingerprintMismatch: + tls_transport.close() + if not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(tls_transport) + raise + except cert_errors as exc: + raise ClientConnectorCertificateError(req.connection_key, exc) from exc + except ssl_errors as exc: + raise ClientConnectorSSLError(req.connection_key, exc) from exc + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise client_error(req.connection_key, exc) from exc + except TypeError as type_err: + # Example cause looks like this: + # TypeError: transport is not supported by start_tls() + + raise ClientConnectionError( + "Cannot initialize a TLS-in-TLS connection to host " + f"{req.host!s}:{req.port:d} through an underlying connection " + f"to an HTTPS proxy {req.proxy!s} ssl:{req.ssl or 'default'} " + f"[{type_err!s}]" + ) from type_err + else: + if tls_transport is None: + msg = "Failed to start TLS (possibly caused by closing transport)" + raise client_error(req.connection_key, OSError(msg)) + tls_proto.connection_made( + tls_transport + ) # Kick the state machine of the new TLS protocol + + return tls_transport, tls_proto + + def _convert_hosts_to_addr_infos( + self, hosts: list[ResolveResult] + ) -> list[AddrInfoType]: + """Converts the list of hosts to a list of addr_infos. + + The list of hosts is the result of a DNS lookup. The list of + addr_infos is the result of a call to `socket.getaddrinfo()`. + """ + addr_infos: list[AddrInfoType] = [] + for hinfo in hosts: + host = hinfo["host"] + is_ipv6 = ":" in host + family = socket.AF_INET6 if is_ipv6 else socket.AF_INET + if self._family and self._family != family: + continue + addr = (host, hinfo["port"], 0, 0) if is_ipv6 else (host, hinfo["port"]) + addr_infos.append( + (family, socket.SOCK_STREAM, socket.IPPROTO_TCP, "", addr) + ) + return addr_infos + + async def _create_direct_connection( + self, + req: ClientRequest, + traces: list["Trace"], + timeout: "ClientTimeout", + *, + client_error: type[Exception] = ClientConnectorError, + ) -> tuple[asyncio.Transport, ResponseHandler]: + sslcontext = self._get_ssl_context(req) + fingerprint = self._get_fingerprint(req) + + host = req.url.raw_host + assert host is not None + # Replace multiple trailing dots with a single one. + # A trailing dot is only present for fully-qualified domain names. + # See https://github.com/aio-libs/aiohttp/pull/7364. + if host.endswith(".."): + host = host.rstrip(".") + "." + port = req.port + assert port is not None + try: + # Cancelling this lookup should not cancel the underlying lookup + # or else the cancel event will get broadcast to all the waiters + # across all connections. + hosts = await self._resolve_host(host, port, traces=traces) + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + # in case of proxy it is not ClientProxyConnectionError + # it is problem of resolving proxy ip itself + raise ClientConnectorDNSError(req.connection_key, exc) from exc + + last_exc: Exception | None = None + addr_infos = self._convert_hosts_to_addr_infos(hosts) + while addr_infos: + # Strip trailing dots, certificates contain FQDN without dots. + # See https://github.com/aio-libs/aiohttp/issues/3636 + server_hostname = ( + (req.server_hostname or host).rstrip(".") if sslcontext else None + ) + + try: + transp, proto = await self._wrap_create_connection( + self._factory, + timeout=timeout, + ssl=sslcontext, + addr_infos=addr_infos, + server_hostname=server_hostname, + req=req, + client_error=client_error, + ) + except (ClientConnectorError, asyncio.TimeoutError) as exc: + last_exc = exc + aiohappyeyeballs.pop_addr_infos_interleave(addr_infos, self._interleave) + continue + + if req.is_ssl() and fingerprint: + try: + fingerprint.check(transp) + except ServerFingerprintMismatch as exc: + transp.close() + if not self._cleanup_closed_disabled: + self._cleanup_closed_transports.append(transp) + last_exc = exc + # Remove the bad peer from the list of addr_infos + sock: socket.socket = transp.get_extra_info("socket") + bad_peer = sock.getpeername() + aiohappyeyeballs.remove_addr_infos(addr_infos, bad_peer) + continue + + return transp, proto + else: + assert last_exc is not None + raise last_exc + + async def _create_proxy_connection( + self, req: ClientRequest, traces: list["Trace"], timeout: "ClientTimeout" + ) -> tuple[asyncio.BaseTransport, ResponseHandler]: + self._fail_on_no_start_tls(req) + runtime_has_start_tls = self._loop_supports_start_tls() + proxy_req = self._update_proxy_auth_header_and_build_proxy_req(req) + + # create connection to proxy server + transport, proto = await self._create_direct_connection( + proxy_req, [], timeout, client_error=ClientProxyConnectionError + ) + + if req.is_ssl(): + if runtime_has_start_tls: + self._warn_about_tls_in_tls(transport, req) + + # For HTTPS requests over HTTP proxy + # we must notify proxy to tunnel connection + # so we send CONNECT command: + # CONNECT www.python.org:443 HTTP/1.1 + # Host: www.python.org + # + # next we must do TLS handshake and so on + # to do this we must wrap raw socket into secure one + # asyncio handles this perfectly + proxy_req.method = hdrs.METH_CONNECT + proxy_req.url = req.url + key = req.connection_key._replace( + proxy=None, proxy_auth=None, proxy_headers_hash=None + ) + conn = _ConnectTunnelConnection(self, key, proto, self._loop) + proxy_resp = await proxy_req.send(conn) + try: + protocol = conn._protocol + assert protocol is not None + + # read_until_eof=True will ensure the connection isn't closed + # once the response is received and processed allowing + # START_TLS to work on the connection below. + protocol.set_response_params( + read_until_eof=runtime_has_start_tls, + timeout_ceil_threshold=self._timeout_ceil_threshold, + ) + resp = await proxy_resp.start(conn) + except BaseException: + proxy_resp.close() + conn.close() + raise + else: + conn._protocol = None + try: + if resp.status != 200: + message = resp.reason + if message is None: + message = HTTPStatus(resp.status).phrase + raise ClientHttpProxyError( + proxy_resp.request_info, + resp.history, + status=resp.status, + message=message, + headers=resp.headers, + ) + if not runtime_has_start_tls: + rawsock = transport.get_extra_info("socket", default=None) + if rawsock is None: + raise RuntimeError( + "Transport does not expose socket instance" + ) + # Duplicate the socket, so now we can close proxy transport + rawsock = rawsock.dup() + except BaseException: + # It shouldn't be closed in `finally` because it's fed to + # `loop.start_tls()` and the docs say not to touch it after + # passing there. + transport.close() + raise + finally: + if not runtime_has_start_tls: + transport.close() + + if not runtime_has_start_tls: + # HTTP proxy with support for upgrade to HTTPS + sslcontext = self._get_ssl_context(req) + return await self._wrap_existing_connection( + self._factory, + timeout=timeout, + ssl=sslcontext, + sock=rawsock, + server_hostname=req.host, + req=req, + ) + + return await self._start_tls_connection( + # Access the old transport for the last time before it's + # closed and forgotten forever: + transport, + req=req, + timeout=timeout, + ) + finally: + proxy_resp.close() + + return transport, proto + + +class UnixConnector(BaseConnector): + """Unix socket connector. + + path - Unix socket path. + keepalive_timeout - (optional) Keep-alive timeout. + force_close - Set to True to force close and do reconnect + after each request (and between redirects). + limit - The total number of simultaneous connections. + limit_per_host - Number of simultaneous connections to one host. + loop - Optional event loop. + """ + + allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"unix"}) + + def __init__( + self, + path: str, + force_close: bool = False, + keepalive_timeout: object | float | None = sentinel, + limit: int = 100, + limit_per_host: int = 0, + loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + super().__init__( + force_close=force_close, + keepalive_timeout=keepalive_timeout, + limit=limit, + limit_per_host=limit_per_host, + loop=loop, + ) + self._path = path + + @property + def path(self) -> str: + """Path to unix socket.""" + return self._path + + async def _create_connection( + self, req: ClientRequest, traces: list["Trace"], timeout: "ClientTimeout" + ) -> ResponseHandler: + try: + async with ceil_timeout( + timeout.sock_connect, ceil_threshold=timeout.ceil_threshold + ): + _, proto = await self._loop.create_unix_connection( + self._factory, self._path + ) + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise UnixClientConnectorError(self.path, req.connection_key, exc) from exc + + return proto + + +class NamedPipeConnector(BaseConnector): + """Named pipe connector. + + Only supported by the proactor event loop. + See also: https://docs.python.org/3/library/asyncio-eventloop.html + + path - Windows named pipe path. + keepalive_timeout - (optional) Keep-alive timeout. + force_close - Set to True to force close and do reconnect + after each request (and between redirects). + limit - The total number of simultaneous connections. + limit_per_host - Number of simultaneous connections to one host. + loop - Optional event loop. + """ + + allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"npipe"}) + + def __init__( + self, + path: str, + force_close: bool = False, + keepalive_timeout: object | float | None = sentinel, + limit: int = 100, + limit_per_host: int = 0, + loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + super().__init__( + force_close=force_close, + keepalive_timeout=keepalive_timeout, + limit=limit, + limit_per_host=limit_per_host, + loop=loop, + ) + if not isinstance( + self._loop, + asyncio.ProactorEventLoop, # type: ignore[attr-defined] + ): + raise RuntimeError( + "Named Pipes only available in proactor loop under windows" + ) + self._path = path + + @property + def path(self) -> str: + """Path to the named pipe.""" + return self._path + + async def _create_connection( + self, req: ClientRequest, traces: list["Trace"], timeout: "ClientTimeout" + ) -> ResponseHandler: + try: + async with ceil_timeout( + timeout.sock_connect, ceil_threshold=timeout.ceil_threshold + ): + _, proto = await self._loop.create_pipe_connection( # type: ignore[attr-defined] + self._factory, self._path + ) + # the drain is required so that the connection_made is called + # and transport is set otherwise it is not set before the + # `assert conn.transport is not None` + # in client.py's _request method + await asyncio.sleep(0) + # other option is to manually set transport like + # `proto.transport = trans` + except OSError as exc: + if exc.errno is None and isinstance(exc, asyncio.TimeoutError): + raise + raise ClientConnectorError(req.connection_key, exc) from exc + + return cast(ResponseHandler, proto) diff --git a/venv/lib/python3.11/site-packages/aiohttp/cookiejar.py b/venv/lib/python3.11/site-packages/aiohttp/cookiejar.py new file mode 100644 index 0000000..2b97224 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/cookiejar.py @@ -0,0 +1,678 @@ +import asyncio +import calendar +import contextlib +import datetime +import heapq +import itertools +import json +import os +import pathlib +import pickle +import re +import time +import warnings +from collections import defaultdict +from collections.abc import Iterable, Iterator, Mapping +from http.cookies import BaseCookie, Morsel, SimpleCookie +from types import MappingProxyType +from typing import Union + +from yarl import URL + +from ._cookie_helpers import preserve_morsel_with_coded_value +from .abc import AbstractCookieJar, ClearCookiePredicate +from .helpers import is_ip_address +from .typedefs import LooseCookies, PathLike, StrOrURL + +__all__ = ("CookieJar", "DummyCookieJar") + + +CookieItem = Union[str, "Morsel[str]"] + +# We cache these string methods here as their use is in performance critical code. +_FORMAT_PATH = "{}/{}".format +_FORMAT_DOMAIN_REVERSED = "{1}.{0}".format + +# The minimum number of scheduled cookie expirations before we start cleaning up +# the expiration heap. This is a performance optimization to avoid cleaning up the +# heap too often when there are only a few scheduled expirations. +_MIN_SCHEDULED_COOKIE_EXPIRATION = 100 +_SIMPLE_COOKIE = SimpleCookie() + +# Not persisted; the absolute deadline is saved instead. +_RELATIVE_EXPIRY_ATTRS = frozenset(("max-age", "expires")) + + +class _RestrictedCookieUnpickler(pickle._Unpickler): + """A restricted unpickler that only allows cookie-related types. + + This prevents arbitrary code execution when loading pickled cookie data + from untrusted sources. Only types that are expected in a serialized + CookieJar are permitted. + + Subclasses :class:`pickle._Unpickler` (the pure-Python implementation) + rather than :class:`pickle.Unpickler` because the accelerated unpickler + on some implementations (notably PyPy) does not dispatch through + :meth:`find_class` overrides. + + See: https://docs.python.org/3/library/pickle.html#restricting-globals + """ + + _ALLOWED_CLASSES: frozenset[tuple[str, str]] = frozenset( + { + # Core cookie types + ("http.cookies", "SimpleCookie"), + ("http.cookies", "Morsel"), + # Container types used by CookieJar._cookies + ("collections", "defaultdict"), + # builtins that pickle uses for reconstruction + ("builtins", "tuple"), + ("builtins", "set"), + ("builtins", "frozenset"), + ("builtins", "dict"), + } + ) + + def find_class(self, module: str, name: str) -> type: + if (module, name) not in self._ALLOWED_CLASSES: + raise pickle.UnpicklingError( + f"Forbidden class: {module}.{name}. " + "CookieJar.load() only allows cookie-related types for security. " + "See https://docs.python.org/3/library/pickle.html#restricting-globals" + ) + return super().find_class(module, name) # type: ignore[no-any-return] + + +class CookieJar(AbstractCookieJar): + """Implements cookie storage adhering to RFC 6265.""" + + DATE_TOKENS_RE = re.compile( + r"[\x09\x20-\x2F\x3B-\x40\x5B-\x60\x7B-\x7E]*" + r"(?P[\x00-\x08\x0A-\x1F\d:a-zA-Z\x7F-\xFF]+)" + ) + + DATE_HMS_TIME_RE = re.compile(r"(\d{1,2}):(\d{1,2}):(\d{1,2})") + + DATE_DAY_OF_MONTH_RE = re.compile(r"(\d{1,2})") + + DATE_MONTH_RE = re.compile( + "(jan)|(feb)|(mar)|(apr)|(may)|(jun)|(jul)|(aug)|(sep)|(oct)|(nov)|(dec)", + re.I, + ) + + DATE_YEAR_RE = re.compile(r"(\d{2,4})") + + # calendar.timegm() fails for timestamps after datetime.datetime.max + # Minus one as a loss of precision occurs when timestamp() is called. + MAX_TIME = ( + int(datetime.datetime.max.replace(tzinfo=datetime.timezone.utc).timestamp()) - 1 + ) + try: + calendar.timegm(time.gmtime(MAX_TIME)) + except OSError: + # Hit the maximum representable time on Windows + # https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/localtime-localtime32-localtime64 + MAX_TIME = calendar.timegm((3000, 12, 31, 23, 59, 59, -1, -1, -1)) + except OverflowError: + # #4515: datetime.max may not be representable on 32-bit platforms + MAX_TIME = 2**31 - 1 + # Avoid minuses in the future, 3x faster + SUB_MAX_TIME = MAX_TIME - 1 + + def __init__( + self, + *, + unsafe: bool = False, + quote_cookie: bool = True, + treat_as_secure_origin: StrOrURL | list[StrOrURL] | None = None, + loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + super().__init__(loop=loop) + self._cookies: defaultdict[tuple[str, str], SimpleCookie] = defaultdict( + SimpleCookie + ) + self._morsel_cache: defaultdict[tuple[str, str], dict[str, Morsel[str]]] = ( + defaultdict(dict) + ) + self._host_only_cookies: set[tuple[str, str]] = set() + self._unsafe = unsafe + self._quote_cookie = quote_cookie + if treat_as_secure_origin is None: + treat_as_secure_origin = [] + elif isinstance(treat_as_secure_origin, URL): + treat_as_secure_origin = [treat_as_secure_origin.origin()] + elif isinstance(treat_as_secure_origin, str): + treat_as_secure_origin = [URL(treat_as_secure_origin).origin()] + else: + treat_as_secure_origin = [ + URL(url).origin() if isinstance(url, str) else url.origin() + for url in treat_as_secure_origin + ] + self._treat_as_secure_origin = treat_as_secure_origin + self._expire_heap: list[tuple[float, tuple[str, str, str]]] = [] + self._expirations: dict[tuple[str, str, str], float] = {} + + @property + def unsafe(self) -> bool: + return self._unsafe + + @property + def quote_cookie(self) -> bool: + return self._quote_cookie + + @property + def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]: + """Return the cookies stored in this jar.""" + return MappingProxyType(self._cookies) + + @property + def host_only_cookies(self) -> frozenset[tuple[str, str]]: + """Return the host-only cookies stored in this jar.""" + return frozenset(self._host_only_cookies) + + def save(self, file_path: PathLike) -> None: + """Save cookies to a file using JSON format. + + :param file_path: Path to file where cookies will be serialized, + :class:`str` or :class:`pathlib.Path` instance. + """ + file_path = pathlib.Path(file_path) + data: dict[str, dict[str, dict[str, str | bool | float]]] = {} + for (domain, path), cookie in self._cookies.items(): + key = f"{domain}|{path}" + data[key] = {} + for name, morsel in cookie.items(): + morsel_data: dict[str, str | bool | float] = { + "key": morsel.key, + "value": morsel.value, + "coded_value": morsel.coded_value, + } + # Skip relative expiry; the absolute deadline is saved below. + for attr in morsel._reserved: # type: ignore[attr-defined] + if attr in _RELATIVE_EXPIRY_ATTRS: + continue + attr_val = morsel[attr] + if attr_val: + morsel_data[attr] = attr_val + # Persist or it reloads as a domain cookie and leaks to subdomains. + if (domain, name) in self._host_only_cookies: + morsel_data["host_only"] = True + if (exp := self._expirations.get((domain, path, name))) is not None: + morsel_data["expires_timestamp"] = exp + data[key][name] = morsel_data + + # Cookie persistence may include authentication/session tokens. + # Use 0o600 at creation time to avoid umask-dependent overexposure + # and enforce least-privilege access to sensitive credential data. + with open( + file_path, + mode="w", + encoding="utf-8", + opener=lambda path, flags: os.open(path, flags, 0o600), + ) as f: + json.dump(data, f, indent=2) + + def load(self, file_path: PathLike) -> None: + """Load cookies from a file. + + Tries to load JSON format first. Falls back to loading legacy + pickle format (using a restricted unpickler) for backward + compatibility with existing cookie files. + + Replaces the current jar contents; loaded cookies pass through the + same acceptance rules as :meth:`update_cookies`. + + :param file_path: Path to file from where cookies will be + imported, :class:`str` or :class:`pathlib.Path` instance. + """ + file_path = pathlib.Path(file_path) + # Try JSON format first + try: + with file_path.open(mode="r", encoding="utf-8") as f: + data = json.load(f) + self._load_json_data(data) + except (json.JSONDecodeError, UnicodeDecodeError, ValueError): + # Fall back to legacy pickle format with restricted unpickler + with file_path.open(mode="rb") as f: + self._cookies = _RestrictedCookieUnpickler(f).load() + + def _load_json_data( + self, data: dict[str, dict[str, dict[str, str | bool | float]]] + ) -> None: + """Replace contents, routing cookies through update_cookies().""" + self.clear() + for compound_key, cookie_data in data.items(): + domain, path = compound_key.split("|", 1) + for name, morsel_data in cookie_data.items(): + morsel: Morsel[str] = Morsel() + # Use __setstate__ to bypass validation, same pattern + # used in _build_morsel and _cookie_helpers. + morsel.__setstate__( # type: ignore[attr-defined] + { + "key": morsel_data["key"], + "value": morsel_data["value"], + "coded_value": morsel_data["coded_value"], + } + ) + # Restore morsel attributes + for attr in morsel._reserved: # type: ignore[attr-defined] + if attr in morsel_data and attr not in ( + "key", + "value", + "coded_value", + ): + morsel[attr] = morsel_data[attr] + # Drop the domain so update_cookies() re-marks it host-only. + if morsel_data.get("host_only"): + morsel["domain"] = "" + response_url = ( + URL.build(scheme="https", host=domain) if domain else URL() + ) + self.update_cookies({name: morsel}, response_url) + # Restore the absolute deadline; update_cookies() schedules none. + if (exp := morsel_data.get("expires_timestamp")) is not None: + self._expire_cookie(float(exp), domain, path, name) + self._do_expiration() + + def clear(self, predicate: ClearCookiePredicate | None = None) -> None: + if predicate is None: + self._expire_heap.clear() + self._cookies.clear() + self._morsel_cache.clear() + self._host_only_cookies.clear() + self._expirations.clear() + return + + now = time.time() + to_del = [ + key + for (domain, path), cookie in self._cookies.items() + for name, morsel in cookie.items() + if ( + (key := (domain, path, name)) in self._expirations + and self._expirations[key] <= now + ) + or predicate(morsel) + ] + if to_del: + self._delete_cookies(to_del) + + def clear_domain(self, domain: str) -> None: + self.clear(lambda x: self._is_domain_match(domain, x["domain"])) + + def __iter__(self) -> "Iterator[Morsel[str]]": + self._do_expiration() + for val in self._cookies.values(): + yield from val.values() + + def __len__(self) -> int: + """Return number of cookies. + + This function does not iterate self to avoid unnecessary expiration + checks. + """ + return sum(len(cookie.values()) for cookie in self._cookies.values()) + + def _do_expiration(self) -> None: + """Remove expired cookies.""" + if not (expire_heap_len := len(self._expire_heap)): + return + + # If the expiration heap grows larger than the number expirations + # times two, we clean it up to avoid keeping expired entries in + # the heap and consuming memory. We guard this with a minimum + # threshold to avoid cleaning up the heap too often when there are + # only a few scheduled expirations. + if ( + expire_heap_len > _MIN_SCHEDULED_COOKIE_EXPIRATION + and expire_heap_len > len(self._expirations) * 2 + ): + # Remove any expired entries from the expiration heap + # that do not match the expiration time in the expirations + # as it means the cookie has been re-added to the heap + # with a different expiration time. + self._expire_heap = [ + entry + for entry in self._expire_heap + if self._expirations.get(entry[1]) == entry[0] + ] + heapq.heapify(self._expire_heap) + + now = time.time() + to_del: list[tuple[str, str, str]] = [] + # Find any expired cookies and add them to the to-delete list + while self._expire_heap: + when, cookie_key = self._expire_heap[0] + if when > now: + break + heapq.heappop(self._expire_heap) + # Check if the cookie hasn't been re-added to the heap + # with a different expiration time as it will be removed + # later when it reaches the top of the heap and its + # expiration time is met. + if self._expirations.get(cookie_key) == when: + to_del.append(cookie_key) + + if to_del: + self._delete_cookies(to_del) + + def _delete_cookies(self, to_del: list[tuple[str, str, str]]) -> None: + for domain, path, name in to_del: + self._host_only_cookies.discard((domain, name)) + self._cookies[(domain, path)].pop(name, None) + self._morsel_cache[(domain, path)].pop(name, None) + self._expirations.pop((domain, path, name), None) + + def _expire_cookie(self, when: float, domain: str, path: str, name: str) -> None: + cookie_key = (domain, path, name) + if self._expirations.get(cookie_key) == when: + # Avoid adding duplicates to the heap + return + heapq.heappush(self._expire_heap, (when, cookie_key)) + self._expirations[cookie_key] = when + + def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None: + """Update cookies.""" + hostname = response_url.raw_host + + if not self._unsafe and is_ip_address(hostname): + # Don't accept cookies from IPs + return + + if isinstance(cookies, Mapping): + cookies = cookies.items() + + for name, cookie in cookies: + if not isinstance(cookie, Morsel): + tmp = SimpleCookie() + tmp[name] = cookie # type: ignore[assignment] + cookie = tmp[name] + + domain = cookie["domain"] + + # ignore domains with trailing dots + if domain and domain[-1] == ".": + domain = "" + del cookie["domain"] + + if not domain and hostname is not None: + # Set the cookie's domain to the response hostname + # and set its host-only-flag + self._host_only_cookies.add((hostname, name)) + domain = cookie["domain"] = hostname + + if domain and domain[0] == ".": + # Remove leading dot + domain = domain[1:] + cookie["domain"] = domain + + if hostname and not self._is_domain_match(domain, hostname): + # Setting cookies for different domains is not allowed + continue + + path = cookie["path"] + if not path or path[0] != "/": + # Set the cookie's path to the response path + path = response_url.path + if not path.startswith("/"): + path = "/" + else: + # Cut everything from the last slash to the end + path = "/" + path[1 : path.rfind("/")] + cookie["path"] = path + path = path.rstrip("/") + + if max_age := cookie["max-age"]: + try: + delta_seconds = int(max_age) + max_age_expiration = min(time.time() + delta_seconds, self.MAX_TIME) + self._expire_cookie(max_age_expiration, domain, path, name) + except ValueError: + cookie["max-age"] = "" + + elif expires := cookie["expires"]: + if expire_time := self._parse_date(expires): + self._expire_cookie(expire_time, domain, path, name) + else: + cookie["expires"] = "" + + key = (domain, path) + if self._cookies[key].get(name) != cookie: + # Don't blow away the cache if the same + # cookie gets set again + self._cookies[key][name] = cookie + self._morsel_cache[key].pop(name, None) + + self._do_expiration() + + def filter_cookies(self, request_url: URL = URL()) -> "BaseCookie[str]": + """Returns this jar's cookies filtered by their attributes.""" + # We always use BaseCookie now since all + # cookies set on on filtered are fully constructed + # Morsels, not just names and values. + filtered: BaseCookie[str] = BaseCookie() + if not self._cookies: + # Skip do_expiration() if there are no cookies. + return filtered + self._do_expiration() + if not self._cookies: + # Skip rest of function if no non-expired cookies. + return filtered + if type(request_url) is not URL: + warnings.warn( + "filter_cookies expects yarl.URL instances only," + f"and will stop working in 4.x, got {type(request_url)}", + DeprecationWarning, + stacklevel=2, + ) + request_url = URL(request_url) + hostname = request_url.raw_host or "" + + is_not_secure = request_url.scheme not in ("https", "wss") + if is_not_secure and self._treat_as_secure_origin: + request_origin = URL() + with contextlib.suppress(ValueError): + request_origin = request_url.origin() + is_not_secure = request_origin not in self._treat_as_secure_origin + + # Send shared cookie + key = ("", "") + for c in self._cookies[key].values(): + # Check cache first + if c.key in self._morsel_cache[key]: + filtered[c.key] = self._morsel_cache[key][c.key] + continue + + # Build and cache the morsel + mrsl_val = self._build_morsel(c) + self._morsel_cache[key][c.key] = mrsl_val + filtered[c.key] = mrsl_val + + if is_ip_address(hostname): + if not self._unsafe: + return filtered + domains: Iterable[str] = (hostname,) + else: + # Get all the subdomains that might match a cookie (e.g. "foo.bar.com", "bar.com", "com") + domains = itertools.accumulate( + reversed(hostname.split(".")), _FORMAT_DOMAIN_REVERSED + ) + + # Get all the path prefixes that might match a cookie (e.g. "", "/foo", "/foo/bar") + paths = itertools.accumulate(request_url.path.split("/"), _FORMAT_PATH) + # Create every combination of (domain, path) pairs. + pairs = itertools.product(domains, paths) + + path_len = len(request_url.path) + # Point 2: https://www.rfc-editor.org/rfc/rfc6265.html#section-5.4 + for p in pairs: + if p not in self._cookies: + continue + for name, cookie in self._cookies[p].items(): + domain = cookie["domain"] + + if (domain, name) in self._host_only_cookies and domain != hostname: + continue + + # Skip edge case when the cookie has a trailing slash but request doesn't. + if len(cookie["path"]) > path_len: + continue + + if is_not_secure and cookie["secure"]: + continue + + # We already built the Morsel so reuse it here + if name in self._morsel_cache[p]: + filtered[name] = self._morsel_cache[p][name] + continue + + # Build and cache the morsel + mrsl_val = self._build_morsel(cookie) + self._morsel_cache[p][name] = mrsl_val + filtered[name] = mrsl_val + + return filtered + + def _build_morsel(self, cookie: Morsel[str]) -> Morsel[str]: + """Build a morsel for sending, respecting quote_cookie setting.""" + if self._quote_cookie and cookie.coded_value and cookie.coded_value[0] == '"': + return preserve_morsel_with_coded_value(cookie) + morsel: Morsel[str] = Morsel() + if self._quote_cookie: + value, coded_value = _SIMPLE_COOKIE.value_encode(cookie.value) + else: + coded_value = value = cookie.value + # We use __setstate__ instead of the public set() API because it allows us to + # bypass validation and set already validated state. This is more stable than + # setting protected attributes directly and unlikely to change since it would + # break pickling. + morsel.__setstate__({"key": cookie.key, "value": value, "coded_value": coded_value}) # type: ignore[attr-defined] + return morsel + + @staticmethod + def _is_domain_match(domain: str, hostname: str) -> bool: + """Implements domain matching adhering to RFC 6265.""" + if hostname == domain: + return True + + if not hostname.endswith(domain): + return False + + non_matching = hostname[: -len(domain)] + + if not non_matching.endswith("."): + return False + + return not is_ip_address(hostname) + + @classmethod + def _parse_date(cls, date_str: str) -> int | None: + """Implements date string parsing adhering to RFC 6265.""" + if not date_str: + return None + + found_time = False + found_day = False + found_month = False + found_year = False + + hour = minute = second = 0 + day = 0 + month = 0 + year = 0 + + for token_match in cls.DATE_TOKENS_RE.finditer(date_str): + + token = token_match.group("token") + + if not found_time: + time_match = cls.DATE_HMS_TIME_RE.match(token) + if time_match: + found_time = True + hour, minute, second = (int(s) for s in time_match.groups()) + continue + + if not found_day: + day_match = cls.DATE_DAY_OF_MONTH_RE.match(token) + if day_match: + found_day = True + day = int(day_match.group()) + continue + + if not found_month: + month_match = cls.DATE_MONTH_RE.match(token) + if month_match: + found_month = True + assert month_match.lastindex is not None + month = month_match.lastindex + continue + + if not found_year: + year_match = cls.DATE_YEAR_RE.match(token) + if year_match: + found_year = True + year = int(year_match.group()) + + if 70 <= year <= 99: + year += 1900 + elif 0 <= year <= 69: + year += 2000 + + if False in (found_day, found_month, found_year, found_time): + return None + + if not 1 <= day <= 31: + return None + + if year < 1601 or hour > 23 or minute > 59 or second > 59: + return None + + return calendar.timegm((year, month, day, hour, minute, second, -1, -1, -1)) + + +class DummyCookieJar(AbstractCookieJar): + """Implements a dummy cookie storage. + + It can be used with the ClientSession when no cookie processing is needed. + + """ + + def __init__(self, *, loop: asyncio.AbstractEventLoop | None = None) -> None: + super().__init__(loop=loop) + + def __iter__(self) -> "Iterator[Morsel[str]]": + while False: + yield None + + def __len__(self) -> int: + return 0 + + @property + def unsafe(self) -> bool: + return False + + @property + def quote_cookie(self) -> bool: + return True + + @property + def cookies(self) -> MappingProxyType[tuple[str, str], SimpleCookie]: + """Return an empty mapping.""" + return MappingProxyType({}) + + @property + def host_only_cookies(self) -> frozenset[tuple[str, str]]: + """Return an empty frozenset.""" + return frozenset() + + def clear(self, predicate: ClearCookiePredicate | None = None) -> None: + pass + + def clear_domain(self, domain: str) -> None: + pass + + def update_cookies(self, cookies: LooseCookies, response_url: URL = URL()) -> None: + pass + + def filter_cookies(self, request_url: URL) -> "BaseCookie[str]": + return SimpleCookie() diff --git a/venv/lib/python3.11/site-packages/aiohttp/formdata.py b/venv/lib/python3.11/site-packages/aiohttp/formdata.py new file mode 100644 index 0000000..f602cdf --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/formdata.py @@ -0,0 +1,184 @@ +import io +import warnings +from collections.abc import Iterable +from typing import Any +from urllib.parse import urlencode + +from multidict import MultiDict, MultiDictProxy + +from . import hdrs, multipart, payload +from .helpers import guess_filename +from .http_writer import _safe_header +from .payload import Payload + +__all__ = ("FormData",) + + +class FormData: + """Helper class for form body generation. + + Supports multipart/form-data and application/x-www-form-urlencoded. + """ + + def __init__( + self, + fields: Iterable[Any] = (), + quote_fields: bool = True, + charset: str | None = None, + *, + default_to_multipart: bool = False, + ) -> None: + self._writer = multipart.MultipartWriter("form-data") + self._fields: list[Any] = [] + self._is_multipart = default_to_multipart + self._quote_fields = quote_fields + self._charset = charset + + if isinstance(fields, dict): + fields = list(fields.items()) + elif not isinstance(fields, (list, tuple)): + fields = (fields,) + self.add_fields(*fields) + + @property + def is_multipart(self) -> bool: + return self._is_multipart + + def add_field( + self, + name: str, + value: Any, + *, + content_type: str | None = None, + filename: str | None = None, + content_transfer_encoding: str | None = None, + ) -> None: + + if isinstance(value, io.IOBase): + self._is_multipart = True + elif isinstance(value, (bytes, bytearray, memoryview)): + msg = ( + "In v4, passing bytes will no longer create a file field. " + "Please explicitly use the filename parameter or pass a BytesIO object." + ) + if filename is None and content_transfer_encoding is None: + warnings.warn(msg, DeprecationWarning) + filename = name + + _safe_header(name) + type_options: MultiDict[str] = MultiDict({"name": name}) + if filename is not None and not isinstance(filename, str): + raise TypeError("filename must be an instance of str. Got: %s" % filename) + if filename is None and isinstance(value, io.IOBase): + filename = guess_filename(value, name) + if filename is not None: + _safe_header(filename) + type_options["filename"] = filename + self._is_multipart = True + + headers = {} + if content_type is not None: + if not isinstance(content_type, str): + raise TypeError( + "content_type must be an instance of str. Got: %s" % content_type + ) + _safe_header(content_type) + headers[hdrs.CONTENT_TYPE] = content_type + self._is_multipart = True + if content_transfer_encoding is not None: + if not isinstance(content_transfer_encoding, str): + raise TypeError( + "content_transfer_encoding must be an instance" + " of str. Got: %s" % content_transfer_encoding + ) + msg = ( + "content_transfer_encoding is deprecated. " + "To maintain compatibility with v4 please pass a BytesPayload." + ) + warnings.warn(msg, DeprecationWarning) + self._is_multipart = True + + self._fields.append((type_options, headers, value)) + + def add_fields(self, *fields: Any) -> None: + to_add = list(fields) + + while to_add: + rec = to_add.pop(0) + + if isinstance(rec, io.IOBase): + k = guess_filename(rec, "unknown") + self.add_field(k, rec) # type: ignore[arg-type] + + elif isinstance(rec, (MultiDictProxy, MultiDict)): + to_add.extend(rec.items()) + + elif isinstance(rec, (list, tuple)) and len(rec) == 2: + k, fp = rec + self.add_field(k, fp) + + else: + raise TypeError( + "Only io.IOBase, multidict and (name, file) " + "pairs allowed, use .add_field() for passing " + f"more complex parameters, got {rec!r}" + ) + + def _gen_form_urlencoded(self) -> payload.BytesPayload: + # form data (x-www-form-urlencoded) + data = [] + for type_options, _, value in self._fields: + data.append((type_options["name"], value)) + + charset = self._charset if self._charset is not None else "utf-8" + + if charset == "utf-8": + content_type = "application/x-www-form-urlencoded" + else: + content_type = "application/x-www-form-urlencoded; charset=%s" % charset + + return payload.BytesPayload( + urlencode(data, doseq=True, encoding=charset).encode(), + content_type=content_type, + ) + + def _gen_form_data(self) -> multipart.MultipartWriter: + """Encode a list of fields using the multipart/form-data MIME format""" + for dispparams, headers, value in self._fields: + try: + if hdrs.CONTENT_TYPE in headers: + part = payload.get_payload( + value, + content_type=headers[hdrs.CONTENT_TYPE], + headers=headers, + encoding=self._charset, + ) + else: + part = payload.get_payload( + value, headers=headers, encoding=self._charset + ) + except Exception as exc: + raise TypeError( + "Can not serialize value type: %r\n " + "headers: %r\n value: %r" % (type(value), headers, value) + ) from exc + + if dispparams: + part.set_content_disposition( + "form-data", quote_fields=self._quote_fields, **dispparams + ) + # FIXME cgi.FieldStorage doesn't likes body parts with + # Content-Length which were sent via chunked transfer encoding + assert part.headers is not None + part.headers.popall(hdrs.CONTENT_LENGTH, None) + + self._writer.append_payload(part) + + self._fields.clear() + return self._writer + + def __call__(self) -> Payload: + if self._is_multipart: + return self._gen_form_data() + else: + return self._gen_form_urlencoded() diff --git a/venv/lib/python3.11/site-packages/aiohttp/hdrs.py b/venv/lib/python3.11/site-packages/aiohttp/hdrs.py new file mode 100644 index 0000000..b64b62e --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/hdrs.py @@ -0,0 +1,121 @@ +"""HTTP Headers constants.""" + +# After changing the file content call ./tools/gen.py +# to regenerate the headers parser +import itertools +from typing import Final + +from multidict import istr + +METH_ANY: Final[str] = "*" +METH_CONNECT: Final[str] = "CONNECT" +METH_HEAD: Final[str] = "HEAD" +METH_GET: Final[str] = "GET" +METH_DELETE: Final[str] = "DELETE" +METH_OPTIONS: Final[str] = "OPTIONS" +METH_PATCH: Final[str] = "PATCH" +METH_POST: Final[str] = "POST" +METH_PUT: Final[str] = "PUT" +METH_TRACE: Final[str] = "TRACE" + +METH_ALL: Final[set[str]] = { + METH_CONNECT, + METH_HEAD, + METH_GET, + METH_DELETE, + METH_OPTIONS, + METH_PATCH, + METH_POST, + METH_PUT, + METH_TRACE, +} + +ACCEPT: Final[istr] = istr("Accept") +ACCEPT_CHARSET: Final[istr] = istr("Accept-Charset") +ACCEPT_ENCODING: Final[istr] = istr("Accept-Encoding") +ACCEPT_LANGUAGE: Final[istr] = istr("Accept-Language") +ACCEPT_RANGES: Final[istr] = istr("Accept-Ranges") +ACCESS_CONTROL_MAX_AGE: Final[istr] = istr("Access-Control-Max-Age") +ACCESS_CONTROL_ALLOW_CREDENTIALS: Final[istr] = istr("Access-Control-Allow-Credentials") +ACCESS_CONTROL_ALLOW_HEADERS: Final[istr] = istr("Access-Control-Allow-Headers") +ACCESS_CONTROL_ALLOW_METHODS: Final[istr] = istr("Access-Control-Allow-Methods") +ACCESS_CONTROL_ALLOW_ORIGIN: Final[istr] = istr("Access-Control-Allow-Origin") +ACCESS_CONTROL_EXPOSE_HEADERS: Final[istr] = istr("Access-Control-Expose-Headers") +ACCESS_CONTROL_REQUEST_HEADERS: Final[istr] = istr("Access-Control-Request-Headers") +ACCESS_CONTROL_REQUEST_METHOD: Final[istr] = istr("Access-Control-Request-Method") +AGE: Final[istr] = istr("Age") +ALLOW: Final[istr] = istr("Allow") +AUTHORIZATION: Final[istr] = istr("Authorization") +CACHE_CONTROL: Final[istr] = istr("Cache-Control") +CONNECTION: Final[istr] = istr("Connection") +CONTENT_DISPOSITION: Final[istr] = istr("Content-Disposition") +CONTENT_ENCODING: Final[istr] = istr("Content-Encoding") +CONTENT_LANGUAGE: Final[istr] = istr("Content-Language") +CONTENT_LENGTH: Final[istr] = istr("Content-Length") +CONTENT_LOCATION: Final[istr] = istr("Content-Location") +CONTENT_MD5: Final[istr] = istr("Content-MD5") +CONTENT_RANGE: Final[istr] = istr("Content-Range") +CONTENT_TRANSFER_ENCODING: Final[istr] = istr("Content-Transfer-Encoding") +CONTENT_TYPE: Final[istr] = istr("Content-Type") +COOKIE: Final[istr] = istr("Cookie") +DATE: Final[istr] = istr("Date") +DESTINATION: Final[istr] = istr("Destination") +DIGEST: Final[istr] = istr("Digest") +ETAG: Final[istr] = istr("Etag") +EXPECT: Final[istr] = istr("Expect") +EXPIRES: Final[istr] = istr("Expires") +FORWARDED: Final[istr] = istr("Forwarded") +FROM: Final[istr] = istr("From") +HOST: Final[istr] = istr("Host") +IF_MATCH: Final[istr] = istr("If-Match") +IF_MODIFIED_SINCE: Final[istr] = istr("If-Modified-Since") +IF_NONE_MATCH: Final[istr] = istr("If-None-Match") +IF_RANGE: Final[istr] = istr("If-Range") +IF_UNMODIFIED_SINCE: Final[istr] = istr("If-Unmodified-Since") +KEEP_ALIVE: Final[istr] = istr("Keep-Alive") +LAST_EVENT_ID: Final[istr] = istr("Last-Event-ID") +LAST_MODIFIED: Final[istr] = istr("Last-Modified") +LINK: Final[istr] = istr("Link") +LOCATION: Final[istr] = istr("Location") +MAX_FORWARDS: Final[istr] = istr("Max-Forwards") +ORIGIN: Final[istr] = istr("Origin") +PRAGMA: Final[istr] = istr("Pragma") +PROXY_AUTHENTICATE: Final[istr] = istr("Proxy-Authenticate") +PROXY_AUTHORIZATION: Final[istr] = istr("Proxy-Authorization") +RANGE: Final[istr] = istr("Range") +REFERER: Final[istr] = istr("Referer") +RETRY_AFTER: Final[istr] = istr("Retry-After") +SEC_WEBSOCKET_ACCEPT: Final[istr] = istr("Sec-WebSocket-Accept") +SEC_WEBSOCKET_VERSION: Final[istr] = istr("Sec-WebSocket-Version") +SEC_WEBSOCKET_PROTOCOL: Final[istr] = istr("Sec-WebSocket-Protocol") +SEC_WEBSOCKET_EXTENSIONS: Final[istr] = istr("Sec-WebSocket-Extensions") +SEC_WEBSOCKET_KEY: Final[istr] = istr("Sec-WebSocket-Key") +SEC_WEBSOCKET_KEY1: Final[istr] = istr("Sec-WebSocket-Key1") +SERVER: Final[istr] = istr("Server") +SET_COOKIE: Final[istr] = istr("Set-Cookie") +TE: Final[istr] = istr("TE") +TRAILER: Final[istr] = istr("Trailer") +TRANSFER_ENCODING: Final[istr] = istr("Transfer-Encoding") +UPGRADE: Final[istr] = istr("Upgrade") +URI: Final[istr] = istr("URI") +USER_AGENT: Final[istr] = istr("User-Agent") +VARY: Final[istr] = istr("Vary") +VIA: Final[istr] = istr("Via") +WANT_DIGEST: Final[istr] = istr("Want-Digest") +WARNING: Final[istr] = istr("Warning") +WWW_AUTHENTICATE: Final[istr] = istr("WWW-Authenticate") +X_FORWARDED_FOR: Final[istr] = istr("X-Forwarded-For") +X_FORWARDED_HOST: Final[istr] = istr("X-Forwarded-Host") +X_FORWARDED_PROTO: Final[istr] = istr("X-Forwarded-Proto") + +# These are the upper/lower case variants of the headers/methods +# Example: {'hOst', 'host', 'HoST', 'HOSt', 'hOsT', 'HosT', 'hoSt', ...} +METH_HEAD_ALL: Final = frozenset( + map("".join, itertools.product(*zip(METH_HEAD.upper(), METH_HEAD.lower()))) +) +METH_CONNECT_ALL: Final = frozenset( + map("".join, itertools.product(*zip(METH_CONNECT.upper(), METH_CONNECT.lower()))) +) +HOST_ALL: Final = frozenset( + map("".join, itertools.product(*zip(HOST.upper(), HOST.lower()))) +) diff --git a/venv/lib/python3.11/site-packages/aiohttp/helpers.py b/venv/lib/python3.11/site-packages/aiohttp/helpers.py new file mode 100644 index 0000000..469c99d --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/helpers.py @@ -0,0 +1,1046 @@ +"""Various helper functions""" + +import asyncio +import base64 +import binascii +import contextlib +import datetime +import enum +import functools +import inspect +import netrc +import os +import platform +import re +import sys +import time +import warnings +import weakref +from collections import namedtuple +from collections.abc import Callable, Generator, Iterable, Iterator, Mapping +from contextlib import suppress +from email.message import EmailMessage +from email.parser import HeaderParser +from email.policy import HTTP +from email.utils import parsedate +from math import ceil +from pathlib import Path +from types import MappingProxyType, TracebackType +from typing import ( + Any, + ContextManager, + Generic, + Optional, + Protocol, + TypeVar, + get_args, + overload, +) +from urllib.parse import quote +from urllib.request import getproxies, proxy_bypass + +import attr +from multidict import MultiDict, MultiDictProxy, MultiMapping +from propcache.api import under_cached_property as reify +from yarl import URL + +from . import hdrs +from .log import client_logger + +if sys.version_info >= (3, 11): + import asyncio as async_timeout +else: + import async_timeout + +__all__ = ("BasicAuth", "ChainMapProxy", "ETag", "reify") + +IS_MACOS = platform.system() == "Darwin" +IS_WINDOWS = platform.system() == "Windows" + +PY_311 = sys.version_info >= (3, 11) + +# This is the default size/limit for several operations. +# Matches the max size we receive from sockets: +# https://github.com/python/cpython/blob/1857a40807daeae3a1bf5efb682de9c9ae6df845/Lib/asyncio/selector_events.py#L766 +DEFAULT_CHUNK_SIZE = 2**18 # 256 KiB + +_T = TypeVar("_T") +_S = TypeVar("_S") + +_SENTINEL = enum.Enum("_SENTINEL", "sentinel") +sentinel = _SENTINEL.sentinel + +NO_EXTENSIONS = bool(os.environ.get("AIOHTTP_NO_EXTENSIONS")) + +# https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1 +EMPTY_BODY_STATUS_CODES = frozenset((204, 304, *range(100, 200))) +# https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.1 +# https://datatracker.ietf.org/doc/html/rfc9112#section-6.3-2.2 +EMPTY_BODY_METHODS = hdrs.METH_HEAD_ALL + +DEBUG = sys.flags.dev_mode or ( + not sys.flags.ignore_environment and bool(os.environ.get("PYTHONASYNCIODEBUG")) +) + + +CHAR = {chr(i) for i in range(0, 128)} +CTL = {chr(i) for i in range(0, 32)} | { + chr(127), +} +SEPARATORS = { + "(", + ")", + "<", + ">", + "@", + ",", + ";", + ":", + "\\", + '"', + "/", + "[", + "]", + "?", + "=", + "{", + "}", + " ", + chr(9), +} +TOKEN = CHAR ^ CTL ^ SEPARATORS + + +class noop: + def __await__(self) -> Generator[None, None, None]: + yield + + +def encode_basic_auth(login: str, password: str = "", encoding: str = "utf-8") -> str: + """Encode HTTP Basic Authentication credentials as an Authorization header value. + + Returns a string of the form ``"Basic "`` suitable for use as the + value of the ``Authorization`` (or ``Proxy-Authorization``) header. + """ + if ":" in login: + raise ValueError('A ":" is not allowed in login (RFC 7617#section-2)') + creds = f"{login}:{password}".encode(encoding) + return "Basic " + base64.b64encode(creds).decode(encoding) + + +class BasicAuth(namedtuple("BasicAuth", ["login", "password", "encoding"])): + """Http basic authentication helper.""" + + def __new__( + cls, login: str, password: str = "", encoding: str = "latin1" + ) -> "BasicAuth": + if login is None: + raise ValueError("None is not allowed as login value") + + if password is None: + raise ValueError("None is not allowed as password value") + + if ":" in login: + raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)') + + warnings.warn( + "BasicAuth is deprecated and will be removed in aiohttp 4.0; " + "use aiohttp.encode_basic_auth() with " + "headers={'Authorization': ...} instead", + DeprecationWarning, + stacklevel=2, + ) + return super().__new__(cls, login, password, encoding) + + @classmethod + def decode(cls, auth_header: str, encoding: str = "latin1") -> "BasicAuth": + """Create a BasicAuth object from an Authorization HTTP header.""" + try: + auth_type, encoded_credentials = auth_header.split(" ", 1) + except ValueError: + raise ValueError("Could not parse authorization header.") + + if auth_type.lower() != "basic": + raise ValueError("Unknown authorization method %s" % auth_type) + + try: + decoded = base64.b64decode( + encoded_credentials.encode("ascii"), validate=True + ).decode(encoding) + except binascii.Error: + raise ValueError("Invalid base64 encoding.") + + try: + # RFC 2617 HTTP Authentication + # https://www.ietf.org/rfc/rfc2617.txt + # the colon must be present, but the username and password may be + # otherwise blank. + username, password = decoded.split(":", 1) + except ValueError: + raise ValueError("Invalid credentials.") + + return _basic_auth_no_warn(username, password, encoding) + + @classmethod + def from_url(cls, url: URL, *, encoding: str = "latin1") -> Optional["BasicAuth"]: + """Create BasicAuth from url.""" + if not isinstance(url, URL): + raise TypeError("url should be yarl.URL instance") + # Check raw_user and raw_password first as yarl is likely + # to already have these values parsed from the netloc in the cache. + if url.raw_user is None and url.raw_password is None: + return None + return _basic_auth_no_warn(url.user or "", url.password or "", encoding) + + def encode(self) -> str: + """Encode credentials.""" + return encode_basic_auth(self.login, self.password, self.encoding) + + +def _basic_auth_no_warn( + login: str, password: str = "", encoding: str = "latin1" +) -> BasicAuth: + """Construct a BasicAuth without emitting the deprecation warning. + + For internal use only. Bypasses BasicAuth.__new__ so that aiohttp's own + machinery doesn't trigger deprecation warnings in user code. + """ + return tuple.__new__(BasicAuth, (login, password, encoding)) + + +def strip_auth_from_url(url: URL) -> tuple[URL, BasicAuth | None]: + """Remove user and password from URL if present and return BasicAuth object.""" + # Check raw_user and raw_password first as yarl is likely + # to already have these values parsed from the netloc in the cache. + if url.raw_user is None and url.raw_password is None: + return url, None + return url.with_user(None), _basic_auth_no_warn(url.user or "", url.password or "") + + +def netrc_from_env() -> netrc.netrc | None: + """Load netrc from file. + + Attempt to load it from the path specified by the env-var + NETRC or in the default location in the user's home directory. + + Returns None if it couldn't be found or fails to parse. + """ + netrc_env = os.environ.get("NETRC") + + if netrc_env is not None: + netrc_path = Path(netrc_env) + else: + try: + home_dir = Path.home() + except RuntimeError as e: # pragma: no cover + # if pathlib can't resolve home, it may raise a RuntimeError + client_logger.debug( + "Could not resolve home directory when " + "trying to look for .netrc file: %s", + e, + ) + return None + + netrc_path = home_dir / ("_netrc" if IS_WINDOWS else ".netrc") + + try: + return netrc.netrc(str(netrc_path)) + except netrc.NetrcParseError as e: + client_logger.warning("Could not parse .netrc file: %s", e) + except OSError as e: + netrc_exists = False + with contextlib.suppress(OSError): + netrc_exists = netrc_path.is_file() + # we couldn't read the file (doesn't exist, permissions, etc.) + if netrc_env or netrc_exists: + # only warn if the environment wanted us to load it, + # or it appears like the default file does actually exist + client_logger.warning("Could not read .netrc file: %s", e) + + return None + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class ProxyInfo: + proxy: URL + proxy_auth: BasicAuth | None + + +def basicauth_from_netrc(netrc_obj: netrc.netrc | None, host: str) -> BasicAuth: + """ + Return :py:class:`~aiohttp.BasicAuth` credentials for ``host`` from ``netrc_obj``. + + :raises LookupError: if ``netrc_obj`` is :py:data:`None` or if no + entry is found for the ``host``. + """ + if netrc_obj is None: + raise LookupError("No .netrc file found") + auth_from_netrc = netrc_obj.authenticators(host) + + if auth_from_netrc is None: + raise LookupError(f"No entry for {host!s} found in the `.netrc` file.") + login, account, password = auth_from_netrc + + # TODO(PY311): username = login or account + # Up to python 3.10, account could be None if not specified, + # and login will be empty string if not specified. From 3.11, + # login and account will be empty string if not specified. + username = login if (login or account is None) else account + + # TODO(PY311): Remove this, as password will be empty string + # if not specified + if password is None: + password = "" + + return _basic_auth_no_warn(username, password) + + +def proxies_from_env() -> dict[str, ProxyInfo]: + proxy_urls = { + k: URL(v) + for k, v in getproxies().items() + if k in ("http", "https", "ws", "wss") + } + netrc_obj = netrc_from_env() + stripped = {k: strip_auth_from_url(v) for k, v in proxy_urls.items()} + ret = {} + for proto, val in stripped.items(): + proxy, auth = val + if proxy.scheme in ("https", "wss"): + client_logger.warning( + "%s proxies %s are not supported, ignoring", proxy.scheme.upper(), proxy + ) + continue + if netrc_obj and auth is None: + if proxy.host is not None: + try: + auth = basicauth_from_netrc(netrc_obj, proxy.host) + except LookupError: + auth = None + ret[proto] = ProxyInfo(proxy, auth) + return ret + + +def get_env_proxy_for_url(url: URL) -> tuple[URL, BasicAuth | None]: + """Get a permitted proxy for the given URL from the env.""" + if url.host is not None and proxy_bypass(url.host): + raise LookupError(f"Proxying is disallowed for `{url.host!r}`") + + proxies_in_env = proxies_from_env() + try: + proxy_info = proxies_in_env[url.scheme] + except KeyError: + raise LookupError(f"No proxies found for `{url!s}` in the env") + else: + return proxy_info.proxy, proxy_info.proxy_auth + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class MimeType: + type: str + subtype: str + suffix: str + parameters: "MultiDictProxy[str]" + + +@functools.lru_cache(maxsize=56) +def parse_mimetype(mimetype: str) -> MimeType: + """Parses a MIME type into its components. + + mimetype is a MIME type string. + + Returns a MimeType object. + + Example: + + >>> parse_mimetype('text/html; charset=utf-8') + MimeType(type='text', subtype='html', suffix='', + parameters={'charset': 'utf-8'}) + + """ + if not mimetype: + return MimeType( + type="", subtype="", suffix="", parameters=MultiDictProxy(MultiDict()) + ) + + parts = mimetype.split(";") + params: MultiDict[str] = MultiDict() + for item in parts[1:]: + if not item: + continue + key, _, value = item.partition("=") + params.add(key.lower().strip(), value.strip(' "')) + + fulltype = parts[0].strip().lower() + if fulltype == "*": + fulltype = "*/*" + + mtype, _, stype = fulltype.partition("/") + stype, _, suffix = stype.partition("+") + + return MimeType( + type=mtype, subtype=stype, suffix=suffix, parameters=MultiDictProxy(params) + ) + + +class EnsureOctetStream(EmailMessage): + def __init__(self) -> None: + super().__init__() + # https://www.rfc-editor.org/rfc/rfc9110#section-8.3-5 + self.set_default_type("application/octet-stream") + + def get_content_type(self) -> str: + """Re-implementation from Message + + Returns application/octet-stream in place of plain/text when + value is wrong. + + The way this class is used guarantees that content-type will + be present so simplify the checks wrt to the base implementation. + """ + value = self.get("content-type", "").lower() + + # Based on the implementation of _splitparam in the standard library + ctype, _, _ = value.partition(";") + ctype = ctype.strip() + if ctype.count("/") != 1: + return self.get_default_type() + return ctype + + +@functools.lru_cache(maxsize=56) +def parse_content_type(raw: str) -> tuple[str, MappingProxyType[str, str]]: + """Parse Content-Type header. + + Returns a tuple of the parsed content type and a + MappingProxyType of parameters. The default returned value + is `application/octet-stream` + """ + msg = HeaderParser(EnsureOctetStream, policy=HTTP).parsestr(f"Content-Type: {raw}") + content_type = msg.get_content_type() + params = msg.get_params(()) + content_dict = dict(params[1:]) # First element is content type again + return content_type, MappingProxyType(content_dict) + + +def guess_filename(obj: Any, default: str | None = None) -> str | None: + name = getattr(obj, "name", None) + if name and isinstance(name, str) and name[0] != "<" and name[-1] != ">": + return Path(name).name + return default + + +not_qtext_re = re.compile(r"[^\041\043-\133\135-\176]") +QCONTENT = {chr(i) for i in range(0x20, 0x7F)} | {"\t"} + + +def quoted_string(content: str) -> str: + """Return 7-bit content as quoted-string. + + Format content into a quoted-string as defined in RFC5322 for + Internet Message Format. Notice that this is not the 8-bit HTTP + format, but the 7-bit email format. Content must be in usascii or + a ValueError is raised. + """ + if not (QCONTENT > set(content)): + raise ValueError(f"bad content for quoted-string {content!r}") + return not_qtext_re.sub(lambda x: "\\" + x.group(0), content) + + +def content_disposition_header( + disptype: str, quote_fields: bool = True, _charset: str = "utf-8", **params: str +) -> str: + """Sets ``Content-Disposition`` header for MIME. + + This is the MIME payload Content-Disposition header from RFC 2183 + and RFC 7579 section 4.2, not the HTTP Content-Disposition from + RFC 6266. + + disptype is a disposition type: inline, attachment, form-data. + Should be valid extension token (see RFC 2183) + + quote_fields performs value quoting to 7-bit MIME headers + according to RFC 7578. Set to quote_fields to False if recipient + can take 8-bit file names and field values. + + _charset specifies the charset to use when quote_fields is True. + + params is a dict with disposition params. + """ + if not disptype or not (TOKEN > set(disptype)): + raise ValueError(f"bad content disposition type {disptype!r}") + + value = disptype + if params: + lparams = [] + for key, val in params.items(): + if not key or not (TOKEN > set(key)): + raise ValueError(f"bad content disposition parameter {key!r}={val!r}") + if quote_fields: + if key.lower() == "filename": + qval = quote(val, "", encoding=_charset) + lparams.append((key, '"%s"' % qval)) + else: + try: + qval = quoted_string(val) + except ValueError: + qval = "".join( + (_charset, "''", quote(val, "", encoding=_charset)) + ) + lparams.append((key + "*", qval)) + else: + lparams.append((key, '"%s"' % qval)) + else: + qval = val.replace("\\", "\\\\").replace('"', '\\"') + lparams.append((key, '"%s"' % qval)) + sparams = "; ".join("=".join(pair) for pair in lparams) + value = "; ".join((value, sparams)) + return value + + +def is_ip_address(host: str | None) -> bool: + """Check if host looks like an IP Address. + + This check is only meant as a heuristic to ensure that + a host is not a domain name. + """ + if not host: + return False + # For a host to be an ipv4 address, it must be all numeric. + # The host must contain a colon to be an IPv6 address. + return ":" in host or host.replace(".", "").isdigit() + + +def is_canonical_ipv4_address(host: str) -> bool: + """Check if host is a canonical dotted-quad IPv4 address. + + Rejects the legacy numeric forms that ``socket`` still accepts and + maps onto an address, e.g. ``2130706433``, ``017700000001``, ``127.1``. + """ + parts = host.split(".") + if len(parts) != 4: + return False + for part in parts: + # Each octet must be 1-3 ASCII digits; reject unicode digits + # (which ``str.isdigit`` accepts but ``int`` may not), octal + # leading zeros, and values above 255. + if not (1 <= len(part) <= 3) or not part.isascii() or not part.isdigit(): + return False + if part[0] == "0" and len(part) != 1: + return False + if int(part) > 255: + return False + return True + + +_cached_current_datetime: int | None = None +_cached_formatted_datetime = "" + + +def rfc822_formatted_time() -> str: + global _cached_current_datetime + global _cached_formatted_datetime + + now = int(time.time()) + if now != _cached_current_datetime: + # Weekday and month names for HTTP date/time formatting; + # always English! + # Tuples are constants stored in codeobject! + _weekdayname = ("Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun") + _monthname = ( + "", # Dummy so we can use 1-based month numbers + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ) + + year, month, day, hh, mm, ss, wd, *tail = time.gmtime(now) + _cached_formatted_datetime = "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( + _weekdayname[wd], + day, + _monthname[month], + year, + hh, + mm, + ss, + ) + _cached_current_datetime = now + return _cached_formatted_datetime + + +def _weakref_handle(info: "tuple[weakref.ref[object], str]") -> None: + ref, name = info + ob = ref() + if ob is not None: + with suppress(Exception): + getattr(ob, name)() + + +def weakref_handle( + ob: object, + name: str, + timeout: float, + loop: asyncio.AbstractEventLoop, + timeout_ceil_threshold: float = 5, +) -> asyncio.TimerHandle | None: + if timeout is not None and timeout > 0: + when = loop.time() + timeout + if timeout >= timeout_ceil_threshold: + when = ceil(when) + + return loop.call_at(when, _weakref_handle, (weakref.ref(ob), name)) + return None + + +def call_later( + cb: Callable[[], Any], + timeout: float, + loop: asyncio.AbstractEventLoop, + timeout_ceil_threshold: float = 5, +) -> asyncio.TimerHandle | None: + if timeout is None or timeout <= 0: + return None + now = loop.time() + when = calculate_timeout_when(now, timeout, timeout_ceil_threshold) + return loop.call_at(when, cb) + + +def calculate_timeout_when( + loop_time: float, + timeout: float, + timeout_ceiling_threshold: float, +) -> float: + """Calculate when to execute a timeout.""" + when = loop_time + timeout + if timeout > timeout_ceiling_threshold: + return ceil(when) + return when + + +class TimeoutHandle: + """Timeout handle""" + + __slots__ = ("_timeout", "_loop", "_ceil_threshold", "_callbacks") + + def __init__( + self, + loop: asyncio.AbstractEventLoop, + timeout: float | None, + ceil_threshold: float = 5, + ) -> None: + self._timeout = timeout + self._loop = loop + self._ceil_threshold = ceil_threshold + self._callbacks: list[ + tuple[Callable[..., None], tuple[Any, ...], dict[str, Any]] + ] = [] + + def register( + self, callback: Callable[..., None], *args: Any, **kwargs: Any + ) -> None: + self._callbacks.append((callback, args, kwargs)) + + def close(self) -> None: + self._callbacks.clear() + + def start(self) -> asyncio.TimerHandle | None: + timeout = self._timeout + if timeout is not None and timeout > 0: + when = self._loop.time() + timeout + if timeout >= self._ceil_threshold: + when = ceil(when) + return self._loop.call_at(when, self.__call__) + else: + return None + + def timer(self) -> "BaseTimerContext": + if self._timeout is not None and self._timeout > 0: + timer = TimerContext(self._loop) + self.register(timer.timeout) + return timer + else: + return TimerNoop() + + def __call__(self) -> None: + for cb, args, kwargs in self._callbacks: + with suppress(Exception): + cb(*args, **kwargs) + + self._callbacks.clear() + + +class BaseTimerContext(ContextManager["BaseTimerContext"]): + + __slots__ = () + + def assert_timeout(self) -> None: + """Raise TimeoutError if timeout has been exceeded.""" + + +class TimerNoop(BaseTimerContext): + + __slots__ = () + + def __enter__(self) -> BaseTimerContext: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + return + + +class TimerContext(BaseTimerContext): + """Low resolution timeout context manager""" + + __slots__ = ("_loop", "_tasks", "_cancelled", "_cancelling") + + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + self._loop = loop + self._tasks: list[asyncio.Task[Any]] = [] + self._cancelled = False + self._cancelling = 0 + + def assert_timeout(self) -> None: + """Raise TimeoutError if timer has already been cancelled.""" + if self._cancelled: + raise asyncio.TimeoutError from None + + def __enter__(self) -> BaseTimerContext: + task = asyncio.current_task(loop=self._loop) + if task is None: + raise RuntimeError("Timeout context manager should be used inside a task") + + if sys.version_info >= (3, 11): + # Remember if the task was already cancelling + # so when we __exit__ we can decide if we should + # raise asyncio.TimeoutError or let the cancellation propagate + self._cancelling = task.cancelling() + + if self._cancelled: + raise asyncio.TimeoutError from None + + self._tasks.append(task) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + enter_task: asyncio.Task[Any] | None = None + if self._tasks: + enter_task = self._tasks.pop() + + if exc_type is asyncio.CancelledError and self._cancelled: + assert enter_task is not None + # The timeout was hit, and the task was cancelled + # so we need to uncancel the last task that entered the context manager + # since the cancellation should not leak out of the context manager + if sys.version_info >= (3, 11): + # If the task was already cancelling don't raise + # asyncio.TimeoutError and instead return None + # to allow the cancellation to propagate + if enter_task.uncancel() > self._cancelling: + return None + raise asyncio.TimeoutError from exc_val + return None + + def timeout(self) -> None: + if not self._cancelled: + for task in set(self._tasks): + task.cancel() + + self._cancelled = True + + +def ceil_timeout( + delay: float | None, ceil_threshold: float = 5 +) -> async_timeout.Timeout: + if delay is None or delay <= 0: + return async_timeout.timeout(None) + + loop = asyncio.get_running_loop() + now = loop.time() + when = now + delay + if delay > ceil_threshold: + when = ceil(when) + return async_timeout.timeout_at(when) + + +class HeadersMixin: + """Mixin for handling headers.""" + + ATTRS = frozenset(["_content_type", "_content_dict", "_stored_content_type"]) + + _headers: MultiMapping[str] + _content_type: str | None = None + _content_dict: dict[str, str] | None = None + _stored_content_type: str | None | _SENTINEL = sentinel + + def _parse_content_type(self, raw: str | None) -> None: + self._stored_content_type = raw + if raw is None: + # default value according to RFC 2616 + self._content_type = "application/octet-stream" + self._content_dict = {} + else: + content_type, content_mapping_proxy = parse_content_type(raw) + self._content_type = content_type + # _content_dict needs to be mutable so we can update it + self._content_dict = content_mapping_proxy.copy() + + @property + def content_type(self) -> str: + """The value of content part for Content-Type HTTP header.""" + raw = self._headers.get(hdrs.CONTENT_TYPE) + if self._stored_content_type != raw: + self._parse_content_type(raw) + assert self._content_type is not None + return self._content_type + + @property + def charset(self) -> str | None: + """The value of charset part for Content-Type HTTP header.""" + raw = self._headers.get(hdrs.CONTENT_TYPE) + if self._stored_content_type != raw: + self._parse_content_type(raw) + assert self._content_dict is not None + return self._content_dict.get("charset") + + @property + def content_length(self) -> int | None: + """The value of Content-Length HTTP header.""" + content_length = self._headers.get(hdrs.CONTENT_LENGTH) + return None if content_length is None else int(content_length) + + +def set_result(fut: "asyncio.Future[_T]", result: _T) -> None: + if not fut.done(): + fut.set_result(result) + + +_EXC_SENTINEL = BaseException() + + +class ErrorableProtocol(Protocol): + def set_exception( + self, + exc: BaseException, + exc_cause: BaseException = ..., + ) -> None: ... # pragma: no cover + + +def set_exception( + fut: "asyncio.Future[_T] | ErrorableProtocol", + exc: BaseException, + exc_cause: BaseException = _EXC_SENTINEL, +) -> None: + """Set future exception. + + If the future is marked as complete, this function is a no-op. + + :param exc_cause: An exception that is a direct cause of ``exc``. + Only set if provided. + """ + if asyncio.isfuture(fut) and fut.done(): + return + + exc_is_sentinel = exc_cause is _EXC_SENTINEL + exc_causes_itself = exc is exc_cause + if not exc_is_sentinel and not exc_causes_itself: + exc.__cause__ = exc_cause + + fut.set_exception(exc) + + +@functools.total_ordering +class BaseKey(Generic[_T]): + """Base for concrete context storage key classes. + + Each storage is provided with its own sub-class for the sake of some additional type safety. + """ + + __slots__ = ("_name", "_t", "__orig_class__") + + # This may be set by Python when instantiating with a generic type. We need to + # support this, in order to support types that are not concrete classes, + # like Iterable, which can't be passed as the second parameter to __init__. + __orig_class__: type[object] + + def __init__(self, name: str, t: type[_T] | None = None): + # Prefix with module name to help deduplicate key names. + frame = inspect.currentframe() + while frame: + if frame.f_code.co_name == "": + module: str = frame.f_globals["__name__"] + break + frame = frame.f_back + + self._name = module + "." + name + self._t = t + + def __lt__(self, other: object) -> bool: + if isinstance(other, BaseKey): + return self._name < other._name + return True # Order BaseKey above other types. + + def __repr__(self) -> str: + t = self._t + if t is None: + with suppress(AttributeError): + # Set to type arg. + t = get_args(self.__orig_class__)[0] + + if t is None: + t_repr = "<>" + elif isinstance(t, type): + if t.__module__ == "builtins": + t_repr = t.__qualname__ + else: + t_repr = f"{t.__module__}.{t.__qualname__}" + else: + t_repr = repr(t) + return f"<{self.__class__.__name__}({self._name}, type={t_repr})>" + + +class AppKey(BaseKey[_T]): + """Keys for static typing support in Application.""" + + +class RequestKey(BaseKey[_T]): + """Keys for static typing support in Request.""" + + +class ResponseKey(BaseKey[_T]): + """Keys for static typing support in Response.""" + + +class ChainMapProxy(Mapping[str | AppKey[Any], Any]): + __slots__ = ("_maps",) + + def __init__(self, maps: Iterable[Mapping[str | AppKey[Any], Any]]) -> None: + self._maps = tuple(maps) + + def __init_subclass__(cls) -> None: + raise TypeError( + f"Inheritance class {cls.__name__} from ChainMapProxy is forbidden" + ) + + @overload # type: ignore[override] + def __getitem__(self, key: AppKey[_T]) -> _T: ... + + @overload + def __getitem__(self, key: str) -> Any: ... + + def __getitem__(self, key: str | AppKey[_T]) -> Any: + for mapping in self._maps: + try: + return mapping[key] + except KeyError: + pass + raise KeyError(key) + + @overload # type: ignore[override] + def get(self, key: AppKey[_T], default: _S) -> _T | _S: ... + + @overload + def get(self, key: AppKey[_T], default: None = ...) -> _T | None: ... + + @overload + def get(self, key: str, default: Any = ...) -> Any: ... + + def get(self, key: str | AppKey[_T], default: Any = None) -> Any: + try: + return self[key] + except KeyError: + return default + + def __len__(self) -> int: + # reuses stored hash values if possible + return len(set().union(*self._maps)) + + def __iter__(self) -> Iterator[str | AppKey[Any]]: + d: dict[str | AppKey[Any], Any] = {} + for mapping in reversed(self._maps): + # reuses stored hash values if possible + d.update(mapping) + return iter(d) + + def __contains__(self, key: object) -> bool: + return any(key in m for m in self._maps) + + def __bool__(self) -> bool: + return any(self._maps) + + def __repr__(self) -> str: + content = ", ".join(map(repr, self._maps)) + return f"ChainMapProxy({content})" + + +# https://tools.ietf.org/html/rfc7232#section-2.3 +_ETAGC = r"[!\x23-\x7E\x80-\xff]+" +_ETAGC_RE = re.compile(_ETAGC) +_QUOTED_ETAG = rf'(W/)?"({_ETAGC})"' +QUOTED_ETAG_RE = re.compile(_QUOTED_ETAG) +LIST_QUOTED_ETAG_RE = re.compile(rf"({_QUOTED_ETAG})(?:\s*,\s*|$)|(.)") + +ETAG_ANY = "*" + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class ETag: + value: str + is_weak: bool = False + + +def validate_etag_value(value: str) -> None: + if value != ETAG_ANY and not _ETAGC_RE.fullmatch(value): + raise ValueError( + f"Value {value!r} is not a valid etag. Maybe it contains '\"'?" + ) + + +def parse_http_date(date_str: str | None) -> datetime.datetime | None: + """Process a date string, return a datetime object""" + if date_str is not None: + timetuple = parsedate(date_str) + if timetuple is not None: + with suppress(ValueError): + return datetime.datetime(*timetuple[:6], tzinfo=datetime.timezone.utc) + return None + + +@functools.lru_cache +def must_be_empty_body(method: str, code: int) -> bool: + """Check if a request must return an empty body.""" + return ( + code in EMPTY_BODY_STATUS_CODES + or method in EMPTY_BODY_METHODS + or (200 <= code < 300 and method in hdrs.METH_CONNECT_ALL) + ) + + +def should_remove_content_length(method: str, code: int) -> bool: + """Check if a Content-Length header should be removed. + + This should always be a subset of must_be_empty_body + """ + # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-8 + # https://www.rfc-editor.org/rfc/rfc9110.html#section-15.4.5-4 + return code in EMPTY_BODY_STATUS_CODES or ( + 200 <= code < 300 and method in hdrs.METH_CONNECT_ALL + ) diff --git a/venv/lib/python3.11/site-packages/aiohttp/http.py b/venv/lib/python3.11/site-packages/aiohttp/http.py new file mode 100644 index 0000000..28c8fee --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/http.py @@ -0,0 +1,78 @@ +import sys +from collections.abc import Mapping +from http import HTTPStatus + +from . import __version__ +from .http_exceptions import HttpProcessingError as HttpProcessingError +from .http_parser import ( + HeadersParser as HeadersParser, + HttpParser as HttpParser, + HttpRequestParser as HttpRequestParser, + HttpResponseParser as HttpResponseParser, + RawRequestMessage as RawRequestMessage, + RawResponseMessage as RawResponseMessage, +) +from .http_websocket import ( + WS_CLOSED_MESSAGE as WS_CLOSED_MESSAGE, + WS_CLOSING_MESSAGE as WS_CLOSING_MESSAGE, + WS_KEY as WS_KEY, + WebSocketError as WebSocketError, + WebSocketReader as WebSocketReader, + WebSocketWriter as WebSocketWriter, + WSCloseCode as WSCloseCode, + WSMessage as WSMessage, + WSMessageDecodeText as WSMessageDecodeText, + WSMessageNoDecodeText as WSMessageNoDecodeText, + WSMessageTextBytes as WSMessageTextBytes, + WSMsgType as WSMsgType, + ws_ext_gen as ws_ext_gen, + ws_ext_parse as ws_ext_parse, +) +from .http_writer import ( + HttpVersion as HttpVersion, + HttpVersion10 as HttpVersion10, + HttpVersion11 as HttpVersion11, + StreamWriter as StreamWriter, +) + +__all__ = ( + "HttpProcessingError", + "RESPONSES", + "SERVER_SOFTWARE", + # .http_writer + "StreamWriter", + "HttpVersion", + "HttpVersion10", + "HttpVersion11", + # .http_parser + "HeadersParser", + "HttpParser", + "HttpRequestParser", + "HttpResponseParser", + "RawRequestMessage", + "RawResponseMessage", + # .http_websocket + "WS_CLOSED_MESSAGE", + "WS_CLOSING_MESSAGE", + "WS_KEY", + "WebSocketReader", + "WebSocketWriter", + "ws_ext_gen", + "ws_ext_parse", + "WSMessage", + "WSMessageDecodeText", + "WSMessageNoDecodeText", + "WSMessageTextBytes", + "WebSocketError", + "WSMsgType", + "WSCloseCode", +) + + +SERVER_SOFTWARE: str = ( + f"Python/{sys.version_info[0]}.{sys.version_info[1]} aiohttp/{__version__}" +) + +RESPONSES: Mapping[int, tuple[str, str]] = { + v: (v.phrase, v.description) for v in HTTPStatus.__members__.values() +} diff --git a/venv/lib/python3.11/site-packages/aiohttp/http_exceptions.py b/venv/lib/python3.11/site-packages/aiohttp/http_exceptions.py new file mode 100644 index 0000000..54cc4af --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/http_exceptions.py @@ -0,0 +1,118 @@ +"""Low-level http related exceptions.""" + +from textwrap import indent + +from .typedefs import _CIMultiDict + +__all__ = ("HttpProcessingError",) + + +class HttpProcessingError(Exception): + """HTTP error. + + Shortcut for raising HTTP errors with custom code, message and headers. + + code: HTTP Error code. + message: (optional) Error message. + headers: (optional) Headers to be sent in response, a list of pairs + """ + + code = 0 + message = "" + headers = None + + def __init__( + self, + *, + code: int | None = None, + message: str = "", + headers: _CIMultiDict | None = None, + ) -> None: + if code is not None: + self.code = code + self.headers = headers + self.message = message + + def __str__(self) -> str: + msg = indent(self.message, " ") + return f"{self.code}, message:\n{msg}" + + def __repr__(self) -> str: + return f"<{self.__class__.__name__}: {self.code}, message={self.message!r}>" + + +class BadHttpMessage(HttpProcessingError): + + code = 400 + message = "Bad Request" + + def __init__(self, message: str, *, headers: _CIMultiDict | None = None) -> None: + super().__init__(message=message, headers=headers) + self.args = (message,) + + +class HttpBadRequest(BadHttpMessage): + + code = 400 + message = "Bad Request" + + +class PayloadEncodingError(BadHttpMessage): + """Base class for payload errors""" + + +class ContentEncodingError(PayloadEncodingError): + """Content encoding error.""" + + +class TransferEncodingError(PayloadEncodingError): + """transfer encoding error.""" + + +class ContentLengthError(PayloadEncodingError): + """Not enough data to satisfy content length header.""" + + +class DecompressSizeError(PayloadEncodingError): + """Deprecated. Removed in v4.""" + + +class LineTooLong(BadHttpMessage): + def __init__( + self, + line: str | bytes, + limit: str | int = "Unknown", + actual_size: str = "Unknown", + ) -> None: + super().__init__(f"Got more than {limit} bytes when reading: {line!r}.") + self.args = (line, limit, actual_size) + + +class InvalidHeader(BadHttpMessage): + def __init__(self, hdr: bytes | str) -> None: + hdr_s = hdr.decode(errors="backslashreplace") if isinstance(hdr, bytes) else hdr + super().__init__(f"Invalid HTTP header: {hdr!r}") + self.hdr = hdr_s + self.args = (hdr,) + + +class BadStatusLine(BadHttpMessage): + def __init__(self, line: str = "", error: str | None = None) -> None: + if not isinstance(line, str): + line = repr(line) + super().__init__(error or f"Bad status line {line!r}") + self.args = (line,) + self.line = line + + +class BadHttpMethod(BadStatusLine): + """Invalid HTTP method in status line.""" + + def __init__(self, line: str = "", error: str | None = None) -> None: + if error is None and line.startswith("\x16\x03"): + error = "Received HTTPS traffic on an HTTP port" + super().__init__(line, error or f"Bad HTTP method in status line {line!r}") + + +class InvalidURLError(BadHttpMessage): + pass diff --git a/venv/lib/python3.11/site-packages/aiohttp/http_parser.py b/venv/lib/python3.11/site-packages/aiohttp/http_parser.py new file mode 100644 index 0000000..5b46101 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/http_parser.py @@ -0,0 +1,1217 @@ +import abc +import asyncio +import re +import string +import sys +from contextlib import suppress +from enum import IntEnum +from re import Pattern +from typing import ( + TYPE_CHECKING, + Any, + ClassVar, + Final, + Generic, + Literal, + NamedTuple, + TypeVar, +) + +from multidict import CIMultiDict, CIMultiDictProxy, istr +from yarl import URL + +from . import hdrs +from .base_protocol import BaseProtocol +from .compression_utils import ( + HAS_BROTLI, + HAS_ZSTD, + BrotliDecompressor, + ZLibDecompressor, + ZSTDDecompressor, +) +from .helpers import ( + _EXC_SENTINEL, + DEBUG, + DEFAULT_CHUNK_SIZE, + EMPTY_BODY_METHODS, + EMPTY_BODY_STATUS_CODES, + NO_EXTENSIONS, + BaseTimerContext, + set_exception, +) +from .http_exceptions import ( + BadHttpMessage, + BadHttpMethod, + BadStatusLine, + ContentEncodingError, + ContentLengthError, + InvalidHeader, + InvalidURLError, + LineTooLong, + TransferEncodingError, +) +from .http_writer import HttpVersion, HttpVersion10, HttpVersion11 +from .streams import EMPTY_PAYLOAD, StreamReader +from .typedefs import RawHeaders + +if TYPE_CHECKING: + from .client_proto import ResponseHandler + +__all__ = ( + "HeadersParser", + "HttpParser", + "HttpRequestParser", + "HttpResponseParser", + "RawRequestMessage", + "RawResponseMessage", +) + +_SEP = Literal[b"\r\n", b"\n"] + +ASCIISET: Final[set[str]] = set(string.printable) + +# See https://www.rfc-editor.org/rfc/rfc9110.html#name-overview +# and https://www.rfc-editor.org/rfc/rfc9110.html#name-tokens +# +# method = token +# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" / "+" / "-" / "." / +# "^" / "_" / "`" / "|" / "~" / DIGIT / ALPHA +# token = 1*tchar +_TCHAR_SPECIALS: Final[str] = re.escape("!#$%&'*+-.^_`|~") +TOKENRE: Final[Pattern[str]] = re.compile(f"[0-9A-Za-z{_TCHAR_SPECIALS}]+") +VERSRE: Final[Pattern[str]] = re.compile(r"HTTP/(\d)\.(\d)", re.ASCII) +DIGITS: Final[Pattern[str]] = re.compile(r"\d+", re.ASCII) +HEXDIGITS: Final[Pattern[bytes]] = re.compile(rb"[0-9a-fA-F]+") +# https://www.rfc-editor.org/rfc/rfc9110#section-5.5-5 +_FIELD_VALUE_FORBIDDEN_CTL_RE: Final[Pattern[str]] = re.compile( + r"[\x00-\x08\x0a-\x1f\x7f]" +) + +# RFC 9110 singleton headers — duplicates are rejected in strict mode. +# In lax mode (response parser default), the check is skipped entirely +# since real-world servers (e.g. Google APIs, Werkzeug) commonly send +# duplicate headers like Content-Type or Server. +# Lowercased for case-insensitive matching against wire names. +SINGLETON_HEADERS: Final[frozenset[str]] = frozenset( + { + "content-length", + "content-location", + "content-range", + "content-type", + "etag", + "host", + "max-forwards", + "server", + "transfer-encoding", + "user-agent", + } +) + + +class RawRequestMessage(NamedTuple): + method: str + path: str + version: HttpVersion + headers: "CIMultiDictProxy[str]" + raw_headers: RawHeaders + should_close: bool + compression: str | None + upgrade: bool + chunked: bool + url: URL + + +class RawResponseMessage(NamedTuple): + version: HttpVersion + code: int + reason: str + headers: CIMultiDictProxy[str] + raw_headers: RawHeaders + should_close: bool + compression: str | None + upgrade: bool + chunked: bool + + +_MsgT = TypeVar("_MsgT", RawRequestMessage, RawResponseMessage) + + +class PayloadState(IntEnum): + PAYLOAD_COMPLETE = 0 + PAYLOAD_NEEDS_INPUT = 1 + PAYLOAD_HAS_PENDING_INPUT = 2 + + +class ParseState(IntEnum): + + PARSE_NONE = 0 + PARSE_LENGTH = 1 + PARSE_CHUNKED = 2 + PARSE_UNTIL_EOF = 3 + + +class ChunkState(IntEnum): + PARSE_CHUNKED_SIZE = 0 + PARSE_CHUNKED_CHUNK = 1 + PARSE_CHUNKED_CHUNK_EOF = 2 + PARSE_MAYBE_TRAILERS = 3 + PARSE_TRAILERS = 4 + + +class HeadersParser: + def __init__( + self, + max_line_size: int = 8190, + max_headers: int = 32768, + max_field_size: int = 8190, + lax: bool = False, + ) -> None: + self.max_line_size = max_line_size + self.max_headers = max_headers + self.max_field_size = max_field_size + self._lax = lax + + def parse_headers( + self, lines: list[bytes] + ) -> tuple["CIMultiDictProxy[str]", RawHeaders]: + headers: CIMultiDict[str] = CIMultiDict() + # note: "raw" does not mean inclusion of OWS before/after the field value + raw_headers = [] + + lines_idx = 0 + line = lines[lines_idx] + line_count = len(lines) + + while line: + # Parse initial header name : value pair. + try: + bname, bvalue = line.split(b":", 1) + except ValueError: + raise InvalidHeader(line) from None + + if len(bname) == 0: + raise InvalidHeader(bname) + + # https://www.rfc-editor.org/rfc/rfc9112.html#section-5.1-2 + if {bname[0], bname[-1]} & {32, 9}: # {" ", "\t"} + raise InvalidHeader(line) + + bvalue = bvalue.lstrip(b" \t") + name = bname.decode("utf-8", "surrogateescape") + if not TOKENRE.fullmatch(name): + raise InvalidHeader(bname) + + # next line + lines_idx += 1 + line = lines[lines_idx] + + # consume continuation lines + continuation = self._lax and line and line[0] in (32, 9) # (' ', '\t') + + # Deprecated: https://www.rfc-editor.org/rfc/rfc9112.html#name-obsolete-line-folding + if continuation: + header_length = len(bvalue) + bvalue_lst = [bvalue] + while continuation: + header_length += len(line) + if header_length > self.max_field_size: + header_line = bname + b": " + b"".join(bvalue_lst) + raise LineTooLong( + header_line[:100] + b"...", self.max_field_size + ) + bvalue_lst.append(line) + + # next line + lines_idx += 1 + if lines_idx < line_count: + line = lines[lines_idx] + if line: + continuation = line[0] in (32, 9) # (' ', '\t') + else: + line = b"" + break + bvalue = b"".join(bvalue_lst) + + bvalue = bvalue.strip(b" \t") + value = bvalue.decode("utf-8", "surrogateescape") + + # https://www.rfc-editor.org/rfc/rfc9110.html#section-5.5-5 + if self._lax: + if "\n" in value or "\r" in value or "\x00" in value: + raise InvalidHeader(bvalue) + elif _FIELD_VALUE_FORBIDDEN_CTL_RE.search(value): + raise InvalidHeader(bvalue) + + if not self._lax and name in headers and name.lower() in SINGLETON_HEADERS: + raise BadHttpMessage(f"Duplicate '{name}' header found.") + headers.add(name, value) + raw_headers.append((bname, bvalue)) + + return (CIMultiDictProxy(headers), tuple(raw_headers)) + + +def _is_supported_upgrade(headers: CIMultiDictProxy[str]) -> bool: + """Check if the upgrade header is supported.""" + u = headers.get(hdrs.UPGRADE, "") + # .lower() can transform non-ascii characters. + return u.isascii() and u.lower() in {"tcp", "websocket"} + + +class HttpParser(abc.ABC, Generic[_MsgT]): + lax: ClassVar[bool] = False + + def __init__( + self, + protocol: BaseProtocol | None = None, + loop: asyncio.AbstractEventLoop | None = None, + limit: int = 2**16, + max_line_size: int = 8190, + max_headers: int = 128, + max_field_size: int = 8190, + timer: BaseTimerContext | None = None, + code: int | None = None, + method: str | None = None, + payload_exception: type[BaseException] | None = None, + response_with_body: bool = True, + read_until_eof: bool = False, + auto_decompress: bool = True, + max_msg_queue_size: int = 0, + ) -> None: + self.protocol = protocol + self.loop = loop + self.max_line_size = max_line_size + self.max_headers = max_headers + self.max_field_size = max_field_size + self.max_headers = max_headers + self.timer = timer + self.code = code + self.method = method + self.payload_exception = payload_exception + self.response_with_body = response_with_body + self.read_until_eof = read_until_eof + + self._lines: list[bytes] = [] + self._tail = b"" + self._upgraded = False + self._payload = None + self._payload_parser: HttpPayloadParser | None = None + self._payload_has_more_data = False + self._auto_decompress = auto_decompress + self._limit = limit + self._headers_parser = HeadersParser( + max_line_size, max_headers, max_field_size, self.lax + ) + # Stop emitting messages once this many are queued unconsumed (0 = off). + self._max_msg_queue_size = max_msg_queue_size + self._msg_in_flight = 0 + + @abc.abstractmethod + def parse_message(self, lines: list[bytes]) -> _MsgT: ... + + @abc.abstractmethod + def _is_chunked_te(self, te: str) -> bool: ... + + def pause_reading(self) -> None: + assert self._payload_parser is not None + self._payload_parser.pause_reading() + + def message_consumed(self) -> None: + """Protocol drained a queued message; free a slot for parsing.""" + if self._msg_in_flight > 0: + self._msg_in_flight -= 1 + + def feed_eof(self) -> _MsgT | None: + if self._payload_parser is not None: + self._payload_parser.feed_eof() + if self._payload_parser.done: + self._payload_parser = None + else: + # try to extract partial message + if self._tail: + self._lines.append(self._tail) + + if self._lines: + if self._lines[-1] != "\r\n": + self._lines.append(b"") + with suppress(Exception): + return self.parse_message(self._lines) + return None + + def feed_data( + self, + data: bytes, + SEP: _SEP = b"\r\n", + EMPTY: bytes = b"", + CONTENT_LENGTH: istr = hdrs.CONTENT_LENGTH, + METH_CONNECT: str = hdrs.METH_CONNECT, + SEC_WEBSOCKET_KEY1: istr = hdrs.SEC_WEBSOCKET_KEY1, + ) -> tuple[list[tuple[_MsgT, StreamReader]], bool, bytes]: + + messages = [] + + if self._tail: + data, self._tail = self._tail + data, b"" + + data_len = len(data) + start_pos = 0 + loop = self.loop + max_line_length = self.max_line_size + + should_close = False + while start_pos < data_len or self._payload_has_more_data: + # read HTTP message (request/response line + headers), \r\n\r\n + # and split by lines + if self._payload_parser is None and not self._upgraded: + if ( + self._max_msg_queue_size + and self._msg_in_flight >= self._max_msg_queue_size + ): + # Queue full: buffer the rest and stop. Safe pause point; + # any preceding body is consumed before the next request + # line. Resumes via feed_data(b"") when the queue drains. + self._tail = data[start_pos:] + break + pos = data.find(SEP, start_pos) + # consume \r\n + if pos == start_pos and not self._lines: + start_pos = pos + len(SEP) + continue + + if pos >= start_pos: + if should_close: + raise BadHttpMessage("Data after `Connection: close`") + + # line found + line = data[start_pos:pos] + if SEP == b"\n": # For lax response parsing + line = line.rstrip(b"\r") + if len(line) > max_line_length: + raise LineTooLong(line[:100] + b"...", max_line_length) + + self._lines.append(line) + # After processing the status/request line, everything is a header. + max_line_length = self.max_field_size + + if len(self._lines) > self.max_headers: + raise BadHttpMessage("Too many headers received") + + start_pos = pos + len(SEP) + + # \r\n\r\n found + if self._lines[-1] == EMPTY: + max_trailers = self.max_headers - len(self._lines) + try: + msg: _MsgT = self.parse_message(self._lines) + finally: + self._lines.clear() + + def get_content_length() -> int | None: + # payload length + length_hdr = msg.headers.get(CONTENT_LENGTH) + if length_hdr is None: + return None + + # Shouldn't allow +/- or other number formats. + # https://www.rfc-editor.org/rfc/rfc9110#section-8.6-2 + # msg.headers is already stripped of leading/trailing wsp + if not DIGITS.fullmatch(length_hdr): + raise InvalidHeader(CONTENT_LENGTH) + + return int(length_hdr) + + length = get_content_length() + # do not support old websocket spec + if SEC_WEBSOCKET_KEY1 in msg.headers: + raise InvalidHeader(SEC_WEBSOCKET_KEY1) + + self._upgraded = msg.upgrade and _is_supported_upgrade( + msg.headers + ) + + method = getattr(msg, "method", self.method) + # code is only present on responses + code = getattr(msg, "code", 0) + + assert self.protocol is not None + # calculate payload + empty_body = code in EMPTY_BODY_STATUS_CODES or bool( + method and method in EMPTY_BODY_METHODS + ) + if not empty_body and ( + ((length is not None and length > 0) or msg.chunked) + and not self._upgraded + ): + payload = StreamReader( + self.protocol, + timer=self.timer, + loop=loop, + limit=self._limit, + ) + payload_parser = HttpPayloadParser( + payload, + length=length, + chunked=msg.chunked, + method=method, + compression=msg.compression, + code=self.code, + response_with_body=self.response_with_body, + auto_decompress=self._auto_decompress, + lax=self.lax, + headers_parser=self._headers_parser, + max_line_size=self.max_line_size, + max_field_size=self.max_field_size, + max_trailers=max_trailers, + limit=self._limit, + ) + if not payload_parser.done: + self._payload_parser = payload_parser + elif method == METH_CONNECT: + assert isinstance(msg, RawRequestMessage) + payload = StreamReader( + self.protocol, + timer=self.timer, + loop=loop, + limit=self._limit, + ) + self._upgraded = True + self._payload_parser = HttpPayloadParser( + payload, + method=msg.method, + compression=msg.compression, + auto_decompress=self._auto_decompress, + lax=self.lax, + headers_parser=self._headers_parser, + max_line_size=self.max_line_size, + max_field_size=self.max_field_size, + max_trailers=max_trailers, + limit=self._limit, + ) + elif not empty_body and length is None and self.read_until_eof: + payload = StreamReader( + self.protocol, + timer=self.timer, + loop=loop, + limit=self._limit, + ) + payload_parser = HttpPayloadParser( + payload, + length=length, + chunked=msg.chunked, + method=method, + compression=msg.compression, + code=self.code, + response_with_body=self.response_with_body, + auto_decompress=self._auto_decompress, + lax=self.lax, + headers_parser=self._headers_parser, + max_line_size=self.max_line_size, + max_field_size=self.max_field_size, + max_trailers=max_trailers, + limit=self._limit, + ) + if not payload_parser.done: + self._payload_parser = payload_parser + else: + payload = EMPTY_PAYLOAD + + messages.append((msg, payload)) + if self._max_msg_queue_size: + self._msg_in_flight += 1 + should_close = msg.should_close + else: + self._tail = data[start_pos:] + if len(self._tail) > self.max_line_size: + raise LineTooLong(self._tail[:100] + b"...", self.max_line_size) + data = EMPTY + break + + # no parser, just store + elif self._payload_parser is None and self._upgraded: + assert not self._lines + break + + # feed payload + else: + assert not self._lines + assert self._payload_parser is not None + try: + payload_state, data = self._payload_parser.feed_data( + data[start_pos:], SEP + ) + except Exception as underlying_exc: + reraised_exc: BaseException = underlying_exc + if self.payload_exception is not None: + reraised_exc = self.payload_exception(str(underlying_exc)) + + set_exception( + self._payload_parser.payload, + reraised_exc, + underlying_exc, + ) + + payload_state = PayloadState.PAYLOAD_COMPLETE + data = b"" + if isinstance( + underlying_exc, (InvalidHeader, TransferEncodingError) + ): + raise + + self._payload_has_more_data = ( + payload_state == PayloadState.PAYLOAD_HAS_PENDING_INPUT + ) + + if payload_state is not PayloadState.PAYLOAD_COMPLETE: + # We've either consumed all available data, or we're pausing + # until the reader buffer is freed up. + break + + start_pos = 0 + data_len = len(data) + self._payload_parser = None + + if data and start_pos < data_len: + data = data[start_pos:] + else: + data = EMPTY + + return messages, self._upgraded, data + + def parse_headers( + self, lines: list[bytes] + ) -> tuple[ + "CIMultiDictProxy[str]", RawHeaders, bool | None, str | None, bool, bool + ]: + """Parses RFC 5322 headers from a stream. + + Line continuations are supported. Returns list of header name + and value pairs. Header name is in upper case. + """ + headers, raw_headers = self._headers_parser.parse_headers(lines) + close_conn = None + encoding = None + upgrade = False + chunked = False + + # keep-alive and protocol switching + # RFC 9110 section 7.6.1 defines Connection as a comma-separated list. + conn_values = headers.getall(hdrs.CONNECTION, ()) + if conn_values: + conn_tokens = { + token.lower() + for conn_value in conn_values + for token in (part.strip(" \t") for part in conn_value.split(",")) + if token and token.isascii() + } + + if "close" in conn_tokens: + close_conn = True + elif "keep-alive" in conn_tokens: + close_conn = False + + # https://www.rfc-editor.org/rfc/rfc9110.html#name-101-switching-protocols + if "upgrade" in conn_tokens and headers.get(hdrs.UPGRADE): + upgrade = True + + # encoding + enc = headers.get(hdrs.CONTENT_ENCODING, "") + if enc.isascii() and enc.lower() in {"gzip", "deflate", "br", "zstd"}: + encoding = enc + + # chunking + te = headers.get(hdrs.TRANSFER_ENCODING) + if te is not None: + if self._is_chunked_te(te): + chunked = True + + if hdrs.CONTENT_LENGTH in headers: + raise BadHttpMessage( + "Transfer-Encoding can't be present with Content-Length", + ) + + return (headers, raw_headers, close_conn, encoding, upgrade, chunked) + + def set_upgraded(self, val: bool) -> None: + """Set connection upgraded (to websocket) mode. + + :param bool val: new state. + """ + self._upgraded = val + + +class HttpRequestParser(HttpParser[RawRequestMessage]): + """Read request status line. + + Exception .http_exceptions.BadStatusLine + could be raised in case of any errors in status line. + Returns RawRequestMessage. + """ + + def parse_message(self, lines: list[bytes]) -> RawRequestMessage: + # request line + line = lines[0].decode("utf-8", "surrogateescape") + try: + method, path, version = line.split(" ", maxsplit=2) + except ValueError: + raise BadHttpMethod(line) from None + + # method + if not TOKENRE.fullmatch(method): + raise BadHttpMethod(method) + + # version + match = VERSRE.fullmatch(version) + if match is None: + raise BadStatusLine(line) + version_o = HttpVersion(int(match.group(1)), int(match.group(2))) + + if method == "CONNECT": + # authority-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3 + url = URL.build(authority=path, encoded=True) + elif path.startswith("/"): + # origin-form, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.1 + path_part, _hash_separator, url_fragment = path.partition("#") + path_part, _question_mark_separator, qs_part = path_part.partition("?") + + # NOTE: `yarl.URL.build()` is used to mimic what the Cython-based + # NOTE: parser does, otherwise it results into the same + # NOTE: HTTP Request-Line input producing different + # NOTE: `yarl.URL()` objects + url = URL.build( + path=path_part, + query_string=qs_part, + fragment=url_fragment, + encoded=True, + ) + elif path == "*" and method == "OPTIONS": + # asterisk-form, + url = URL(path, encoded=True) + else: + # absolute-form for proxy maybe, + # https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.2 + url = URL(path, encoded=True) + if url.scheme == "": + # not absolute-form + raise InvalidURLError( + path.encode(errors="surrogateescape").decode("latin1") + ) + + # read headers + ( + headers, + raw_headers, + close, + compression, + upgrade, + chunked, + ) = self.parse_headers(lines[1:]) + + if version_o == HttpVersion11 and hdrs.HOST not in headers: + raise BadHttpMessage("Missing 'Host' header in request.") + + if close is None: # then the headers weren't set in the request + if version_o <= HttpVersion10: # HTTP 1.0 must asks to not close + close = True + else: # HTTP 1.1 must ask to close. + close = False + + return RawRequestMessage( + method, + path, + version_o, + headers, + raw_headers, + close, + compression, + upgrade, + chunked, + url, + ) + + def _is_chunked_te(self, te: str) -> bool: + te = te.rsplit(",", maxsplit=1)[-1].strip(" \t") + # .lower() transforms some non-ascii chars, so must check first. + if te.isascii() and te.lower() == "chunked": + return True + # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.3 + raise BadHttpMessage("Request has invalid `Transfer-Encoding`") + + +class HttpResponseParser(HttpParser[RawResponseMessage]): + """Read response status line and headers. + + BadStatusLine could be raised in case of any errors in status line. + Returns RawResponseMessage. + """ + + protocol: "ResponseHandler" + + # Lax mode should only be enabled on response parser. + lax = not DEBUG + + def feed_data( + self, + data: bytes, + SEP: _SEP | None = None, + *args: Any, + **kwargs: Any, + ) -> tuple[list[tuple[RawResponseMessage, StreamReader]], bool, bytes]: + if SEP is None: + SEP = b"\r\n" if DEBUG else b"\n" + return super().feed_data(data, SEP, *args, **kwargs) + + def parse_message(self, lines: list[bytes]) -> RawResponseMessage: + line = lines[0].decode("utf-8", "surrogateescape") + try: + version, status = line.split(maxsplit=1) + except ValueError: + raise BadStatusLine(line) from None + + try: + status, reason = status.split(maxsplit=1) + except ValueError: + status = status.strip() + reason = "" + + # version + match = VERSRE.fullmatch(version) + if match is None: + raise BadStatusLine(line) + version_o = HttpVersion(int(match.group(1)), int(match.group(2))) + + # The status code is a three-digit ASCII number, no padding + if len(status) != 3 or not DIGITS.fullmatch(status): + raise BadStatusLine(line) + status_i = int(status) + + # read headers + ( + headers, + raw_headers, + close, + compression, + upgrade, + chunked, + ) = self.parse_headers(lines[1:]) + + if close is None: + if version_o <= HttpVersion10: + close = True + # https://www.rfc-editor.org/rfc/rfc9112.html#name-message-body-length + elif 100 <= status_i < 200 or status_i in {204, 304}: + close = False + elif hdrs.CONTENT_LENGTH in headers or hdrs.TRANSFER_ENCODING in headers: + close = False + else: + # https://www.rfc-editor.org/rfc/rfc9112.html#section-6.3-2.8 + close = True + + return RawResponseMessage( + version_o, + status_i, + reason.strip(), + headers, + raw_headers, + close, + compression, + upgrade, + chunked, + ) + + def _is_chunked_te(self, te: str) -> bool: + # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.4.2 + return te.rsplit(",", maxsplit=1)[-1].strip(" \t").lower() == "chunked" + + +class HttpPayloadParser: + def __init__( + self, + payload: StreamReader, + length: int | None = None, + chunked: bool = False, + compression: str | None = None, + code: int | None = None, + method: str | None = None, + response_with_body: bool = True, + auto_decompress: bool = True, + lax: bool = False, + *, + headers_parser: HeadersParser, + max_line_size: int = 8190, + max_field_size: int = 8190, + max_trailers: int = 128, + limit: int = DEFAULT_CHUNK_SIZE, + ) -> None: + self._length = 0 + self._paused = False + self._type = ParseState.PARSE_UNTIL_EOF + self._chunk = ChunkState.PARSE_CHUNKED_SIZE + self._chunk_size = 0 + self._chunk_tail = b"" + self._auto_decompress = auto_decompress + self._lax = lax + self._headers_parser = headers_parser + self._max_line_size = max_line_size + self._max_field_size = max_field_size + self._max_trailers = max_trailers + self._more_data_available = False + self._trailer_lines: list[bytes] = [] + self.done = False + self._eof_pending = False + + # payload decompression wrapper + if response_with_body and compression and self._auto_decompress: + real_payload: StreamReader | DeflateBuffer = DeflateBuffer( + payload, compression, max_decompress_size=limit + ) + else: + real_payload = payload + + # payload parser + if not response_with_body: + # don't parse payload if it's not expected to be received + self._type = ParseState.PARSE_NONE + real_payload.feed_eof() + self.done = True + elif chunked: + self._type = ParseState.PARSE_CHUNKED + elif length is not None: + self._type = ParseState.PARSE_LENGTH + self._length = length + self._length_expected = length + if self._length == 0: + real_payload.feed_eof() + self.done = True + + self.payload = real_payload + + def pause_reading(self) -> None: + self._paused = True + + def feed_eof(self) -> None: + if self._type == ParseState.PARSE_UNTIL_EOF: + self._eof_pending = True + while self._more_data_available: + if self._paused: + self._paused = False + return # Will resume via feed_data(b"") later + self._more_data_available = self.payload.feed_data(b"", 0) + self.payload.feed_eof() + self.done = True + self._eof_pending = False + elif self._type == ParseState.PARSE_LENGTH: + received = self._length_expected - self._length + raise ContentLengthError( + f"Not enough data to satisfy content length header " + f"(received {received} of {self._length_expected} bytes)." + ) + elif self._type == ParseState.PARSE_CHUNKED: + raise TransferEncodingError( + "Not enough data to satisfy transfer length header." + ) + + def feed_data( + self, chunk: bytes, SEP: _SEP = b"\r\n", CHUNK_EXT: bytes = b";" + ) -> tuple[PayloadState, bytes]: + """Receive a chunk of data to process. + + Return: + PayloadState - The current state of payload processing. + This function may be called with empty bytes after returning + PAYLOAD_HAS_PENDING_INPUT to continue processing after a pause. + bytes - If payload is complete, this is the unconsumed bytes intended for the + next message/payload, b"" otherwise. + """ + # Read specified amount of bytes + if self._type == ParseState.PARSE_LENGTH: + if self._chunk_tail: + chunk = self._chunk_tail + chunk + self._chunk_tail = b"" + + required = self._length + self._length = max(required - len(chunk), 0) + self._more_data_available = self.payload.feed_data( + chunk[:required], required + ) + while self._more_data_available: + if self._paused: + self._paused = False + self._chunk_tail = chunk[required:] + return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" + self._more_data_available = self.payload.feed_data(b"", 0) + + if self._length == 0: + self.payload.feed_eof() + return PayloadState.PAYLOAD_COMPLETE, chunk[required:] + # Chunked transfer encoding parser + elif self._type == ParseState.PARSE_CHUNKED: + if self._chunk_tail: + # We should check the length is sane when not processing payload body. + if self._chunk != ChunkState.PARSE_CHUNKED_CHUNK: + max_line_length = self._max_line_size + if self._chunk == ChunkState.PARSE_TRAILERS: + max_line_length = self._max_field_size + if len(self._chunk_tail) > max_line_length: + raise LineTooLong( + self._chunk_tail[:100] + b"...", max_line_length + ) + + chunk = self._chunk_tail + chunk + self._chunk_tail = b"" + + while chunk or self._more_data_available: + # read next chunk size + if self._chunk == ChunkState.PARSE_CHUNKED_SIZE: + pos = chunk.find(SEP) + if pos >= 0: + # Only chunk-size lines reach here; trailers enforce + # _max_field_size separately in PARSE_TRAILERS below. + if pos > self._max_line_size: + raise LineTooLong(chunk[:100] + b"...", self._max_line_size) + i = chunk.find(CHUNK_EXT, 0, pos) + if i >= 0: + size_b = chunk[:i] # strip chunk-extensions + # Verify no LF in the chunk-extension + if b"\n" in (ext := chunk[i:pos]): + exc = TransferEncodingError( + f"Unexpected LF in chunk-extension: {ext!r}" + ) + set_exception(self.payload, exc) + raise exc + else: + size_b = chunk[:pos] + + if self._lax: # Allow whitespace in lax mode. + size_b = size_b.strip() + + if not re.fullmatch(HEXDIGITS, size_b): + exc = TransferEncodingError( + chunk[:pos].decode("ascii", "surrogateescape") + ) + set_exception(self.payload, exc) + raise exc + size = int(bytes(size_b), 16) + + chunk = chunk[pos + len(SEP) :] + if size == 0: # eof marker + self._chunk = ChunkState.PARSE_TRAILERS + if self._lax and chunk.startswith(b"\r"): + chunk = chunk[1:] + else: + self._chunk = ChunkState.PARSE_CHUNKED_CHUNK + self._chunk_size = size + self.payload.begin_http_chunk_receiving() + else: + self._chunk_tail = chunk + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" + + # read chunk and feed buffer + if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK: + if self._paused: + self._paused = False + self._chunk_tail = chunk + return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" + + required = self._chunk_size + self._chunk_size = max(required - len(chunk), 0) + self._more_data_available = self.payload.feed_data( + chunk[:required], required + ) + chunk = chunk[required:] + + if self._more_data_available: + continue + + if self._chunk_size: + self._paused = False + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" + self._chunk = ChunkState.PARSE_CHUNKED_CHUNK_EOF + self.payload.end_http_chunk_receiving() + + # toss the CRLF at the end of the chunk + if self._chunk == ChunkState.PARSE_CHUNKED_CHUNK_EOF: + if self._lax and chunk.startswith(b"\r"): + chunk = chunk[1:] + if chunk[: len(SEP)] == SEP: + chunk = chunk[len(SEP) :] + self._chunk = ChunkState.PARSE_CHUNKED_SIZE + elif len(chunk) >= len(SEP) or chunk != SEP[: len(chunk)]: + exc = TransferEncodingError( + "Chunk size mismatch: expected CRLF after chunk data" + ) + set_exception(self.payload, exc) + raise exc + else: + self._chunk_tail = chunk + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" + + if self._chunk == ChunkState.PARSE_TRAILERS: + pos = chunk.find(SEP) + if pos < 0: # No line found + self._chunk_tail = chunk + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" + + line = chunk[:pos] + chunk = chunk[pos + len(SEP) :] + if SEP == b"\n": # For lax response parsing + line = line.rstrip(b"\r") + + if len(line) > self._max_field_size: + raise LineTooLong(line[:100] + b"...", self._max_field_size) + + self._trailer_lines.append(line) + + if len(self._trailer_lines) > self._max_trailers: + raise BadHttpMessage("Too many trailers received") + + # \r\n\r\n found, end of stream + if self._trailer_lines[-1] == b"": + # Headers and trailers are defined the same way, + # so we reuse the HeadersParser here. + try: + trailers, raw_trailers = self._headers_parser.parse_headers( + self._trailer_lines + ) + finally: + self._trailer_lines.clear() + self.payload.feed_eof() + return PayloadState.PAYLOAD_COMPLETE, chunk + + # Read all bytes until eof + elif self._type == ParseState.PARSE_UNTIL_EOF: + self._more_data_available = self.payload.feed_data(chunk, len(chunk)) + while self._more_data_available: + if self._paused: + self._paused = False + return PayloadState.PAYLOAD_HAS_PENDING_INPUT, b"" + self._more_data_available = self.payload.feed_data(b"", 0) + + if self._eof_pending: + self.payload.feed_eof() + self.done = True + self._eof_pending = False + return PayloadState.PAYLOAD_COMPLETE, b"" + + return PayloadState.PAYLOAD_NEEDS_INPUT, b"" + + +class DeflateBuffer: + """DeflateStream decompress stream and feed data into specified stream.""" + + decompressor: Any + + def __init__( + self, + out: StreamReader, + encoding: str | None, + max_decompress_size: int = DEFAULT_CHUNK_SIZE, + ) -> None: + self.out = out + self.size = 0 + out.total_compressed_bytes = self.size + self.encoding = encoding + self._started_decoding = False + + self.decompressor: BrotliDecompressor | ZLibDecompressor | ZSTDDecompressor + if encoding == "br": + if not HAS_BROTLI: # pragma: no cover + raise ContentEncodingError( + "Can not decode content-encoding: brotli (br). " + "Please install `Brotli`" + ) + self.decompressor = BrotliDecompressor() + elif encoding == "zstd": + if not HAS_ZSTD: + raise ContentEncodingError( + "Can not decode content-encoding: zstandard (zstd). " + "Please install `backports.zstd`" + ) + self.decompressor = ZSTDDecompressor() + else: + self.decompressor = ZLibDecompressor(encoding=encoding) + + self._max_decompress_size = max_decompress_size + + def set_exception( + self, + exc: BaseException, + exc_cause: BaseException = _EXC_SENTINEL, + ) -> None: + set_exception(self.out, exc, exc_cause) + + def feed_data(self, chunk: bytes, size: int) -> bool: + self.size += size + self.out.total_compressed_bytes = self.size + + # RFC1950 + # bits 0..3 = CM = 0b1000 = 8 = "deflate" + # bits 4..7 = CINFO = 1..7 = windows size. + if ( + not self._started_decoding + and self.encoding == "deflate" + and chunk[0] & 0xF != 8 + ): + # Change the decoder to decompress incorrectly compressed data + # Actually we should issue a warning about non-RFC-compliant data. + self.decompressor = ZLibDecompressor( + encoding=self.encoding, suppress_deflate_header=True + ) + + low_water = self.out._low_water + max_length = ( + 0 if low_water >= sys.maxsize else max(self._max_decompress_size, low_water) + ) + try: + chunk = self.decompressor.decompress_sync(chunk, max_length=max_length) + except Exception: + raise ContentEncodingError( + "Can not decode content-encoding: %s" % self.encoding + ) + + self._started_decoding = True + + if chunk: + self.out.feed_data(chunk, len(chunk)) + return self.decompressor.data_available # type: ignore[no-any-return] + + def feed_eof(self) -> None: + chunk = self.decompressor.flush() + # This should never contain data as we defer the call until exhausting + # the decompression. If .flush() is returning data, this may indicate a + # zip bomb vulnerability as it will decompress all remaining data at once. + assert not chunk + + if self.size > 0: + if self.encoding == "deflate" and not self.decompressor.eof: + raise ContentEncodingError("deflate") + + self.out.feed_eof() + + def begin_http_chunk_receiving(self) -> None: + self.out.begin_http_chunk_receiving() + + def end_http_chunk_receiving(self) -> None: + self.out.end_http_chunk_receiving() + + +HttpRequestParserPy = HttpRequestParser +HttpResponseParserPy = HttpResponseParser +RawRequestMessagePy = RawRequestMessage +RawResponseMessagePy = RawResponseMessage + +try: + if not NO_EXTENSIONS: + from ._http_parser import ( # type: ignore[import-not-found,no-redef] + HttpRequestParser, + HttpResponseParser, + RawRequestMessage, + RawResponseMessage, + ) + + HttpRequestParserC = HttpRequestParser + HttpResponseParserC = HttpResponseParser + RawRequestMessageC = RawRequestMessage + RawResponseMessageC = RawResponseMessage +except ImportError: # pragma: no cover + pass diff --git a/venv/lib/python3.11/site-packages/aiohttp/http_websocket.py b/venv/lib/python3.11/site-packages/aiohttp/http_websocket.py new file mode 100644 index 0000000..9fb9fba --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/http_websocket.py @@ -0,0 +1,41 @@ +"""WebSocket protocol versions 13 and 8.""" + +from ._websocket.helpers import WS_KEY, ws_ext_gen, ws_ext_parse +from ._websocket.models import ( + WS_CLOSED_MESSAGE, + WS_CLOSING_MESSAGE, + WebSocketError, + WSCloseCode, + WSHandshakeError, + WSMessage, + WSMessageDecodeText, + WSMessageNoDecodeText, + WSMessageTextBytes, + WSMsgType, +) +from ._websocket.reader import WebSocketReader +from ._websocket.writer import WebSocketWriter + +# Messages that the WebSocketResponse.receive needs to handle internally +_INTERNAL_RECEIVE_TYPES = frozenset( + (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.PING, WSMsgType.PONG) +) + + +__all__ = ( + "WS_CLOSED_MESSAGE", + "WS_CLOSING_MESSAGE", + "WS_KEY", + "WebSocketReader", + "WebSocketWriter", + "WSMessage", + "WSMessageDecodeText", + "WSMessageNoDecodeText", + "WSMessageTextBytes", + "WebSocketError", + "WSMsgType", + "WSCloseCode", + "ws_ext_gen", + "ws_ext_parse", + "WSHandshakeError", +) diff --git a/venv/lib/python3.11/site-packages/aiohttp/http_writer.py b/venv/lib/python3.11/site-packages/aiohttp/http_writer.py new file mode 100644 index 0000000..a252d92 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/http_writer.py @@ -0,0 +1,381 @@ +"""Http related parsers and protocol.""" + +import asyncio +import re +import sys +from typing import ( # noqa + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Iterable, + List, + NamedTuple, + Optional, +) + +from multidict import CIMultiDict + +from .abc import AbstractStreamWriter +from .base_protocol import BaseProtocol +from .client_exceptions import ClientConnectionResetError +from .compression_utils import ZLibCompressor +from .helpers import NO_EXTENSIONS + +__all__ = ("StreamWriter", "HttpVersion", "HttpVersion10", "HttpVersion11") + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + from typing import Union + + Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] + + +MIN_PAYLOAD_FOR_WRITELINES = 2048 +IS_PY313_BEFORE_313_2 = (3, 13, 0) <= sys.version_info < (3, 13, 2) +IS_PY_BEFORE_312_9 = sys.version_info < (3, 12, 9) +SKIP_WRITELINES = IS_PY313_BEFORE_313_2 or IS_PY_BEFORE_312_9 +# writelines is not safe for use +# on Python 3.12+ until 3.12.9 +# on Python 3.13+ until 3.13.2 +# and on older versions it not any faster than write +# CVE-2024-12254: https://github.com/python/cpython/pull/127656 + + +class HttpVersion(NamedTuple): + major: int + minor: int + + +HttpVersion10 = HttpVersion(1, 0) +HttpVersion11 = HttpVersion(1, 1) + + +_T_OnChunkSent = Optional[Callable[[Buffer], Awaitable[None]]] +_T_OnHeadersSent = Optional[Callable[["CIMultiDict[str]"], Awaitable[None]]] + + +class StreamWriter(AbstractStreamWriter): + + length: int | None = None + chunked: bool = False + _eof: bool = False + _compress: ZLibCompressor | None = None + + def __init__( + self, + protocol: BaseProtocol, + loop: asyncio.AbstractEventLoop, + on_chunk_sent: _T_OnChunkSent = None, + on_headers_sent: _T_OnHeadersSent = None, + ) -> None: + self._protocol = protocol + self.loop = loop + self._on_chunk_sent: _T_OnChunkSent = on_chunk_sent + self._on_headers_sent: _T_OnHeadersSent = on_headers_sent + self._headers_buf: bytes | None = None + self._headers_written: bool = False + + @property + def transport(self) -> asyncio.Transport | None: + return self._protocol.transport + + @property + def protocol(self) -> BaseProtocol: + return self._protocol + + def enable_chunking(self) -> None: + self.chunked = True + + def enable_compression( + self, encoding: str = "deflate", strategy: int | None = None + ) -> None: + self._compress = ZLibCompressor(encoding=encoding, strategy=strategy) + + def _write(self, chunk: Buffer) -> None: + size = len(chunk) + self.buffer_size += size + self.output_size += size + transport = self._protocol.transport + if transport is None or transport.is_closing(): + raise ClientConnectionResetError("Cannot write to closing transport") + transport.write(chunk) + + def _writelines(self, chunks: Iterable[Buffer]) -> None: + size = 0 + for chunk in chunks: + size += len(chunk) + self.buffer_size += size + self.output_size += size + transport = self._protocol.transport + if transport is None or transport.is_closing(): + raise ClientConnectionResetError("Cannot write to closing transport") + if SKIP_WRITELINES or size < MIN_PAYLOAD_FOR_WRITELINES: + transport.write(b"".join(chunks)) + else: + transport.writelines(chunks) + + def _write_chunked_payload(self, chunk: Buffer) -> None: + """Write a chunk with proper chunked encoding.""" + chunk_len_pre = f"{len(chunk):x}\r\n".encode("ascii") + self._writelines((chunk_len_pre, chunk, b"\r\n")) + + def _send_headers_with_payload(self, chunk: Buffer, is_eof: bool) -> None: + """Send buffered headers with payload, coalescing into single write.""" + # Mark headers as written + self._headers_written = True + headers_buf = self._headers_buf + self._headers_buf = None + + if TYPE_CHECKING: + # Safe because callers (write() and write_eof()) only invoke this method + # after checking that self._headers_buf is truthy + assert headers_buf is not None + + if not self.chunked: + # Non-chunked: coalesce headers with body + if chunk: + self._writelines((headers_buf, chunk)) + else: + self._write(headers_buf) + return + + # Coalesce headers with chunked data + if chunk: + chunk_len_pre = f"{len(chunk):x}\r\n".encode("ascii") + if is_eof: + self._writelines((headers_buf, chunk_len_pre, chunk, b"\r\n0\r\n\r\n")) + else: + self._writelines((headers_buf, chunk_len_pre, chunk, b"\r\n")) + elif is_eof: + self._writelines((headers_buf, b"0\r\n\r\n")) + else: + self._write(headers_buf) + + async def write( + self, chunk: Buffer, *, drain: bool = True, LIMIT: int = 0x10000 + ) -> None: + """ + Writes chunk of data to a stream. + + write_eof() indicates end of stream. + writer can't be used after write_eof() method being called. + write() return drain future. + """ + if self._on_chunk_sent is not None: + await self._on_chunk_sent(chunk) + + if isinstance(chunk, memoryview): + if chunk.nbytes != len(chunk): + # just reshape it + chunk = chunk.cast("c") + + if self._compress is not None: + chunk = await self._compress.compress(chunk) + if not chunk: + return + + if self.length is not None: + chunk_len = len(chunk) + if self.length >= chunk_len: + self.length = self.length - chunk_len + else: + chunk = chunk[: self.length] + self.length = 0 + if not chunk: + return + + # Handle buffered headers for small payload optimization + if self._headers_buf and not self._headers_written: + self._send_headers_with_payload(chunk, False) + if drain and self.buffer_size > LIMIT: + self.buffer_size = 0 + await self.drain() + return + + if chunk: + if self.chunked: + self._write_chunked_payload(chunk) + else: + self._write(chunk) + + if drain and self.buffer_size > LIMIT: + self.buffer_size = 0 + await self.drain() + + async def write_headers( + self, status_line: str, headers: "CIMultiDict[str]" + ) -> None: + """Write headers to the stream.""" + if self._on_headers_sent is not None: + await self._on_headers_sent(headers) + # status + headers + buf = _serialize_headers(status_line, headers) + self._headers_written = False + self._headers_buf = buf + + def send_headers(self) -> None: + """Force sending buffered headers if not already sent.""" + if not self._headers_buf or self._headers_written: + return + + self._headers_written = True + headers_buf = self._headers_buf + self._headers_buf = None + + if TYPE_CHECKING: + # Safe because we only enter this block when self._headers_buf is truthy + assert headers_buf is not None + + self._write(headers_buf) + + def set_eof(self) -> None: + """Indicate that the message is complete.""" + if self._eof: + return + + # If headers haven't been sent yet, send them now + # This handles the case where there's no body at all + if self._headers_buf and not self._headers_written: + self._headers_written = True + headers_buf = self._headers_buf + self._headers_buf = None + + if TYPE_CHECKING: + # Safe because we only enter this block when self._headers_buf is truthy + assert headers_buf is not None + + # Combine headers and chunked EOF marker in a single write + if self.chunked: + self._writelines((headers_buf, b"0\r\n\r\n")) + else: + self._write(headers_buf) + elif self.chunked and self._headers_written: + # Headers already sent, just send the final chunk marker + self._write(b"0\r\n\r\n") + + self._eof = True + + async def write_eof(self, chunk: bytes = b"") -> None: + if self._eof: + return + + if chunk and self._on_chunk_sent is not None: + await self._on_chunk_sent(chunk) + + # Handle body/compression + if self._compress: + chunks: list[bytes] = [] + chunks_len = 0 + if chunk and (compressed_chunk := await self._compress.compress(chunk)): + chunks_len = len(compressed_chunk) + chunks.append(compressed_chunk) + + flush_chunk = self._compress.flush() + chunks_len += len(flush_chunk) + chunks.append(flush_chunk) + assert chunks_len + + # Send buffered headers with compressed data if not yet sent + if self._headers_buf and not self._headers_written: + self._headers_written = True + headers_buf = self._headers_buf + self._headers_buf = None + + if self.chunked: + # Coalesce headers with compressed chunked data + chunk_len_pre = f"{chunks_len:x}\r\n".encode("ascii") + self._writelines( + (headers_buf, chunk_len_pre, *chunks, b"\r\n0\r\n\r\n") + ) + else: + # Coalesce headers with compressed data + self._writelines((headers_buf, *chunks)) + await self.drain() + self._eof = True + return + + # Headers already sent, just write compressed data + if self.chunked: + chunk_len_pre = f"{chunks_len:x}\r\n".encode("ascii") + self._writelines((chunk_len_pre, *chunks, b"\r\n0\r\n\r\n")) + elif len(chunks) > 1: + self._writelines(chunks) + else: + self._write(chunks[0]) + await self.drain() + self._eof = True + return + + # No compression - send buffered headers if not yet sent + if self._headers_buf and not self._headers_written: + # Use helper to send headers with payload + self._send_headers_with_payload(chunk, True) + await self.drain() + self._eof = True + return + + # Handle remaining body + if self.chunked: + if chunk: + # Write final chunk with EOF marker + self._writelines( + (f"{len(chunk):x}\r\n".encode("ascii"), chunk, b"\r\n0\r\n\r\n") + ) + else: + self._write(b"0\r\n\r\n") + await self.drain() + self._eof = True + return + + if chunk: + self._write(chunk) + await self.drain() + + self._eof = True + + async def drain(self) -> None: + """Flush the write buffer. + + The intended use is to write + + await w.write(data) + await w.drain() + """ + protocol = self._protocol + if protocol.transport is not None and protocol._paused: + await protocol._drain_helper() + + +# https://www.rfc-editor.org/info/rfc9110/#section-5.5-5 +# https://www.rfc-editor.org/info/rfc9112/#section-4-3 +_FORBIDDEN_HEADER_CHARS_RE = re.compile(r"[\x00-\x08\x0a-\x1f\x7f]") + + +def _safe_header(string: str) -> str: + if _FORBIDDEN_HEADER_CHARS_RE.search(string) is not None: + raise ValueError( + "Forbidden control character detected in headers. " + "Potential header injection attack." + ) + return string + + +def _py_serialize_headers(status_line: str, headers: "CIMultiDict[str]") -> bytes: + _safe_header(status_line) + headers_gen = (_safe_header(k) + ": " + _safe_header(v) for k, v in headers.items()) + line = status_line + "\r\n" + "\r\n".join(headers_gen) + "\r\n\r\n" + return line.encode("utf-8") + + +_serialize_headers = _py_serialize_headers + +try: + import aiohttp._http_writer as _http_writer # type: ignore[import-not-found] + + _c_serialize_headers = _http_writer._serialize_headers + if not NO_EXTENSIONS: + _serialize_headers = _c_serialize_headers +except ImportError: + pass diff --git a/venv/lib/python3.11/site-packages/aiohttp/log.py b/venv/lib/python3.11/site-packages/aiohttp/log.py new file mode 100644 index 0000000..3cecea2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/log.py @@ -0,0 +1,8 @@ +import logging + +access_logger = logging.getLogger("aiohttp.access") +client_logger = logging.getLogger("aiohttp.client") +internal_logger = logging.getLogger("aiohttp.internal") +server_logger = logging.getLogger("aiohttp.server") +web_logger = logging.getLogger("aiohttp.web") +ws_logger = logging.getLogger("aiohttp.websocket") diff --git a/venv/lib/python3.11/site-packages/aiohttp/multipart.py b/venv/lib/python3.11/site-packages/aiohttp/multipart.py new file mode 100644 index 0000000..7cc3d4d --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/multipart.py @@ -0,0 +1,1228 @@ +import base64 +import binascii +import json +import re +import sys +import uuid +import warnings +from collections import deque +from collections.abc import AsyncIterator, Iterator, Mapping, Sequence +from types import TracebackType +from typing import TYPE_CHECKING, Any, TypeVar, Union, cast +from urllib.parse import parse_qsl, unquote, urlencode + +from multidict import CIMultiDict, CIMultiDictProxy + +from .abc import AbstractStreamWriter +from .compression_utils import ZLibCompressor, ZLibDecompressor +from .hdrs import ( + CONTENT_DISPOSITION, + CONTENT_ENCODING, + CONTENT_LENGTH, + CONTENT_TRANSFER_ENCODING, + CONTENT_TYPE, +) +from .helpers import CHAR, DEFAULT_CHUNK_SIZE, TOKEN, parse_mimetype, reify +from .http import HeadersParser +from .http_exceptions import BadHttpMessage +from .log import internal_logger +from .payload import ( + JsonPayload, + LookupError, + Order, + Payload, + StringPayload, + get_payload, + payload_type, +) +from .streams import StreamReader + +if sys.version_info >= (3, 11): + from typing import Self +else: + Self = TypeVar("Self", bound="BodyPartReader") + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] + +_Buffer = TypeVar("_Buffer", bound=Buffer) + +__all__ = ( + "MultipartReader", + "MultipartWriter", + "BodyPartReader", + "BadContentDispositionHeader", + "BadContentDispositionParam", + "parse_content_disposition", + "content_disposition_filename", +) + + +if TYPE_CHECKING: + from .client_reqrep import ClientResponse + + +class BadContentDispositionHeader(RuntimeWarning): + pass + + +class BadContentDispositionParam(RuntimeWarning): + pass + + +def parse_content_disposition( + header: str | None, +) -> tuple[str | None, dict[str, str]]: + def is_token(string: str) -> bool: + return bool(string) and TOKEN >= set(string) + + def is_quoted(string: str) -> bool: + return string[0] == string[-1] == '"' + + def is_rfc5987(string: str) -> bool: + return is_token(string) and string.count("'") == 2 + + def is_extended_param(string: str) -> bool: + return string.endswith("*") + + def is_continuous_param(string: str) -> bool: + pos = string.find("*") + 1 + if not pos: + return False + substring = string[pos:-1] if string.endswith("*") else string[pos:] + return substring.isdigit() + + def unescape(text: str, *, chars: str = "".join(map(re.escape, CHAR))) -> str: + return re.sub(f"\\\\([{chars}])", "\\1", text) + + if not header: + return None, {} + + disptype, *parts = header.split(";") + if not is_token(disptype): + warnings.warn(BadContentDispositionHeader(header)) + return None, {} + + params: dict[str, str] = {} + while parts: + item = parts.pop(0) + + if not item: # To handle trailing semicolons + warnings.warn(BadContentDispositionHeader(header)) + continue + + if "=" not in item: + warnings.warn(BadContentDispositionHeader(header)) + return None, {} + + key, value = item.split("=", 1) + key = key.lower().strip() + value = value.lstrip() + + if key in params: + warnings.warn(BadContentDispositionHeader(header)) + return None, {} + + if not is_token(key): + warnings.warn(BadContentDispositionParam(item)) + continue + + elif is_continuous_param(key): + if is_quoted(value): + value = unescape(value[1:-1]) + elif not is_token(value): + warnings.warn(BadContentDispositionParam(item)) + continue + + elif is_extended_param(key): + if is_rfc5987(value): + encoding, _, value = value.split("'", 2) + encoding = encoding or "utf-8" + else: + warnings.warn(BadContentDispositionParam(item)) + continue + + try: + value = unquote(value, encoding, "strict") + except UnicodeDecodeError: # pragma: nocover + warnings.warn(BadContentDispositionParam(item)) + continue + + else: + failed = True + if is_quoted(value): + failed = False + value = unescape(value[1:-1].lstrip("\\/")) + elif is_token(value): + failed = False + elif parts: + # maybe just ; in filename, in any case this is just + # one case fix, for proper fix we need to redesign parser + _value = f"{value};{parts[0]}" + if is_quoted(_value): + parts.pop(0) + value = unescape(_value[1:-1].lstrip("\\/")) + failed = False + + if failed: + warnings.warn(BadContentDispositionHeader(header)) + return None, {} + + params[key] = value + + return disptype.lower(), params + + +def content_disposition_filename( + params: Mapping[str, str], name: str = "filename" +) -> str | None: + name_suf = "%s*" % name + if not params: + return None + elif name_suf in params: + return params[name_suf] + elif name in params: + return params[name] + else: + parts = [] + fnparams = sorted( + (key, value) for key, value in params.items() if key.startswith(name_suf) + ) + for num, (key, value) in enumerate(fnparams): + _, tail = key.split("*", 1) + if tail.endswith("*"): + tail = tail[:-1] + if tail == str(num): + parts.append(value) + else: + break + if not parts: + return None + value = "".join(parts) + if "'" in value: + encoding, _, value = value.split("'", 2) + encoding = encoding or "utf-8" + return unquote(value, encoding, "strict") + return value + + +class MultipartResponseWrapper: + """Wrapper around the MultipartReader. + + It takes care about + underlying connection and close it when it needs in. + """ + + def __init__( + self, + resp: "ClientResponse", + stream: "MultipartReader", + ) -> None: + self.resp = resp + self.stream = stream + + def __aiter__(self) -> "MultipartResponseWrapper": + return self + + async def __anext__( + self, + ) -> Union["MultipartReader", "BodyPartReader"]: + part = await self.next() + if part is None: + raise StopAsyncIteration + return part + + def at_eof(self) -> bool: + """Returns True when all response data had been read.""" + return self.resp.content.at_eof() + + async def next( + self, + ) -> Union["MultipartReader", "BodyPartReader"] | None: + """Emits next multipart reader object.""" + item = await self.stream.next() + if self.stream.at_eof(): + await self.release() + return item + + async def release(self) -> None: + """Release the connection gracefully. + + All remaining content is read to the void. + """ + await self.resp.release() + + +class BodyPartReader: + """Multipart reader for single body part.""" + + chunk_size = 8192 + + def __init__( + self, + boundary: bytes, + headers: "CIMultiDictProxy[str]", + content: StreamReader, + *, + subtype: str = "mixed", + default_charset: str | None = None, + max_decompress_size: int = DEFAULT_CHUNK_SIZE, + client_max_size: int = sys.maxsize, + max_size_error_cls: type[Exception] = ValueError, + ) -> None: + self.headers = headers + self._boundary = boundary + self._boundary_len = len(boundary) + 2 # Boundary + \r\n + self._content = content + self._default_charset = default_charset + self._at_eof = False + self._is_form_data = subtype == "form-data" + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 + length = None if self._is_form_data else self.headers.get(CONTENT_LENGTH, None) + self._length = int(length) if length is not None else None + self._read_bytes = 0 + self._unread: deque[bytes] = deque() + self._prev_chunk: bytes | None = None + self._content_eof = 0 + self._cache: dict[str, Any] = {} + self._max_decompress_size = max_decompress_size + self._client_max_size = client_max_size + self._max_size_error_cls = max_size_error_cls + + def __aiter__(self: Self) -> Self: + return self + + async def __anext__(self) -> bytes: + part = await self.next() + if part is None: + raise StopAsyncIteration + return part + + async def next(self) -> bytes | None: + item = await self.read() + if not item: + return None + return item + + async def read(self, *, decode: bool = False) -> bytes: + """Reads body part data. + + decode: Decodes data following by encoding + method from Content-Encoding header. If it missed + data remains untouched + """ + if self._at_eof: + return b"" + data = bytearray() + while not self._at_eof: + data.extend(await self.read_chunk(self.chunk_size)) + if len(data) > self._client_max_size: + raise self._max_size_error_cls(self._client_max_size) + if decode: + decoded_data = bytearray() + async for d in self.decode_iter(data): + decoded_data.extend(d) + if len(decoded_data) > self._client_max_size: + raise self._max_size_error_cls(self._client_max_size) + return decoded_data + return data + + async def read_chunk(self, size: int = chunk_size) -> bytes: + """Reads body part content chunk of the specified size. + + size: chunk size + """ + if self._at_eof: + return b"" + if self._length: + chunk = await self._read_chunk_from_length(size) + else: + chunk = await self._read_chunk_from_stream(size) + + # For the case of base64 data, we must read a fragment of size with a + # remainder of 0 by dividing by 4 for string without symbols \n or \r + encoding = self.headers.get(CONTENT_TRANSFER_ENCODING) + if encoding and encoding.lower() == "base64": + stripped_chunk = b"".join(chunk.split()) + remainder = len(stripped_chunk) % 4 + + while remainder != 0 and not self.at_eof(): + over_chunk_size = 4 - remainder + over_chunk = b"" + + if self._prev_chunk: + over_chunk = self._prev_chunk[:over_chunk_size] + self._prev_chunk = self._prev_chunk[len(over_chunk) :] + + if len(over_chunk) != over_chunk_size: + over_chunk += await self._content.read(4 - len(over_chunk)) + + if not over_chunk: + self._at_eof = True + + stripped_chunk += b"".join(over_chunk.split()) + chunk += over_chunk + remainder = len(stripped_chunk) % 4 + + self._read_bytes += len(chunk) + if self._read_bytes == self._length: + self._at_eof = True + if self._at_eof and await self._content.readline() != b"\r\n": + raise ValueError("Reader did not read all the data or it is malformed") + return chunk + + async def _read_chunk_from_length(self, size: int) -> bytes: + # Reads body part content chunk of the specified size. + # The body part must has Content-Length header with proper value. + assert self._length is not None, "Content-Length required for chunked read" + chunk_size = min(size, self._length - self._read_bytes) + chunk = await self._content.read(chunk_size) + if self._content.at_eof(): + self._at_eof = True + return chunk + + async def _read_chunk_from_stream(self, size: int) -> bytes: + # Reads content chunk of body part with unknown length. + # The Content-Length header for body part is not necessary. + assert ( + size >= self._boundary_len + ), "Chunk size must be greater or equal than boundary length + 2" + first_chunk = self._prev_chunk is None + if first_chunk: + # We need to re-add the CRLF that got removed from headers parsing. + self._prev_chunk = b"\r\n" + await self._content.read(size) + + chunk = b"" + # content.read() may return less than size, so we need to loop to ensure + # we have enough data to detect the boundary. + while len(chunk) < self._boundary_len: + chunk += await self._content.read(size) + self._content_eof += int(self._content.at_eof()) + if self._content_eof > 2: + raise ValueError("Reading after EOF") + if self._content_eof: + break + if len(chunk) > size: + self._content.unread_data(chunk[size:]) + chunk = chunk[:size] + + assert self._prev_chunk is not None + window = self._prev_chunk + chunk + sub = b"\r\n" + self._boundary + if first_chunk: + idx = window.find(sub) + else: + idx = window.find(sub, max(0, len(self._prev_chunk) - len(sub))) + if idx >= 0: + # pushing boundary back to content + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", category=DeprecationWarning) + self._content.unread_data(window[idx:]) + self._prev_chunk = self._prev_chunk[:idx] + chunk = window[len(self._prev_chunk) : idx] + if not chunk: + self._at_eof = True + result = self._prev_chunk[2 if first_chunk else 0 :] # Strip initial CRLF + self._prev_chunk = chunk + return result + + async def readline(self) -> bytes: + """Reads body part by line by line.""" + if self._at_eof: + return b"" + + if self._unread: + line = self._unread.popleft() + else: + line = await self._content.readline() + + if line.startswith(self._boundary): + # the very last boundary may not come with \r\n, + # so set single rules for everyone + sline = line.rstrip(b"\r\n") + boundary = self._boundary + last_boundary = self._boundary + b"--" + # ensure that we read exactly the boundary, not something alike + if sline == boundary or sline == last_boundary: + self._at_eof = True + self._unread.append(line) + return b"" + else: + next_line = await self._content.readline() + if next_line.startswith(self._boundary): + line = line[:-2] # strip CRLF but only once + self._unread.append(next_line) + + return line + + async def release(self) -> None: + """Like read(), but reads all the data to the void.""" + if self._at_eof: + return + while not self._at_eof: + await self.read_chunk(self.chunk_size) + + async def text(self, *, encoding: str | None = None) -> str: + """Like read(), but assumes that body part contains text data.""" + data = await self.read(decode=True) + # see https://www.w3.org/TR/html5/forms.html#multipart/form-data-encoding-algorithm + # and https://dvcs.w3.org/hg/xhr/raw-file/tip/Overview.html#dom-xmlhttprequest-send + encoding = encoding or self.get_charset(default="utf-8") + return data.decode(encoding) + + async def json(self, *, encoding: str | None = None) -> dict[str, Any] | None: + """Like read(), but assumes that body parts contains JSON data.""" + data = await self.read(decode=True) + if not data: + return None + encoding = encoding or self.get_charset(default="utf-8") + return cast(dict[str, Any], json.loads(data.decode(encoding))) + + async def form(self, *, encoding: str | None = None) -> list[tuple[str, str]]: + """Like read(), but assumes that body parts contain form urlencoded data.""" + data = await self.read(decode=True) + if not data: + return [] + if encoding is not None: + real_encoding = encoding + else: + real_encoding = self.get_charset(default="utf-8") + try: + decoded_data = data.rstrip().decode(real_encoding) + except UnicodeDecodeError: + raise ValueError("data cannot be decoded with %s encoding" % real_encoding) + + return parse_qsl( + decoded_data, + keep_blank_values=True, + encoding=real_encoding, + ) + + def at_eof(self) -> bool: + """Returns True if the boundary was reached or False otherwise.""" + return self._at_eof + + def _apply_content_transfer_decoding(self, data: _Buffer) -> _Buffer | bytes: + """Apply Content-Transfer-Encoding decoding if header is present.""" + if CONTENT_TRANSFER_ENCODING in self.headers: + return self._decode_content_transfer(data) + return data + + def _needs_content_decoding(self) -> bool: + """Check if Content-Encoding decoding should be applied.""" + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 + return not self._is_form_data and CONTENT_ENCODING in self.headers + + def decode(self, data: _Buffer) -> _Buffer | bytes: + """Decodes data synchronously. + + Decodes data according the specified Content-Encoding + or Content-Transfer-Encoding headers value. + + Note: For large payloads, consider using decode_iter() instead + to avoid blocking the event loop during decompression. + """ + decoded = self._apply_content_transfer_decoding(data) + if self._needs_content_decoding(): + return self._decode_content(decoded) + return decoded + + async def decode_iter(self, data: _Buffer) -> AsyncIterator[_Buffer | bytes]: + """Async generator that yields decoded data chunks. + + Decodes data according the specified Content-Encoding + or Content-Transfer-Encoding headers value. + + This method offloads decompression to an executor for large payloads + to avoid blocking the event loop. + """ + decoded = self._apply_content_transfer_decoding(data) + if self._needs_content_decoding(): + async for d in self._decode_content_async(decoded): + yield d + else: + yield decoded + + def _decode_content(self, data: _Buffer) -> _Buffer | bytes: + encoding = self.headers.get(CONTENT_ENCODING, "").lower() + if encoding == "identity": + return data + if encoding in {"deflate", "gzip"}: + return ZLibDecompressor( + encoding=encoding, + suppress_deflate_header=True, + ).decompress_sync(data, max_length=self._max_decompress_size) + + raise RuntimeError(f"unknown content encoding: {encoding}") + + async def _decode_content_async( + self, data: _Buffer + ) -> AsyncIterator[_Buffer | bytes]: + encoding = self.headers.get(CONTENT_ENCODING, "").lower() + if encoding == "identity": + yield data + elif encoding in {"deflate", "gzip"}: + d = ZLibDecompressor( + encoding=encoding, + suppress_deflate_header=True, + ) + yield await d.decompress(data, max_length=self._max_decompress_size) + while d.data_available: + yield await d.decompress(b"", max_length=self._max_decompress_size) + else: + raise RuntimeError(f"unknown content encoding: {encoding}") + + def _decode_content_transfer(self, data: _Buffer) -> _Buffer | bytes: + encoding = self.headers.get(CONTENT_TRANSFER_ENCODING, "").lower() + + if encoding == "base64": + return base64.b64decode(data) + elif encoding == "quoted-printable": + return binascii.a2b_qp(data) + elif encoding in ("binary", "8bit", "7bit"): + return data + else: + raise RuntimeError(f"unknown content transfer encoding: {encoding}") + + def get_charset(self, default: str) -> str: + """Returns charset parameter from Content-Type header or default.""" + ctype = self.headers.get(CONTENT_TYPE, "") + mimetype = parse_mimetype(ctype) + return mimetype.parameters.get("charset", self._default_charset or default) + + @reify + def name(self) -> str | None: + """Returns name specified in Content-Disposition header. + + If the header is missing or malformed, returns None. + """ + _, params = parse_content_disposition(self.headers.get(CONTENT_DISPOSITION)) + return content_disposition_filename(params, "name") + + @reify + def filename(self) -> str | None: + """Returns filename specified in Content-Disposition header. + + Returns None if the header is missing or malformed. + """ + _, params = parse_content_disposition(self.headers.get(CONTENT_DISPOSITION)) + return content_disposition_filename(params, "filename") + + +@payload_type(BodyPartReader, order=Order.try_first) +class BodyPartReaderPayload(Payload): + _value: BodyPartReader + # _autoclose = False (inherited) - Streaming reader that may have resources + + def __init__(self, value: BodyPartReader, *args: Any, **kwargs: Any) -> None: + super().__init__(value, *args, **kwargs) + + params: dict[str, str] = {} + if value.name is not None: + params["name"] = value.name + if value.filename is not None: + params["filename"] = value.filename + + if params: + self.set_content_disposition("attachment", True, **params) + + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + raise TypeError("Unable to decode.") + + async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: + """Raises TypeError as body parts should be consumed via write(). + + This is intentional: BodyPartReader payloads are designed for streaming + large data (potentially gigabytes) and must be consumed only once via + the write() method to avoid memory exhaustion. They cannot be buffered + in memory for reuse. + """ + raise TypeError("Unable to read body part as bytes. Use write() to consume.") + + async def write(self, writer: AbstractStreamWriter) -> None: + field = self._value + while chunk := await field.read_chunk(size=DEFAULT_CHUNK_SIZE): + async for d in field.decode_iter(chunk): + await writer.write(d) + + +class MultipartReader: + """Multipart body reader.""" + + #: Response wrapper, used when multipart readers constructs from response. + response_wrapper_cls = MultipartResponseWrapper + #: Multipart reader class, used to handle multipart/* body parts. + #: None points to type(self) + multipart_reader_cls: type["MultipartReader"] | None = None + #: Body part reader class for non multipart/* content types. + part_reader_cls = BodyPartReader + + def __init__( + self, + headers: Mapping[str, str], + content: StreamReader, + *, + client_max_size: int = sys.maxsize, + max_field_size: int = 8190, + max_headers: int = 128, + max_size_error_cls: type[Exception] = ValueError, + ) -> None: + self._mimetype = parse_mimetype(headers[CONTENT_TYPE]) + assert self._mimetype.type == "multipart", "multipart/* content type expected" + if "boundary" not in self._mimetype.parameters: + raise ValueError( + "boundary missed for Content-Type: %s" % headers[CONTENT_TYPE] + ) + + self.headers = headers + self._boundary = ("--" + self._get_boundary()).encode() + self._client_max_size = client_max_size + self._content = content + self._default_charset: str | None = None + self._last_part: MultipartReader | BodyPartReader | None = None + self._max_field_size = max_field_size + self._max_headers = max_headers + self._max_size_error_cls = max_size_error_cls + self._at_eof = False + self._at_bof = True + self._unread: list[bytes] = [] + + def __aiter__(self: Self) -> Self: + return self + + async def __anext__( + self, + ) -> Union["MultipartReader", BodyPartReader] | None: + part = await self.next() + if part is None: + raise StopAsyncIteration + return part + + @classmethod + def from_response( + cls, + response: "ClientResponse", + ) -> MultipartResponseWrapper: + """Constructs reader instance from HTTP response. + + :param response: :class:`~aiohttp.client.ClientResponse` instance + """ + obj = cls.response_wrapper_cls( + response, cls(response.headers, response.content) + ) + return obj + + def at_eof(self) -> bool: + """Returns True if the final boundary was reached, false otherwise.""" + return self._at_eof + + async def next( + self, + ) -> Union["MultipartReader", BodyPartReader] | None: + """Emits the next multipart body part.""" + # So, if we're at BOF, we need to skip till the boundary. + if self._at_eof: + return None + await self._maybe_release_last_part() + if self._at_bof: + await self._read_until_first_boundary() + self._at_bof = False + else: + await self._read_boundary() + if self._at_eof: # we just read the last boundary, nothing to do there + return None + + part = await self.fetch_next_part() + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.6 + if ( + self._last_part is None + and self._mimetype.subtype == "form-data" + and isinstance(part, BodyPartReader) + ): + _, params = parse_content_disposition(part.headers.get(CONTENT_DISPOSITION)) + if params.get("name") == "_charset_": + # Longest encoding in https://encoding.spec.whatwg.org/encodings.json + # is 19 characters, so 32 should be more than enough for any valid encoding. + charset = await part.read_chunk(32) + if len(charset) > 31: + raise RuntimeError("Invalid default charset") + self._default_charset = charset.strip().decode() + part = await self.fetch_next_part() + self._last_part = part + return self._last_part + + async def release(self) -> None: + """Reads all the body parts to the void till the final boundary.""" + while not self._at_eof: + item = await self.next() + if item is None: + break + await item.release() + + async def fetch_next_part( + self, + ) -> Union["MultipartReader", BodyPartReader]: + """Returns the next body part reader.""" + headers = await self._read_headers() + return self._get_part_reader(headers) + + def _get_part_reader( + self, + headers: "CIMultiDictProxy[str]", + ) -> Union["MultipartReader", BodyPartReader]: + """Dispatches the response by the `Content-Type` header. + + Returns a suitable reader instance. + + :param dict headers: Response headers + """ + ctype = headers.get(CONTENT_TYPE, "") + mimetype = parse_mimetype(ctype) + + if mimetype.type == "multipart": + if self.multipart_reader_cls is None: + return type(self)( + headers, + self._content, + client_max_size=self._client_max_size, + max_field_size=self._max_field_size, + max_headers=self._max_headers, + max_size_error_cls=self._max_size_error_cls, + ) + return self.multipart_reader_cls( + headers, + self._content, + client_max_size=self._client_max_size, + max_field_size=self._max_field_size, + max_headers=self._max_headers, + max_size_error_cls=self._max_size_error_cls, + ) + else: + return self.part_reader_cls( + self._boundary, + headers, + self._content, + subtype=self._mimetype.subtype, + default_charset=self._default_charset, + client_max_size=self._client_max_size, + max_size_error_cls=self._max_size_error_cls, + ) + + def _get_boundary(self) -> str: + boundary = self._mimetype.parameters["boundary"] + if len(boundary) > 70: + raise ValueError("boundary %r is too long (70 chars max)" % boundary) + + return boundary + + async def _readline(self) -> bytes: + if self._unread: + return self._unread.pop() + return await self._content.readline() + + async def _read_until_first_boundary(self) -> None: + while True: + chunk = await self._readline() + if chunk == b"": + raise ValueError( + "Could not find starting boundary %r" % (self._boundary) + ) + chunk = chunk.rstrip() + if chunk == self._boundary: + return + elif chunk == self._boundary + b"--": + self._at_eof = True + return + + async def _read_boundary(self) -> None: + chunk = (await self._readline()).rstrip() + if chunk == self._boundary: + pass + elif chunk == self._boundary + b"--": + self._at_eof = True + epilogue = await self._readline() + next_line = await self._readline() + + # the epilogue is expected and then either the end of input or the + # parent multipart boundary, if the parent boundary is found then + # it should be marked as unread and handed to the parent for + # processing + if next_line[:2] == b"--": + self._unread.append(next_line) + # otherwise the request is likely missing an epilogue and both + # lines should be passed to the parent for processing + # (this handles the old behavior gracefully) + else: + self._unread.extend([next_line, epilogue]) + else: + raise ValueError(f"Invalid boundary {chunk!r}, expected {self._boundary!r}") + + async def _read_headers(self) -> "CIMultiDictProxy[str]": + lines = [] + while True: + chunk = await self._content.readline(max_line_length=self._max_field_size) + chunk = chunk.rstrip(b"\r\n") + lines.append(chunk) + if not chunk: + break + if len(lines) > self._max_headers: + raise BadHttpMessage("Too many headers received") + parser = HeadersParser(max_field_size=self._max_field_size) + headers, raw_headers = parser.parse_headers(lines) + return headers + + async def _maybe_release_last_part(self) -> None: + """Ensures that the last read body part is read completely.""" + if self._last_part is not None: + if not self._last_part.at_eof(): + await self._last_part.release() + self._unread.extend(self._last_part._unread) + self._last_part = None + + +_Part = tuple[Payload, str, str] + + +class MultipartWriter(Payload): + """Multipart body writer.""" + + _value: None + # _consumed = False (inherited) - Can be encoded multiple times + _autoclose = True # No file handles, just collects parts in memory + + def __init__(self, subtype: str = "mixed", boundary: str | None = None) -> None: + boundary = boundary if boundary is not None else uuid.uuid4().hex + # The underlying Payload API demands a str (utf-8), not bytes, + # so we need to ensure we don't lose anything during conversion. + # As a result, require the boundary to be ASCII only. + # In both situations. + + try: + self._boundary = boundary.encode("ascii") + except UnicodeEncodeError: + raise ValueError("boundary should contain ASCII only chars") from None + ctype = f"multipart/{subtype}; boundary={self._boundary_value}" + + super().__init__(None, content_type=ctype) + + self._parts: list[_Part] = [] + self._is_form_data = subtype == "form-data" + + def __enter__(self) -> "MultipartWriter": + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + pass + + def __iter__(self) -> Iterator[_Part]: + return iter(self._parts) + + def __len__(self) -> int: + return len(self._parts) + + def __bool__(self) -> bool: + return True + + _valid_tchar_regex = re.compile(rb"\A[!#$%&'*+\-.^_`|~\w]+\Z") + _invalid_qdtext_char_regex = re.compile(rb"[\x00-\x08\x0A-\x1F\x7F]") + + @property + def _boundary_value(self) -> str: + """Wrap boundary parameter value in quotes, if necessary. + + Reads self.boundary and returns a unicode string. + """ + # Refer to RFCs 7231, 7230, 5234. + # + # parameter = token "=" ( token / quoted-string ) + # token = 1*tchar + # quoted-string = DQUOTE *( qdtext / quoted-pair ) DQUOTE + # qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / obs-text + # obs-text = %x80-FF + # quoted-pair = "\" ( HTAB / SP / VCHAR / obs-text ) + # tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" + # / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" + # / DIGIT / ALPHA + # ; any VCHAR, except delimiters + # VCHAR = %x21-7E + value = self._boundary + if re.match(self._valid_tchar_regex, value): + return value.decode("ascii") # cannot fail + + if re.search(self._invalid_qdtext_char_regex, value): + raise ValueError("boundary value contains invalid characters") + + # escape %x5C and %x22 + quoted_value_content = value.replace(b"\\", b"\\\\") + quoted_value_content = quoted_value_content.replace(b'"', b'\\"') + + return '"' + quoted_value_content.decode("ascii") + '"' + + @property + def boundary(self) -> str: + return self._boundary.decode("ascii") + + def append(self, obj: Any, headers: Mapping[str, str] | None = None) -> Payload: + if headers is None: + headers = CIMultiDict() + + if isinstance(obj, Payload): + obj.headers.update(headers) + return self.append_payload(obj) + else: + try: + payload = get_payload(obj, headers=headers) + except LookupError: + raise TypeError("Cannot create payload from %r" % obj) + else: + return self.append_payload(payload) + + def append_payload(self, payload: Payload) -> Payload: + """Adds a new body part to multipart writer.""" + encoding: str | None = None + te_encoding: str | None = None + if self._is_form_data: + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.7 + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.8 + assert ( + not {CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TRANSFER_ENCODING} + & payload.headers.keys() + ) + # Set default Content-Disposition in case user doesn't create one + if CONTENT_DISPOSITION not in payload.headers: + name = f"section-{len(self._parts)}" + payload.set_content_disposition("form-data", name=name) + else: + # compression + encoding = payload.headers.get(CONTENT_ENCODING, "").lower() + if encoding and encoding not in ("deflate", "gzip", "identity"): + raise RuntimeError(f"unknown content encoding: {encoding}") + if encoding == "identity": + encoding = None + + # te encoding + te_encoding = payload.headers.get(CONTENT_TRANSFER_ENCODING, "").lower() + if te_encoding not in ("", "base64", "quoted-printable", "binary"): + raise RuntimeError(f"unknown content transfer encoding: {te_encoding}") + if te_encoding == "binary": + te_encoding = None + + # size + size = payload.size + if size is not None and not (encoding or te_encoding): + payload.headers[CONTENT_LENGTH] = str(size) + + self._parts.append((payload, encoding, te_encoding)) # type: ignore[arg-type] + return payload + + def append_json( + self, obj: Any, headers: Mapping[str, str] | None = None + ) -> Payload: + """Helper to append JSON part.""" + if headers is None: + headers = CIMultiDict() + + return self.append_payload(JsonPayload(obj, headers=headers)) + + def append_form( + self, + obj: Sequence[tuple[str, str]] | Mapping[str, str], + headers: Mapping[str, str] | None = None, + ) -> Payload: + """Helper to append form urlencoded part.""" + assert isinstance(obj, (Sequence, Mapping)) + + if headers is None: + headers = CIMultiDict() + + if isinstance(obj, Mapping): + obj = list(obj.items()) + data = urlencode(obj, doseq=True) + + return self.append_payload( + StringPayload( + data, headers=headers, content_type="application/x-www-form-urlencoded" + ) + ) + + @property + def size(self) -> int | None: + """Size of the payload.""" + total = 0 + for part, encoding, te_encoding in self._parts: + part_size = part.size + if encoding or te_encoding or part_size is None: + return None + + total += int( + 2 + + len(self._boundary) + + 2 + + part_size # b'--'+self._boundary+b'\r\n' + + len(part._binary_headers) + + 2 # b'\r\n' + ) + + total += 2 + len(self._boundary) + 4 # b'--'+self._boundary+b'--\r\n' + return total + + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + """Return string representation of the multipart data. + + WARNING: This method may do blocking I/O if parts contain file payloads. + It should not be called in the event loop. Use as_bytes().decode() instead. + """ + return "".join( + "--" + + self.boundary + + "\r\n" + + part._binary_headers.decode(encoding, errors) + + part.decode() + for part, _e, _te in self._parts + ) + + async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: + """Return bytes representation of the multipart data. + + This method is async-safe and calls as_bytes on underlying payloads. + """ + parts: list[bytes] = [] + + # Process each part + for part, _e, _te in self._parts: + # Add boundary + parts.append(b"--" + self._boundary + b"\r\n") + + # Add headers + parts.append(part._binary_headers) + + # Add payload content using as_bytes for async safety + part_bytes = await part.as_bytes(encoding, errors) + parts.append(part_bytes) + + # Add trailing CRLF + parts.append(b"\r\n") + + # Add closing boundary + parts.append(b"--" + self._boundary + b"--\r\n") + + return b"".join(parts) + + async def write( + self, writer: AbstractStreamWriter, close_boundary: bool = True + ) -> None: + """Write body.""" + for part, encoding, te_encoding in self._parts: + if self._is_form_data: + # https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 + assert CONTENT_DISPOSITION in part.headers + assert "name=" in part.headers[CONTENT_DISPOSITION] + + await writer.write(b"--" + self._boundary + b"\r\n") + await writer.write(part._binary_headers) + + if encoding or te_encoding: + w = MultipartPayloadWriter(writer) + if encoding: + w.enable_compression(encoding) + if te_encoding: + w.enable_encoding(te_encoding) + await part.write(w) # type: ignore[arg-type] + await w.write_eof() + else: + await part.write(writer) + + await writer.write(b"\r\n") + + if close_boundary: + await writer.write(b"--" + self._boundary + b"--\r\n") + + async def close(self) -> None: + """ + Close all part payloads that need explicit closing. + + IMPORTANT: This method must not await anything that might not finish + immediately, as it may be called during cleanup/cancellation. Schedule + any long-running operations without awaiting them. + """ + if self._consumed: + return + self._consumed = True + + # Close all parts that need explicit closing + # We catch and log exceptions to ensure all parts get a chance to close + # we do not use asyncio.gather() here because we are not allowed + # to suspend given we may be called during cleanup + for idx, (part, _, _) in enumerate(self._parts): + if not part.autoclose and not part.consumed: + try: + await part.close() + except Exception as exc: + internal_logger.error( + "Failed to close multipart part %d: %s", idx, exc, exc_info=True + ) + + +class MultipartPayloadWriter: + def __init__(self, writer: AbstractStreamWriter) -> None: + self._writer = writer + self._encoding: str | None = None + self._compress: ZLibCompressor | None = None + self._encoding_buffer: bytearray | None = None + + def enable_encoding(self, encoding: str) -> None: + if encoding == "base64": + self._encoding = encoding + self._encoding_buffer = bytearray() + elif encoding == "quoted-printable": + self._encoding = "quoted-printable" + + def enable_compression( + self, encoding: str = "deflate", strategy: int | None = None + ) -> None: + self._compress = ZLibCompressor( + encoding=encoding, + suppress_deflate_header=True, + strategy=strategy, + ) + + async def write_eof(self) -> None: + if self._compress is not None: + chunk = self._compress.flush() + if chunk: + self._compress = None + await self.write(chunk) + + if self._encoding == "base64": + if self._encoding_buffer: + await self._writer.write(base64.b64encode(self._encoding_buffer)) + + async def write(self, chunk: bytes) -> None: + if self._compress is not None: + if chunk: + chunk = await self._compress.compress(chunk) + if not chunk: + return + + if self._encoding == "base64": + buf = self._encoding_buffer + assert buf is not None + buf.extend(chunk) + + if buf: + div, mod = divmod(len(buf), 3) + enc_chunk, self._encoding_buffer = (buf[: div * 3], buf[div * 3 :]) + if enc_chunk: + b64chunk = base64.b64encode(enc_chunk) + await self._writer.write(b64chunk) + elif self._encoding == "quoted-printable": + await self._writer.write(binascii.b2a_qp(chunk)) + else: + await self._writer.write(chunk) diff --git a/venv/lib/python3.11/site-packages/aiohttp/payload.py b/venv/lib/python3.11/site-packages/aiohttp/payload.py new file mode 100644 index 0000000..b337bcf --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/payload.py @@ -0,0 +1,1138 @@ +import asyncio +import enum +import io +import json +import mimetypes +import os +import sys +import warnings +from abc import ABC, abstractmethod +from collections.abc import Iterable +from itertools import chain +from typing import IO, TYPE_CHECKING, Any, Final, TextIO + +from multidict import CIMultiDict + +from . import hdrs +from .abc import AbstractStreamWriter +from .helpers import ( + _SENTINEL, + DEFAULT_CHUNK_SIZE, + content_disposition_header, + guess_filename, + parse_mimetype, + sentinel, +) +from .http_writer import _safe_header +from .streams import StreamReader +from .typedefs import JSONBytesEncoder, JSONEncoder, _CIMultiDict + +__all__ = ( + "PAYLOAD_REGISTRY", + "get_payload", + "payload_type", + "Payload", + "BytesPayload", + "StringPayload", + "IOBasePayload", + "BytesIOPayload", + "BufferedReaderPayload", + "TextIOPayload", + "StringIOPayload", + "JsonPayload", + "JsonBytesPayload", + "AsyncIterablePayload", +) + +TOO_LARGE_BYTES_BODY: Final[int] = 2**20 # 1 MB +_CLOSE_FUTURES: set[asyncio.Future[None]] = set() + + +class LookupError(Exception): + """Raised when no payload factory is found for the given data type.""" + + +class Order(str, enum.Enum): + normal = "normal" + try_first = "try_first" + try_last = "try_last" + + +def get_payload(data: Any, *args: Any, **kwargs: Any) -> "Payload": + return PAYLOAD_REGISTRY.get(data, *args, **kwargs) + + +def register_payload( + factory: type["Payload"], type: Any, *, order: Order = Order.normal +) -> None: + PAYLOAD_REGISTRY.register(factory, type, order=order) + + +class payload_type: + def __init__(self, type: Any, *, order: Order = Order.normal) -> None: + self.type = type + self.order = order + + def __call__(self, factory: type["Payload"]) -> type["Payload"]: + register_payload(factory, self.type, order=self.order) + return factory + + +PayloadType = type["Payload"] +_PayloadRegistryItem = tuple[PayloadType, Any] + + +class PayloadRegistry: + """Payload registry. + + note: we need zope.interface for more efficient adapter search + """ + + __slots__ = ("_first", "_normal", "_last", "_normal_lookup") + + def __init__(self) -> None: + self._first: list[_PayloadRegistryItem] = [] + self._normal: list[_PayloadRegistryItem] = [] + self._last: list[_PayloadRegistryItem] = [] + self._normal_lookup: dict[Any, PayloadType] = {} + + def get( + self, + data: Any, + *args: Any, + _CHAIN: "type[chain[_PayloadRegistryItem]]" = chain, + **kwargs: Any, + ) -> "Payload": + if self._first: + for factory, type_ in self._first: + if isinstance(data, type_): + return factory(data, *args, **kwargs) + # Try the fast lookup first + if lookup_factory := self._normal_lookup.get(type(data)): + return lookup_factory(data, *args, **kwargs) + # Bail early if its already a Payload + if isinstance(data, Payload): + return data + # Fallback to the slower linear search + for factory, type_ in _CHAIN(self._normal, self._last): + if isinstance(data, type_): + return factory(data, *args, **kwargs) + raise LookupError() + + def register( + self, factory: PayloadType, type: Any, *, order: Order = Order.normal + ) -> None: + if order is Order.try_first: + self._first.append((factory, type)) + elif order is Order.normal: + self._normal.append((factory, type)) + if isinstance(type, Iterable): + for t in type: + self._normal_lookup[t] = factory + else: + self._normal_lookup[type] = factory + elif order is Order.try_last: + self._last.append((factory, type)) + else: + raise ValueError(f"Unsupported order {order!r}") + + +class Payload(ABC): + + _default_content_type: str = "application/octet-stream" + _size: int | None = None + _consumed: bool = False # Default: payload has not been consumed yet + _autoclose: bool = False # Default: assume resource needs explicit closing + + def __init__( + self, + value: Any, + headers: ( + _CIMultiDict | dict[str, str] | Iterable[tuple[str, str]] | None + ) = None, + content_type: str | None | _SENTINEL = sentinel, + filename: str | None = None, + encoding: str | None = None, + **kwargs: Any, + ) -> None: + self._encoding = encoding + self._filename = filename + self._headers: _CIMultiDict = CIMultiDict() + self._value = value + if content_type is not sentinel and content_type is not None: + self._headers[hdrs.CONTENT_TYPE] = content_type + elif self._filename is not None: + if sys.version_info >= (3, 13): + guesser = mimetypes.guess_file_type + else: + guesser = mimetypes.guess_type + content_type = guesser(self._filename)[0] + if content_type is None: + content_type = self._default_content_type + self._headers[hdrs.CONTENT_TYPE] = content_type + else: + self._headers[hdrs.CONTENT_TYPE] = self._default_content_type + if headers: + self._headers.update(headers) + + @property + def size(self) -> int | None: + """Size of the payload in bytes. + + Returns the number of bytes that will be transmitted when the payload + is written. For string payloads, this is the size after encoding to bytes, + not the length of the string. + """ + return self._size + + @property + def filename(self) -> str | None: + """Filename of the payload.""" + return self._filename + + @property + def headers(self) -> _CIMultiDict: + """Custom item headers""" + return self._headers + + @property + def _binary_headers(self) -> bytes: + return ( + "".join( + _safe_header(k) + ": " + _safe_header(v) + "\r\n" + for k, v in self.headers.items() + ).encode("utf-8") + + b"\r\n" + ) + + @property + def encoding(self) -> str | None: + """Payload encoding""" + return self._encoding + + @property + def content_type(self) -> str: + """Content type""" + return self._headers[hdrs.CONTENT_TYPE] + + @property + def consumed(self) -> bool: + """Whether the payload has been consumed and cannot be reused.""" + return self._consumed + + @property + def autoclose(self) -> bool: + """ + Whether the payload can close itself automatically. + + Returns True if the payload has no file handles or resources that need + explicit closing. If False, callers must await close() to release resources. + """ + return self._autoclose + + def set_content_disposition( + self, + disptype: str, + quote_fields: bool = True, + _charset: str = "utf-8", + **params: Any, + ) -> None: + """Sets ``Content-Disposition`` header.""" + self._headers[hdrs.CONTENT_DISPOSITION] = content_disposition_header( + disptype, quote_fields=quote_fields, _charset=_charset, **params + ) + + @abstractmethod + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + """ + Return string representation of the value. + + This is named decode() to allow compatibility with bytes objects. + """ + + @abstractmethod + async def write(self, writer: AbstractStreamWriter) -> None: + """ + Write payload to the writer stream. + + Args: + writer: An AbstractStreamWriter instance that handles the actual writing + + This is a legacy method that writes the entire payload without length constraints. + + Important: + For new implementations, use write_with_length() instead of this method. + This method is maintained for backwards compatibility and will eventually + delegate to write_with_length(writer, None) in all implementations. + + All payload subclasses must override this method for backwards compatibility, + but new code should use write_with_length for more flexibility and control. + + """ + + # write_with_length is new in aiohttp 3.12 + # it should be overridden by subclasses + async def write_with_length( + self, writer: AbstractStreamWriter, content_length: int | None + ) -> None: + """ + Write payload with a specific content length constraint. + + Args: + writer: An AbstractStreamWriter instance that handles the actual writing + content_length: Maximum number of bytes to write (None for unlimited) + + This method allows writing payload content with a specific length constraint, + which is particularly useful for HTTP responses with Content-Length header. + + Note: + This is the base implementation that provides backwards compatibility + for subclasses that don't override this method. Specific payload types + should override this method to implement proper length-constrained writing. + + """ + # Backwards compatibility for subclasses that don't override this method + # and for the default implementation + await self.write(writer) + + async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: + """ + Return bytes representation of the value. + + This is a convenience method that calls decode() and encodes the result + to bytes using the specified encoding. + """ + # Use instance encoding if available, otherwise use parameter + actual_encoding = self._encoding or encoding + return self.decode(actual_encoding, errors).encode(actual_encoding) + + def _close(self) -> None: + """ + Async safe synchronous close operations for backwards compatibility. + + This method exists only for backwards compatibility with code that + needs to clean up payloads synchronously. In the future, we will + drop this method and only support the async close() method. + + WARNING: This method must be safe to call from within the event loop + without blocking. Subclasses should not perform any blocking I/O here. + + WARNING: This method must be called from within an event loop for + certain payload types (e.g., IOBasePayload). Calling it outside an + event loop may raise RuntimeError. + """ + # This is a no-op by default, but subclasses can override it + # for non-blocking cleanup operations. + + async def close(self) -> None: + """ + Close the payload if it holds any resources. + + IMPORTANT: This method must not await anything that might not finish + immediately, as it may be called during cleanup/cancellation. Schedule + any long-running operations without awaiting them. + + In the future, this will be the only close method supported. + """ + self._close() + + +class BytesPayload(Payload): + _value: bytes + # _consumed = False (inherited) - Bytes are immutable and can be reused + _autoclose = True # No file handle, just bytes in memory + + def __init__( + self, value: bytes | bytearray | memoryview, *args: Any, **kwargs: Any + ) -> None: + if "content_type" not in kwargs: + kwargs["content_type"] = "application/octet-stream" + + super().__init__(value, *args, **kwargs) + + if isinstance(value, memoryview): + self._size = value.nbytes + elif isinstance(value, (bytes, bytearray)): + self._size = len(value) + else: + raise TypeError(f"value argument must be byte-ish, not {type(value)!r}") + + if self._size > TOO_LARGE_BYTES_BODY: + kwargs = {"source": self} + warnings.warn( + "Sending a large body directly with raw bytes might" + " lock the event loop. You should probably pass an " + "io.BytesIO object instead", + ResourceWarning, + **kwargs, + ) + + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + return self._value.decode(encoding, errors) + + async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: + """ + Return bytes representation of the value. + + This method returns the raw bytes content of the payload. + It is equivalent to accessing the _value attribute directly. + """ + return self._value + + async def write(self, writer: AbstractStreamWriter) -> None: + """ + Write the entire bytes payload to the writer stream. + + Args: + writer: An AbstractStreamWriter instance that handles the actual writing + + This method writes the entire bytes content without any length constraint. + + Note: + For new implementations that need length control, use write_with_length(). + This method is maintained for backwards compatibility and is equivalent + to write_with_length(writer, None). + + """ + await writer.write(self._value) + + async def write_with_length( + self, writer: AbstractStreamWriter, content_length: int | None + ) -> None: + """ + Write bytes payload with a specific content length constraint. + + Args: + writer: An AbstractStreamWriter instance that handles the actual writing + content_length: Maximum number of bytes to write (None for unlimited) + + This method writes either the entire byte sequence or a slice of it + up to the specified content_length. For BytesPayload, this operation + is performed efficiently using array slicing. + + """ + if content_length is not None: + await writer.write(self._value[:content_length]) + else: + await writer.write(self._value) + + +class StringPayload(BytesPayload): + def __init__( + self, + value: str, + *args: Any, + encoding: str | None = None, + content_type: str | None = None, + **kwargs: Any, + ) -> None: + + if encoding is None: + if content_type is None: + real_encoding = "utf-8" + content_type = "text/plain; charset=utf-8" + else: + mimetype = parse_mimetype(content_type) + real_encoding = mimetype.parameters.get("charset", "utf-8") + else: + if content_type is None: + content_type = "text/plain; charset=%s" % encoding + real_encoding = encoding + + super().__init__( + value.encode(real_encoding), + encoding=real_encoding, + content_type=content_type, + *args, + **kwargs, + ) + + +class StringIOPayload(StringPayload): + def __init__(self, value: IO[str], *args: Any, **kwargs: Any) -> None: + super().__init__(value.read(), *args, **kwargs) + + +class IOBasePayload(Payload): + _value: io.IOBase + # _consumed = False (inherited) - File can be re-read from the same position + _start_position: int | None = None + # _autoclose = False (inherited) - Has file handle that needs explicit closing + + def __init__( + self, value: IO[Any], disposition: str = "attachment", *args: Any, **kwargs: Any + ) -> None: + if "filename" not in kwargs: + kwargs["filename"] = guess_filename(value) + + super().__init__(value, *args, **kwargs) + + if self._filename is not None and disposition is not None: + if hdrs.CONTENT_DISPOSITION not in self.headers: + self.set_content_disposition(disposition, filename=self._filename) + + def _set_or_restore_start_position(self) -> None: + """Set or restore the start position of the file-like object.""" + if self._start_position is None: + try: + self._start_position = self._value.tell() + except (OSError, AttributeError): + self._consumed = True # Cannot seek, mark as consumed + return + try: + self._value.seek(self._start_position) + except (OSError, AttributeError): + # Failed to seek back - mark as consumed since we've already read + self._consumed = True + + def _read_and_available_len( + self, remaining_content_len: int | None + ) -> tuple[int | None, bytes]: + """ + Read the file-like object and return both its total size and the first chunk. + + Args: + remaining_content_len: Optional limit on how many bytes to read in this operation. + If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size. + + Returns: + A tuple containing: + - The total size of the remaining unread content (None if size cannot be determined) + - The first chunk of bytes read from the file object + + This method is optimized to perform both size calculation and initial read + in a single operation, which is executed in a single executor job to minimize + context switches and file operations when streaming content. + + """ + self._set_or_restore_start_position() + size = self.size # Call size only once since it does I/O + return size, self._value.read( + min( + DEFAULT_CHUNK_SIZE, + size or DEFAULT_CHUNK_SIZE, + remaining_content_len or DEFAULT_CHUNK_SIZE, + ) + ) + + def _read(self, remaining_content_len: int | None) -> bytes: + """ + Read a chunk of data from the file-like object. + + Args: + remaining_content_len: Optional maximum number of bytes to read. + If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size. + + Returns: + A chunk of bytes read from the file object, respecting the + remaining_content_len limit if specified. + + This method is used for subsequent reads during streaming after + the initial _read_and_available_len call has been made. + + """ + return self._value.read(remaining_content_len or DEFAULT_CHUNK_SIZE) # type: ignore[no-any-return] + + @property + def size(self) -> int | None: + """ + Size of the payload in bytes. + + Returns the total size of the payload content from the initial position. + This ensures consistent Content-Length for requests, including 307/308 redirects + where the same payload instance is reused. + + Returns None if the size cannot be determined (e.g., for unseekable streams). + """ + try: + # Store the start position on first access. + # This is critical when the same payload instance is reused (e.g., 307/308 + # redirects). Without storing the initial position, after the payload is + # read once, the file position would be at EOF, which would cause the + # size calculation to return 0 (file_size - EOF position). + # By storing the start position, we ensure the size calculation always + # returns the correct total size for any subsequent use. + if self._start_position is None: + self._start_position = self._value.tell() + + # Return the total size from the start position + # This ensures Content-Length is correct even after reading + return os.fstat(self._value.fileno()).st_size - self._start_position + except (AttributeError, OSError): + return None + + async def write(self, writer: AbstractStreamWriter) -> None: + """ + Write the entire file-like payload to the writer stream. + + Args: + writer: An AbstractStreamWriter instance that handles the actual writing + + This method writes the entire file content without any length constraint. + It delegates to write_with_length() with no length limit for implementation + consistency. + + Note: + For new implementations that need length control, use write_with_length() directly. + This method is maintained for backwards compatibility with existing code. + + """ + await self.write_with_length(writer, None) + + async def write_with_length( + self, writer: AbstractStreamWriter, content_length: int | None + ) -> None: + """ + Write file-like payload with a specific content length constraint. + + Args: + writer: An AbstractStreamWriter instance that handles the actual writing + content_length: Maximum number of bytes to write (None for unlimited) + + This method implements optimized streaming of file content with length constraints: + + 1. File reading is performed in a thread pool to avoid blocking the event loop + 2. Content is read and written in chunks to maintain memory efficiency + 3. Writing stops when either: + - All available file content has been written (when size is known) + - The specified content_length has been reached + 4. File resources are properly closed even if the operation is cancelled + + The implementation carefully handles both known-size and unknown-size payloads, + as well as constrained and unconstrained content lengths. + + """ + loop = asyncio.get_running_loop() + total_written_len = 0 + remaining_content_len = content_length + + # Get initial data and available length + available_len, chunk = await loop.run_in_executor( + None, self._read_and_available_len, remaining_content_len + ) + # Process data chunks until done + while chunk: + chunk_len = len(chunk) + + # Write data with or without length constraint + if remaining_content_len is None: + await writer.write(chunk) + else: + await writer.write(chunk[:remaining_content_len]) + remaining_content_len -= chunk_len + + total_written_len += chunk_len + + # Check if we're done writing + if self._should_stop_writing( + available_len, total_written_len, remaining_content_len + ): + return + + # Read next chunk + chunk = await loop.run_in_executor( + None, + self._read, + ( + min(DEFAULT_CHUNK_SIZE, remaining_content_len) + if remaining_content_len is not None + else DEFAULT_CHUNK_SIZE + ), + ) + + def _should_stop_writing( + self, + available_len: int | None, + total_written_len: int, + remaining_content_len: int | None, + ) -> bool: + """ + Determine if we should stop writing data. + + Args: + available_len: Known size of the payload if available (None if unknown) + total_written_len: Number of bytes already written + remaining_content_len: Remaining bytes to be written for content-length limited responses + + Returns: + True if we should stop writing data, based on either: + - Having written all available data (when size is known) + - Having written all requested content (when content-length is specified) + + """ + return (available_len is not None and total_written_len >= available_len) or ( + remaining_content_len is not None and remaining_content_len <= 0 + ) + + def _close(self) -> None: + """ + Async safe synchronous close operations for backwards compatibility. + + This method exists only for backwards + compatibility. Use the async close() method instead. + + WARNING: This method MUST be called from within an event loop. + Calling it outside an event loop will raise RuntimeError. + """ + # Skip if already consumed + if self._consumed: + return + self._consumed = True # Mark as consumed to prevent further writes + # Schedule file closing without awaiting to prevent cancellation issues + loop = asyncio.get_running_loop() + close_future = loop.run_in_executor(None, self._value.close) + # Hold a strong reference to the future to prevent it from being + # garbage collected before it completes. + _CLOSE_FUTURES.add(close_future) + close_future.add_done_callback(_CLOSE_FUTURES.remove) + + async def close(self) -> None: + """ + Close the payload if it holds any resources. + + IMPORTANT: This method must not await anything that might not finish + immediately, as it may be called during cleanup/cancellation. Schedule + any long-running operations without awaiting them. + """ + self._close() + + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + """ + Return string representation of the value. + + WARNING: This method does blocking I/O and should not be called in the event loop. + """ + return self._read_all().decode(encoding, errors) + + def _read_all(self) -> bytes: + """Read the entire file-like object and return its content as bytes.""" + self._set_or_restore_start_position() + # Use readlines() to ensure we get all content + return b"".join(self._value.readlines()) + + async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: + """ + Return bytes representation of the value. + + This method reads the entire file content and returns it as bytes. + It is equivalent to reading the file-like object directly. + The file reading is performed in an executor to avoid blocking the event loop. + """ + loop = asyncio.get_running_loop() + return await loop.run_in_executor(None, self._read_all) + + +class TextIOPayload(IOBasePayload): + _value: io.TextIOBase + # _autoclose = False (inherited) - Has text file handle that needs explicit closing + + def __init__( + self, + value: TextIO, + *args: Any, + encoding: str | None = None, + content_type: str | None = None, + **kwargs: Any, + ) -> None: + + if encoding is None: + if content_type is None: + encoding = "utf-8" + content_type = "text/plain; charset=utf-8" + else: + mimetype = parse_mimetype(content_type) + encoding = mimetype.parameters.get("charset", "utf-8") + else: + if content_type is None: + content_type = "text/plain; charset=%s" % encoding + + super().__init__( + value, + content_type=content_type, + encoding=encoding, + *args, + **kwargs, + ) + + def _read_and_available_len( + self, remaining_content_len: int | None + ) -> tuple[int | None, bytes]: + """ + Read the text file-like object and return both its total size and the first chunk. + + Args: + remaining_content_len: Optional limit on how many bytes to read in this operation. + If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size. + + Returns: + A tuple containing: + - The total size of the remaining unread content (None if size cannot be determined) + - The first chunk of bytes read from the file object, encoded using the payload's encoding + + This method is optimized to perform both size calculation and initial read + in a single operation, which is executed in a single executor job to minimize + context switches and file operations when streaming content. + + Note: + TextIOPayload handles encoding of the text content before writing it + to the stream. If no encoding is specified, UTF-8 is used as the default. + + """ + self._set_or_restore_start_position() + size = self.size + chunk = self._value.read( + min( + DEFAULT_CHUNK_SIZE, + size or DEFAULT_CHUNK_SIZE, + remaining_content_len or DEFAULT_CHUNK_SIZE, + ) + ) + return size, chunk.encode(self._encoding) if self._encoding else chunk.encode() + + def _read(self, remaining_content_len: int | None) -> bytes: + """ + Read a chunk of data from the text file-like object. + + Args: + remaining_content_len: Optional maximum number of bytes to read. + If None, DEFAULT_CHUNK_SIZE will be used as the default chunk size. + + Returns: + A chunk of bytes read from the file object and encoded using the payload's + encoding. The data is automatically converted from text to bytes. + + This method is used for subsequent reads during streaming after + the initial _read_and_available_len call has been made. It properly + handles text encoding, converting the text content to bytes using + the specified encoding (or UTF-8 if none was provided). + + """ + chunk = self._value.read(remaining_content_len or DEFAULT_CHUNK_SIZE) + return chunk.encode(self._encoding) if self._encoding else chunk.encode() + + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + """ + Return string representation of the value. + + WARNING: This method does blocking I/O and should not be called in the event loop. + """ + self._set_or_restore_start_position() + return self._value.read() + + async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: + """ + Return bytes representation of the value. + + This method reads the entire text file content and returns it as bytes. + It encodes the text content using the specified encoding. + The file reading is performed in an executor to avoid blocking the event loop. + """ + loop = asyncio.get_running_loop() + + # Use instance encoding if available, otherwise use parameter + actual_encoding = self._encoding or encoding + + def _read_and_encode() -> bytes: + self._set_or_restore_start_position() + # TextIO read() always returns the full content + return self._value.read().encode(actual_encoding, errors) + + return await loop.run_in_executor(None, _read_and_encode) + + +class BytesIOPayload(IOBasePayload): + _value: io.BytesIO + _size: int # Always initialized in __init__ + _autoclose = True # BytesIO is in-memory, safe to auto-close + + def __init__(self, value: io.BytesIO, *args: Any, **kwargs: Any) -> None: + super().__init__(value, *args, **kwargs) + # Calculate size once during initialization + self._size = len(self._value.getbuffer()) - self._value.tell() + + @property + def size(self) -> int: + """Size of the payload in bytes. + + Returns the number of bytes in the BytesIO buffer that will be transmitted. + This is calculated once during initialization for efficiency. + """ + return self._size + + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + self._set_or_restore_start_position() + return self._value.read().decode(encoding, errors) + + async def write(self, writer: AbstractStreamWriter) -> None: + return await self.write_with_length(writer, None) + + async def write_with_length( + self, writer: AbstractStreamWriter, content_length: int | None + ) -> None: + """ + Write BytesIO payload with a specific content length constraint. + + Args: + writer: An AbstractStreamWriter instance that handles the actual writing + content_length: Maximum number of bytes to write (None for unlimited) + + This implementation is specifically optimized for BytesIO objects: + + 1. Reads content in chunks to maintain memory efficiency + 2. Yields control back to the event loop periodically to prevent blocking + when dealing with large BytesIO objects + 3. Respects content_length constraints when specified + 4. Properly cleans up by closing the BytesIO object when done or on error + + The periodic yielding to the event loop is important for maintaining + responsiveness when processing large in-memory buffers. + + """ + self._set_or_restore_start_position() + loop_count = 0 + remaining_bytes = content_length + while chunk := self._value.read(DEFAULT_CHUNK_SIZE): + if loop_count > 0: + # Avoid blocking the event loop + # if they pass a large BytesIO object + # and we are not in the first iteration + # of the loop + await asyncio.sleep(0) + if remaining_bytes is None: + await writer.write(chunk) + else: + await writer.write(chunk[:remaining_bytes]) + remaining_bytes -= len(chunk) + if remaining_bytes <= 0: + return + loop_count += 1 + + async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: + """ + Return bytes representation of the value. + + This method reads the entire BytesIO content and returns it as bytes. + It is equivalent to accessing the _value attribute directly. + """ + self._set_or_restore_start_position() + return self._value.read() + + async def close(self) -> None: + """ + Close the BytesIO payload. + + This does nothing since BytesIO is in-memory and does not require explicit closing. + """ + + +class BufferedReaderPayload(IOBasePayload): + _value: io.BufferedIOBase + # _autoclose = False (inherited) - Has buffered file handle that needs explicit closing + + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + self._set_or_restore_start_position() + return self._value.read().decode(encoding, errors) + + +class JsonPayload(BytesPayload): + def __init__( + self, + value: Any, + encoding: str = "utf-8", + content_type: str = "application/json", + dumps: JSONEncoder = json.dumps, + *args: Any, + **kwargs: Any, + ) -> None: + + super().__init__( + dumps(value).encode(encoding), + content_type=content_type, + encoding=encoding, + *args, + **kwargs, + ) + + +class JsonBytesPayload(BytesPayload): + """JSON payload for encoders that return bytes directly. + + Use this when your JSON encoder (like orjson) returns bytes + instead of str, avoiding the encode/decode overhead. + """ + + def __init__( + self, + value: Any, + dumps: JSONBytesEncoder, + content_type: str = "application/json", + *args: Any, + **kwargs: Any, + ) -> None: + super().__init__( + dumps(value), + content_type=content_type, + *args, + **kwargs, + ) + + +if TYPE_CHECKING: + from collections.abc import AsyncIterable, AsyncIterator + + _AsyncIterator = AsyncIterator[bytes] + _AsyncIterable = AsyncIterable[bytes] +else: + from collections.abc import AsyncIterable, AsyncIterator + + _AsyncIterator = AsyncIterator + _AsyncIterable = AsyncIterable + + +class AsyncIterablePayload(Payload): + + _iter: _AsyncIterator | None = None + _value: _AsyncIterable + _cached_chunks: list[bytes] | None = None + # _consumed stays False to allow reuse with cached content + _autoclose = True # Iterator doesn't need explicit closing + + def __init__(self, value: _AsyncIterable, *args: Any, **kwargs: Any) -> None: + if not isinstance(value, AsyncIterable): + raise TypeError( + "value argument must support " + "collections.abc.AsyncIterable interface, " + f"got {type(value)!r}" + ) + + if "content_type" not in kwargs: + kwargs["content_type"] = "application/octet-stream" + + super().__init__(value, *args, **kwargs) + + self._iter = value.__aiter__() + + async def write(self, writer: AbstractStreamWriter) -> None: + """ + Write the entire async iterable payload to the writer stream. + + Args: + writer: An AbstractStreamWriter instance that handles the actual writing + + This method iterates through the async iterable and writes each chunk + to the writer without any length constraint. + + Note: + For new implementations that need length control, use write_with_length() directly. + This method is maintained for backwards compatibility with existing code. + + """ + await self.write_with_length(writer, None) + + async def write_with_length( + self, writer: AbstractStreamWriter, content_length: int | None + ) -> None: + """ + Write async iterable payload with a specific content length constraint. + + Args: + writer: An AbstractStreamWriter instance that handles the actual writing + content_length: Maximum number of bytes to write (None for unlimited) + + This implementation handles streaming of async iterable content with length constraints: + + 1. If cached chunks are available, writes from them + 2. Otherwise iterates through the async iterable one chunk at a time + 3. Respects content_length constraints when specified + 4. Does NOT generate cache - that's done by as_bytes() + + """ + # If we have cached chunks, use them + if self._cached_chunks is not None: + remaining_bytes = content_length + for chunk in self._cached_chunks: + if remaining_bytes is None: + await writer.write(chunk) + elif remaining_bytes > 0: + await writer.write(chunk[:remaining_bytes]) + remaining_bytes -= len(chunk) + else: + break + return + + # If iterator is exhausted and we don't have cached chunks, nothing to write + if self._iter is None: + return + + # Stream from the iterator + remaining_bytes = content_length + + try: + while True: + chunk = await anext(self._iter) + if remaining_bytes is None: + await writer.write(chunk) + # If we have a content length limit + elif remaining_bytes > 0: + await writer.write(chunk[:remaining_bytes]) + remaining_bytes -= len(chunk) + # We still want to exhaust the iterator even + # if we have reached the content length limit + # since the file handle may not get closed by + # the iterator if we don't do this + except StopAsyncIteration: + # Iterator is exhausted + self._iter = None + self._consumed = True # Mark as consumed when streamed without caching + + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + """Decode the payload content as a string if cached chunks are available.""" + if self._cached_chunks is not None: + return b"".join(self._cached_chunks).decode(encoding, errors) + raise TypeError("Unable to decode - content not cached. Call as_bytes() first.") + + async def as_bytes(self, encoding: str = "utf-8", errors: str = "strict") -> bytes: + """ + Return bytes representation of the value. + + This method reads the entire async iterable content and returns it as bytes. + It generates and caches the chunks for future reuse. + """ + # If we have cached chunks, return them joined + if self._cached_chunks is not None: + return b"".join(self._cached_chunks) + + # If iterator is exhausted and no cache, return empty + if self._iter is None: + return b"" + + # Read all chunks and cache them + chunks: list[bytes] = [] + async for chunk in self._iter: + chunks.append(chunk) + + # Iterator is exhausted, cache the chunks + self._iter = None + self._cached_chunks = chunks + # Keep _consumed as False to allow reuse with cached chunks + + return b"".join(chunks) + + +class StreamReaderPayload(AsyncIterablePayload): + def __init__(self, value: StreamReader, *args: Any, **kwargs: Any) -> None: + super().__init__(value.iter_any(), *args, **kwargs) + + +PAYLOAD_REGISTRY = PayloadRegistry() +PAYLOAD_REGISTRY.register(BytesPayload, (bytes, bytearray, memoryview)) +PAYLOAD_REGISTRY.register(StringPayload, str) +PAYLOAD_REGISTRY.register(StringIOPayload, io.StringIO) +PAYLOAD_REGISTRY.register(TextIOPayload, io.TextIOBase) +PAYLOAD_REGISTRY.register(BytesIOPayload, io.BytesIO) +PAYLOAD_REGISTRY.register(BufferedReaderPayload, (io.BufferedReader, io.BufferedRandom)) +PAYLOAD_REGISTRY.register(IOBasePayload, io.IOBase) +PAYLOAD_REGISTRY.register(StreamReaderPayload, StreamReader) +# try_last for giving a chance to more specialized async interables like +# multipart.BodyPartReaderPayload override the default +PAYLOAD_REGISTRY.register(AsyncIterablePayload, AsyncIterable, order=Order.try_last) diff --git a/venv/lib/python3.11/site-packages/aiohttp/payload_streamer.py b/venv/lib/python3.11/site-packages/aiohttp/payload_streamer.py new file mode 100644 index 0000000..67afedf --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/payload_streamer.py @@ -0,0 +1,79 @@ +""" +Payload implementation for coroutines as data provider. + +As a simple case, you can upload data from file:: + + @aiohttp.streamer + async def file_sender(writer, file_name=None): + with open(file_name, 'rb') as f: + chunk = f.read(2**16) + while chunk: + await writer.write(chunk) + + chunk = f.read(2**16) + +Then you can use `file_sender` like this: + + async with session.post('http://httpbin.org/post', + data=file_sender(file_name='huge_file')) as resp: + print(await resp.text()) + +..note:: Coroutine must accept `writer` as first argument + +""" + +import types +import warnings +from collections.abc import Awaitable, Callable +from typing import Any + +from .abc import AbstractStreamWriter +from .payload import Payload, payload_type + +__all__ = ("streamer",) + + +class _stream_wrapper: + def __init__( + self, + coro: Callable[..., Awaitable[None]], + args: tuple[Any, ...], + kwargs: dict[str, Any], + ) -> None: + self.coro = types.coroutine(coro) + self.args = args + self.kwargs = kwargs + + async def __call__(self, writer: AbstractStreamWriter) -> None: + await self.coro(writer, *self.args, **self.kwargs) + + +class streamer: + def __init__(self, coro: Callable[..., Awaitable[None]]) -> None: + warnings.warn( + "@streamer is deprecated, use async generators instead", + DeprecationWarning, + stacklevel=2, + ) + self.coro = coro + + def __call__(self, *args: Any, **kwargs: Any) -> _stream_wrapper: + return _stream_wrapper(self.coro, args, kwargs) + + +@payload_type(_stream_wrapper) +class StreamWrapperPayload(Payload): + async def write(self, writer: AbstractStreamWriter) -> None: + await self._value(writer) + + def decode(self, encoding: str = "utf-8", errors: str = "strict") -> str: + raise TypeError("Unable to decode.") + + +@payload_type(streamer) +class StreamPayload(StreamWrapperPayload): + def __init__(self, value: Any, *args: Any, **kwargs: Any) -> None: + super().__init__(value(), *args, **kwargs) + + async def write(self, writer: AbstractStreamWriter) -> None: + await self._value(writer) diff --git a/venv/lib/python3.11/site-packages/aiohttp/py.typed b/venv/lib/python3.11/site-packages/aiohttp/py.typed new file mode 100644 index 0000000..f5642f7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/py.typed @@ -0,0 +1 @@ +Marker diff --git a/venv/lib/python3.11/site-packages/aiohttp/pytest_plugin.py b/venv/lib/python3.11/site-packages/aiohttp/pytest_plugin.py new file mode 100644 index 0000000..a2c7bdc --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/pytest_plugin.py @@ -0,0 +1,439 @@ +import asyncio +import contextlib +import inspect +import warnings +from collections.abc import Awaitable, Callable, Iterator +from typing import Any, Protocol, overload + +import pytest + +from .test_utils import ( + BaseTestServer, + RawTestServer, + TestClient, + TestServer, + loop_context, + setup_test_loop, + teardown_test_loop, + unused_port as _unused_port, +) +from .web import Application, BaseRequest, Request +from .web_protocol import _RequestHandler + +try: + import uvloop +except ImportError: # pragma: no cover + uvloop = None # type: ignore[assignment] + + +class AiohttpClient(Protocol): + @overload + async def __call__( + self, + __param: Application, + *, + server_kwargs: dict[str, Any] | None = None, + **kwargs: Any, + ) -> TestClient[Request, Application]: ... + @overload + async def __call__( + self, + __param: BaseTestServer, + *, + server_kwargs: dict[str, Any] | None = None, + **kwargs: Any, + ) -> TestClient[BaseRequest, None]: ... + + +class AiohttpServer(Protocol): + def __call__( + self, app: Application, *, port: int | None = None, **kwargs: Any + ) -> Awaitable[TestServer]: ... + + +class AiohttpRawServer(Protocol): + def __call__( + self, handler: _RequestHandler, *, port: int | None = None, **kwargs: Any + ) -> Awaitable[RawTestServer]: ... + + +def pytest_addoption(parser): # type: ignore[no-untyped-def] + parser.addoption( + "--aiohttp-fast", + action="store_true", + default=False, + help="run tests faster by disabling extra checks", + ) + parser.addoption( + "--aiohttp-loop", + action="store", + default="pyloop", + help="run tests with specific loop: pyloop, uvloop or all", + ) + parser.addoption( + "--aiohttp-enable-loop-debug", + action="store_true", + default=False, + help="enable event loop debug mode", + ) + + +def pytest_fixture_setup(fixturedef): # type: ignore[no-untyped-def] + """Set up pytest fixture. + + Allow fixtures to be coroutines. Run coroutine fixtures in an event loop. + """ + func = fixturedef.func + + if inspect.isasyncgenfunction(func): + # async generator fixture + is_async_gen = True + elif inspect.iscoroutinefunction(func): + # regular async fixture + is_async_gen = False + else: + # not an async fixture, nothing to do + return + + strip_request = False + if "request" not in fixturedef.argnames: + fixturedef.argnames += ("request",) + strip_request = True + + def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] + request = kwargs["request"] + if strip_request: + del kwargs["request"] + + # if neither the fixture nor the test use the 'loop' fixture, + # 'getfixturevalue' will fail because the test is not parameterized + # (this can be removed someday if 'loop' is no longer parameterized) + if "loop" not in request.fixturenames: + raise Exception( + "Asynchronous fixtures must depend on the 'loop' fixture or " + "be used in tests depending from it." + ) + + _loop = request.getfixturevalue("loop") + + if is_async_gen: + # for async generators, we need to advance the generator once, + # then advance it again in a finalizer + gen = func(*args, **kwargs) + + def finalizer(): # type: ignore[no-untyped-def] + try: + return _loop.run_until_complete(gen.__anext__()) + except StopAsyncIteration: + pass + + request.addfinalizer(finalizer) + return _loop.run_until_complete(gen.__anext__()) + else: + return _loop.run_until_complete(func(*args, **kwargs)) + + fixturedef.func = wrapper + + +@pytest.fixture +def fast(request): # type: ignore[no-untyped-def] + """--fast config option""" + return request.config.getoption("--aiohttp-fast") + + +@pytest.fixture +def loop_debug(request): # type: ignore[no-untyped-def] + """--enable-loop-debug config option""" + return request.config.getoption("--aiohttp-enable-loop-debug") + + +@contextlib.contextmanager +def _runtime_warning_context(): # type: ignore[no-untyped-def] + """Context manager which checks for RuntimeWarnings. + + This exists specifically to + avoid "coroutine 'X' was never awaited" warnings being missed. + + If RuntimeWarnings occur in the context a RuntimeError is raised. + """ + with warnings.catch_warnings(record=True) as _warnings: + yield + rw = [ + f"{w.filename}:{w.lineno}:{w.message}" + for w in _warnings + if w.category == RuntimeWarning + ] + if rw: + raise RuntimeError( + "{} Runtime Warning{},\n{}".format( + len(rw), "" if len(rw) == 1 else "s", "\n".join(rw) + ) + ) + + +@contextlib.contextmanager +def _passthrough_loop_context(loop, fast=False): # type: ignore[no-untyped-def] + """Passthrough loop context. + + Sets up and tears down a loop unless one is passed in via the loop + argument when it's passed straight through. + """ + if loop: + # loop already exists, pass it straight through + yield loop + else: + # this shadows loop_context's standard behavior + loop = setup_test_loop() + yield loop + teardown_test_loop(loop, fast=fast) + + +def pytest_pycollect_makeitem(collector, name, obj): # type: ignore[no-untyped-def] + """Fix pytest collecting for coroutines.""" + if collector.funcnamefilter(name) and inspect.iscoroutinefunction(obj): + return list(collector._genfunctions(name, obj)) + + +def pytest_pyfunc_call(pyfuncitem): # type: ignore[no-untyped-def] + """Run coroutines in an event loop instead of a normal function call.""" + fast = pyfuncitem.config.getoption("--aiohttp-fast") + if inspect.iscoroutinefunction(pyfuncitem.function): + warnings.warn( + "aiohttp.pytest_plugin will be removed in v4. Please install pytest-aiohttp.", + DeprecationWarning, + ) + existing_loop = ( + pyfuncitem.funcargs.get("proactor_loop") + or pyfuncitem.funcargs.get("selector_loop") + or pyfuncitem.funcargs.get("uvloop_loop") + or pyfuncitem.funcargs.get("loop", None) + ) + + with _runtime_warning_context(): + with _passthrough_loop_context(existing_loop, fast=fast) as _loop: + testargs = { + arg: pyfuncitem.funcargs[arg] + for arg in pyfuncitem._fixtureinfo.argnames + } + _loop.run_until_complete(pyfuncitem.obj(**testargs)) + + return True + + +def pytest_generate_tests(metafunc): # type: ignore[no-untyped-def] + if "loop_factory" not in metafunc.fixturenames: + return + + loops = metafunc.config.option.aiohttp_loop + avail_factories: dict[str, Callable[[], asyncio.AbstractEventLoop]] + avail_factories = {"pyloop": asyncio.new_event_loop} + + if uvloop is not None: # pragma: no cover + avail_factories["uvloop"] = uvloop.new_event_loop + + if loops == "all": + loops = "pyloop,uvloop?" + + factories = {} # type: ignore[var-annotated] + for name in loops.split(","): + required = not name.endswith("?") + name = name.strip(" ?") + if name not in avail_factories: # pragma: no cover + if required: + raise ValueError( + "Unknown loop '%s', available loops: %s" + % (name, list(factories.keys())) + ) + else: + continue + factories[name] = avail_factories[name] + metafunc.parametrize( + "loop_factory", list(factories.values()), ids=list(factories.keys()) + ) + + +@pytest.fixture +def loop( + loop_factory: Callable[[], asyncio.AbstractEventLoop], + fast: bool, + loop_debug: bool, +) -> Iterator[asyncio.AbstractEventLoop]: + """Return an instance of the event loop.""" + with loop_context(loop_factory, fast=fast) as _loop: + if loop_debug: + _loop.set_debug(True) # pragma: no cover + asyncio.set_event_loop(_loop) + yield _loop + + +@pytest.fixture +def proactor_loop() -> Iterator[asyncio.AbstractEventLoop]: + factory = asyncio.ProactorEventLoop # type: ignore[attr-defined] + + with loop_context(factory) as _loop: + asyncio.set_event_loop(_loop) + yield _loop + + +@pytest.fixture +def unused_port(aiohttp_unused_port: Callable[[], int]) -> Callable[[], int]: + warnings.warn( + "Deprecated, use aiohttp_unused_port fixture instead", + DeprecationWarning, + stacklevel=2, + ) + return aiohttp_unused_port + + +@pytest.fixture +def aiohttp_unused_port() -> Callable[[], int]: + """Return a port that is unused on the current host.""" + return _unused_port + + +@pytest.fixture +def aiohttp_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpServer]: + """Factory to create a TestServer instance, given an app. + + aiohttp_server(app, **kwargs) + """ + servers = [] + + async def go( + app: Application, + *, + host: str = "127.0.0.1", + port: int | None = None, + **kwargs: Any, + ) -> TestServer: + server = TestServer(app, host=host, port=port) + await server.start_server(loop=loop, **kwargs) + servers.append(server) + return server + + yield go + + async def finalize() -> None: + while servers: + await servers.pop().close() + + loop.run_until_complete(finalize()) + + +@pytest.fixture +def test_server(aiohttp_server): # type: ignore[no-untyped-def] # pragma: no cover + warnings.warn( + "Deprecated, use aiohttp_server fixture instead", + DeprecationWarning, + stacklevel=2, + ) + return aiohttp_server + + +@pytest.fixture +def aiohttp_raw_server(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpRawServer]: + """Factory to create a RawTestServer instance, given a web handler. + + aiohttp_raw_server(handler, **kwargs) + """ + servers = [] + + async def go( + handler: _RequestHandler, *, port: int | None = None, **kwargs: Any + ) -> RawTestServer: + server = RawTestServer(handler, port=port) + await server.start_server(loop=loop, **kwargs) + servers.append(server) + return server + + yield go + + async def finalize() -> None: + while servers: + await servers.pop().close() + + loop.run_until_complete(finalize()) + + +@pytest.fixture +def raw_test_server( # type: ignore[no-untyped-def] # pragma: no cover + aiohttp_raw_server, +): + warnings.warn( + "Deprecated, use aiohttp_raw_server fixture instead", + DeprecationWarning, + stacklevel=2, + ) + return aiohttp_raw_server + + +@pytest.fixture +def aiohttp_client(loop: asyncio.AbstractEventLoop) -> Iterator[AiohttpClient]: + """Factory to create a TestClient instance. + + aiohttp_client(app, **kwargs) + aiohttp_client(server, **kwargs) + aiohttp_client(raw_server, **kwargs) + """ + clients = [] + + @overload + async def go( + __param: Application, + *, + server_kwargs: dict[str, Any] | None = None, + **kwargs: Any, + ) -> TestClient[Request, Application]: ... + + @overload + async def go( + __param: BaseTestServer, + *, + server_kwargs: dict[str, Any] | None = None, + **kwargs: Any, + ) -> TestClient[BaseRequest, None]: ... + + async def go( + __param: Application | BaseTestServer, + *args: Any, + server_kwargs: dict[str, Any] | None = None, + **kwargs: Any, + ) -> TestClient[Any, Any]: + if isinstance(__param, Callable) and not isinstance( # type: ignore[arg-type] + __param, (Application, BaseTestServer) + ): + __param = __param(loop, *args, **kwargs) + kwargs = {} + else: + assert not args, "args should be empty" + + if isinstance(__param, Application): + server_kwargs = server_kwargs or {} + server = TestServer(__param, loop=loop, **server_kwargs) + client = TestClient(server, loop=loop, **kwargs) + elif isinstance(__param, BaseTestServer): + client = TestClient(__param, loop=loop, **kwargs) + else: + raise ValueError("Unknown argument type: %r" % type(__param)) + + await client.start_server() + clients.append(client) + return client + + yield go + + async def finalize() -> None: + while clients: + await clients.pop().close() + + loop.run_until_complete(finalize()) + + +@pytest.fixture +def test_client(aiohttp_client): # type: ignore[no-untyped-def] # pragma: no cover + warnings.warn( + "Deprecated, use aiohttp_client fixture instead", + DeprecationWarning, + stacklevel=2, + ) + return aiohttp_client diff --git a/venv/lib/python3.11/site-packages/aiohttp/resolver.py b/venv/lib/python3.11/site-packages/aiohttp/resolver.py new file mode 100644 index 0000000..84b5ffb --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/resolver.py @@ -0,0 +1,273 @@ +import asyncio +import socket +import weakref +from typing import Any, Final, Optional + +from .abc import AbstractResolver, ResolveResult + +__all__ = ("ThreadedResolver", "AsyncResolver", "DefaultResolver") + + +try: + import aiodns + + aiodns_default = hasattr(aiodns.DNSResolver, "getaddrinfo") +except ImportError: # pragma: no cover + aiodns = None # type: ignore[assignment] + aiodns_default = False + + +_NUMERIC_SOCKET_FLAGS = socket.AI_NUMERICHOST | socket.AI_NUMERICSERV +_NAME_SOCKET_FLAGS = socket.NI_NUMERICHOST | socket.NI_NUMERICSERV +_AI_ADDRCONFIG = socket.AI_ADDRCONFIG +if hasattr(socket, "AI_MASK"): + _AI_ADDRCONFIG &= socket.AI_MASK + + +class ThreadedResolver(AbstractResolver): + """Threaded resolver. + + Uses an Executor for synchronous getaddrinfo() calls. + concurrent.futures.ThreadPoolExecutor is used by default. + """ + + def __init__(self, loop: asyncio.AbstractEventLoop | None = None) -> None: + self._loop = loop or asyncio.get_running_loop() + + async def resolve( + self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET + ) -> list[ResolveResult]: + infos = await self._loop.getaddrinfo( + host, + port, + type=socket.SOCK_STREAM, + family=family, + flags=_AI_ADDRCONFIG, + ) + + hosts: list[ResolveResult] = [] + for family, _, proto, _, address in infos: + if family == socket.AF_INET6: + if len(address) < 3: + # IPv6 is not supported by Python build, + # or IPv6 is not enabled in the host + continue + if address[3]: + # This is essential for link-local IPv6 addresses. + # LL IPv6 is a VERY rare case. Strictly speaking, we should use + # getnameinfo() unconditionally, but performance makes sense. + resolved_host, _port = await self._loop.getnameinfo( + address, _NAME_SOCKET_FLAGS + ) + port = int(_port) + else: + resolved_host, port = address[:2] + else: # IPv4 + assert family == socket.AF_INET + resolved_host, port = address # type: ignore[misc] + hosts.append( + ResolveResult( + hostname=host, + host=resolved_host, + port=port, + family=family, + proto=proto, + flags=_NUMERIC_SOCKET_FLAGS, + ) + ) + + return hosts + + async def close(self) -> None: + pass + + +class AsyncResolver(AbstractResolver): + """Use the `aiodns` package to make asynchronous DNS lookups""" + + def __init__( + self, + loop: asyncio.AbstractEventLoop | None = None, + *args: Any, + **kwargs: Any, + ) -> None: + if aiodns is None: + raise RuntimeError("Resolver requires aiodns library") + + self._loop = loop or asyncio.get_running_loop() + self._manager: _DNSResolverManager | None = None + # If custom args are provided, create a dedicated resolver instance + # This means each AsyncResolver with custom args gets its own + # aiodns.DNSResolver instance + if args or kwargs: + self._resolver = aiodns.DNSResolver(*args, **kwargs) + return + # Use the shared resolver from the manager for default arguments + self._manager = _DNSResolverManager() + self._resolver = self._manager.get_resolver(self, self._loop) + + if not hasattr(self._resolver, "gethostbyname"): + # aiodns 1.1 is not available, fallback to DNSResolver.query + self.resolve = self._resolve_with_query # type: ignore + + async def resolve( + self, host: str, port: int = 0, family: socket.AddressFamily = socket.AF_INET + ) -> list[ResolveResult]: + try: + resp = await self._resolver.getaddrinfo( + host, + port=port, + type=socket.SOCK_STREAM, + family=family, + flags=_AI_ADDRCONFIG, + ) + except aiodns.error.DNSError as exc: + msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed" + raise OSError(None, msg) from exc + hosts: list[ResolveResult] = [] + for node in resp.nodes: + address: tuple[bytes, int] | tuple[bytes, int, int, int] = node.addr + if node.family == socket.AF_INET6: + if len(address) > 3 and address[3]: + # This is essential for link-local IPv6 addresses. + # LL IPv6 is a VERY rare case. Strictly speaking, we should use + # getnameinfo() unconditionally, but performance makes sense. + result = await self._resolver.getnameinfo( + (address[0].decode("ascii"), *address[1:]), + _NAME_SOCKET_FLAGS, + ) + resolved_host = result.node + else: + resolved_host = address[0].decode("ascii") + port = address[1] + else: # IPv4 + assert node.family == socket.AF_INET + resolved_host = address[0].decode("ascii") + port = address[1] + hosts.append( + ResolveResult( + hostname=host, + host=resolved_host, + port=port, + family=node.family, + proto=0, + flags=_NUMERIC_SOCKET_FLAGS, + ) + ) + + if not hosts: + raise OSError(None, "DNS lookup failed") + + return hosts + + async def _resolve_with_query( + self, host: str, port: int = 0, family: int = socket.AF_INET + ) -> list[dict[str, Any]]: + qtype: Final = "AAAA" if family == socket.AF_INET6 else "A" + + try: + resp = await self._resolver.query(host, qtype) + except aiodns.error.DNSError as exc: + msg = exc.args[1] if len(exc.args) >= 1 else "DNS lookup failed" + raise OSError(None, msg) from exc + + hosts = [] + for rr in resp: + hosts.append( + { + "hostname": host, + "host": rr.host, + "port": port, + "family": family, + "proto": 0, + "flags": socket.AI_NUMERICHOST, + } + ) + + if not hosts: + raise OSError(None, "DNS lookup failed") + + return hosts + + async def close(self) -> None: + if self._manager: + # Release the resolver from the manager if using the shared resolver + self._manager.release_resolver(self, self._loop) + self._manager = None # Clear reference to manager + self._resolver = None # type: ignore[assignment] # Clear reference to resolver + return + # Otherwise cancel our dedicated resolver + if self._resolver is not None: + self._resolver.cancel() + self._resolver = None # type: ignore[assignment] # Clear reference + + +class _DNSResolverManager: + """Manager for aiodns.DNSResolver objects. + + This class manages shared aiodns.DNSResolver instances + with no custom arguments across different event loops. + """ + + _instance: Optional["_DNSResolverManager"] = None + + def __new__(cls) -> "_DNSResolverManager": + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._init() + return cls._instance + + def _init(self) -> None: + # Use WeakKeyDictionary to allow event loops to be garbage collected + self._loop_data: weakref.WeakKeyDictionary[ + asyncio.AbstractEventLoop, + tuple[aiodns.DNSResolver, weakref.WeakSet[AsyncResolver]], + ] = weakref.WeakKeyDictionary() + + def get_resolver( + self, client: "AsyncResolver", loop: asyncio.AbstractEventLoop + ) -> "aiodns.DNSResolver": + """Get or create the shared aiodns.DNSResolver instance for a specific event loop. + + Args: + client: The AsyncResolver instance requesting the resolver. + This is required to track resolver usage. + loop: The event loop to use for the resolver. + """ + # Create a new resolver and client set for this loop if it doesn't exist + if loop not in self._loop_data: + resolver = aiodns.DNSResolver(loop=loop) + client_set: weakref.WeakSet[AsyncResolver] = weakref.WeakSet() + self._loop_data[loop] = (resolver, client_set) + else: + # Get the existing resolver and client set + resolver, client_set = self._loop_data[loop] + + # Register this client with the loop + client_set.add(client) + return resolver + + def release_resolver( + self, client: "AsyncResolver", loop: asyncio.AbstractEventLoop + ) -> None: + """Release the resolver for an AsyncResolver client when it's closed. + + Args: + client: The AsyncResolver instance to release. + loop: The event loop the resolver was using. + """ + # Remove client from its loop's tracking + current_loop_data = self._loop_data.get(loop) + if current_loop_data is None: + return + resolver, client_set = current_loop_data + client_set.discard(client) + # If no more clients for this loop, cancel and remove its resolver + if not client_set: + if resolver is not None: + resolver.cancel() + del self._loop_data[loop] + + +_DefaultType = type[AsyncResolver | ThreadedResolver] +DefaultResolver: _DefaultType = AsyncResolver if aiodns_default else ThreadedResolver diff --git a/venv/lib/python3.11/site-packages/aiohttp/streams.py b/venv/lib/python3.11/site-packages/aiohttp/streams.py new file mode 100644 index 0000000..e1a5b53 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/streams.py @@ -0,0 +1,760 @@ +import asyncio +import collections +import sys +import warnings +from collections.abc import Awaitable, Callable +from typing import Final, Generic, TypeVar + +from .base_protocol import BaseProtocol +from .helpers import ( + _EXC_SENTINEL, + DEFAULT_CHUNK_SIZE, + BaseTimerContext, + TimerNoop, + set_exception, + set_result, +) +from .http_exceptions import LineTooLong +from .log import internal_logger + +__all__ = ( + "EMPTY_PAYLOAD", + "EofStream", + "StreamReader", + "DataQueue", +) + +_T = TypeVar("_T") + + +class EofStream(Exception): + """eof stream indication.""" + + +class AsyncStreamIterator(Generic[_T]): + + __slots__ = ("read_func",) + + def __init__(self, read_func: Callable[[], Awaitable[_T]]) -> None: + self.read_func = read_func + + def __aiter__(self) -> "AsyncStreamIterator[_T]": + return self + + async def __anext__(self) -> _T: + try: + rv = await self.read_func() + except EofStream: + raise StopAsyncIteration + if rv == b"": + raise StopAsyncIteration + return rv + + +class ChunkTupleAsyncStreamIterator: + + __slots__ = ("_stream",) + + def __init__(self, stream: "StreamReader") -> None: + self._stream = stream + + def __aiter__(self) -> "ChunkTupleAsyncStreamIterator": + return self + + async def __anext__(self) -> tuple[bytes, bool]: + rv = await self._stream.readchunk() + if rv == (b"", False): + raise StopAsyncIteration + return rv + + +class StreamReader: + """An enhancement of asyncio.StreamReader. + + Supports asynchronous iteration by line, chunk or as available:: + + async for line in reader: + ... + async for chunk in reader.iter_chunked(1024): + ... + async for slice in reader.iter_any(): + ... + + """ + + __slots__ = ( + "_protocol", + "_low_water", + "_high_water", + "_low_water_chunks", + "_high_water_chunks", + "_loop", + "_size", + "_cursor", + "_http_chunk_splits", + "_buffer", + "_buffer_offset", + "_eof", + "_waiter", + "_eof_waiter", + "_exception", + "_timer", + "_eof_callbacks", + "_eof_counter", + "total_bytes", + "total_compressed_bytes", + ) + + def __init__( + self, + protocol: BaseProtocol, + limit: int, + *, + timer: BaseTimerContext | None = None, + loop: asyncio.AbstractEventLoop | None = None, + ) -> None: + self._protocol = protocol + self._low_water = limit + self._high_water = limit * 2 + if loop is None: + loop = asyncio.get_event_loop() + # Use max(4, ...) because there's always at least 1 chunk split remaining + # (the current position), so we need low_water >= 2 to allow resume. + # limit // 16 gets us a reasonable value of 16k with default 256KiB limit. + self._high_water_chunks = max(4, limit // 16) + self._low_water_chunks = self._high_water_chunks // 2 + self._loop = loop + self._size = 0 + self._cursor = 0 + self._http_chunk_splits: collections.deque[int] | None = None + self._buffer: collections.deque[bytes] = collections.deque() + self._buffer_offset = 0 + self._eof = False + self._waiter: asyncio.Future[None] | None = None + self._eof_waiter: asyncio.Future[None] | None = None + self._exception: BaseException | None = None + self._timer = TimerNoop() if timer is None else timer + self._eof_callbacks: list[Callable[[], None]] = [] + self._eof_counter = 0 + self.total_bytes = 0 + self.total_compressed_bytes: int | None = None + + def __repr__(self) -> str: + info = [self.__class__.__name__] + if self._size: + info.append("%d bytes" % self._size) + if self._eof: + info.append("eof") + if self._low_water != DEFAULT_CHUNK_SIZE: + info.append("low=%d high=%d" % (self._low_water, self._high_water)) + if self._waiter: + info.append("w=%r" % self._waiter) + if self._exception: + info.append("e=%r" % self._exception) + return "<%s>" % " ".join(info) + + def __aiter__(self) -> AsyncStreamIterator[bytes]: + return AsyncStreamIterator(self.readline) + + def iter_chunked(self, n: int) -> AsyncStreamIterator[bytes]: + """Returns an asynchronous iterator that yields chunks of size n.""" + self.set_read_chunk_size(n) + return AsyncStreamIterator(lambda: self.read(n)) + + def iter_any(self) -> AsyncStreamIterator[bytes]: + """Yield all available data as soon as it is received.""" + return AsyncStreamIterator(self.readany) + + def iter_chunks(self) -> ChunkTupleAsyncStreamIterator: + """Yield chunks of data as they are received by the server. + + The yielded objects are tuples + of (bytes, bool) as returned by the StreamReader.readchunk method. + """ + return ChunkTupleAsyncStreamIterator(self) + + def get_read_buffer_limits(self) -> tuple[int, int]: + return (self._low_water, self._high_water) + + def set_read_chunk_size(self, n: int) -> None: + """Raise buffer limits to match the consumer's chunk size.""" + if n > self._low_water: + self._low_water = n + self._high_water = n * 2 + + def exception(self) -> BaseException | None: + return self._exception + + def set_exception( + self, + exc: BaseException, + exc_cause: BaseException = _EXC_SENTINEL, + ) -> None: + self._exception = exc + self._eof_callbacks.clear() + + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_exception(waiter, exc, exc_cause) + + waiter = self._eof_waiter + if waiter is not None: + self._eof_waiter = None + set_exception(waiter, exc, exc_cause) + + def on_eof(self, callback: Callable[[], None]) -> None: + if self._eof: + try: + callback() + except Exception: + internal_logger.exception("Exception in eof callback") + else: + self._eof_callbacks.append(callback) + + def feed_eof(self) -> None: + self._eof = True + + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_result(waiter, None) + + waiter = self._eof_waiter + if waiter is not None: + self._eof_waiter = None + set_result(waiter, None) + + # At EOF the parser is done, there won't be unprocessed data. + self._protocol.resume_reading(resume_parser=False) + + for cb in self._eof_callbacks: + try: + cb() + except Exception: + internal_logger.exception("Exception in eof callback") + + self._eof_callbacks.clear() + + def is_eof(self) -> bool: + """Return True if 'feed_eof' was called.""" + return self._eof + + def at_eof(self) -> bool: + """Return True if the buffer is empty and 'feed_eof' was called.""" + return self._eof and not self._buffer + + async def wait_eof(self) -> None: + if self._eof: + return + + assert self._eof_waiter is None + self._eof_waiter = self._loop.create_future() + try: + await self._eof_waiter + finally: + self._eof_waiter = None + + @property + def total_raw_bytes(self) -> int: + if self.total_compressed_bytes is None: + return self.total_bytes + return self.total_compressed_bytes + + def unread_data(self, data: bytes) -> None: + """rollback reading some data from stream, inserting it to buffer head.""" + warnings.warn( + "unread_data() is deprecated " + "and will be removed in future releases (#3260)", + DeprecationWarning, + stacklevel=2, + ) + if not data: + return + + if self._buffer_offset: + self._buffer[0] = self._buffer[0][self._buffer_offset :] + self._buffer_offset = 0 + self._size += len(data) + self._cursor -= len(data) + self._buffer.appendleft(data) + self._eof_counter = 0 + + # TODO: size is ignored, remove the param later + def feed_data(self, data: bytes, size: int = 0) -> bool: + assert not self._eof, "feed_data after feed_eof" + + if not data: + return False + + data_len = len(data) + self._size += data_len + self._buffer.append(data) + self.total_bytes += data_len + + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_result(waiter, None) + + if self._size > self._high_water: + self._protocol.pause_reading() + return False + + def begin_http_chunk_receiving(self) -> None: + if self._http_chunk_splits is None: + if self.total_bytes: + raise RuntimeError( + "Called begin_http_chunk_receiving when some data was already fed" + ) + self._http_chunk_splits = collections.deque() + + def end_http_chunk_receiving(self) -> None: + if self._http_chunk_splits is None: + raise RuntimeError( + "Called end_chunk_receiving without calling " + "begin_chunk_receiving first" + ) + + # self._http_chunk_splits contains logical byte offsets from start of + # the body transfer. Each offset is the offset of the end of a chunk. + # "Logical" means bytes, accessible for a user. + # If no chunks containing logical data were received, current position + # is difinitely zero. + pos = self._http_chunk_splits[-1] if self._http_chunk_splits else 0 + + if self.total_bytes == pos: + # We should not add empty chunks here. So we check for that. + # Note, when chunked + gzip is used, we can receive a chunk + # of compressed data, but that data may not be enough for gzip FSM + # to yield any uncompressed data. That's why current position may + # not change after receiving a chunk. + return + + self._http_chunk_splits.append(self.total_bytes) + + # If we get too many small chunks before self._high_water is reached, then any + # .read() call becomes computationally expensive, and could block the event loop + # for too long, hence an additional self._high_water_chunks here. + if len(self._http_chunk_splits) > self._high_water_chunks: + self._protocol.pause_reading() + + # wake up readchunk when end of http chunk received + waiter = self._waiter + if waiter is not None: + self._waiter = None + set_result(waiter, None) + + async def _wait(self, func_name: str) -> None: + if not self._protocol.connected: + raise RuntimeError("Connection closed.") + + # StreamReader uses a future to link the protocol feed_data() method + # to a read coroutine. Running two read coroutines at the same time + # would have an unexpected behaviour. It would not possible to know + # which coroutine would get the next data. + if self._waiter is not None: + raise RuntimeError( + "%s() called while another coroutine is " + "already waiting for incoming data" % func_name + ) + + waiter = self._waiter = self._loop.create_future() + try: + with self._timer: + await waiter + finally: + self._waiter = None + + async def readline(self, *, max_line_length: int | None = None) -> bytes: + return await self.readuntil(max_size=max_line_length) + + async def readuntil( + self, separator: bytes = b"\n", *, max_size: int | None = None + ) -> bytes: + seplen = len(separator) + if seplen == 0: + raise ValueError("Separator should be at least one-byte string") + + if self._exception is not None: + raise self._exception + + chunk = b"" + chunk_size = 0 + not_enough = True + max_size = max_size or self._high_water + + while not_enough: + while self._buffer and not_enough: + offset = self._buffer_offset + ichar = self._buffer[0].find(separator, offset) + 1 + # Read from current offset to found separator or to the end. + data = self._read_nowait_chunk( + ichar - offset + seplen - 1 if ichar else -1 + ) + chunk += data + chunk_size += len(data) + if ichar: + not_enough = False + + if chunk_size > max_size: + raise LineTooLong(chunk[:100] + b"...", max_size) + + if self._eof: + break + + if not_enough: + await self._wait("readuntil") + + return chunk + + async def read(self, n: int = -1) -> bytes: + if self._exception is not None: + raise self._exception + + # migration problem; with DataQueue you have to catch + # EofStream exception, so common way is to run payload.read() inside + # infinite loop. what can cause real infinite loop with StreamReader + # lets keep this code one major release. + if __debug__: + if self._eof and not self._buffer: + self._eof_counter = getattr(self, "_eof_counter", 0) + 1 + if self._eof_counter > 5: + internal_logger.warning( + "Multiple access to StreamReader in eof state, " + "might be infinite loop.", + stack_info=True, + ) + + if not n: + return b"" + + if n < 0: + # Reading everything — remove decompression chunk limit. + self.set_read_chunk_size(sys.maxsize) + blocks = [] + while True: + block = await self.readany() + if not block: + break + blocks.append(block) + return b"".join(blocks) + + self.set_read_chunk_size(n) + # TODO: should be `if` instead of `while` + # because waiter maybe triggered on chunk end, + # without feeding any data + while not self._buffer and not self._eof: + await self._wait("read") + + return self._read_nowait(n) + + async def readany(self) -> bytes: + if self._exception is not None: + raise self._exception + + # TODO: should be `if` instead of `while` + # because waiter maybe triggered on chunk end, + # without feeding any data + while not self._buffer and not self._eof: + await self._wait("readany") + + return self._read_nowait(-1) + + async def readchunk(self) -> tuple[bytes, bool]: + """Returns a tuple of (data, end_of_http_chunk). + + When chunked transfer + encoding is used, end_of_http_chunk is a boolean indicating if the end + of the data corresponds to the end of a HTTP chunk , otherwise it is + always False. + """ + while True: + if self._exception is not None: + raise self._exception + + while self._http_chunk_splits: + pos = self._http_chunk_splits.popleft() + if pos == self._cursor: + return (b"", True) + if pos > self._cursor: + return (self._read_nowait(pos - self._cursor), True) + internal_logger.warning( + "Skipping HTTP chunk end due to data " + "consumption beyond chunk boundary" + ) + + if self._buffer: + return (self._read_nowait_chunk(-1), False) + # return (self._read_nowait(-1), False) + + if self._eof: + # Special case for signifying EOF. + # (b'', True) is not a final return value actually. + return (b"", False) + + await self._wait("readchunk") + + async def readexactly(self, n: int) -> bytes: + if self._exception is not None: + raise self._exception + + blocks: list[bytes] = [] + while n > 0: + block = await self.read(n) + if not block: + partial = b"".join(blocks) + raise asyncio.IncompleteReadError(partial, len(partial) + n) + blocks.append(block) + n -= len(block) + + return b"".join(blocks) + + def read_nowait(self, n: int = -1) -> bytes: + # default was changed to be consistent with .read(-1) + # + # I believe the most users don't know about the method and + # they are not affected. + if self._exception is not None: + raise self._exception + + if self._waiter and not self._waiter.done(): + raise RuntimeError( + "Called while some coroutine is waiting for incoming data." + ) + + return self._read_nowait(n) + + def _read_nowait_chunk(self, n: int) -> bytes: + first_buffer = self._buffer[0] + offset = self._buffer_offset + if n != -1 and len(first_buffer) - offset > n: + data = first_buffer[offset : offset + n] + self._buffer_offset += n + + elif offset: + self._buffer.popleft() + data = first_buffer[offset:] + self._buffer_offset = 0 + + else: + data = self._buffer.popleft() + + data_len = len(data) + self._size -= data_len + self._cursor += data_len + + chunk_splits = self._http_chunk_splits + # Prevent memory leak: drop useless chunk splits + while chunk_splits and chunk_splits[0] < self._cursor: + chunk_splits.popleft() + + if self._size < self._low_water and ( + self._http_chunk_splits is None + or len(self._http_chunk_splits) < self._low_water_chunks + ): + self._protocol.resume_reading() + return data + + def _read_nowait(self, n: int) -> bytes: + """Read not more than n bytes, or whole buffer if n == -1""" + self._timer.assert_timeout() + + if n == -1: + # Drain only chunks present now; _read_nowait_chunk() can + # re-entrantly resume_reading() and refill the buffer. + count = len(self._buffer) + if count == 1: + return self._read_nowait_chunk(-1) + return b"".join([self._read_nowait_chunk(-1) for _ in range(count)]) + + chunks: list[bytes] = [] + while self._buffer: + chunk = self._read_nowait_chunk(n) + chunks.append(chunk) + n -= len(chunk) + if n == 0: + break + + return b"".join(chunks) if chunks else b"" + + +class EmptyStreamReader(StreamReader): # lgtm [py/missing-call-to-init] + + __slots__ = ("_read_eof_chunk",) + + def __init__(self) -> None: + self._read_eof_chunk = False + self.total_bytes = 0 + + def __repr__(self) -> str: + return "<%s>" % self.__class__.__name__ + + def exception(self) -> BaseException | None: + return None + + def set_exception( + self, + exc: BaseException, + exc_cause: BaseException = _EXC_SENTINEL, + ) -> None: + pass + + def on_eof(self, callback: Callable[[], None]) -> None: + try: + callback() + except Exception: + internal_logger.exception("Exception in eof callback") + + def feed_eof(self) -> None: + pass + + def is_eof(self) -> bool: + return True + + def at_eof(self) -> bool: + return True + + async def wait_eof(self) -> None: + return + + def feed_data(self, data: bytes, n: int = 0) -> bool: + return False + + def set_read_chunk_size(self, n: int) -> None: + return + + async def readline(self, *, max_line_length: int | None = None) -> bytes: + return b"" + + async def read(self, n: int = -1) -> bytes: + return b"" + + # TODO add async def readuntil + + async def readany(self) -> bytes: + return b"" + + async def readchunk(self) -> tuple[bytes, bool]: + if not self._read_eof_chunk: + self._read_eof_chunk = True + return (b"", False) + + return (b"", True) + + async def readexactly(self, n: int) -> bytes: + raise asyncio.IncompleteReadError(b"", n) + + def read_nowait(self, n: int = -1) -> bytes: + return b"" + + +EMPTY_PAYLOAD: Final[StreamReader] = EmptyStreamReader() + + +class DataQueue(Generic[_T]): + """DataQueue is a general-purpose blocking queue with one reader.""" + + def __init__(self, loop: asyncio.AbstractEventLoop) -> None: + self._loop = loop + self._eof = False + self._waiter: asyncio.Future[None] | None = None + self._exception: BaseException | None = None + self._buffer: collections.deque[tuple[_T, int]] = collections.deque() + + def __len__(self) -> int: + return len(self._buffer) + + def is_eof(self) -> bool: + return self._eof + + def at_eof(self) -> bool: + return self._eof and not self._buffer + + def exception(self) -> BaseException | None: + return self._exception + + def set_exception( + self, + exc: BaseException, + exc_cause: BaseException = _EXC_SENTINEL, + ) -> None: + self._eof = True + self._exception = exc + if (waiter := self._waiter) is not None: + self._waiter = None + set_exception(waiter, exc, exc_cause) + + def feed_data(self, data: _T, size: int = 0) -> None: + self._buffer.append((data, size)) + if (waiter := self._waiter) is not None: + self._waiter = None + set_result(waiter, None) + + def feed_eof(self) -> None: + self._eof = True + if (waiter := self._waiter) is not None: + self._waiter = None + set_result(waiter, None) + + async def read(self) -> _T: + if not self._buffer and not self._eof: + assert not self._waiter + self._waiter = self._loop.create_future() + try: + await self._waiter + except (asyncio.CancelledError, asyncio.TimeoutError): + self._waiter = None + raise + if self._buffer: + data, _ = self._buffer.popleft() + return data + if self._exception is not None: + raise self._exception + raise EofStream + + def __aiter__(self) -> AsyncStreamIterator[_T]: + return AsyncStreamIterator(self.read) + + +class FlowControlDataQueue(DataQueue[_T]): + """FlowControlDataQueue resumes and pauses an underlying stream. + + It is a destination for parsed data. + + This class is deprecated and will be removed in version 4.0. + """ + + def __init__( + self, protocol: BaseProtocol, limit: int, *, loop: asyncio.AbstractEventLoop + ) -> None: + super().__init__(loop=loop) + self._size = 0 + self._protocol = protocol + self._limit = limit * 2 + + def feed_data(self, data: _T, size: int = 0) -> None: + super().feed_data(data, size) + self._size += size + + if self._size > self._limit and not self._protocol._reading_paused: + self._protocol.pause_reading() + + async def read(self) -> _T: + if not self._buffer and not self._eof: + assert not self._waiter + self._waiter = self._loop.create_future() + try: + await self._waiter + except (asyncio.CancelledError, asyncio.TimeoutError): + self._waiter = None + raise + if self._buffer: + data, size = self._buffer.popleft() + self._size -= size + if self._size < self._limit and self._protocol._reading_paused: + self._protocol.resume_reading() + return data + if self._exception is not None: + raise self._exception + raise EofStream diff --git a/venv/lib/python3.11/site-packages/aiohttp/tcp_helpers.py b/venv/lib/python3.11/site-packages/aiohttp/tcp_helpers.py new file mode 100644 index 0000000..88b2442 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/tcp_helpers.py @@ -0,0 +1,37 @@ +"""Helper methods to tune a TCP connection""" + +import asyncio +import socket +from contextlib import suppress +from typing import Optional # noqa + +__all__ = ("tcp_keepalive", "tcp_nodelay") + + +if hasattr(socket, "SO_KEEPALIVE"): + + def tcp_keepalive(transport: asyncio.Transport) -> None: + sock = transport.get_extra_info("socket") + if sock is not None: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + +else: + + def tcp_keepalive(transport: asyncio.Transport) -> None: # pragma: no cover + pass + + +def tcp_nodelay(transport: asyncio.Transport, value: bool) -> None: + sock = transport.get_extra_info("socket") + + if sock is None: + return + + if sock.family not in (socket.AF_INET, socket.AF_INET6): + return + + value = bool(value) + + # socket may be closed already, on windows OSError get raised + with suppress(OSError): + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, value) diff --git a/venv/lib/python3.11/site-packages/aiohttp/test_utils.py b/venv/lib/python3.11/site-packages/aiohttp/test_utils.py new file mode 100644 index 0000000..6b50e23 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/test_utils.py @@ -0,0 +1,808 @@ +"""Utilities shared by tests.""" + +import asyncio +import contextlib +import gc +import inspect +import ipaddress +import os +import socket +import sys +import warnings +from abc import ABC, abstractmethod +from collections.abc import Callable, Iterator +from types import TracebackType +from typing import TYPE_CHECKING, Any, Generic, Literal, TypeVar, cast, overload +from unittest import IsolatedAsyncioTestCase, mock + +from aiosignal import Signal +from multidict import CIMultiDict, CIMultiDictProxy +from yarl import URL + +import aiohttp +from aiohttp.client import ( + _BaseRequestContextManager, + _RequestContextManager, + _RequestOptions, + _WSRequestContextManager, +) + +from . import ClientSession, hdrs +from .abc import AbstractCookieJar +from .client_reqrep import ClientResponse +from .client_ws import ClientWebSocketResponse +from .helpers import sentinel +from .http import HttpVersion, RawRequestMessage +from .streams import EMPTY_PAYLOAD, StreamReader +from .typedefs import StrOrURL +from .web import ( + Application, + AppRunner, + BaseRequest, + BaseRunner, + Request, + Server, + ServerRunner, + SockSite, + UrlMappingMatchInfo, +) +from .web_protocol import _RequestHandler + +if TYPE_CHECKING: + from ssl import SSLContext +else: + SSLContext = Any + +if sys.version_info >= (3, 11) and TYPE_CHECKING: + from typing import Unpack + +if sys.version_info >= (3, 11): + from typing import Self +else: + Self = Any + +_ApplicationNone = TypeVar("_ApplicationNone", Application, None) +_Request = TypeVar("_Request", bound=BaseRequest) + +REUSE_ADDRESS = os.name == "posix" and sys.platform != "cygwin" + + +def get_unused_port_socket( + host: str, family: socket.AddressFamily = socket.AF_INET +) -> socket.socket: + return get_port_socket(host, 0, family) + + +def get_port_socket( + host: str, port: int, family: socket.AddressFamily +) -> socket.socket: + s = socket.socket(family, socket.SOCK_STREAM) + if REUSE_ADDRESS: + # Windows has different semantics for SO_REUSEADDR, + # so don't set it. Ref: + # https://docs.microsoft.com/en-us/windows/win32/winsock/using-so-reuseaddr-and-so-exclusiveaddruse + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + s.bind((host, port)) + return s + + +def unused_port() -> int: + """Return a port that is unused on the current host.""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(("127.0.0.1", 0)) + return cast(int, s.getsockname()[1]) + + +class BaseTestServer(ABC): + __test__ = False + + def __init__( + self, + *, + scheme: str = "", + loop: asyncio.AbstractEventLoop | None = None, + host: str = "127.0.0.1", + port: int | None = None, + skip_url_asserts: bool = False, + socket_factory: Callable[ + [str, int, socket.AddressFamily], socket.socket + ] = get_port_socket, + **kwargs: Any, + ) -> None: + self._loop = loop + self.runner: BaseRunner | None = None + self._root: URL | None = None + self.host = host + self.port = port + self._closed = False + self.scheme = scheme + self.skip_url_asserts = skip_url_asserts + self.socket_factory = socket_factory + + async def start_server( + self, loop: asyncio.AbstractEventLoop | None = None, **kwargs: Any + ) -> None: + if self.runner: + return + self._loop = loop + self._ssl = kwargs.pop("ssl", None) + self.runner = await self._make_runner(handler_cancellation=True, **kwargs) + await self.runner.setup() + if not self.port: + self.port = 0 + absolute_host = self.host + try: + version = ipaddress.ip_address(self.host).version + except ValueError: + version = 4 + if version == 6: + absolute_host = f"[{self.host}]" + family = socket.AF_INET6 if version == 6 else socket.AF_INET + _sock = self.socket_factory(self.host, self.port, family) + self.host, self.port = _sock.getsockname()[:2] + site = SockSite(self.runner, sock=_sock, ssl_context=self._ssl) + await site.start() + server = site._server + assert server is not None + sockets = server.sockets # type: ignore[attr-defined] + assert sockets is not None + self.port = sockets[0].getsockname()[1] + if not self.scheme: + self.scheme = "https" if self._ssl else "http" + self._root = URL(f"{self.scheme}://{absolute_host}:{self.port}") + + @abstractmethod # pragma: no cover + async def _make_runner(self, **kwargs: Any) -> BaseRunner: + pass + + def make_url(self, path: StrOrURL) -> URL: + assert self._root is not None + url = URL(path) + if not self.skip_url_asserts: + assert not url.absolute + return self._root.join(url) + else: + return URL(str(self._root) + str(path)) + + @property + def started(self) -> bool: + return self.runner is not None + + @property + def closed(self) -> bool: + return self._closed + + @property + def handler(self) -> Server: + # for backward compatibility + # web.Server instance + runner = self.runner + assert runner is not None + assert runner.server is not None + return runner.server + + async def close(self) -> None: + """Close all fixtures created by the test client. + + After that point, the TestClient is no longer usable. + + This is an idempotent function: running close multiple times + will not have any additional effects. + + close is also run when the object is garbage collected, and on + exit when used as a context manager. + + """ + if self.started and not self.closed: + assert self.runner is not None + await self.runner.cleanup() + self._root = None + self.port = None + self._closed = True + + def __enter__(self) -> None: + raise TypeError("Use async with instead") + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + # __exit__ should exist in pair with __enter__ but never executed + pass # pragma: no cover + + async def __aenter__(self) -> "BaseTestServer": + await self.start_server(loop=self._loop) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self.close() + + +class TestServer(BaseTestServer): + def __init__( + self, + app: Application, + *, + scheme: str = "", + host: str = "127.0.0.1", + port: int | None = None, + **kwargs: Any, + ): + self.app = app + super().__init__(scheme=scheme, host=host, port=port, **kwargs) + + async def _make_runner(self, **kwargs: Any) -> BaseRunner: + return AppRunner(self.app, **kwargs) + + +class RawTestServer(BaseTestServer): + def __init__( + self, + handler: _RequestHandler, + *, + scheme: str = "", + host: str = "127.0.0.1", + port: int | None = None, + **kwargs: Any, + ) -> None: + self._handler = handler + super().__init__(scheme=scheme, host=host, port=port, **kwargs) + + async def _make_runner(self, debug: bool = True, **kwargs: Any) -> ServerRunner: + srv = Server(self._handler, loop=self._loop, debug=debug, **kwargs) + return ServerRunner(srv, debug=debug, **kwargs) + + +class TestClient(Generic[_Request, _ApplicationNone]): + """ + A test client implementation. + + To write functional tests for aiohttp based servers. + + """ + + __test__ = False + + @overload + def __init__( + self: "TestClient[Request, Application]", + server: TestServer, + *, + cookie_jar: AbstractCookieJar | None = None, + **kwargs: Any, + ) -> None: ... + @overload + def __init__( + self: "TestClient[_Request, None]", + server: BaseTestServer, + *, + cookie_jar: AbstractCookieJar | None = None, + **kwargs: Any, + ) -> None: ... + def __init__( + self, + server: BaseTestServer, + *, + cookie_jar: AbstractCookieJar | None = None, + loop: asyncio.AbstractEventLoop | None = None, + **kwargs: Any, + ) -> None: + if not isinstance(server, BaseTestServer): + raise TypeError( + "server must be TestServer instance, found type: %r" % type(server) + ) + self._server = server + self._loop = loop + if cookie_jar is None: + cookie_jar = aiohttp.CookieJar(unsafe=True, loop=loop) + self._session = ClientSession(loop=loop, cookie_jar=cookie_jar, **kwargs) + self._session._retry_connection = False + self._closed = False + self._responses: list[ClientResponse] = [] + self._websockets: list[ClientWebSocketResponse[bool]] = [] + + async def start_server(self) -> None: + await self._server.start_server(loop=self._loop) + + @property + def host(self) -> str: + return self._server.host + + @property + def port(self) -> int | None: + return self._server.port + + @property + def server(self) -> BaseTestServer: + return self._server + + @property + def app(self) -> _ApplicationNone: + return getattr(self._server, "app", None) # type: ignore[return-value] + + @property + def session(self) -> ClientSession: + """An internal aiohttp.ClientSession. + + Unlike the methods on the TestClient, client session requests + do not automatically include the host in the url queried, and + will require an absolute path to the resource. + + """ + return self._session + + def make_url(self, path: StrOrURL) -> URL: + return self._server.make_url(path) + + async def _request( + self, method: str, path: StrOrURL, **kwargs: Any + ) -> ClientResponse: + resp = await self._session.request(method, self.make_url(path), **kwargs) + # save it to close later + self._responses.append(resp) + return resp + + if sys.version_info >= (3, 11) and TYPE_CHECKING: + + def request( + self, method: str, path: StrOrURL, **kwargs: Unpack[_RequestOptions] + ) -> _RequestContextManager: ... + + def get( + self, + path: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> _RequestContextManager: ... + + def options( + self, + path: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> _RequestContextManager: ... + + def head( + self, + path: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> _RequestContextManager: ... + + def post( + self, + path: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> _RequestContextManager: ... + + def put( + self, + path: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> _RequestContextManager: ... + + def patch( + self, + path: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> _RequestContextManager: ... + + def delete( + self, + path: StrOrURL, + **kwargs: Unpack[_RequestOptions], + ) -> _RequestContextManager: ... + + else: + + def request( + self, method: str, path: StrOrURL, **kwargs: Any + ) -> _RequestContextManager: + """Routes a request to tested http server. + + The interface is identical to aiohttp.ClientSession.request, + except the loop kwarg is overridden by the instance used by the + test server. + + """ + return _RequestContextManager(self._request(method, path, **kwargs)) + + def get(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP GET request.""" + return _RequestContextManager(self._request(hdrs.METH_GET, path, **kwargs)) + + def post(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP POST request.""" + return _RequestContextManager(self._request(hdrs.METH_POST, path, **kwargs)) + + def options(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP OPTIONS request.""" + return _RequestContextManager( + self._request(hdrs.METH_OPTIONS, path, **kwargs) + ) + + def head(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP HEAD request.""" + return _RequestContextManager(self._request(hdrs.METH_HEAD, path, **kwargs)) + + def put(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP PUT request.""" + return _RequestContextManager(self._request(hdrs.METH_PUT, path, **kwargs)) + + def patch(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP PATCH request.""" + return _RequestContextManager( + self._request(hdrs.METH_PATCH, path, **kwargs) + ) + + def delete(self, path: StrOrURL, **kwargs: Any) -> _RequestContextManager: + """Perform an HTTP PATCH request.""" + return _RequestContextManager( + self._request(hdrs.METH_DELETE, path, **kwargs) + ) + + @overload + def ws_connect( + self, path: StrOrURL, *, decode_text: Literal[True] = ..., **kwargs: Any + ) -> "_BaseRequestContextManager[ClientWebSocketResponse[Literal[True]]]": ... + + @overload + def ws_connect( + self, path: StrOrURL, *, decode_text: Literal[False], **kwargs: Any + ) -> "_BaseRequestContextManager[ClientWebSocketResponse[Literal[False]]]": ... + + @overload + def ws_connect( + self, path: StrOrURL, *, decode_text: bool = ..., **kwargs: Any + ) -> "_BaseRequestContextManager[ClientWebSocketResponse[bool]]": ... + + def ws_connect( + self, path: StrOrURL, *, decode_text: bool = True, **kwargs: Any + ) -> "_BaseRequestContextManager[ClientWebSocketResponse[bool]]": + """Initiate websocket connection. + + The api corresponds to aiohttp.ClientSession.ws_connect. + + """ + return _WSRequestContextManager( + self._ws_connect(path, decode_text=decode_text, **kwargs) + ) + + @overload + async def _ws_connect( + self, path: StrOrURL, *, decode_text: Literal[True] = ..., **kwargs: Any + ) -> "ClientWebSocketResponse[Literal[True]]": ... + + @overload + async def _ws_connect( + self, path: StrOrURL, *, decode_text: Literal[False], **kwargs: Any + ) -> "ClientWebSocketResponse[Literal[False]]": ... + + @overload + async def _ws_connect( + self, path: StrOrURL, *, decode_text: bool = ..., **kwargs: Any + ) -> "ClientWebSocketResponse[bool]": ... + + async def _ws_connect( + self, path: StrOrURL, *, decode_text: bool = True, **kwargs: Any + ) -> "ClientWebSocketResponse[bool]": + ws = await self._session.ws_connect( + self.make_url(path), decode_text=decode_text, **kwargs + ) + self._websockets.append(ws) + return ws + + async def close(self) -> None: + """Close all fixtures created by the test client. + + After that point, the TestClient is no longer usable. + + This is an idempotent function: running close multiple times + will not have any additional effects. + + close is also run on exit when used as a(n) (asynchronous) + context manager. + + """ + if not self._closed: + for resp in self._responses: + resp.close() + for ws in self._websockets: + await ws.close() + await self._session.close() + await self._server.close() + self._closed = True + + def __enter__(self) -> None: + raise TypeError("Use async with instead") + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + # __exit__ should exist in pair with __enter__ but never executed + pass # pragma: no cover + + async def __aenter__(self) -> Self: + await self.start_server() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc: BaseException | None, + tb: TracebackType | None, + ) -> None: + await self.close() + + +class AioHTTPTestCase(IsolatedAsyncioTestCase): + """A base class to allow for unittest web applications using aiohttp. + + Provides the following: + + * self.client (aiohttp.test_utils.TestClient): an aiohttp test client. + * self.loop (asyncio.BaseEventLoop): the event loop in which the + application and server are running. + * self.app (aiohttp.web.Application): the application returned by + self.get_application() + + Note that the TestClient's methods are asynchronous: you have to + execute function on the test client using asynchronous methods. + """ + + async def get_application(self) -> Application: + """Get application. + + This method should be overridden + to return the aiohttp.web.Application + object to test. + """ + return self.get_app() + + def get_app(self) -> Application: + """Obsolete method used to constructing web application. + + Use .get_application() coroutine instead. + """ + raise RuntimeError("Did you forget to define get_application()?") + + async def asyncSetUp(self) -> None: + self.loop = asyncio.get_running_loop() + return await self.setUpAsync() + + async def setUpAsync(self) -> None: + self.app = await self.get_application() + self.server = await self.get_server(self.app) + self.client = await self.get_client(self.server) + + await self.client.start_server() + + async def asyncTearDown(self) -> None: + return await self.tearDownAsync() + + async def tearDownAsync(self) -> None: + await self.client.close() + + async def get_server(self, app: Application) -> TestServer: + """Return a TestServer instance.""" + return TestServer(app, loop=self.loop) + + async def get_client(self, server: TestServer) -> TestClient[Request, Application]: + """Return a TestClient instance.""" + return TestClient(server, loop=self.loop) + + +def unittest_run_loop(func: Any, *args: Any, **kwargs: Any) -> Any: + """ + A decorator dedicated to use with asynchronous AioHTTPTestCase test methods. + + In 3.8+, this does nothing. + """ + warnings.warn( + "Decorator `@unittest_run_loop` is no longer needed in aiohttp 3.8+", + DeprecationWarning, + stacklevel=2, + ) + return func + + +_LOOP_FACTORY = Callable[[], asyncio.AbstractEventLoop] + + +@contextlib.contextmanager +def loop_context( + loop_factory: _LOOP_FACTORY = asyncio.new_event_loop, fast: bool = False +) -> Iterator[asyncio.AbstractEventLoop]: + """A contextmanager that creates an event_loop, for test purposes. + + Handles the creation and cleanup of a test loop. + """ + loop = setup_test_loop(loop_factory) + yield loop + teardown_test_loop(loop, fast=fast) + + +def setup_test_loop( + loop_factory: _LOOP_FACTORY = asyncio.new_event_loop, +) -> asyncio.AbstractEventLoop: + """Create and return an asyncio.BaseEventLoop instance. + + The caller should also call teardown_test_loop, + once they are done with the loop. + """ + loop = loop_factory() + asyncio.set_event_loop(loop) + return loop + + +def teardown_test_loop(loop: asyncio.AbstractEventLoop, fast: bool = False) -> None: + """Teardown and cleanup an event_loop created by setup_test_loop.""" + closed = loop.is_closed() + if not closed: + loop.call_soon(loop.stop) + loop.run_forever() + loop.close() + + if not fast: + gc.collect() + + asyncio.set_event_loop(None) + + +def _create_app_mock() -> mock.MagicMock: + def get_dict(app: Any, key: str) -> Any: + return app.__app_dict[key] + + def set_dict(app: Any, key: str, value: Any) -> None: + app.__app_dict[key] = value + + app = mock.MagicMock(spec=Application) + app.__app_dict = {} + app.__getitem__ = get_dict + app.__setitem__ = set_dict + + app._debug = False + app.on_response_prepare = Signal(app) + app.on_response_prepare.freeze() + return app + + +def _create_transport(sslcontext: SSLContext | None = None) -> mock.Mock: + transport = mock.Mock() + + def get_extra_info(key: str) -> SSLContext | tuple[str, int] | None: + if key == "sslcontext": + return sslcontext + return ("127.0.0.1", 80) if key == "sockname" else None + + transport.get_extra_info.side_effect = get_extra_info + return transport + + +def make_mocked_request( + method: str, + path: str, + headers: Any = None, + *, + match_info: Any = sentinel, + version: HttpVersion = HttpVersion(1, 1), + closing: bool = False, + app: Any = None, + writer: Any = sentinel, + protocol: Any = sentinel, + transport: Any = sentinel, + payload: StreamReader = EMPTY_PAYLOAD, + sslcontext: SSLContext | None = None, + client_max_size: int = 1024**2, + loop: Any = ..., +) -> Request: + """Creates mocked web.Request testing purposes. + + Useful in unit tests, when spinning full web server is overkill or + specific conditions and errors are hard to trigger. + """ + task = mock.Mock() + if loop is ...: + # no loop passed, try to get the current one if + # its is running as we need a real loop to create + # executor jobs to be able to do testing + # with a real executor + try: + loop = asyncio.get_running_loop() + except RuntimeError: + loop = mock.Mock() + loop.create_future.return_value = () + + if version < HttpVersion(1, 1): + closing = True + + if headers: + headers = CIMultiDictProxy(CIMultiDict(headers)) + raw_hdrs = tuple( + (k.encode("utf-8"), v.encode("utf-8")) for k, v in headers.items() + ) + else: + headers = CIMultiDictProxy(CIMultiDict()) + raw_hdrs = () + + chunked = "chunked" in headers.get(hdrs.TRANSFER_ENCODING, "").lower() + upgrade = headers.get(hdrs.CONNECTION, "").lower() == "upgrade" and bool( + headers.get(hdrs.UPGRADE) + ) + + message = RawRequestMessage( + method, + path, + version, + headers, + raw_hdrs, + closing, + None, + upgrade, + chunked, + URL(path), + ) + if app is None: + app = _create_app_mock() + + if transport is sentinel: + transport = _create_transport(sslcontext) + + if protocol is sentinel: + protocol = mock.Mock() + protocol.max_field_size = 8190 + protocol.max_line_length = 8190 + protocol.max_headers = 128 + protocol.transport = transport + type(protocol).peername = mock.PropertyMock( + return_value=transport.get_extra_info("peername") + ) + type(protocol).sockname = mock.PropertyMock( + return_value=transport.get_extra_info("sockname") + ) + type(protocol).ssl_context = mock.PropertyMock(return_value=sslcontext) + + if writer is sentinel: + writer = mock.Mock() + writer.write_headers = make_mocked_coro(None) + writer.write = make_mocked_coro(None) + writer.write_eof = make_mocked_coro(None) + writer.drain = make_mocked_coro(None) + writer.transport = transport + + protocol.transport = transport + protocol.writer = writer + + req = Request( + message, payload, protocol, writer, task, loop, client_max_size=client_max_size + ) + + match_info = UrlMappingMatchInfo( + {} if match_info is sentinel else match_info, mock.Mock() + ) + match_info.add_app(app) + req._match_info = match_info + + return req + + +def make_mocked_coro( + return_value: Any = sentinel, raise_exception: Any = sentinel +) -> Any: + """Creates a coroutine mock.""" + + async def mock_coro(*args: Any, **kwargs: Any) -> Any: + if raise_exception is not sentinel: + raise raise_exception + if not inspect.isawaitable(return_value): + return return_value + await return_value + + return mock.Mock(wraps=mock_coro) diff --git a/venv/lib/python3.11/site-packages/aiohttp/tracing.py b/venv/lib/python3.11/site-packages/aiohttp/tracing.py new file mode 100644 index 0000000..cfee054 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/tracing.py @@ -0,0 +1,453 @@ +from types import SimpleNamespace +from typing import TYPE_CHECKING, Any, TypeVar + +import attr +from aiosignal import Signal +from multidict import CIMultiDict +from yarl import URL + +from .client_reqrep import ClientResponse + +if TYPE_CHECKING: + from .client import ClientSession + + _ParamT_contra = TypeVar("_ParamT_contra", contravariant=True) + _TracingSignal = Signal[ClientSession, SimpleNamespace, _ParamT_contra] + + +__all__ = ( + "TraceConfig", + "TraceRequestStartParams", + "TraceRequestEndParams", + "TraceRequestExceptionParams", + "TraceConnectionQueuedStartParams", + "TraceConnectionQueuedEndParams", + "TraceConnectionCreateStartParams", + "TraceConnectionCreateEndParams", + "TraceConnectionReuseconnParams", + "TraceDnsResolveHostStartParams", + "TraceDnsResolveHostEndParams", + "TraceDnsCacheHitParams", + "TraceDnsCacheMissParams", + "TraceRequestRedirectParams", + "TraceRequestChunkSentParams", + "TraceResponseChunkReceivedParams", + "TraceRequestHeadersSentParams", +) + + +class TraceConfig: + """First-class used to trace requests launched via ClientSession objects.""" + + def __init__( + self, trace_config_ctx_factory: type[SimpleNamespace] = SimpleNamespace + ) -> None: + self._on_request_start: _TracingSignal[TraceRequestStartParams] = Signal(self) + self._on_request_chunk_sent: _TracingSignal[TraceRequestChunkSentParams] = ( + Signal(self) + ) + self._on_response_chunk_received: _TracingSignal[ + TraceResponseChunkReceivedParams + ] = Signal(self) + self._on_request_end: _TracingSignal[TraceRequestEndParams] = Signal(self) + self._on_request_exception: _TracingSignal[TraceRequestExceptionParams] = ( + Signal(self) + ) + self._on_request_redirect: _TracingSignal[TraceRequestRedirectParams] = Signal( + self + ) + self._on_connection_queued_start: _TracingSignal[ + TraceConnectionQueuedStartParams + ] = Signal(self) + self._on_connection_queued_end: _TracingSignal[ + TraceConnectionQueuedEndParams + ] = Signal(self) + self._on_connection_create_start: _TracingSignal[ + TraceConnectionCreateStartParams + ] = Signal(self) + self._on_connection_create_end: _TracingSignal[ + TraceConnectionCreateEndParams + ] = Signal(self) + self._on_connection_reuseconn: _TracingSignal[ + TraceConnectionReuseconnParams + ] = Signal(self) + self._on_dns_resolvehost_start: _TracingSignal[ + TraceDnsResolveHostStartParams + ] = Signal(self) + self._on_dns_resolvehost_end: _TracingSignal[TraceDnsResolveHostEndParams] = ( + Signal(self) + ) + self._on_dns_cache_hit: _TracingSignal[TraceDnsCacheHitParams] = Signal(self) + self._on_dns_cache_miss: _TracingSignal[TraceDnsCacheMissParams] = Signal(self) + self._on_request_headers_sent: _TracingSignal[TraceRequestHeadersSentParams] = ( + Signal(self) + ) + + self._trace_config_ctx_factory = trace_config_ctx_factory + + def trace_config_ctx(self, trace_request_ctx: Any = None) -> SimpleNamespace: + """Return a new trace_config_ctx instance""" + return self._trace_config_ctx_factory(trace_request_ctx=trace_request_ctx) + + def freeze(self) -> None: + self._on_request_start.freeze() + self._on_request_chunk_sent.freeze() + self._on_response_chunk_received.freeze() + self._on_request_end.freeze() + self._on_request_exception.freeze() + self._on_request_redirect.freeze() + self._on_connection_queued_start.freeze() + self._on_connection_queued_end.freeze() + self._on_connection_create_start.freeze() + self._on_connection_create_end.freeze() + self._on_connection_reuseconn.freeze() + self._on_dns_resolvehost_start.freeze() + self._on_dns_resolvehost_end.freeze() + self._on_dns_cache_hit.freeze() + self._on_dns_cache_miss.freeze() + self._on_request_headers_sent.freeze() + + @property + def on_request_start(self) -> "_TracingSignal[TraceRequestStartParams]": + return self._on_request_start + + @property + def on_request_chunk_sent( + self, + ) -> "_TracingSignal[TraceRequestChunkSentParams]": + return self._on_request_chunk_sent + + @property + def on_response_chunk_received( + self, + ) -> "_TracingSignal[TraceResponseChunkReceivedParams]": + return self._on_response_chunk_received + + @property + def on_request_end(self) -> "_TracingSignal[TraceRequestEndParams]": + return self._on_request_end + + @property + def on_request_exception( + self, + ) -> "_TracingSignal[TraceRequestExceptionParams]": + return self._on_request_exception + + @property + def on_request_redirect( + self, + ) -> "_TracingSignal[TraceRequestRedirectParams]": + return self._on_request_redirect + + @property + def on_connection_queued_start( + self, + ) -> "_TracingSignal[TraceConnectionQueuedStartParams]": + return self._on_connection_queued_start + + @property + def on_connection_queued_end( + self, + ) -> "_TracingSignal[TraceConnectionQueuedEndParams]": + return self._on_connection_queued_end + + @property + def on_connection_create_start( + self, + ) -> "_TracingSignal[TraceConnectionCreateStartParams]": + return self._on_connection_create_start + + @property + def on_connection_create_end( + self, + ) -> "_TracingSignal[TraceConnectionCreateEndParams]": + return self._on_connection_create_end + + @property + def on_connection_reuseconn( + self, + ) -> "_TracingSignal[TraceConnectionReuseconnParams]": + return self._on_connection_reuseconn + + @property + def on_dns_resolvehost_start( + self, + ) -> "_TracingSignal[TraceDnsResolveHostStartParams]": + return self._on_dns_resolvehost_start + + @property + def on_dns_resolvehost_end( + self, + ) -> "_TracingSignal[TraceDnsResolveHostEndParams]": + return self._on_dns_resolvehost_end + + @property + def on_dns_cache_hit(self) -> "_TracingSignal[TraceDnsCacheHitParams]": + return self._on_dns_cache_hit + + @property + def on_dns_cache_miss(self) -> "_TracingSignal[TraceDnsCacheMissParams]": + return self._on_dns_cache_miss + + @property + def on_request_headers_sent( + self, + ) -> "_TracingSignal[TraceRequestHeadersSentParams]": + return self._on_request_headers_sent + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceRequestStartParams: + """Parameters sent by the `on_request_start` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceRequestChunkSentParams: + """Parameters sent by the `on_request_chunk_sent` signal""" + + method: str + url: URL + chunk: bytes + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceResponseChunkReceivedParams: + """Parameters sent by the `on_response_chunk_received` signal""" + + method: str + url: URL + chunk: bytes + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceRequestEndParams: + """Parameters sent by the `on_request_end` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + response: ClientResponse + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceRequestExceptionParams: + """Parameters sent by the `on_request_exception` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + exception: BaseException + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceRequestRedirectParams: + """Parameters sent by the `on_request_redirect` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + response: ClientResponse + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceConnectionQueuedStartParams: + """Parameters sent by the `on_connection_queued_start` signal""" + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceConnectionQueuedEndParams: + """Parameters sent by the `on_connection_queued_end` signal""" + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceConnectionCreateStartParams: + """Parameters sent by the `on_connection_create_start` signal""" + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceConnectionCreateEndParams: + """Parameters sent by the `on_connection_create_end` signal""" + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceConnectionReuseconnParams: + """Parameters sent by the `on_connection_reuseconn` signal""" + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceDnsResolveHostStartParams: + """Parameters sent by the `on_dns_resolvehost_start` signal""" + + host: str + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceDnsResolveHostEndParams: + """Parameters sent by the `on_dns_resolvehost_end` signal""" + + host: str + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceDnsCacheHitParams: + """Parameters sent by the `on_dns_cache_hit` signal""" + + host: str + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceDnsCacheMissParams: + """Parameters sent by the `on_dns_cache_miss` signal""" + + host: str + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class TraceRequestHeadersSentParams: + """Parameters sent by the `on_request_headers_sent` signal""" + + method: str + url: URL + headers: "CIMultiDict[str]" + + +class Trace: + """Internal dependency holder class. + + Used to keep together the main dependencies used + at the moment of send a signal. + """ + + def __init__( + self, + session: "ClientSession", + trace_config: TraceConfig, + trace_config_ctx: SimpleNamespace, + ) -> None: + self._trace_config = trace_config + self._trace_config_ctx = trace_config_ctx + self._session = session + + async def send_request_start( + self, method: str, url: URL, headers: "CIMultiDict[str]" + ) -> None: + return await self._trace_config.on_request_start.send( + self._session, + self._trace_config_ctx, + TraceRequestStartParams(method, url, headers), + ) + + async def send_request_chunk_sent( + self, method: str, url: URL, chunk: bytes + ) -> None: + return await self._trace_config.on_request_chunk_sent.send( + self._session, + self._trace_config_ctx, + TraceRequestChunkSentParams(method, url, chunk), + ) + + async def send_response_chunk_received( + self, method: str, url: URL, chunk: bytes + ) -> None: + return await self._trace_config.on_response_chunk_received.send( + self._session, + self._trace_config_ctx, + TraceResponseChunkReceivedParams(method, url, chunk), + ) + + async def send_request_end( + self, + method: str, + url: URL, + headers: "CIMultiDict[str]", + response: ClientResponse, + ) -> None: + return await self._trace_config.on_request_end.send( + self._session, + self._trace_config_ctx, + TraceRequestEndParams(method, url, headers, response), + ) + + async def send_request_exception( + self, + method: str, + url: URL, + headers: "CIMultiDict[str]", + exception: BaseException, + ) -> None: + return await self._trace_config.on_request_exception.send( + self._session, + self._trace_config_ctx, + TraceRequestExceptionParams(method, url, headers, exception), + ) + + async def send_request_redirect( + self, + method: str, + url: URL, + headers: "CIMultiDict[str]", + response: ClientResponse, + ) -> None: + return await self._trace_config._on_request_redirect.send( + self._session, + self._trace_config_ctx, + TraceRequestRedirectParams(method, url, headers, response), + ) + + async def send_connection_queued_start(self) -> None: + return await self._trace_config.on_connection_queued_start.send( + self._session, self._trace_config_ctx, TraceConnectionQueuedStartParams() + ) + + async def send_connection_queued_end(self) -> None: + return await self._trace_config.on_connection_queued_end.send( + self._session, self._trace_config_ctx, TraceConnectionQueuedEndParams() + ) + + async def send_connection_create_start(self) -> None: + return await self._trace_config.on_connection_create_start.send( + self._session, self._trace_config_ctx, TraceConnectionCreateStartParams() + ) + + async def send_connection_create_end(self) -> None: + return await self._trace_config.on_connection_create_end.send( + self._session, self._trace_config_ctx, TraceConnectionCreateEndParams() + ) + + async def send_connection_reuseconn(self) -> None: + return await self._trace_config.on_connection_reuseconn.send( + self._session, self._trace_config_ctx, TraceConnectionReuseconnParams() + ) + + async def send_dns_resolvehost_start(self, host: str) -> None: + return await self._trace_config.on_dns_resolvehost_start.send( + self._session, self._trace_config_ctx, TraceDnsResolveHostStartParams(host) + ) + + async def send_dns_resolvehost_end(self, host: str) -> None: + return await self._trace_config.on_dns_resolvehost_end.send( + self._session, self._trace_config_ctx, TraceDnsResolveHostEndParams(host) + ) + + async def send_dns_cache_hit(self, host: str) -> None: + return await self._trace_config.on_dns_cache_hit.send( + self._session, self._trace_config_ctx, TraceDnsCacheHitParams(host) + ) + + async def send_dns_cache_miss(self, host: str) -> None: + return await self._trace_config.on_dns_cache_miss.send( + self._session, self._trace_config_ctx, TraceDnsCacheMissParams(host) + ) + + async def send_request_headers( + self, method: str, url: URL, headers: "CIMultiDict[str]" + ) -> None: + return await self._trace_config._on_request_headers_sent.send( + self._session, + self._trace_config_ctx, + TraceRequestHeadersSentParams(method, url, headers), + ) diff --git a/venv/lib/python3.11/site-packages/aiohttp/typedefs.py b/venv/lib/python3.11/site-packages/aiohttp/typedefs.py new file mode 100644 index 0000000..ef27aff --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/typedefs.py @@ -0,0 +1,61 @@ +import json +import os +from collections.abc import Awaitable, Callable, Iterable, Mapping +from typing import TYPE_CHECKING, Any, Protocol, Union + +from multidict import CIMultiDict, CIMultiDictProxy, MultiDict, MultiDictProxy, istr +from yarl import URL, Query as _Query + +Query = _Query + +DEFAULT_JSON_ENCODER = json.dumps +DEFAULT_JSON_DECODER = json.loads + +if TYPE_CHECKING: + _CIMultiDict = CIMultiDict[str] + _CIMultiDictProxy = CIMultiDictProxy[str] + _MultiDict = MultiDict[str] + _MultiDictProxy = MultiDictProxy[str] + from http.cookies import BaseCookie, Morsel + + from .web import Request, StreamResponse +else: + _CIMultiDict = CIMultiDict + _CIMultiDictProxy = CIMultiDictProxy + _MultiDict = MultiDict + _MultiDictProxy = MultiDictProxy + +Byteish = Union[bytes, bytearray, memoryview] +JSONEncoder = Callable[[Any], str] +JSONBytesEncoder = Callable[[Any], bytes] +JSONDecoder = Callable[[str], Any] +LooseHeaders = Union[ + Mapping[str, str], + Mapping[istr, str], + _CIMultiDict, + _CIMultiDictProxy, + Iterable[tuple[str | istr, str]], +] +RawHeaders = tuple[tuple[bytes, bytes], ...] +StrOrURL = Union[str, URL] + +LooseCookiesMappings = Mapping[str, Union[str, "BaseCookie[str]", "Morsel[Any]"]] +LooseCookiesIterables = Iterable[ + tuple[str, Union[str, "BaseCookie[str]", "Morsel[Any]"]] +] +LooseCookies = Union[ + LooseCookiesMappings, + LooseCookiesIterables, + "BaseCookie[str]", +] + +Handler = Callable[["Request"], Awaitable["StreamResponse"]] + + +class Middleware(Protocol): + def __call__( + self, request: "Request", handler: Handler + ) -> Awaitable["StreamResponse"]: ... + + +PathLike = Union[str, "os.PathLike[str]"] diff --git a/venv/lib/python3.11/site-packages/aiohttp/web.py b/venv/lib/python3.11/site-packages/aiohttp/web.py new file mode 100644 index 0000000..f717d94 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web.py @@ -0,0 +1,593 @@ +import asyncio +import logging +import os +import socket +import sys +import warnings +from argparse import ArgumentParser +from collections.abc import Awaitable, Callable, Iterable, Iterable as TypingIterable +from contextlib import suppress +from importlib import import_module +from typing import TYPE_CHECKING, Any, cast + +from .abc import AbstractAccessLogger +from .helpers import AppKey, RequestKey, ResponseKey +from .log import access_logger +from .typedefs import PathLike +from .web_app import Application as Application, CleanupError as CleanupError +from .web_exceptions import ( + HTTPAccepted as HTTPAccepted, + HTTPBadGateway as HTTPBadGateway, + HTTPBadRequest as HTTPBadRequest, + HTTPClientError as HTTPClientError, + HTTPConflict as HTTPConflict, + HTTPCreated as HTTPCreated, + HTTPError as HTTPError, + HTTPException as HTTPException, + HTTPExpectationFailed as HTTPExpectationFailed, + HTTPFailedDependency as HTTPFailedDependency, + HTTPForbidden as HTTPForbidden, + HTTPFound as HTTPFound, + HTTPGatewayTimeout as HTTPGatewayTimeout, + HTTPGone as HTTPGone, + HTTPInsufficientStorage as HTTPInsufficientStorage, + HTTPInternalServerError as HTTPInternalServerError, + HTTPLengthRequired as HTTPLengthRequired, + HTTPMethodNotAllowed as HTTPMethodNotAllowed, + HTTPMisdirectedRequest as HTTPMisdirectedRequest, + HTTPMove as HTTPMove, + HTTPMovedPermanently as HTTPMovedPermanently, + HTTPMultipleChoices as HTTPMultipleChoices, + HTTPNetworkAuthenticationRequired as HTTPNetworkAuthenticationRequired, + HTTPNoContent as HTTPNoContent, + HTTPNonAuthoritativeInformation as HTTPNonAuthoritativeInformation, + HTTPNotAcceptable as HTTPNotAcceptable, + HTTPNotExtended as HTTPNotExtended, + HTTPNotFound as HTTPNotFound, + HTTPNotImplemented as HTTPNotImplemented, + HTTPNotModified as HTTPNotModified, + HTTPOk as HTTPOk, + HTTPPartialContent as HTTPPartialContent, + HTTPPaymentRequired as HTTPPaymentRequired, + HTTPPermanentRedirect as HTTPPermanentRedirect, + HTTPPreconditionFailed as HTTPPreconditionFailed, + HTTPPreconditionRequired as HTTPPreconditionRequired, + HTTPProxyAuthenticationRequired as HTTPProxyAuthenticationRequired, + HTTPRedirection as HTTPRedirection, + HTTPRequestEntityTooLarge as HTTPRequestEntityTooLarge, + HTTPRequestHeaderFieldsTooLarge as HTTPRequestHeaderFieldsTooLarge, + HTTPRequestRangeNotSatisfiable as HTTPRequestRangeNotSatisfiable, + HTTPRequestTimeout as HTTPRequestTimeout, + HTTPRequestURITooLong as HTTPRequestURITooLong, + HTTPResetContent as HTTPResetContent, + HTTPSeeOther as HTTPSeeOther, + HTTPServerError as HTTPServerError, + HTTPServiceUnavailable as HTTPServiceUnavailable, + HTTPSuccessful as HTTPSuccessful, + HTTPTemporaryRedirect as HTTPTemporaryRedirect, + HTTPTooManyRequests as HTTPTooManyRequests, + HTTPUnauthorized as HTTPUnauthorized, + HTTPUnavailableForLegalReasons as HTTPUnavailableForLegalReasons, + HTTPUnprocessableEntity as HTTPUnprocessableEntity, + HTTPUnsupportedMediaType as HTTPUnsupportedMediaType, + HTTPUpgradeRequired as HTTPUpgradeRequired, + HTTPUseProxy as HTTPUseProxy, + HTTPVariantAlsoNegotiates as HTTPVariantAlsoNegotiates, + HTTPVersionNotSupported as HTTPVersionNotSupported, + NotAppKeyWarning as NotAppKeyWarning, +) +from .web_fileresponse import FileResponse as FileResponse +from .web_log import AccessLogger +from .web_middlewares import ( + middleware as middleware, + normalize_path_middleware as normalize_path_middleware, +) +from .web_protocol import ( + PayloadAccessError as PayloadAccessError, + RequestHandler as RequestHandler, + RequestPayloadError as RequestPayloadError, +) +from .web_request import ( + BaseRequest as BaseRequest, + FileField as FileField, + Request as Request, +) +from .web_response import ( + ContentCoding as ContentCoding, + Response as Response, + StreamResponse as StreamResponse, + json_bytes_response as json_bytes_response, + json_response as json_response, +) +from .web_routedef import ( + AbstractRouteDef as AbstractRouteDef, + RouteDef as RouteDef, + RouteTableDef as RouteTableDef, + StaticDef as StaticDef, + delete as delete, + get as get, + head as head, + options as options, + patch as patch, + post as post, + put as put, + route as route, + static as static, + view as view, +) +from .web_runner import ( + AppRunner as AppRunner, + BaseRunner as BaseRunner, + BaseSite as BaseSite, + GracefulExit as GracefulExit, + NamedPipeSite as NamedPipeSite, + ServerRunner as ServerRunner, + SockSite as SockSite, + TCPSite as TCPSite, + UnixSite as UnixSite, +) +from .web_server import Server as Server +from .web_urldispatcher import ( + AbstractResource as AbstractResource, + AbstractRoute as AbstractRoute, + DynamicResource as DynamicResource, + PlainResource as PlainResource, + PrefixedSubAppResource as PrefixedSubAppResource, + Resource as Resource, + ResourceRoute as ResourceRoute, + StaticResource as StaticResource, + UrlDispatcher as UrlDispatcher, + UrlMappingMatchInfo as UrlMappingMatchInfo, + View as View, +) +from .web_ws import ( + WebSocketReady as WebSocketReady, + WebSocketResponse as WebSocketResponse, + WSMsgType as WSMsgType, +) + +__all__ = ( + # web_app + "AppKey", + "Application", + "CleanupError", + # web_exceptions + "NotAppKeyWarning", + "HTTPAccepted", + "HTTPBadGateway", + "HTTPBadRequest", + "HTTPClientError", + "HTTPConflict", + "HTTPCreated", + "HTTPError", + "HTTPException", + "HTTPExpectationFailed", + "HTTPFailedDependency", + "HTTPForbidden", + "HTTPFound", + "HTTPGatewayTimeout", + "HTTPGone", + "HTTPInsufficientStorage", + "HTTPInternalServerError", + "HTTPLengthRequired", + "HTTPMethodNotAllowed", + "HTTPMisdirectedRequest", + "HTTPMove", + "HTTPMovedPermanently", + "HTTPMultipleChoices", + "HTTPNetworkAuthenticationRequired", + "HTTPNoContent", + "HTTPNonAuthoritativeInformation", + "HTTPNotAcceptable", + "HTTPNotExtended", + "HTTPNotFound", + "HTTPNotImplemented", + "HTTPNotModified", + "HTTPOk", + "HTTPPartialContent", + "HTTPPaymentRequired", + "HTTPPermanentRedirect", + "HTTPPreconditionFailed", + "HTTPPreconditionRequired", + "HTTPProxyAuthenticationRequired", + "HTTPRedirection", + "HTTPRequestEntityTooLarge", + "HTTPRequestHeaderFieldsTooLarge", + "HTTPRequestRangeNotSatisfiable", + "HTTPRequestTimeout", + "HTTPRequestURITooLong", + "HTTPResetContent", + "HTTPSeeOther", + "HTTPServerError", + "HTTPServiceUnavailable", + "HTTPSuccessful", + "HTTPTemporaryRedirect", + "HTTPTooManyRequests", + "HTTPUnauthorized", + "HTTPUnavailableForLegalReasons", + "HTTPUnprocessableEntity", + "HTTPUnsupportedMediaType", + "HTTPUpgradeRequired", + "HTTPUseProxy", + "HTTPVariantAlsoNegotiates", + "HTTPVersionNotSupported", + # web_fileresponse + "FileResponse", + # web_middlewares + "middleware", + "normalize_path_middleware", + # web_protocol + "PayloadAccessError", + "RequestHandler", + "RequestPayloadError", + # web_request + "BaseRequest", + "FileField", + "Request", + "RequestKey", + # web_response + "ContentCoding", + "Response", + "StreamResponse", + "json_bytes_response", + "json_response", + "ResponseKey", + # web_routedef + "AbstractRouteDef", + "RouteDef", + "RouteTableDef", + "StaticDef", + "delete", + "get", + "head", + "options", + "patch", + "post", + "put", + "route", + "static", + "view", + # web_runner + "AppRunner", + "BaseRunner", + "BaseSite", + "GracefulExit", + "ServerRunner", + "SockSite", + "TCPSite", + "UnixSite", + "NamedPipeSite", + # web_server + "Server", + # web_urldispatcher + "AbstractResource", + "AbstractRoute", + "DynamicResource", + "PlainResource", + "PrefixedSubAppResource", + "Resource", + "ResourceRoute", + "StaticResource", + "UrlDispatcher", + "UrlMappingMatchInfo", + "View", + # web_ws + "WebSocketReady", + "WebSocketResponse", + "WSMsgType", + # web + "run_app", +) + + +if TYPE_CHECKING: + from ssl import SSLContext +else: + try: + from ssl import SSLContext + except ImportError: # pragma: no cover + SSLContext = object # type: ignore[misc,assignment] + +# Only display warning when using -Wdefault, -We, -X dev or similar. +warnings.filterwarnings("ignore", category=NotAppKeyWarning, append=True) + +HostSequence = TypingIterable[str] + + +async def _run_app( + app: Application | Awaitable[Application], + *, + host: str | HostSequence | None = None, + port: int | None = None, + path: PathLike | TypingIterable[PathLike] | None = None, + sock: socket.socket | TypingIterable[socket.socket] | None = None, + ssl_context: SSLContext | None = None, + print: Callable[..., None] | None = print, + backlog: int = 128, + reuse_address: bool | None = None, + reuse_port: bool | None = None, + **kwargs: Any, # TODO(PY311): Use Unpack +) -> None: + # An internal function to actually do all dirty job for application running + if asyncio.iscoroutine(app): + app = await app + + app = cast(Application, app) + + runner = AppRunner(app, **kwargs) + + await runner.setup() + + sites: list[BaseSite] = [] + + try: + if host is not None: + if isinstance(host, str): + sites.append( + TCPSite( + runner, + host, + port, + ssl_context=ssl_context, + backlog=backlog, + reuse_address=reuse_address, + reuse_port=reuse_port, + ) + ) + else: + for h in host: + sites.append( + TCPSite( + runner, + h, + port, + ssl_context=ssl_context, + backlog=backlog, + reuse_address=reuse_address, + reuse_port=reuse_port, + ) + ) + elif path is None and sock is None or port is not None: + sites.append( + TCPSite( + runner, + port=port, + ssl_context=ssl_context, + backlog=backlog, + reuse_address=reuse_address, + reuse_port=reuse_port, + ) + ) + + if path is not None: + if isinstance(path, (str, os.PathLike)): + sites.append( + UnixSite( + runner, + path, + ssl_context=ssl_context, + backlog=backlog, + ) + ) + else: + for p in path: + sites.append( + UnixSite( + runner, + p, + ssl_context=ssl_context, + backlog=backlog, + ) + ) + + if sock is not None: + if not isinstance(sock, Iterable): + sites.append( + SockSite( + runner, + sock, + ssl_context=ssl_context, + backlog=backlog, + ) + ) + else: + for s in sock: + sites.append( + SockSite( + runner, + s, + ssl_context=ssl_context, + backlog=backlog, + ) + ) + for site in sites: + await site.start() + + if print: # pragma: no branch + names = sorted(str(s.name) for s in runner.sites) + print( + "======== Running on {} ========\n" + "(Press CTRL+C to quit)".format(", ".join(names)) + ) + + # sleep forever by 1 hour intervals, + while True: + await asyncio.sleep(3600) + finally: + await runner.cleanup() + + +def _cancel_tasks( + to_cancel: set["asyncio.Task[Any]"], loop: asyncio.AbstractEventLoop +) -> None: + if not to_cancel: + return + + for task in to_cancel: + task.cancel() + + loop.run_until_complete(asyncio.gather(*to_cancel, return_exceptions=True)) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "unhandled exception during asyncio.run() shutdown", + "exception": task.exception(), + "task": task, + } + ) + + +def run_app( + app: Application | Awaitable[Application], + *, + host: str | HostSequence | None = None, + port: int | None = None, + path: PathLike | TypingIterable[PathLike] | None = None, + sock: socket.socket | TypingIterable[socket.socket] | None = None, + shutdown_timeout: float = 60.0, + keepalive_timeout: float = 75.0, + ssl_context: SSLContext | None = None, + print: Callable[..., None] | None = print, + backlog: int = 128, + access_log_class: type[AbstractAccessLogger] = AccessLogger, + access_log_format: str = AccessLogger.LOG_FORMAT, + access_log: logging.Logger | None = access_logger, + handle_signals: bool = True, + reuse_address: bool | None = None, + reuse_port: bool | None = None, + handler_cancellation: bool = False, + loop: asyncio.AbstractEventLoop | None = None, + **kwargs: Any, +) -> None: + """Run an app locally""" + if loop is None: + loop = asyncio.new_event_loop() + + # Configure if and only if in debugging mode and using the default logger + if loop.get_debug() and access_log and access_log.name == "aiohttp.access": + if access_log.level == logging.NOTSET: + access_log.setLevel(logging.DEBUG) + if not access_log.hasHandlers(): + access_log.addHandler(logging.StreamHandler()) + + main_task = loop.create_task( + _run_app( + app, + host=host, + port=port, + path=path, + sock=sock, + shutdown_timeout=shutdown_timeout, + keepalive_timeout=keepalive_timeout, + ssl_context=ssl_context, + print=print, + backlog=backlog, + access_log_class=access_log_class, + access_log_format=access_log_format, + access_log=access_log, + handle_signals=handle_signals, + reuse_address=reuse_address, + reuse_port=reuse_port, + handler_cancellation=handler_cancellation, + **kwargs, + ) + ) + + try: + asyncio.set_event_loop(loop) + loop.run_until_complete(main_task) + except (GracefulExit, KeyboardInterrupt): # pragma: no cover + pass + finally: + try: + # Skip when ``main_task`` is already done (e.g. raised during startup). + # Re-running ``loop.run_until_complete`` on a finished task calls + # ``Future.result`` again, which does + # ``raise self._exception.with_traceback(self._exception_tb)`` and + # resets ``exc.__traceback__`` to the originally saved tb — by then + # shallow — clobbering the deep traceback the caller would otherwise + # see (frames from ``cleanup_ctx`` / ``on_startup`` and the user code + # that actually raised). + if not main_task.done(): + main_task.cancel() + with suppress(asyncio.CancelledError): + loop.run_until_complete(main_task) + finally: + _cancel_tasks(asyncio.all_tasks(loop), loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + loop.close() + + +def main(argv: list[str]) -> None: + arg_parser = ArgumentParser( + description="aiohttp.web Application server", prog="aiohttp.web" + ) + arg_parser.add_argument( + "entry_func", + help=( + "Callable returning the `aiohttp.web.Application` instance to " + "run. Should be specified in the 'module:function' syntax." + ), + metavar="entry-func", + ) + arg_parser.add_argument( + "-H", + "--hostname", + help="TCP/IP hostname to serve on (default: localhost)", + default=None, + ) + arg_parser.add_argument( + "-P", + "--port", + help="TCP/IP port to serve on (default: %(default)r)", + type=int, + default=8080, + ) + arg_parser.add_argument( + "-U", + "--path", + help="Unix file system path to serve on. Can be combined with hostname " + "to serve on both Unix and TCP.", + ) + args, extra_argv = arg_parser.parse_known_args(argv) + + # Import logic + mod_str, _, func_str = args.entry_func.partition(":") + if not func_str or not mod_str: + arg_parser.error("'entry-func' not in 'module:function' syntax") + if mod_str.startswith("."): + arg_parser.error("relative module names not supported") + try: + module = import_module(mod_str) + except ImportError as ex: + arg_parser.error(f"unable to import {mod_str}: {ex}") + try: + func = getattr(module, func_str) + except AttributeError: + arg_parser.error(f"module {mod_str!r} has no attribute {func_str!r}") + + # Compatibility logic + if args.path is not None and not hasattr(socket, "AF_UNIX"): + arg_parser.error( + "file system paths not supported by your operating environment" + ) + + logging.basicConfig(level=logging.DEBUG) + + if args.path and args.hostname is None: + host = port = None + else: + host = args.hostname or "localhost" + port = args.port + + app = func(extra_argv) + run_app(app, host=host, port=port, path=args.path) + arg_parser.exit(message="Stopped\n") + + +if __name__ == "__main__": # pragma: no branch + main(sys.argv[1:]) # pragma: no cover diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_app.py b/venv/lib/python3.11/site-packages/aiohttp/web_app.py new file mode 100644 index 0000000..dc748af --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_app.py @@ -0,0 +1,610 @@ +import asyncio +import logging +import warnings +from collections.abc import ( + AsyncIterator, + Awaitable, + Callable, + Iterable, + Iterator, + Mapping, + MutableMapping, + Sequence, +) +from contextlib import AbstractAsyncContextManager, asynccontextmanager +from functools import lru_cache, partial, update_wrapper +from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast, overload + +from aiosignal import Signal +from frozenlist import FrozenList + +from . import hdrs +from .abc import ( + AbstractAccessLogger, + AbstractMatchInfo, + AbstractRouter, + AbstractStreamWriter, +) +from .helpers import DEBUG, AppKey +from .http_parser import RawRequestMessage +from .log import web_logger +from .streams import StreamReader +from .typedefs import Handler, Middleware +from .web_exceptions import NotAppKeyWarning +from .web_log import AccessLogger +from .web_middlewares import _fix_request_current_app +from .web_protocol import RequestHandler +from .web_request import Request +from .web_response import StreamResponse +from .web_routedef import AbstractRouteDef +from .web_server import Server +from .web_urldispatcher import ( + AbstractResource, + AbstractRoute, + Domain, + MaskDomain, + MatchedSubAppResource, + PrefixedSubAppResource, + SystemRoute, + UrlDispatcher, +) + +__all__ = ("Application", "CleanupError") + + +if TYPE_CHECKING: + _AppSignal = Signal["Application"] + _RespPrepareSignal = Signal[Request, StreamResponse] + _Middlewares = FrozenList[Middleware] + _MiddlewaresHandlers = Optional[Sequence[tuple[Middleware, bool]]] + _Subapps = list["Application"] +else: + # No type checker mode, skip types + _AppSignal = Signal + _RespPrepareSignal = Signal + _Middlewares = FrozenList + _MiddlewaresHandlers = Optional[Sequence] + _Subapps = list + +_T = TypeVar("_T") +_U = TypeVar("_U") +_Resource = TypeVar("_Resource", bound=AbstractResource) + + +def _build_middlewares( + handler: Handler, apps: tuple["Application", ...] +) -> Callable[[Request], Awaitable[StreamResponse]]: + """Apply middlewares to handler.""" + for app in apps[::-1]: + for m, _ in app._middlewares_handlers: # type: ignore[union-attr] + handler = update_wrapper(partial(m, handler=handler), handler) + return handler + + +_cached_build_middleware = lru_cache(maxsize=1024)(_build_middlewares) + + +class Application(MutableMapping[str | AppKey[Any], Any]): + ATTRS = frozenset( + [ + "logger", + "_debug", + "_router", + "_loop", + "_handler_args", + "_middlewares", + "_middlewares_handlers", + "_has_legacy_middlewares", + "_run_middlewares", + "_state", + "_frozen", + "_pre_frozen", + "_subapps", + "_on_response_prepare", + "_on_startup", + "_on_shutdown", + "_on_cleanup", + "_client_max_size", + "_cleanup_ctx", + ] + ) + + def __init__( + self, + *, + logger: logging.Logger = web_logger, + router: UrlDispatcher | None = None, + middlewares: Iterable[Middleware] = (), + handler_args: Mapping[str, Any] | None = None, + client_max_size: int = 1024**2, + loop: asyncio.AbstractEventLoop | None = None, + debug: Any = ..., # mypy doesn't support ellipsis + ) -> None: + if router is None: + router = UrlDispatcher() + else: + warnings.warn( + "router argument is deprecated", DeprecationWarning, stacklevel=2 + ) + assert isinstance(router, AbstractRouter), router + + if loop is not None: + warnings.warn( + "loop argument is deprecated", DeprecationWarning, stacklevel=2 + ) + + if debug is not ...: + warnings.warn( + "debug argument is deprecated", DeprecationWarning, stacklevel=2 + ) + self._debug = debug + self._router: UrlDispatcher = router + self._loop = loop + self._handler_args = handler_args + self.logger = logger + + self._middlewares: _Middlewares = FrozenList(middlewares) + + # initialized on freezing + self._middlewares_handlers: _MiddlewaresHandlers = None + # initialized on freezing + self._run_middlewares: bool | None = None + self._has_legacy_middlewares: bool = True + + self._state: dict[AppKey[Any] | str, object] = {} + self._frozen = False + self._pre_frozen = False + self._subapps: _Subapps = [] + + self._on_response_prepare: _RespPrepareSignal = Signal(self) + self._on_startup: _AppSignal = Signal(self) + self._on_shutdown: _AppSignal = Signal(self) + self._on_cleanup: _AppSignal = Signal(self) + self._cleanup_ctx = CleanupContext() + self._on_startup.append(self._cleanup_ctx._on_startup) + self._on_cleanup.append(self._cleanup_ctx._on_cleanup) + self._client_max_size = client_max_size + + def __init_subclass__(cls: type["Application"]) -> None: + warnings.warn( + f"Inheritance class {cls.__name__} from web.Application is discouraged", + DeprecationWarning, + stacklevel=3, + ) + + if DEBUG: # pragma: no cover + + def __setattr__(self, name: str, val: Any) -> None: + if name not in self.ATTRS: + warnings.warn( + f"Setting custom web.Application.{name} attribute " + "is discouraged", + DeprecationWarning, + stacklevel=2, + ) + super().__setattr__(name, val) + + # MutableMapping API + + def __eq__(self, other: object) -> bool: + return self is other + + @overload # type: ignore[override] + def __getitem__(self, key: AppKey[_T]) -> _T: ... + + @overload + def __getitem__(self, key: str) -> Any: ... + + def __getitem__(self, key: str | AppKey[_T]) -> Any: + return self._state[key] + + def _check_frozen(self) -> None: + if self._frozen: + warnings.warn( + "Changing state of started or joined application is deprecated", + DeprecationWarning, + stacklevel=3, + ) + + @overload # type: ignore[override] + def __setitem__(self, key: AppKey[_T], value: _T) -> None: ... + + @overload + def __setitem__(self, key: str, value: Any) -> None: ... + + def __setitem__(self, key: str | AppKey[_T], value: Any) -> None: + self._check_frozen() + if not isinstance(key, AppKey): + warnings.warn( + "It is recommended to use web.AppKey instances for keys.\n" + + "https://docs.aiohttp.org/en/stable/web_advanced.html" + + "#application-s-config", + category=NotAppKeyWarning, + stacklevel=2, + ) + self._state[key] = value + + def __delitem__(self, key: str | AppKey[_T]) -> None: + self._check_frozen() + del self._state[key] + + def __len__(self) -> int: + return len(self._state) + + def __iter__(self) -> Iterator[str | AppKey[Any]]: + return iter(self._state) + + def __hash__(self) -> int: + return id(self) + + @overload # type: ignore[override] + def get(self, key: AppKey[_T], default: None = ...) -> _T | None: ... + + @overload + def get(self, key: AppKey[_T], default: _U) -> _T | _U: ... + + @overload + def get(self, key: str, default: Any = ...) -> Any: ... + + def get(self, key: str | AppKey[_T], default: Any = None) -> Any: + return self._state.get(key, default) + + ######## + @property + def loop(self) -> asyncio.AbstractEventLoop: + # Technically the loop can be None + # but we mask it by explicit type cast + # to provide more convenient type annotation + warnings.warn("loop property is deprecated", DeprecationWarning, stacklevel=2) + return cast(asyncio.AbstractEventLoop, self._loop) + + def _set_loop(self, loop: asyncio.AbstractEventLoop | None) -> None: + if loop is None: + loop = asyncio.get_event_loop() + if self._loop is not None and self._loop is not loop: + raise RuntimeError( + "web.Application instance initialized with different loop" + ) + + self._loop = loop + + # set loop debug + if self._debug is ...: + self._debug = loop.get_debug() + + # set loop to sub applications + for subapp in self._subapps: + subapp._set_loop(loop) + + @property + def pre_frozen(self) -> bool: + return self._pre_frozen + + def pre_freeze(self) -> None: + if self._pre_frozen: + return + + self._pre_frozen = True + self._middlewares.freeze() + self._router.freeze() + self._on_response_prepare.freeze() + self._cleanup_ctx.freeze() + self._on_startup.freeze() + self._on_shutdown.freeze() + self._on_cleanup.freeze() + self._middlewares_handlers = tuple(self._prepare_middleware()) + self._has_legacy_middlewares = any( + not new_style for _, new_style in self._middlewares_handlers + ) + + # If current app and any subapp do not have middlewares avoid run all + # of the code footprint that it implies, which have a middleware + # hardcoded per app that sets up the current_app attribute. If no + # middlewares are configured the handler will receive the proper + # current_app without needing all of this code. + self._run_middlewares = True if self.middlewares else False + + for subapp in self._subapps: + subapp.pre_freeze() + self._run_middlewares = self._run_middlewares or subapp._run_middlewares + + @property + def frozen(self) -> bool: + return self._frozen + + def freeze(self) -> None: + if self._frozen: + return + + self.pre_freeze() + self._frozen = True + for subapp in self._subapps: + subapp.freeze() + + @property + def debug(self) -> bool: + warnings.warn("debug property is deprecated", DeprecationWarning, stacklevel=2) + return self._debug # type: ignore[no-any-return] + + def _reg_subapp_signals(self, subapp: "Application") -> None: + def reg_handler(signame: str) -> None: + subsig = getattr(subapp, signame) + + async def handler(app: "Application") -> None: + await subsig.send(subapp) + + appsig = getattr(self, signame) + appsig.append(handler) + + reg_handler("on_startup") + reg_handler("on_shutdown") + reg_handler("on_cleanup") + + def add_subapp(self, prefix: str, subapp: "Application") -> PrefixedSubAppResource: + if not isinstance(prefix, str): + raise TypeError("Prefix must be str") + prefix = prefix.rstrip("/") + if not prefix: + raise ValueError("Prefix cannot be empty") + factory = partial(PrefixedSubAppResource, prefix, subapp) + return self._add_subapp(factory, subapp) + + def _add_subapp( + self, resource_factory: Callable[[], _Resource], subapp: "Application" + ) -> _Resource: + if self.frozen: + raise RuntimeError("Cannot add sub application to frozen application") + if subapp.frozen: + raise RuntimeError("Cannot add frozen application") + resource = resource_factory() + self.router.register_resource(resource) + self._reg_subapp_signals(subapp) + self._subapps.append(subapp) + subapp.pre_freeze() + if self._loop is not None: + subapp._set_loop(self._loop) + return resource + + def add_domain(self, domain: str, subapp: "Application") -> MatchedSubAppResource: + if not isinstance(domain, str): + raise TypeError("Domain must be str") + elif "*" in domain: + rule: Domain = MaskDomain(domain) + else: + rule = Domain(domain) + factory = partial(MatchedSubAppResource, rule, subapp) + return self._add_subapp(factory, subapp) + + def add_routes(self, routes: Iterable[AbstractRouteDef]) -> list[AbstractRoute]: + return self.router.add_routes(routes) + + @property + def on_response_prepare(self) -> _RespPrepareSignal: + return self._on_response_prepare + + @property + def on_startup(self) -> _AppSignal: + return self._on_startup + + @property + def on_shutdown(self) -> _AppSignal: + return self._on_shutdown + + @property + def on_cleanup(self) -> _AppSignal: + return self._on_cleanup + + @property + def cleanup_ctx(self) -> "CleanupContext": + return self._cleanup_ctx + + @property + def router(self) -> UrlDispatcher: + return self._router + + @property + def middlewares(self) -> _Middlewares: + return self._middlewares + + def _make_handler( + self, + *, + loop: asyncio.AbstractEventLoop | None = None, + access_log_class: type[AbstractAccessLogger] = AccessLogger, + **kwargs: Any, + ) -> Server: + + if not issubclass(access_log_class, AbstractAccessLogger): + raise TypeError( + "access_log_class must be subclass of " + f"aiohttp.abc.AbstractAccessLogger, got {access_log_class}" + ) + + self._set_loop(loop) + self.freeze() + + kwargs["debug"] = self._debug + kwargs["access_log_class"] = access_log_class + if self._handler_args: + for k, v in self._handler_args.items(): + kwargs[k] = v + + return Server( + self._handle, # type: ignore[arg-type] + request_factory=self._make_request, + loop=self._loop, + **kwargs, + ) + + def make_handler( + self, + *, + loop: asyncio.AbstractEventLoop | None = None, + access_log_class: type[AbstractAccessLogger] = AccessLogger, + **kwargs: Any, + ) -> Server: + + warnings.warn( + "Application.make_handler(...) is deprecated, use AppRunner API instead", + DeprecationWarning, + stacklevel=2, + ) + + return self._make_handler( + loop=loop, access_log_class=access_log_class, **kwargs + ) + + async def startup(self) -> None: + """Causes on_startup signal + + Should be called in the event loop along with the request handler. + """ + await self.on_startup.send(self) + + async def shutdown(self) -> None: + """Causes on_shutdown signal + + Should be called before cleanup() + """ + await self.on_shutdown.send(self) + + async def cleanup(self) -> None: + """Causes on_cleanup signal + + Should be called after shutdown() + """ + if self.on_cleanup.frozen: + await self.on_cleanup.send(self) + else: + # If an exception occurs in startup, ensure cleanup contexts are completed. + await self._cleanup_ctx._on_cleanup(self) + + def _make_request( + self, + message: RawRequestMessage, + payload: StreamReader, + protocol: RequestHandler, + writer: AbstractStreamWriter, + task: "asyncio.Task[None]", + _cls: type[Request] = Request, + ) -> Request: + if TYPE_CHECKING: + assert self._loop is not None + return _cls( + message, + payload, + protocol, + writer, + task, + self._loop, + client_max_size=self._client_max_size, + ) + + def _prepare_middleware(self) -> Iterator[tuple[Middleware, bool]]: + for m in reversed(self._middlewares): + if getattr(m, "__middleware_version__", None) == 1: + yield m, True + else: + warnings.warn( + f'old-style middleware "{m!r}" deprecated, see #2252', + DeprecationWarning, + stacklevel=2, + ) + yield m, False + + yield _fix_request_current_app(self), True + + async def _handle(self, request: Request) -> StreamResponse: + loop = asyncio.get_event_loop() + debug = loop.get_debug() + match_info = await self._router.resolve(request) + if debug: # pragma: no cover + if not isinstance(match_info, AbstractMatchInfo): + raise TypeError( + "match_info should be AbstractMatchInfo " + f"instance, not {match_info!r}" + ) + match_info.add_app(self) + + match_info.freeze() + + request._match_info = match_info + + if request.headers.get(hdrs.EXPECT): + resp = await match_info.expect_handler(request) + await request.writer.drain() + if resp is not None: + return resp + + handler = match_info.handler + + if self._run_middlewares: + # If its a SystemRoute, don't cache building the middlewares since + # they are constructed for every MatchInfoError as a new handler + # is made each time. + if not self._has_legacy_middlewares and not isinstance( + match_info.route, SystemRoute + ): + handler = _cached_build_middleware(handler, match_info.apps) + else: + for app in match_info.apps[::-1]: + for m, new_style in app._middlewares_handlers: # type: ignore[union-attr] + if new_style: + handler = update_wrapper( + partial(m, handler=handler), handler + ) + else: + handler = await m(app, handler) # type: ignore[arg-type,assignment] + + return await handler(request) + + def __call__(self) -> "Application": + """gunicorn compatibility""" + return self + + def __repr__(self) -> str: + return f"" + + def __bool__(self) -> bool: + return True + + +class CleanupError(RuntimeError): + @property + def exceptions(self) -> list[BaseException]: + return cast(list[BaseException], self.args[1]) + + +_CleanupContextCallable = ( + Callable[[Application], AbstractAsyncContextManager[None]] + | Callable[[Application], AsyncIterator[None]] +) + + +class CleanupContext(FrozenList[_CleanupContextCallable]): + def __init__(self) -> None: + super().__init__() + self._exits: list[AbstractAsyncContextManager[None]] = [] + + async def _on_startup(self, app: Application) -> None: + for cb in self: + ctx = cb(app) + + if not isinstance(ctx, AbstractAsyncContextManager): + ctx = asynccontextmanager(cb)(app) # type: ignore[arg-type] + + await ctx.__aenter__() + self._exits.append(ctx) + + async def _on_cleanup(self, app: Application) -> None: + errors = [] + for it in reversed(self._exits): + try: + await it.__aexit__(None, None, None) + except (Exception, asyncio.CancelledError) as exc: + errors.append(exc) + if errors: + if len(errors) == 1: + raise errors[0] + else: + raise CleanupError("Multiple errors on cleanup stage", errors) diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_exceptions.py b/venv/lib/python3.11/site-packages/aiohttp/web_exceptions.py new file mode 100644 index 0000000..b5b3b30 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_exceptions.py @@ -0,0 +1,450 @@ +import warnings +from typing import Any, Dict, Iterable, List, Optional, Set # noqa + +from yarl import URL + +from .typedefs import LooseHeaders, StrOrURL +from .web_response import Response + +__all__ = ( + "HTTPException", + "HTTPError", + "HTTPRedirection", + "HTTPSuccessful", + "HTTPOk", + "HTTPCreated", + "HTTPAccepted", + "HTTPNonAuthoritativeInformation", + "HTTPNoContent", + "HTTPResetContent", + "HTTPPartialContent", + "HTTPMove", + "HTTPMultipleChoices", + "HTTPMovedPermanently", + "HTTPFound", + "HTTPSeeOther", + "HTTPNotModified", + "HTTPUseProxy", + "HTTPTemporaryRedirect", + "HTTPPermanentRedirect", + "HTTPClientError", + "HTTPBadRequest", + "HTTPUnauthorized", + "HTTPPaymentRequired", + "HTTPForbidden", + "HTTPNotFound", + "HTTPMethodNotAllowed", + "HTTPNotAcceptable", + "HTTPProxyAuthenticationRequired", + "HTTPRequestTimeout", + "HTTPConflict", + "HTTPGone", + "HTTPLengthRequired", + "HTTPPreconditionFailed", + "HTTPRequestEntityTooLarge", + "HTTPRequestURITooLong", + "HTTPUnsupportedMediaType", + "HTTPRequestRangeNotSatisfiable", + "HTTPExpectationFailed", + "HTTPMisdirectedRequest", + "HTTPUnprocessableEntity", + "HTTPFailedDependency", + "HTTPUpgradeRequired", + "HTTPPreconditionRequired", + "HTTPTooManyRequests", + "HTTPRequestHeaderFieldsTooLarge", + "HTTPUnavailableForLegalReasons", + "HTTPServerError", + "HTTPInternalServerError", + "HTTPNotImplemented", + "HTTPBadGateway", + "HTTPServiceUnavailable", + "HTTPGatewayTimeout", + "HTTPVersionNotSupported", + "HTTPVariantAlsoNegotiates", + "HTTPInsufficientStorage", + "HTTPNotExtended", + "HTTPNetworkAuthenticationRequired", +) + + +class NotAppKeyWarning(UserWarning): + """Warning when not using AppKey in Application.""" + + +############################################################ +# HTTP Exceptions +############################################################ + + +class HTTPException(Response, Exception): + + # You should set in subclasses: + # status = 200 + + status_code = -1 + empty_body = False + + __http_exception__ = True + + def __init__( + self, + *, + headers: LooseHeaders | None = None, + reason: str | None = None, + body: Any = None, + text: str | None = None, + content_type: str | None = None, + ) -> None: + if body is not None: + warnings.warn( + "body argument is deprecated for http web exceptions", + DeprecationWarning, + ) + if reason is not None and ("\r" in reason or "\n" in reason): + raise ValueError("Reason cannot contain \\r or \\n") + Response.__init__( + self, + status=self.status_code, + headers=headers, + reason=reason, + body=body, + text=text, + content_type=content_type, + ) + Exception.__init__(self, self.reason) + if self.body is None and not self.empty_body: + self.text = f"{self.status}: {self.reason}" + + def __bool__(self) -> bool: + return True + + +class HTTPError(HTTPException): + """Base class for exceptions with status codes in the 400s and 500s.""" + + +class HTTPRedirection(HTTPException): + """Base class for exceptions with status codes in the 300s.""" + + +class HTTPSuccessful(HTTPException): + """Base class for exceptions with status codes in the 200s.""" + + +class HTTPOk(HTTPSuccessful): + status_code = 200 + + +class HTTPCreated(HTTPSuccessful): + status_code = 201 + + +class HTTPAccepted(HTTPSuccessful): + status_code = 202 + + +class HTTPNonAuthoritativeInformation(HTTPSuccessful): + status_code = 203 + + +class HTTPNoContent(HTTPSuccessful): + status_code = 204 + empty_body = True + + +class HTTPResetContent(HTTPSuccessful): + status_code = 205 + empty_body = True + + +class HTTPPartialContent(HTTPSuccessful): + status_code = 206 + + +############################################################ +# 3xx redirection +############################################################ + + +class HTTPMove(HTTPRedirection): + def __init__( + self, + location: StrOrURL, + *, + headers: LooseHeaders | None = None, + reason: str | None = None, + body: Any = None, + text: str | None = None, + content_type: str | None = None, + ) -> None: + if not location: + raise ValueError("HTTP redirects need a location to redirect to.") + super().__init__( + headers=headers, + reason=reason, + body=body, + text=text, + content_type=content_type, + ) + self.headers["Location"] = str(URL(location)) + self.location = location + + +class HTTPMultipleChoices(HTTPMove): + status_code = 300 + + +class HTTPMovedPermanently(HTTPMove): + status_code = 301 + + +class HTTPFound(HTTPMove): + status_code = 302 + + +# This one is safe after a POST (the redirected location will be +# retrieved with GET): +class HTTPSeeOther(HTTPMove): + status_code = 303 + + +class HTTPNotModified(HTTPRedirection): + # FIXME: this should include a date or etag header + status_code = 304 + empty_body = True + + +class HTTPUseProxy(HTTPMove): + # Not a move, but looks a little like one + status_code = 305 + + +class HTTPTemporaryRedirect(HTTPMove): + status_code = 307 + + +class HTTPPermanentRedirect(HTTPMove): + status_code = 308 + + +############################################################ +# 4xx client error +############################################################ + + +class HTTPClientError(HTTPError): + pass + + +class HTTPBadRequest(HTTPClientError): + status_code = 400 + + +class HTTPUnauthorized(HTTPClientError): + status_code = 401 + + +class HTTPPaymentRequired(HTTPClientError): + status_code = 402 + + +class HTTPForbidden(HTTPClientError): + status_code = 403 + + +class HTTPNotFound(HTTPClientError): + status_code = 404 + + +class HTTPMethodNotAllowed(HTTPClientError): + status_code = 405 + + def __init__( + self, + method: str, + allowed_methods: Iterable[str], + *, + headers: LooseHeaders | None = None, + reason: str | None = None, + body: Any = None, + text: str | None = None, + content_type: str | None = None, + ) -> None: + allow = ",".join(sorted(allowed_methods)) + super().__init__( + headers=headers, + reason=reason, + body=body, + text=text, + content_type=content_type, + ) + self.headers["Allow"] = allow + self.allowed_methods: set[str] = set(allowed_methods) + self.method = method.upper() + + +class HTTPNotAcceptable(HTTPClientError): + status_code = 406 + + +class HTTPProxyAuthenticationRequired(HTTPClientError): + status_code = 407 + + +class HTTPRequestTimeout(HTTPClientError): + status_code = 408 + + +class HTTPConflict(HTTPClientError): + status_code = 409 + + +class HTTPGone(HTTPClientError): + status_code = 410 + + +class HTTPLengthRequired(HTTPClientError): + status_code = 411 + + +class HTTPPreconditionFailed(HTTPClientError): + status_code = 412 + + +class HTTPRequestEntityTooLarge(HTTPClientError): + status_code = 413 + + def __init__(self, max_size: float, actual_size: float = 0, **kwargs: Any) -> None: + kwargs.setdefault("text", f"Maximum request body size {max_size} exceeded.") + super().__init__(**kwargs) + + +class HTTPRequestURITooLong(HTTPClientError): + status_code = 414 + + +class HTTPUnsupportedMediaType(HTTPClientError): + status_code = 415 + + +class HTTPRequestRangeNotSatisfiable(HTTPClientError): + status_code = 416 + + +class HTTPExpectationFailed(HTTPClientError): + status_code = 417 + + +class HTTPMisdirectedRequest(HTTPClientError): + status_code = 421 + + +class HTTPUnprocessableEntity(HTTPClientError): + status_code = 422 + + +class HTTPFailedDependency(HTTPClientError): + status_code = 424 + + +class HTTPUpgradeRequired(HTTPClientError): + status_code = 426 + + +class HTTPPreconditionRequired(HTTPClientError): + status_code = 428 + + +class HTTPTooManyRequests(HTTPClientError): + status_code = 429 + + +class HTTPRequestHeaderFieldsTooLarge(HTTPClientError): + status_code = 431 + + +class HTTPUnavailableForLegalReasons(HTTPClientError): + status_code = 451 + + def __init__( + self, + link: StrOrURL | None, + *, + headers: LooseHeaders | None = None, + reason: str | None = None, + body: Any = None, + text: str | None = None, + content_type: str | None = None, + ) -> None: + super().__init__( + headers=headers, + reason=reason, + body=body, + text=text, + content_type=content_type, + ) + self._link = None + if link: + self._link = URL(link) + self.headers["Link"] = f'<{str(self._link)}>; rel="blocked-by"' + + @property + def link(self) -> URL | None: + return self._link + + +############################################################ +# 5xx Server Error +############################################################ +# Response status codes beginning with the digit "5" indicate cases in +# which the server is aware that it has erred or is incapable of +# performing the request. Except when responding to a HEAD request, the +# server SHOULD include an entity containing an explanation of the error +# situation, and whether it is a temporary or permanent condition. User +# agents SHOULD display any included entity to the user. These response +# codes are applicable to any request method. + + +class HTTPServerError(HTTPError): + pass + + +class HTTPInternalServerError(HTTPServerError): + status_code = 500 + + +class HTTPNotImplemented(HTTPServerError): + status_code = 501 + + +class HTTPBadGateway(HTTPServerError): + status_code = 502 + + +class HTTPServiceUnavailable(HTTPServerError): + status_code = 503 + + +class HTTPGatewayTimeout(HTTPServerError): + status_code = 504 + + +class HTTPVersionNotSupported(HTTPServerError): + status_code = 505 + + +class HTTPVariantAlsoNegotiates(HTTPServerError): + status_code = 506 + + +class HTTPInsufficientStorage(HTTPServerError): + status_code = 507 + + +class HTTPNotExtended(HTTPServerError): + status_code = 510 + + +class HTTPNetworkAuthenticationRequired(HTTPServerError): + status_code = 511 diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_fileresponse.py b/venv/lib/python3.11/site-packages/aiohttp/web_fileresponse.py new file mode 100644 index 0000000..8aa67ce --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_fileresponse.py @@ -0,0 +1,419 @@ +import asyncio +import io +import os +import pathlib +import sys +from contextlib import suppress +from enum import Enum, auto +from mimetypes import MimeTypes +from stat import S_ISREG +from types import MappingProxyType +from typing import ( # noqa + IO, + TYPE_CHECKING, + Any, + Awaitable, + Callable, + Final, + Iterator, + List, + Optional, + Set, + Tuple, + Union, + cast, +) + +from . import hdrs +from .abc import AbstractStreamWriter +from .helpers import DEFAULT_CHUNK_SIZE, ETAG_ANY, ETag, must_be_empty_body +from .typedefs import LooseHeaders, PathLike +from .web_exceptions import ( + HTTPForbidden, + HTTPNotFound, + HTTPNotModified, + HTTPPartialContent, + HTTPPreconditionFailed, + HTTPRequestRangeNotSatisfiable, +) +from .web_response import StreamResponse + +__all__ = ("FileResponse",) + +if TYPE_CHECKING: + from .web_request import BaseRequest + + +_T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]] + + +NOSENDFILE: Final[bool] = bool(os.environ.get("AIOHTTP_NOSENDFILE")) + +CONTENT_TYPES: Final[MimeTypes] = MimeTypes() + +# File extension to IANA encodings map that will be checked in the order defined. +ENCODING_EXTENSIONS = MappingProxyType( + {ext: CONTENT_TYPES.encodings_map[ext] for ext in (".br", ".gz")} +) + +FALLBACK_CONTENT_TYPE = "application/octet-stream" + +# Provide additional MIME type/extension pairs to be recognized. +# https://en.wikipedia.org/wiki/List_of_archive_formats#Compression_only +ADDITIONAL_CONTENT_TYPES = MappingProxyType( + { + "application/gzip": ".gz", + "application/x-brotli": ".br", + "application/x-bzip2": ".bz2", + "application/x-compress": ".Z", + "application/x-xz": ".xz", + } +) + + +class _FileResponseResult(Enum): + """The result of the file response.""" + + SEND_FILE = auto() # Ie a regular file to send + NOT_ACCEPTABLE = auto() # Ie a socket, or non-regular file + PRE_CONDITION_FAILED = auto() # Ie If-Match or If-None-Match failed + NOT_MODIFIED = auto() # 304 Not Modified + + +# Add custom pairs and clear the encodings map so guess_type ignores them. +CONTENT_TYPES.encodings_map.clear() +for content_type, extension in ADDITIONAL_CONTENT_TYPES.items(): + CONTENT_TYPES.add_type(content_type, extension) + + +_CLOSE_FUTURES: set[asyncio.Future[None]] = set() + + +class FileResponse(StreamResponse): + """A response object can be used to send files.""" + + def __init__( + self, + path: PathLike, + chunk_size: int = DEFAULT_CHUNK_SIZE, + status: int = 200, + reason: str | None = None, + headers: LooseHeaders | None = None, + ) -> None: + super().__init__(status=status, reason=reason, headers=headers) + + self._path = pathlib.Path(path) + self._chunk_size = chunk_size + + def _seek_and_read(self, fobj: IO[Any], offset: int, chunk_size: int) -> bytes: + fobj.seek(offset) + return fobj.read(chunk_size) # type: ignore[no-any-return] + + async def _sendfile_fallback( + self, writer: AbstractStreamWriter, fobj: IO[Any], offset: int, count: int + ) -> AbstractStreamWriter: + # To keep memory usage low,fobj is transferred in chunks + # controlled by the constructor's chunk_size argument. + + chunk_size = self._chunk_size + loop = asyncio.get_event_loop() + chunk = await loop.run_in_executor( + None, self._seek_and_read, fobj, offset, min(chunk_size, count) + ) + while chunk: + await writer.write(chunk) + count = count - len(chunk) + if count <= 0: + break + chunk = await loop.run_in_executor(None, fobj.read, min(chunk_size, count)) + + await writer.drain() + return writer + + async def _sendfile( + self, request: "BaseRequest", fobj: IO[Any], offset: int, count: int + ) -> AbstractStreamWriter: + writer = await super().prepare(request) + assert writer is not None + + if NOSENDFILE or self.compression: + return await self._sendfile_fallback(writer, fobj, offset, count) + + loop = request._loop + transport = request.transport + if transport is None: + raise ConnectionResetError("Connection lost") + + try: + await loop.sendfile(transport, fobj, offset, count) + except NotImplementedError: + return await self._sendfile_fallback(writer, fobj, offset, count) + + await super().write_eof() + return writer + + @staticmethod + def _etag_match(etag_value: str, etags: tuple[ETag, ...], *, weak: bool) -> bool: + if len(etags) == 1 and etags[0].value == ETAG_ANY: + return True + return any( + etag.value == etag_value for etag in etags if weak or not etag.is_weak + ) + + async def _not_modified( + self, request: "BaseRequest", etag_value: str, last_modified: float + ) -> AbstractStreamWriter | None: + self.set_status(HTTPNotModified.status_code) + self._length_check = False + self.etag = etag_value + self.last_modified = last_modified + # Delete any Content-Length headers provided by user. HTTP 304 + # should always have empty response body + return await super().prepare(request) + + async def _precondition_failed( + self, request: "BaseRequest" + ) -> AbstractStreamWriter | None: + self.set_status(HTTPPreconditionFailed.status_code) + self.content_length = 0 + return await super().prepare(request) + + def _make_response( + self, request: "BaseRequest", accept_encoding: str + ) -> tuple[ + _FileResponseResult, io.BufferedReader | None, os.stat_result, str | None + ]: + """Return the response result, io object, stat result, and encoding. + + If an uncompressed file is returned, the encoding is set to + :py:data:`None`. + + This method should be called from a thread executor + since it calls os.stat which may block. + """ + file_path, st, file_encoding = self._get_file_path_stat_encoding( + accept_encoding + ) + if not file_path: + return _FileResponseResult.NOT_ACCEPTABLE, None, st, None + + etag_value = f"{st.st_mtime_ns:x}-{st.st_size:x}" + + # https://www.rfc-editor.org/rfc/rfc9110#section-13.1.1-2 + if (ifmatch := request.if_match) is not None and not self._etag_match( + etag_value, ifmatch, weak=False + ): + return _FileResponseResult.PRE_CONDITION_FAILED, None, st, file_encoding + + if ( + (unmodsince := request.if_unmodified_since) is not None + and ifmatch is None + and st.st_mtime > unmodsince.timestamp() + ): + return _FileResponseResult.PRE_CONDITION_FAILED, None, st, file_encoding + + # https://www.rfc-editor.org/rfc/rfc9110#section-13.1.2-2 + if (ifnonematch := request.if_none_match) is not None and self._etag_match( + etag_value, ifnonematch, weak=True + ): + return _FileResponseResult.NOT_MODIFIED, None, st, file_encoding + + if ( + (modsince := request.if_modified_since) is not None + and ifnonematch is None + and st.st_mtime <= modsince.timestamp() + ): + return _FileResponseResult.NOT_MODIFIED, None, st, file_encoding + + fobj = file_path.open("rb") + with suppress(OSError): + # fstat() may not be available on all platforms + # Once we open the file, we want the fstat() to ensure + # the file has not changed between the first stat() + # and the open(). + st = os.stat(fobj.fileno()) + return _FileResponseResult.SEND_FILE, fobj, st, file_encoding + + def _get_file_path_stat_encoding( + self, accept_encoding: str + ) -> tuple[pathlib.Path | None, os.stat_result, str | None]: + file_path = self._path + for file_extension, file_encoding in ENCODING_EXTENSIONS.items(): + if file_encoding not in accept_encoding: + continue + + compressed_path = file_path.with_suffix(file_path.suffix + file_extension) + with suppress(OSError): + # Do not follow symlinks and ignore any non-regular files. + st = compressed_path.lstat() + if S_ISREG(st.st_mode): + return compressed_path, st, file_encoding + + # Fallback to the uncompressed file + st = file_path.stat() + return file_path if S_ISREG(st.st_mode) else None, st, None + + async def prepare(self, request: "BaseRequest") -> AbstractStreamWriter | None: + loop = asyncio.get_running_loop() + # Encoding comparisons should be case-insensitive + # https://www.rfc-editor.org/rfc/rfc9110#section-8.4.1 + accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, "").lower() + try: + response_result, fobj, st, file_encoding = await loop.run_in_executor( + None, self._make_response, request, accept_encoding + ) + except PermissionError: + self.set_status(HTTPForbidden.status_code) + return await super().prepare(request) + except OSError: + # Most likely to be FileNotFoundError or OSError for circular + # symlinks in python >= 3.13, so respond with 404. + self.set_status(HTTPNotFound.status_code) + return await super().prepare(request) + + # Forbid special files like sockets, pipes, devices, etc. + if response_result is _FileResponseResult.NOT_ACCEPTABLE: + self.set_status(HTTPForbidden.status_code) + return await super().prepare(request) + + if response_result is _FileResponseResult.PRE_CONDITION_FAILED: + return await self._precondition_failed(request) + + if response_result is _FileResponseResult.NOT_MODIFIED: + etag_value = f"{st.st_mtime_ns:x}-{st.st_size:x}" + last_modified = st.st_mtime + return await self._not_modified(request, etag_value, last_modified) + + assert fobj is not None + try: + return await self._prepare_open_file(request, fobj, st, file_encoding) + finally: + # We do not await here because we do not want to wait + # for the executor to finish before returning the response + # so the connection can begin servicing another request + # as soon as possible. + close_future = loop.run_in_executor(None, fobj.close) + # Hold a strong reference to the future to prevent it from being + # garbage collected before it completes. + _CLOSE_FUTURES.add(close_future) + close_future.add_done_callback(_CLOSE_FUTURES.remove) + + async def _prepare_open_file( + self, + request: "BaseRequest", + fobj: io.BufferedReader, + st: os.stat_result, + file_encoding: str | None, + ) -> AbstractStreamWriter | None: + status = self._status + file_size: int = st.st_size + file_mtime: float = st.st_mtime + count: int = file_size + start: int | None = None + + if (ifrange := request.if_range) is None or file_mtime <= ifrange.timestamp(): + # If-Range header check: + # condition = cached date >= last modification date + # return 206 if True else 200. + # if False: + # Range header would not be processed, return 200 + # if True but Range header missing + # return 200 + try: + rng = request.http_range + start = rng.start + end: int | None = rng.stop + except ValueError: + # https://tools.ietf.org/html/rfc7233: + # A server generating a 416 (Range Not Satisfiable) response to + # a byte-range request SHOULD send a Content-Range header field + # with an unsatisfied-range value. + # The complete-length in a 416 response indicates the current + # length of the selected representation. + # + # Will do the same below. Many servers ignore this and do not + # send a Content-Range header with HTTP 416 + self._headers[hdrs.CONTENT_RANGE] = f"bytes */{file_size}" + self.set_status(HTTPRequestRangeNotSatisfiable.status_code) + return await super().prepare(request) + + # If a range request has been made, convert start, end slice + # notation into file pointer offset and count + if start is not None: + if start < 0 and end is None: # return tail of file + start += file_size + if start < 0: + # if Range:bytes=-1000 in request header but file size + # is only 200, there would be trouble without this + start = 0 + count = file_size - start + else: + # rfc7233:If the last-byte-pos value is + # absent, or if the value is greater than or equal to + # the current length of the representation data, + # the byte range is interpreted as the remainder + # of the representation (i.e., the server replaces the + # value of last-byte-pos with a value that is one less than + # the current length of the selected representation). + count = ( + min(end if end is not None else file_size, file_size) - start + ) + + if start >= file_size: + # HTTP 416 should be returned in this case. + # + # According to https://tools.ietf.org/html/rfc7233: + # If a valid byte-range-set includes at least one + # byte-range-spec with a first-byte-pos that is less than + # the current length of the representation, or at least one + # suffix-byte-range-spec with a non-zero suffix-length, + # then the byte-range-set is satisfiable. Otherwise, the + # byte-range-set is unsatisfiable. + self._headers[hdrs.CONTENT_RANGE] = f"bytes */{file_size}" + self.set_status(HTTPRequestRangeNotSatisfiable.status_code) + return await super().prepare(request) + + status = HTTPPartialContent.status_code + # Even though you are sending the whole file, you should still + # return a HTTP 206 for a Range request. + self.set_status(status) + + # If the Content-Type header is not already set, guess it based on the + # extension of the request path. The encoding returned by guess_type + # can be ignored since the map was cleared above. + if hdrs.CONTENT_TYPE not in self._headers: + if sys.version_info >= (3, 13): + guesser = CONTENT_TYPES.guess_file_type + else: + guesser = CONTENT_TYPES.guess_type + self.content_type = guesser(self._path)[0] or FALLBACK_CONTENT_TYPE + + if file_encoding: + self._headers[hdrs.CONTENT_ENCODING] = file_encoding + self._headers[hdrs.VARY] = hdrs.ACCEPT_ENCODING + # Disable compression if we are already sending + # a compressed file since we don't want to double + # compress. + self._compression = False + + self.etag = f"{st.st_mtime_ns:x}-{st.st_size:x}" + self.last_modified = file_mtime + self.content_length = count + + self._headers[hdrs.ACCEPT_RANGES] = "bytes" + + if status == HTTPPartialContent.status_code: + real_start = start + assert real_start is not None + self._headers[hdrs.CONTENT_RANGE] = ( + f"bytes {real_start}-{real_start + count - 1}/{file_size}" + ) + + # If we are sending 0 bytes calling sendfile() will throw a ValueError + if count == 0 or must_be_empty_body(request.method, status): + return await super().prepare(request) + + # be aware that start could be None or int=0 here. + offset = start or 0 + + return await self._sendfile(request, fobj, offset, count) diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_log.py b/venv/lib/python3.11/site-packages/aiohttp/web_log.py new file mode 100644 index 0000000..9283cc0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_log.py @@ -0,0 +1,231 @@ +import datetime +import functools +import logging +import os +import re +import time as time_mod +from collections.abc import Iterable +from typing import Callable, ClassVar, NamedTuple + +from .abc import AbstractAccessLogger +from .web_request import BaseRequest +from .web_response import StreamResponse + + +class KeyMethod(NamedTuple): + key: str | tuple[str, str] + method: Callable[[BaseRequest, StreamResponse, float], str] + + +class AccessLogger(AbstractAccessLogger): + """Helper object to log access. + + Usage: + log = logging.getLogger("spam") + log_format = "%a %{User-Agent}i" + access_logger = AccessLogger(log, log_format) + access_logger.log(request, response, time) + + Format: + %% The percent sign + %a Remote IP-address (IP-address of proxy if using reverse proxy) + %t Time when the request was started to process + %P The process ID of the child that serviced the request + %r First line of request + %s Response status code + %b Size of response in bytes, including HTTP headers + %T Time taken to serve the request, in seconds + %Tf Time taken to serve the request, in seconds with floating fraction + in .06f format + %D Time taken to serve the request, in microseconds + %{FOO}i request.headers['FOO'] + %{FOO}o response.headers['FOO'] + %{FOO}e os.environ['FOO'] + + """ + + LOG_FORMAT_MAP = { + "a": "remote_address", + "t": "request_start_time", + "P": "process_id", + "r": "first_request_line", + "s": "response_status", + "b": "response_size", + "T": "request_time", + "Tf": "request_time_frac", + "D": "request_time_micro", + "i": "request_header", + "o": "response_header", + } + + LOG_FORMAT = '%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i"' + FORMAT_RE = re.compile(r"%(\{([A-Za-z0-9\-_]+)\}([ioe])|[atPrsbOD]|Tf?)") + CLEANUP_RE = re.compile(r"(%[^s])") + _FORMAT_CACHE: dict[str, tuple[str, list[KeyMethod]]] = {} + + _cached_tz: ClassVar[datetime.timezone | None] = None + _cached_tz_expires: ClassVar[float] = 0.0 + + def __init__(self, logger: logging.Logger, log_format: str = LOG_FORMAT) -> None: + """Initialise the logger. + + logger is a logger object to be used for logging. + log_format is a string with apache compatible log format description. + + """ + super().__init__(logger, log_format=log_format) + + _compiled_format = AccessLogger._FORMAT_CACHE.get(log_format) + if not _compiled_format: + _compiled_format = self.compile_format(log_format) + AccessLogger._FORMAT_CACHE[log_format] = _compiled_format + + self._log_format, self._methods = _compiled_format + + def compile_format(self, log_format: str) -> tuple[str, list[KeyMethod]]: + """Translate log_format into form usable by modulo formatting + + All known atoms will be replaced with %s + Also methods for formatting of those atoms will be added to + _methods in appropriate order + + For example we have log_format = "%a %t" + This format will be translated to "%s %s" + Also contents of _methods will be + [self._format_a, self._format_t] + These method will be called and results will be passed + to translated string format. + + Each _format_* method receive 'args' which is list of arguments + given to self.log + + Exceptions are _format_e, _format_i and _format_o methods which + also receive key name (by functools.partial) + + """ + # list of (key, method) tuples, we don't use an OrderedDict as users + # can repeat the same key more than once + methods = list() + + for atom in self.FORMAT_RE.findall(log_format): + if atom[1] == "": + format_key1 = self.LOG_FORMAT_MAP[atom[0]] + m = getattr(AccessLogger, "_format_%s" % atom[0]) + key_method = KeyMethod(format_key1, m) + else: + format_key2 = (self.LOG_FORMAT_MAP[atom[2]], atom[1]) + m = getattr(AccessLogger, "_format_%s" % atom[2]) + key_method = KeyMethod(format_key2, functools.partial(m, atom[1])) + + methods.append(key_method) + + log_format = self.FORMAT_RE.sub(r"%s", log_format) + log_format = self.CLEANUP_RE.sub(r"%\1", log_format) + return log_format, methods + + @staticmethod + def _format_i( + key: str, request: BaseRequest, response: StreamResponse, time: float + ) -> str: + if request is None: + return "(no headers)" + + # suboptimal, make istr(key) once + return request.headers.get(key, "-") + + @staticmethod + def _format_o( + key: str, request: BaseRequest, response: StreamResponse, time: float + ) -> str: + # suboptimal, make istr(key) once + return response.headers.get(key, "-") + + @staticmethod + def _format_a(request: BaseRequest, response: StreamResponse, time: float) -> str: + if request is None: + return "-" + ip = request.remote + return ip if ip is not None else "-" + + @classmethod + def _get_local_time(cls) -> datetime.datetime: + if cls._cached_tz is None or time_mod.time() >= cls._cached_tz_expires: + gmtoff = time_mod.localtime().tm_gmtoff + cls._cached_tz = tz = datetime.timezone(datetime.timedelta(seconds=gmtoff)) + + now = datetime.datetime.now(tz) + # Expire at every 30 mins, as any DST change should occur at 0/30 mins past. + d = now + datetime.timedelta(minutes=30) + d = d.replace(minute=30 if d.minute >= 30 else 0, second=0, microsecond=0) + cls._cached_tz_expires = d.timestamp() + return now + + return datetime.datetime.now(cls._cached_tz) + + @staticmethod + def _format_t(request: BaseRequest, response: StreamResponse, time: float) -> str: + now = AccessLogger._get_local_time() + start_time = now - datetime.timedelta(seconds=time) + return start_time.strftime("[%d/%b/%Y:%H:%M:%S %z]") + + @staticmethod + def _format_P(request: BaseRequest, response: StreamResponse, time: float) -> str: + return "<%s>" % os.getpid() + + @staticmethod + def _format_r(request: BaseRequest, response: StreamResponse, time: float) -> str: + if request is None: + return "-" + return f"{request.method} {request.path_qs} HTTP/{request.version.major}.{request.version.minor}" + + @staticmethod + def _format_s(request: BaseRequest, response: StreamResponse, time: float) -> int: + return response.status + + @staticmethod + def _format_b(request: BaseRequest, response: StreamResponse, time: float) -> int: + return response.body_length + + @staticmethod + def _format_T(request: BaseRequest, response: StreamResponse, time: float) -> str: + return str(round(time)) + + @staticmethod + def _format_Tf(request: BaseRequest, response: StreamResponse, time: float) -> str: + return "%06f" % time + + @staticmethod + def _format_D(request: BaseRequest, response: StreamResponse, time: float) -> str: + return str(round(time * 1000000)) + + def _format_line( + self, request: BaseRequest, response: StreamResponse, time: float + ) -> Iterable[tuple[str | tuple[str, str], str]]: + return [(key, method(request, response, time)) for key, method in self._methods] + + @property + def enabled(self) -> bool: + """Check if logger is enabled.""" + # Avoid formatting the log line if it will not be emitted. + return self.logger.isEnabledFor(logging.INFO) + + def log(self, request: BaseRequest, response: StreamResponse, time: float) -> None: + try: + fmt_info = self._format_line(request, response, time) + + values = list() + extra: dict[str, str | dict[str, str]] = dict() + for key, value in fmt_info: + values.append(value) + + if isinstance(key, str): + extra[key] = value + else: + k1, k2 = key + dct: dict[str, str] = extra.get(k1, {}) # type: ignore[assignment] + dct[k2] = value + extra[k1] = dct + + self.logger.info(self._log_format % tuple(values), extra=extra) + except Exception: + self.logger.exception("Error in logging") diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_middlewares.py b/venv/lib/python3.11/site-packages/aiohttp/web_middlewares.py new file mode 100644 index 0000000..9816a38 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_middlewares.py @@ -0,0 +1,121 @@ +import re +from typing import TYPE_CHECKING, TypeVar + +from .typedefs import Handler, Middleware +from .web_exceptions import HTTPMove, HTTPPermanentRedirect +from .web_request import Request +from .web_response import StreamResponse +from .web_urldispatcher import SystemRoute + +__all__ = ( + "middleware", + "normalize_path_middleware", +) + +if TYPE_CHECKING: + from .web_app import Application + +_Func = TypeVar("_Func") + + +async def _check_request_resolves(request: Request, path: str) -> tuple[bool, Request]: + alt_request = request.clone(rel_url=path) + + match_info = await request.app.router.resolve(alt_request) + alt_request._match_info = match_info + + if match_info.http_exception is None: + return True, alt_request + + return False, request + + +def middleware(f: _Func) -> _Func: + f.__middleware_version__ = 1 # type: ignore[attr-defined] + return f + + +def normalize_path_middleware( + *, + append_slash: bool = True, + remove_slash: bool = False, + merge_slashes: bool = True, + redirect_class: type[HTTPMove] = HTTPPermanentRedirect, +) -> Middleware: + """Factory for producing a middleware that normalizes the path of a request. + + Normalizing means: + - Add or remove a trailing slash to the path. + - Double slashes are replaced by one. + + The middleware returns as soon as it finds a path that resolves + correctly. The order if both merge and append/remove are enabled is + 1) merge slashes + 2) append/remove slash + 3) both merge slashes and append/remove slash. + If the path resolves with at least one of those conditions, it will + redirect to the new path. + + Only one of `append_slash` and `remove_slash` can be enabled. If both + are `True` the factory will raise an assertion error + + If `append_slash` is `True` the middleware will append a slash when + needed. If a resource is defined with trailing slash and the request + comes without it, it will append it automatically. + + If `remove_slash` is `True`, `append_slash` must be `False`. When enabled + the middleware will remove trailing slashes and redirect if the resource + is defined + + If merge_slashes is True, merge multiple consecutive slashes in the + path into one. + """ + correct_configuration = not (append_slash and remove_slash) + assert correct_configuration, "Cannot both remove and append slash" + + @middleware + async def impl(request: Request, handler: Handler) -> StreamResponse: + if isinstance(request.match_info.route, SystemRoute): + paths_to_check = [] + if "?" in request.raw_path: + path, query = request.raw_path.split("?", 1) + query = "?" + query + else: + query = "" + path = request.raw_path + + if merge_slashes: + paths_to_check.append(re.sub("//+", "/", path)) + if append_slash and not request.path.endswith("/"): + paths_to_check.append(path + "/") + if remove_slash and request.path.endswith("/"): + paths_to_check.append(path[:-1]) + if merge_slashes and append_slash: + paths_to_check.append(re.sub("//+", "/", path + "/")) + if merge_slashes and remove_slash: + merged_slashes = re.sub("//+", "/", path) + paths_to_check.append(merged_slashes[:-1]) + + for path in paths_to_check: + path = re.sub("^//+", "/", path) # SECURITY: GHSA-v6wp-4m6f-gcjg + resolves, request = await _check_request_resolves(request, path) + if resolves: + raise redirect_class(request.raw_path + query) + + return await handler(request) + + return impl + + +def _fix_request_current_app(app: "Application") -> Middleware: + @middleware + async def impl(request: Request, handler: Handler) -> StreamResponse: + match_info = request.match_info + prev = match_info.current_app + match_info.current_app = app + try: + return await handler(request) + finally: + match_info.current_app = prev + + return impl diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_protocol.py b/venv/lib/python3.11/site-packages/aiohttp/web_protocol.py new file mode 100644 index 0000000..f1b3844 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_protocol.py @@ -0,0 +1,874 @@ +import asyncio +import asyncio.streams +import sys +import traceback +import warnings +from collections import deque +from collections.abc import Awaitable, Callable, Sequence +from contextlib import suppress +from html import escape as html_escape +from http import HTTPStatus +from logging import Logger +from typing import TYPE_CHECKING, Any, Optional, cast + +import attr +import yarl +from propcache import under_cached_property + +from .abc import AbstractAccessLogger, AbstractStreamWriter +from .base_protocol import PAUSE_RESUME_READING_ERRORS, BaseProtocol +from .helpers import DEFAULT_CHUNK_SIZE, ceil_timeout +from .http import ( + HttpProcessingError, + HttpRequestParser, + HttpVersion10, + RawRequestMessage, + StreamWriter, + WebSocketReader, +) +from .http_exceptions import BadHttpMethod +from .log import access_logger, server_logger +from .streams import EMPTY_PAYLOAD, StreamReader +from .tcp_helpers import tcp_keepalive +from .web_exceptions import HTTPException, HTTPInternalServerError +from .web_log import AccessLogger +from .web_request import BaseRequest +from .web_response import Response, StreamResponse + +__all__ = ("RequestHandler", "RequestPayloadError", "PayloadAccessError") + +# Max parsed-but-unhandled pipelined requests buffered per connection before +# reading is paused. Bounds memory a client can pin by keeping one handler busy +# and pipelining behind it; reading resumes as the queue drains. +MAX_MSG_QUEUE_SIZE = 32 + +if TYPE_CHECKING: + import ssl + + from .web_server import Server + + +_RequestFactory = Callable[ + [ + RawRequestMessage, + StreamReader, + "RequestHandler", + AbstractStreamWriter, + "asyncio.Task[None]", + ], + BaseRequest, +] + +_RequestHandler = Callable[[BaseRequest], Awaitable[StreamResponse]] + +ERROR = RawRequestMessage( + "UNKNOWN", + "/", + HttpVersion10, + {}, # type: ignore[arg-type] + {}, # type: ignore[arg-type] + True, + None, + False, + False, + yarl.URL("/"), +) + + +class RequestPayloadError(Exception): + """Payload parsing error.""" + + +class PayloadAccessError(Exception): + """Payload was accessed after response was sent.""" + + +_PAYLOAD_ACCESS_ERROR = PayloadAccessError() + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class _ErrInfo: + status: int + exc: BaseException + message: str + + +_MsgType = tuple[RawRequestMessage | _ErrInfo, StreamReader] + + +class RequestHandler(BaseProtocol): + """HTTP protocol implementation. + + RequestHandler handles incoming HTTP request. It reads request line, + request headers and request payload and calls handle_request() method. + By default it always returns with 404 response. + + RequestHandler handles errors in incoming request, like bad + status line, bad headers or incomplete payload. If any error occurs, + connection gets closed. + + keepalive_timeout -- number of seconds before closing + keep-alive connection + + tcp_keepalive -- TCP keep-alive is on, default is on + + debug -- enable debug mode + + logger -- custom logger object + + access_log_class -- custom class for access_logger + + access_log -- custom logging object + + access_log_format -- access log format string + + loop -- Optional event loop + + max_line_size -- Optional maximum header line size + + max_field_size -- Optional maximum header field size + + max_headers -- Optional maximum header size + + timeout_ceil_threshold -- Optional value to specify + threshold to ceil() timeout + values + + """ + + __slots__ = ( + "max_field_size", + "max_headers", + "max_line_size", + "_request_count", + "_keepalive", + "_manager", + "_request_handler", + "_request_factory", + "_tcp_keepalive", + "_next_keepalive_close_time", + "_keepalive_handle", + "_keepalive_timeout", + "_lingering_time", + "_messages", + "_max_msg_queue_size", + "_msg_queue_resume_size", + "_msg_queue_paused", + "_message_tail", + "_handler_waiter", + "_waiter", + "_task_handler", + "_payload_parser", + "_data_received_cb", + "logger", + "debug", + "access_log", + "access_logger", + "_close", + "_force_close", + "_current_request", + "_timeout_ceil_threshold", + "_request_in_progress", + "_logging_enabled", + "_cache", + ) + + def __init__( + self, + manager: "Server", + *, + loop: asyncio.AbstractEventLoop, + # Default should be high enough that it's likely longer than a reverse proxy. + keepalive_timeout: float = 3630, + tcp_keepalive: bool = True, + logger: Logger = server_logger, + access_log_class: type[AbstractAccessLogger] = AccessLogger, + access_log: Logger = access_logger, + access_log_format: str = AccessLogger.LOG_FORMAT, + debug: bool = False, + max_line_size: int = 8190, + max_headers: int = 128, + max_field_size: int = 8190, + lingering_time: float = 10.0, + read_bufsize: int = DEFAULT_CHUNK_SIZE, + auto_decompress: bool = True, + timeout_ceil_threshold: float = 5, + ): + self._max_msg_queue_size = MAX_MSG_QUEUE_SIZE + # Low-water mark: resume reading once the queue drains to half the limit + # so we refill in batches instead of churning pause/resume per request. + self._msg_queue_resume_size = MAX_MSG_QUEUE_SIZE // 2 + # Set before super().__init__ so _reading_paused_for_msg_queue() is safe + # if BaseProtocol ever triggers a resume during init. + self._msg_queue_paused = False + parser = HttpRequestParser( + self, + loop, + read_bufsize, + max_line_size=max_line_size, + max_field_size=max_field_size, + max_headers=max_headers, + payload_exception=RequestPayloadError, + auto_decompress=auto_decompress, + max_msg_queue_size=MAX_MSG_QUEUE_SIZE, + ) + super().__init__(loop, parser) + + # _request_count is the number of requests processed with the same connection. + self._request_count = 0 + self._keepalive = False + self._current_request: BaseRequest | None = None + self._manager: Server | None = manager + self._request_handler: _RequestHandler | None = manager.request_handler + self._request_factory: _RequestFactory | None = manager.request_factory + + self.max_line_size = max_line_size + self.max_headers = max_headers + self.max_field_size = max_field_size + + self._tcp_keepalive = tcp_keepalive + # placeholder to be replaced on keepalive timeout setup + self._next_keepalive_close_time = 0.0 + self._keepalive_handle: asyncio.Handle | None = None + self._keepalive_timeout = keepalive_timeout + self._lingering_time = float(lingering_time) + + self._messages: deque[_MsgType] = deque() + self._message_tail = b"" + self._data_received_cb: Callable[[], None] | None = None + + self._waiter: asyncio.Future[None] | None = None + self._handler_waiter: asyncio.Future[None] | None = None + self._task_handler: asyncio.Task[None] | None = None + self._payload_parser: Any = None + + self._timeout_ceil_threshold: float = 5 + try: + self._timeout_ceil_threshold = float(timeout_ceil_threshold) + except (TypeError, ValueError): + pass + + self.logger = logger + self.debug = debug + self.access_log = access_log + if access_log: + self.access_logger: AbstractAccessLogger | None = access_log_class( + access_log, access_log_format + ) + self._logging_enabled = self.access_logger.enabled + else: + self.access_logger = None + self._logging_enabled = False + + self._close = False + self._force_close = False + self._request_in_progress = False + self._cache: dict[str, Any] = {} + + def __repr__(self) -> str: + return "<{} {}>".format( + self.__class__.__name__, + "connected" if self.transport is not None else "disconnected", + ) + + @under_cached_property + def ssl_context(self) -> Optional["ssl.SSLContext"]: + """Return SSLContext if available.""" + return ( + None + if self.transport is None + else self.transport.get_extra_info("sslcontext") + ) + + @under_cached_property + def peername( + self, + ) -> str | tuple[str, int, int, int] | tuple[str, int] | None: + """Return peername if available.""" + return ( + None + if self.transport is None + else self.transport.get_extra_info("peername") + ) + + @under_cached_property + def sockname( + self, + ) -> str | tuple[str, int, int, int] | tuple[str, int] | None: + """Return sockname if available.""" + return ( + None + if self.transport is None + else self.transport.get_extra_info("sockname") + ) + + @property + def keepalive_timeout(self) -> float: + return self._keepalive_timeout + + async def shutdown(self, timeout: float | None = 15.0) -> None: + """Do worker process exit preparations. + + We need to clean up everything and stop accepting requests. + It is especially important for keep-alive connections. + """ + self._force_close = True + + if self._keepalive_handle is not None: + self._keepalive_handle.cancel() + + # Wait for graceful handler completion + if self._request_in_progress: + # The future is only created when we are shutting + # down while the handler is still processing a request + # to avoid creating a future for every request. + self._handler_waiter = self._loop.create_future() + try: + async with ceil_timeout(timeout): + await self._handler_waiter + except (asyncio.CancelledError, asyncio.TimeoutError): + self._handler_waiter = None + if ( + sys.version_info >= (3, 11) + and (task := asyncio.current_task()) + and task.cancelling() + ): + raise + # Then cancel handler and wait + try: + async with ceil_timeout(timeout): + if self._current_request is not None: + self._current_request._cancel(asyncio.CancelledError()) + + if self._task_handler is not None and not self._task_handler.done(): + await asyncio.shield(self._task_handler) + except (asyncio.CancelledError, asyncio.TimeoutError): + if ( + sys.version_info >= (3, 11) + and (task := asyncio.current_task()) + and task.cancelling() + ): + raise + + # force-close non-idle handler + if self._task_handler is not None: + self._task_handler.cancel() + + self.force_close() + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + super().connection_made(transport) + + real_transport = cast(asyncio.Transport, transport) + if self._tcp_keepalive: + tcp_keepalive(real_transport) + + assert self._manager is not None + self._manager.connection_made(self, real_transport) + + loop = self._loop + if sys.version_info >= (3, 12): + task = asyncio.Task(self.start(), loop=loop, eager_start=True) + else: + task = loop.create_task(self.start()) + self._task_handler = task + + def connection_lost(self, exc: BaseException | None) -> None: + if self._manager is None: + return + self._manager.connection_lost(self, exc) + + # Grab value before setting _manager to None. + handler_cancellation = self._manager.handler_cancellation + + self.force_close() + super().connection_lost(exc) + self._manager = None + self._request_factory = None + self._request_handler = None + self._parser = None + + if self._keepalive_handle is not None: + self._keepalive_handle.cancel() + + if self._current_request is not None: + if exc is None: + exc = ConnectionResetError("Connection lost") + self._current_request._cancel(exc) + + if handler_cancellation and self._task_handler is not None: + self._task_handler.cancel() + + self._task_handler = None + + if self._payload_parser is not None: + self._payload_parser.feed_eof() + self._payload_parser = None + + def set_parser( + self, + parser: WebSocketReader, + data_received_cb: Callable[[], None] | None = None, + ) -> None: + assert self._payload_parser is None + + self._payload_parser = parser + self._data_received_cb = data_received_cb + + if self._message_tail: + self._payload_parser.feed_data(self._message_tail) + self._message_tail = b"" + + def eof_received(self) -> None: + pass + + def data_received(self, data: bytes) -> None: + if self._force_close or self._close: + return + # parse http messages + messages: Sequence[_MsgType] + if self._payload_parser is None and not self._upgraded: + assert self._parser is not None + try: + messages, upgraded, tail = self._parser.feed_data(data) + except HttpProcessingError as exc: + messages = [ + (_ErrInfo(status=400, exc=exc, message=exc.message), EMPTY_PAYLOAD) + ] + upgraded = False + tail = b"" + + for msg, payload in messages: + self._request_count += 1 + self._messages.append((msg, payload)) + + waiter = self._waiter + if messages and waiter is not None and not waiter.done(): + # don't set result twice + waiter.set_result(None) + + # Queue full: pause the transport (the parser already stopped + # emitting). start() resumes as it drains the queue. + if ( + not self._msg_queue_paused + and len(self._messages) >= self._max_msg_queue_size + ): + self._pause_msg_queue_reading() + + self._upgraded = upgraded + if upgraded and tail: + self._message_tail = tail + + # no parser, just store + elif self._payload_parser is None and self._upgraded and data: + self._message_tail += data + + # feed payload + elif data: + if self._data_received_cb is not None: + self._data_received_cb() + eof, tail = self._payload_parser.feed_data(data) + if eof: + self.close() + + def _reading_paused_for_msg_queue(self) -> bool: + return self._msg_queue_paused + + def _pause_msg_queue_reading(self) -> None: + self._msg_queue_paused = True + if self.transport is not None: + try: + self.transport.pause_reading() + except PAUSE_RESUME_READING_ERRORS: + # Transport lacks flow control; nothing to pause. Intentionally + # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). + pass + + def _resume_msg_queue_reading(self) -> None: + if not self._upgraded: + # Reparse buffered pipelined requests while still marked paused so + # a refill past the limit does not re-pause an already-paused + # transport; only resume below once it stayed under the limit. + self.data_received(b"") + if len(self._messages) >= self._max_msg_queue_size: + return + self._msg_queue_paused = False + if not self._reading_paused and self.transport is not None: + try: + self.transport.resume_reading() + except PAUSE_RESUME_READING_ERRORS: + # Transport lacks flow control; nothing to resume. Intentionally + # ignored (see PAUSE_RESUME_READING_ERRORS; do not use suppress). + pass + + def keep_alive(self, val: bool) -> None: + """Set keep-alive connection mode. + + :param bool val: new state. + """ + self._keepalive = val + if self._keepalive_handle: + self._keepalive_handle.cancel() + self._keepalive_handle = None + + def close(self) -> None: + """Close connection. + + Stop accepting new pipelining messages and close + connection when handlers done processing messages. + """ + self._close = True + if self._waiter: + self._waiter.cancel() + + def force_close(self) -> None: + """Forcefully close connection.""" + self._force_close = True + if self._waiter: + self._waiter.cancel() + if self.transport is not None: + self.transport.close() + self.transport = None + + def log_access( + self, request: BaseRequest, response: StreamResponse, time: float | None + ) -> None: + if self._logging_enabled and self.access_logger is not None: + if TYPE_CHECKING: + assert time is not None + self.access_logger.log(request, response, self._loop.time() - time) + + def log_debug(self, *args: Any, **kw: Any) -> None: + if self.debug: + self.logger.debug(*args, **kw) + + def log_exception(self, *args: Any, **kw: Any) -> None: + self.logger.exception(*args, **kw) + + def _process_keepalive(self) -> None: + self._keepalive_handle = None + if self._force_close or not self._keepalive: + return + + loop = self._loop + now = loop.time() + close_time = self._next_keepalive_close_time + if now < close_time: + # Keep alive close check fired too early, reschedule + self._keepalive_handle = loop.call_at(close_time, self._process_keepalive) + return + + # handler in idle state + if self._waiter and not self._waiter.done(): + self.force_close() + + async def _handle_request( + self, + request: BaseRequest, + start_time: float | None, + request_handler: Callable[[BaseRequest], Awaitable[StreamResponse]], + ) -> tuple[StreamResponse, bool]: + self._request_in_progress = True + try: + try: + self._current_request = request + resp = await request_handler(request) + finally: + self._current_request = None + except HTTPException as exc: + resp = exc + resp, reset = await self.finish_response(request, resp, start_time) + except asyncio.CancelledError: + raise + except asyncio.TimeoutError as exc: + self.log_debug("Request handler timed out.", exc_info=exc) + resp = self.handle_error(request, 504) + resp, reset = await self.finish_response(request, resp, start_time) + except Exception as exc: + resp = self.handle_error(request, 500, exc) + resp, reset = await self.finish_response(request, resp, start_time) + else: + # Deprecation warning (See #2415) + if getattr(resp, "__http_exception__", False): + warnings.warn( + "returning HTTPException object is deprecated " + "(#2415) and will be removed, " + "please raise the exception instead", + DeprecationWarning, + ) + + resp, reset = await self.finish_response(request, resp, start_time) + finally: + self._request_in_progress = False + if self._handler_waiter is not None: + self._handler_waiter.set_result(None) + + return resp, reset + + async def start(self) -> None: + """Process incoming request. + + It reads request line, request headers and request payload, then + calls handle_request() method. Subclass has to override + handle_request(). start() handles various exceptions in request + or response handling. Connection is being closed always unless + keep_alive(True) specified. + """ + loop = self._loop + manager = self._manager + assert manager is not None + keepalive_timeout = self._keepalive_timeout + resp = None + assert self._request_factory is not None + assert self._request_handler is not None + + while not self._force_close: + if not self._messages: + try: + # wait for next request + self._waiter = loop.create_future() + await self._waiter + finally: + self._waiter = None + + message, payload = self._messages.popleft() + + # Free a parser slot; resume reading once drained to low water so + # pipelining keeps flowing while this request is handled. + # no branch: _parser is only None after connection_lost, whose path + # exits this loop, so the None case is not reachably exercisable. + if self._parser is not None: # pragma: no branch + self._parser.message_consumed() + if ( + self._msg_queue_paused + and len(self._messages) <= self._msg_queue_resume_size + ): + self._resume_msg_queue_reading() + + # time is only fetched if logging is enabled as otherwise + # its thrown away and never used. + start = loop.time() if self._logging_enabled else None + + manager.requests_count += 1 + writer = StreamWriter(self, loop) + if isinstance(message, _ErrInfo): + # make request_factory work + request_handler = self._make_error_handler(message) + message = ERROR + else: + request_handler = self._request_handler + + # Important don't hold a reference to the current task + # as on traceback it will prevent the task from being + # collected and will cause a memory leak. + request = self._request_factory( + message, + payload, + self, + writer, + self._task_handler or asyncio.current_task(loop), # type: ignore[arg-type] + ) + try: + # a new task is used for copy context vars (#3406) + coro = self._handle_request(request, start, request_handler) + if sys.version_info >= (3, 12): + task = asyncio.Task(coro, loop=loop, eager_start=True) + else: + task = loop.create_task(coro) + try: + resp, reset = await task + except ConnectionError: + self.log_debug("Ignored premature client disconnection") + break + + # Drop the processed task from asyncio.Task.all_tasks() early + del task + if reset: + self.log_debug("Ignored premature client disconnection 2") + break + + # notify server about keep-alive + self._keepalive = bool(resp.keep_alive) + + # check payload + if not payload.is_eof(): + lingering_time = self._lingering_time + if not self._force_close and lingering_time: + self.log_debug( + "Start lingering close timer for %s sec.", lingering_time + ) + + now = loop.time() + end_t = now + lingering_time + + try: + while not payload.is_eof() and now < end_t: + async with ceil_timeout(end_t - now): + # read and ignore + await payload.readany() + now = loop.time() + except (asyncio.CancelledError, asyncio.TimeoutError): + if ( + sys.version_info >= (3, 11) + and (t := asyncio.current_task()) + and t.cancelling() + ): + raise + + # if payload still uncompleted + if not payload.is_eof() and not self._force_close: + self.log_debug("Uncompleted request.") + self.close() + + payload.set_exception(_PAYLOAD_ACCESS_ERROR) + + except asyncio.CancelledError: + self.log_debug("Ignored premature client disconnection") + self.force_close() + raise + except Exception as exc: + self.log_exception("Unhandled exception", exc_info=exc) + self.force_close() + except BaseException: + self.force_close() + raise + finally: + request._task = None # type: ignore[assignment] # Break reference cycle in case of exception + if self.transport is None and resp is not None: + self.log_debug("Ignored premature client disconnection.") + + if self._keepalive and not self._close and not self._force_close: + # start keep-alive timer + close_time = loop.time() + keepalive_timeout + self._next_keepalive_close_time = close_time + if self._keepalive_handle is None: + self._keepalive_handle = loop.call_at( + close_time, self._process_keepalive + ) + else: + break + + # remove handler, close transport if no handlers left + if not self._force_close: + self._task_handler = None + if self.transport is not None: + self.transport.close() + + async def finish_response( + self, request: BaseRequest, resp: StreamResponse, start_time: float | None + ) -> tuple[StreamResponse, bool]: + """Prepare the response and write_eof, then log access. + + This has to + be called within the context of any exception so the access logger + can get exception information. Returns True if the client disconnects + prematurely. + """ + request._finish() + if self._parser is not None: + self._parser.set_upgraded(False) + self._upgraded = False + if self._message_tail: + messages, _upgraded, tail = self._parser.feed_data(self._message_tail) + self._message_tail = tail + for msg, payload in messages: + self._request_count += 1 + self._messages.append((msg, payload)) + # This shouldn't be possible. If a future refactor results in this + # failing, then the code may need to be updated to set the waiter. + assert self._waiter is None + try: + prepare_meth = resp.prepare + except AttributeError: + if resp is None: + self.log_exception("Missing return statement on request handler") + else: + self.log_exception( + f"Web-handler should return a response instance, got {resp!r}" + ) + exc = HTTPInternalServerError() + resp = Response( + status=exc.status, reason=exc.reason, text=exc.text, headers=exc.headers + ) + prepare_meth = resp.prepare + try: + await prepare_meth(request) + await resp.write_eof() + except ConnectionError: + self.log_access(request, resp, start_time) + return resp, True + + self.log_access(request, resp, start_time) + return resp, False + + def handle_error( + self, + request: BaseRequest, + status: int = 500, + exc: BaseException | None = None, + message: str | None = None, + ) -> StreamResponse: + """Handle errors. + + Returns HTTP response with specific status code. Logs additional + information. It always closes current connection. + """ + if self._request_count == 1 and isinstance(exc, BadHttpMethod): + # BadHttpMethod is common when a client sends non-HTTP + # or encrypted traffic to an HTTP port. This is expected + # to happen when connected to the public internet so we log + # it at the debug level as to not fill logs with noise. + self.logger.debug( + "Error handling request from %s", request.remote, exc_info=exc + ) + else: + self.log_exception( + "Error handling request from %s", request.remote, exc_info=exc + ) + + # some data already got sent, connection is broken + if request.writer.output_size > 0: + raise ConnectionError( + "Response is sent already, cannot send another response " + "with the error message" + ) + + ct = "text/plain" + if status == HTTPStatus.INTERNAL_SERVER_ERROR: + title = f"{HTTPStatus.INTERNAL_SERVER_ERROR.value} {HTTPStatus.INTERNAL_SERVER_ERROR.phrase}" + msg = HTTPStatus.INTERNAL_SERVER_ERROR.description + tb = None + if self.debug: + with suppress(Exception): + tb = traceback.format_exc() + + if "text/html" in request.headers.get("Accept", ""): + if tb: + tb = html_escape(tb) + msg = f"

Traceback:

\n
{tb}
" + message = ( + "" + f"{title}" + f"\n

{title}

" + f"\n{msg}\n\n" + ) + ct = "text/html" + else: + if tb: + msg = tb + message = title + "\n\n" + msg + + resp = Response(status=status, text=message, content_type=ct) + resp.force_close() + + return resp + + def _make_error_handler( + self, err_info: _ErrInfo + ) -> Callable[[BaseRequest], Awaitable[StreamResponse]]: + async def handler(request: BaseRequest) -> StreamResponse: + return self.handle_error( + request, err_info.status, err_info.exc, err_info.message + ) + + return handler diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_request.py b/venv/lib/python3.11/site-packages/aiohttp/web_request.py new file mode 100644 index 0000000..d3d9cc2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_request.py @@ -0,0 +1,948 @@ +import asyncio +import datetime +import io +import re +import string +import tempfile +import types +import warnings +from collections.abc import Iterator, Mapping, MutableMapping +from re import Pattern +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, Final, Optional, TypeVar, cast, overload +from urllib.parse import parse_qsl + +import attr +from multidict import ( + CIMultiDict, + CIMultiDictProxy, + MultiDict, + MultiDictProxy, + MultiMapping, +) +from yarl import URL + +from . import hdrs +from ._cookie_helpers import parse_cookie_header +from .abc import AbstractStreamWriter +from .helpers import ( + _SENTINEL, + DEBUG, + DEFAULT_CHUNK_SIZE, + ETAG_ANY, + LIST_QUOTED_ETAG_RE, + ChainMapProxy, + ETag, + HeadersMixin, + RequestKey, + parse_http_date, + reify, + sentinel, + set_exception, +) +from .http_parser import RawRequestMessage +from .http_writer import HttpVersion +from .multipart import BodyPartReader, MultipartReader +from .streams import EmptyStreamReader, StreamReader +from .typedefs import ( + DEFAULT_JSON_DECODER, + JSONDecoder, + LooseHeaders, + RawHeaders, + StrOrURL, +) +from .web_exceptions import HTTPRequestEntityTooLarge, NotAppKeyWarning +from .web_response import StreamResponse + +__all__ = ("BaseRequest", "FileField", "Request") + + +if TYPE_CHECKING: + from .web_app import Application + from .web_protocol import RequestHandler + from .web_urldispatcher import UrlMappingMatchInfo + + +_T = TypeVar("_T") + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class FileField: + name: str + filename: str + file: io.BufferedReader + content_type: str + headers: CIMultiDictProxy[str] + + +_Post = str | bytes | bytearray | FileField +_TCHAR: Final[str] = string.digits + string.ascii_letters + r"!#$%&'*+.^_`|~-" +# '-' at the end to prevent interpretation as range in a char class + +_TOKEN: Final[str] = rf"[{_TCHAR}]+" + +_QDTEXT: Final[str] = r"[{}]".format( + r"".join(chr(c) for c in (0x09, 0x20, 0x21) + tuple(range(0x23, 0x7F))) +) +# qdtext includes 0x5C to escape 0x5D ('\]') +# qdtext excludes obs-text (because obsoleted, and encoding not specified) + +_QUOTED_PAIR: Final[str] = r"\\[\t !-~]" + +_QUOTED_STRING: Final[str] = rf'"(?:{_QUOTED_PAIR}|{_QDTEXT})*"' + +# This does not have a ReDOS/performance concern as long as it used with re.match(). +_FORWARDED_PAIR: Final[str] = rf"({_TOKEN})=({_TOKEN}|{_QUOTED_STRING})(:\d{{1,4}})?" + +_QUOTED_PAIR_REPLACE_RE: Final[Pattern[str]] = re.compile(r"\\([\t !-~])") +# same pattern as _QUOTED_PAIR but contains a capture group + +_FORWARDED_PAIR_RE: Final[Pattern[str]] = re.compile(_FORWARDED_PAIR) + +############################################################ +# HTTP Request +############################################################ + + +class BaseRequest(MutableMapping[str | RequestKey[Any], Any], HeadersMixin): + POST_METHODS = { + hdrs.METH_PATCH, + hdrs.METH_POST, + hdrs.METH_PUT, + hdrs.METH_TRACE, + hdrs.METH_DELETE, + } + + ATTRS = HeadersMixin.ATTRS | frozenset( + [ + "_message", + "_protocol", + "_payload_writer", + "_payload", + "_headers", + "_method", + "_version", + "_rel_url", + "_post", + "_read_bytes", + "_state", + "_cache", + "_task", + "_client_max_size", + "_loop", + "_transport_sslcontext", + "_transport_peername", + ] + ) + _post: MultiDictProxy[_Post] | None = None + _read_bytes: bytes | None = None + _seen_str_keys: set[str] = set() + + def __init__( + self, + message: RawRequestMessage, + payload: StreamReader, + protocol: "RequestHandler", + payload_writer: AbstractStreamWriter, + task: "asyncio.Task[None]", + loop: asyncio.AbstractEventLoop, + *, + client_max_size: int = 1024**2, + state: dict[RequestKey[Any] | str, Any] | None = None, + scheme: str | None = None, + host: str | None = None, + remote: str | None = None, + ) -> None: + self._message = message + self._protocol = protocol + self._payload_writer = payload_writer + + self._payload = payload + self._headers: CIMultiDictProxy[str] = message.headers + self._method = message.method + self._version = message.version + self._cache: dict[str, Any] = {} + url = message.url + if url.absolute: + if scheme is not None: + url = url.with_scheme(scheme) + if host is not None: + url = url.with_host(host) + # absolute URL is given, + # override auto-calculating url, host, and scheme + # all other properties should be good + self._cache["url"] = url + self._cache["host"] = url.host + self._cache["scheme"] = url.scheme + self._rel_url = url.relative() + else: + self._rel_url = url + if scheme is not None: + self._cache["scheme"] = scheme + if host is not None: + self._cache["host"] = host + + self._state = {} if state is None else state + self._task = task + self._client_max_size = client_max_size + self._loop = loop + + self._transport_sslcontext = protocol.ssl_context + self._transport_peername = protocol.peername + self._transport_sockname = protocol.sockname + + if remote is not None: + self._cache["remote"] = remote + + def clone( + self, + *, + method: str | _SENTINEL = sentinel, + rel_url: StrOrURL | _SENTINEL = sentinel, + headers: LooseHeaders | _SENTINEL = sentinel, + scheme: str | _SENTINEL = sentinel, + host: str | _SENTINEL = sentinel, + remote: str | _SENTINEL = sentinel, + client_max_size: int | _SENTINEL = sentinel, + ) -> "BaseRequest": + """Clone itself with replacement some attributes. + + Creates and returns a new instance of Request object. If no parameters + are given, an exact copy is returned. If a parameter is not passed, it + will reuse the one from the current request object. + """ + if self._read_bytes: + raise RuntimeError("Cannot clone request after reading its content") + + dct: dict[str, Any] = {} + if method is not sentinel: + dct["method"] = method + if rel_url is not sentinel: + new_url: URL = URL(rel_url) + dct["url"] = new_url + dct["path"] = str(new_url) + if headers is not sentinel: + # a copy semantic + dct["headers"] = CIMultiDictProxy(CIMultiDict(headers)) + dct["raw_headers"] = tuple( + (k.encode("utf-8"), v.encode("utf-8")) + for k, v in dct["headers"].items() + ) + + message = self._message._replace(**dct) + + kwargs = {} + if scheme is not sentinel: + kwargs["scheme"] = scheme + if host is not sentinel: + kwargs["host"] = host + if remote is not sentinel: + kwargs["remote"] = remote + if client_max_size is sentinel: + client_max_size = self._client_max_size + + return self.__class__( + message, + self._payload, + self._protocol, + self._payload_writer, + self._task, + self._loop, + client_max_size=client_max_size, + state=self._state.copy(), + **kwargs, + ) + + @property + def task(self) -> "asyncio.Task[None]": + return self._task + + @property + def protocol(self) -> "RequestHandler": + return self._protocol + + @property + def transport(self) -> asyncio.Transport | None: + if self._protocol is None: + return None + return self._protocol.transport + + @property + def writer(self) -> AbstractStreamWriter: + return self._payload_writer + + @property + def client_max_size(self) -> int: + return self._client_max_size + + @reify + def message(self) -> RawRequestMessage: + warnings.warn("Request.message is deprecated", DeprecationWarning, stacklevel=3) + return self._message + + @reify + def rel_url(self) -> URL: + return self._rel_url + + @reify + def loop(self) -> asyncio.AbstractEventLoop: + warnings.warn( + "request.loop property is deprecated", DeprecationWarning, stacklevel=2 + ) + return self._loop + + # MutableMapping API + + @overload # type: ignore[override] + def __getitem__(self, key: RequestKey[_T]) -> _T: ... + + @overload + def __getitem__(self, key: str) -> Any: ... + + def __getitem__(self, key: str | RequestKey[_T]) -> Any: + return self._state[key] + + @overload # type: ignore[override] + def __setitem__(self, key: RequestKey[_T], value: _T) -> None: ... + + @overload + def __setitem__(self, key: str, value: Any) -> None: ... + + def __setitem__(self, key: str | RequestKey[_T], value: Any) -> None: + if not isinstance(key, RequestKey) and key not in BaseRequest._seen_str_keys: + BaseRequest._seen_str_keys.add(key) + warnings.warn( + "It is recommended to use web.RequestKey instances for keys.\n" + + "https://docs.aiohttp.org/en/stable/web_advanced.html" + + "#request-s-storage", + category=NotAppKeyWarning, + stacklevel=2, + ) + self._state[key] = value + + def __delitem__(self, key: str | RequestKey[_T]) -> None: + del self._state[key] + + def __len__(self) -> int: + return len(self._state) + + def __iter__(self) -> Iterator[str | RequestKey[Any]]: + return iter(self._state) + + ######## + + @reify + def secure(self) -> bool: + """A bool indicating if the request is handled with SSL.""" + return self.scheme == "https" + + @reify + def forwarded(self) -> tuple[Mapping[str, str], ...]: + """A tuple containing all parsed Forwarded header(s). + + Makes an effort to parse Forwarded headers as specified by RFC 7239: + + - It adds one (immutable) dictionary per Forwarded 'field-value', ie + per proxy. The element corresponds to the data in the Forwarded + field-value added by the first proxy encountered by the client. Each + subsequent item corresponds to those added by later proxies. + - It checks that every value has valid syntax in general as specified + in section 4: either a 'token' or a 'quoted-string'. + - It un-escapes found escape sequences. + - It does NOT validate 'by' and 'for' contents as specified in section + 6. + - It does NOT validate 'host' contents (Host ABNF). + - It does NOT validate 'proto' contents for valid URI scheme names. + + Returns a tuple containing one or more immutable dicts + """ + elems = [] + for field_value in self._message.headers.getall(hdrs.FORWARDED, ()): + length = len(field_value) + pos = 0 + need_separator = False + elem: dict[str, str] = {} + elems.append(types.MappingProxyType(elem)) + while 0 <= pos < length: + match = _FORWARDED_PAIR_RE.match(field_value, pos) + if match is not None: # got a valid forwarded-pair + if need_separator: + # bad syntax here, skip to next comma + pos = field_value.find(",", pos) + else: + name, value, port = match.groups() + if value[0] == '"': + # quoted string: remove quotes and unescape + value = _QUOTED_PAIR_REPLACE_RE.sub(r"\1", value[1:-1]) + if port: + value += port + elem[name.lower()] = value + pos += len(match.group(0)) + need_separator = True + elif field_value[pos] == ",": # next forwarded-element + need_separator = False + elem = {} + elems.append(types.MappingProxyType(elem)) + pos += 1 + elif field_value[pos] == ";": # next forwarded-pair + need_separator = False + pos += 1 + elif field_value[pos] in " \t": + # Allow whitespace even between forwarded-pairs, though + # RFC 7239 doesn't. This simplifies code and is in line + # with Postel's law. + pos += 1 + else: + # bad syntax here, skip to next comma + pos = field_value.find(",", pos) + return tuple(elems) + + @reify + def scheme(self) -> str: + """A string representing the scheme of the request. + + Hostname is resolved in this order: + + - overridden value by .clone(scheme=new_scheme) call. + - type of connection to peer: HTTPS if socket is SSL, HTTP otherwise. + + 'http' or 'https'. + """ + if self._transport_sslcontext: + return "https" + else: + return "http" + + @reify + def method(self) -> str: + """Read only property for getting HTTP method. + + The value is upper-cased str like 'GET', 'POST', 'PUT' etc. + """ + return self._method + + @reify + def version(self) -> HttpVersion: + """Read only property for getting HTTP version of request. + + Returns aiohttp.protocol.HttpVersion instance. + """ + return self._version + + @reify + def host(self) -> str: + """Hostname of the request. + + Hostname is resolved in this order: + + - overridden value by .clone(host=new_host) call. + - HOST HTTP header + - local socket address the request arrived on + (transport ``sockname``) + - empty string if no transport information is available + + For example, 'example.com' or 'localhost:8080'. + + For historical reasons, the port number may be included. + """ + host = self._message.headers.get(hdrs.HOST) + if host is not None: + return host + sockname = self._transport_sockname + if sockname is None: + return "" + if isinstance(sockname, tuple): + # AF_INET6 returns a 4-tuple (host, port, flowinfo, scopeid); + # bracket the bare address so it matches the Host-header shape + # and is a valid URL authority component. + if len(sockname) == 4: + return f"[{sockname[0]}]" + return str(sockname[0]) + return str(sockname) + + @reify + def remote(self) -> str | None: + """Remote IP of client initiated HTTP request. + + The IP is resolved in this order: + + - overridden value by .clone(remote=new_remote) call. + - peername of opened socket + """ + if self._transport_peername is None: + return None + if isinstance(self._transport_peername, (list, tuple)): + return str(self._transport_peername[0]) + return str(self._transport_peername) + + @reify + def url(self) -> URL: + """The full URL of the request.""" + # authority is used here because it may include the port number + # and we want yarl to parse it correctly + return URL.build(scheme=self.scheme, authority=self.host).join(self._rel_url) + + @reify + def path(self) -> str: + """The URL including *PATH INFO* without the host or scheme. + + E.g., ``/app/blog`` + """ + return self._rel_url.path + + @reify + def path_qs(self) -> str: + """The URL including PATH_INFO and the query string. + + E.g, /app/blog?id=10 + """ + return str(self._rel_url) + + @reify + def raw_path(self) -> str: + """The URL including raw *PATH INFO* without the host or scheme. + + Warning, the path is unquoted and may contains non valid URL characters + + E.g., ``/my%2Fpath%7Cwith%21some%25strange%24characters`` + """ + return self._message.path + + @reify + def query(self) -> "MultiMapping[str]": + """A multidict with all the variables in the query string.""" + return self._rel_url.query + + @reify + def query_string(self) -> str: + """The query string in the URL. + + E.g., id=10 + """ + return self._rel_url.query_string + + @reify + def headers(self) -> CIMultiDictProxy[str]: + """A case-insensitive multidict proxy with all headers.""" + return self._headers + + @reify + def raw_headers(self) -> RawHeaders: + """A sequence of pairs for all headers.""" + return self._message.raw_headers + + @reify + def if_modified_since(self) -> datetime.datetime | None: + """The value of If-Modified-Since HTTP header, or None. + + This header is represented as a `datetime` object. + """ + return parse_http_date(self.headers.get(hdrs.IF_MODIFIED_SINCE)) + + @reify + def if_unmodified_since(self) -> datetime.datetime | None: + """The value of If-Unmodified-Since HTTP header, or None. + + This header is represented as a `datetime` object. + """ + return parse_http_date(self.headers.get(hdrs.IF_UNMODIFIED_SINCE)) + + @staticmethod + def _etag_values(etag_header: str) -> Iterator[ETag]: + """Extract `ETag` objects from raw header.""" + if etag_header == ETAG_ANY: + yield ETag( + is_weak=False, + value=ETAG_ANY, + ) + else: + for match in LIST_QUOTED_ETAG_RE.finditer(etag_header): + is_weak, value, garbage = match.group(2, 3, 4) + # Any symbol captured by 4th group means + # that the following sequence is invalid. + if garbage: + break + + yield ETag( + is_weak=bool(is_weak), + value=value, + ) + + @classmethod + def _if_match_or_none_impl( + cls, header_value: str | None + ) -> tuple[ETag, ...] | None: + if not header_value: + return None + + return tuple(cls._etag_values(header_value)) + + @reify + def if_match(self) -> tuple[ETag, ...] | None: + """The value of If-Match HTTP header, or None. + + This header is represented as a `tuple` of `ETag` objects. + """ + return self._if_match_or_none_impl(self.headers.get(hdrs.IF_MATCH)) + + @reify + def if_none_match(self) -> tuple[ETag, ...] | None: + """The value of If-None-Match HTTP header, or None. + + This header is represented as a `tuple` of `ETag` objects. + """ + return self._if_match_or_none_impl(self.headers.get(hdrs.IF_NONE_MATCH)) + + @reify + def if_range(self) -> datetime.datetime | None: + """The value of If-Range HTTP header, or None. + + This header is represented as a `datetime` object. + """ + return parse_http_date(self.headers.get(hdrs.IF_RANGE)) + + @reify + def keep_alive(self) -> bool: + """Is keepalive enabled by client?""" + return not self._message.should_close + + @reify + def cookies(self) -> Mapping[str, str]: + """Return request cookies. + + A read-only dictionary-like object. + """ + # Use parse_cookie_header for RFC 6265 compliant Cookie header parsing + # that accepts special characters in cookie names (fixes #2683) + parsed = parse_cookie_header(self.headers.get(hdrs.COOKIE, "")) + # Extract values from Morsel objects + return MappingProxyType({name: morsel.value for name, morsel in parsed}) + + @reify + def http_range(self) -> slice: + """The content of Range HTTP header. + + Return a slice instance. + + """ + rng = self._headers.get(hdrs.RANGE) + start, end = None, None + if rng is not None: + try: + pattern = r"^bytes=(\d*)-(\d*)$" + start, end = re.findall(pattern, rng, re.ASCII)[0] + except IndexError: # pattern was not found in header + raise ValueError("range not in acceptable format") + + end = int(end) if end else None + start = int(start) if start else None + + if start is None and end is not None: + # end with no start is to return tail of content + start = -end + end = None + + if start is not None and end is not None: + # end is inclusive in range header, exclusive for slice + end += 1 + + if start >= end: + raise ValueError("start cannot be after end") + + if start is end is None: # No valid range supplied + raise ValueError("No start or end of range specified") + + return slice(start, end, 1) + + @reify + def content(self) -> StreamReader: + """Return raw payload stream.""" + return self._payload + + @property + def has_body(self) -> bool: + """Return True if request's HTTP BODY can be read, False otherwise.""" + warnings.warn( + "Deprecated, use .can_read_body #2005", DeprecationWarning, stacklevel=2 + ) + return not self._payload.at_eof() + + @property + def can_read_body(self) -> bool: + """Return True if request's HTTP BODY can be read, False otherwise.""" + return not self._payload.at_eof() + + @reify + def body_exists(self) -> bool: + """Return True if request has HTTP BODY, False otherwise.""" + return type(self._payload) is not EmptyStreamReader + + async def release(self) -> None: + """Release request. + + Eat unread part of HTTP BODY if present. + """ + while not self._payload.at_eof(): + await self._payload.readany() + + async def read(self) -> bytes: + """Read request body if present. + + Returns bytes object with full request content. + """ + if self._read_bytes is None: + # Raise the buffer limits so compressed payloads decompress in + # larger chunks instead of many small pause/resume cycles. + if self._client_max_size: + self._payload.set_read_chunk_size(self._client_max_size) + body = bytearray() + while True: + chunk = await self._payload.readany() + body.extend(chunk) + if self._client_max_size: + body_size = len(body) + if body_size > self._client_max_size: + raise HTTPRequestEntityTooLarge(self._client_max_size) + if not chunk: + break + self._read_bytes = bytes(body) + return self._read_bytes + + async def text(self) -> str: + """Return BODY as text using encoding from .charset.""" + bytes_body = await self.read() + encoding = self.charset or "utf-8" + return bytes_body.decode(encoding) + + async def json(self, *, loads: JSONDecoder = DEFAULT_JSON_DECODER) -> Any: + """Return BODY as JSON.""" + body = await self.text() + return loads(body) + + async def multipart(self) -> MultipartReader: + """Return async iterator to process BODY as multipart.""" + return MultipartReader( + self._headers, + self._payload, + client_max_size=self._client_max_size, + max_field_size=self._protocol.max_field_size, + max_headers=self._protocol.max_headers, + max_size_error_cls=HTTPRequestEntityTooLarge, + ) + + async def post(self) -> "MultiDictProxy[_Post]": + """Return POST parameters.""" + if self._post is not None: + return self._post + if self._method not in self.POST_METHODS: + self._post = MultiDictProxy(MultiDict()) + return self._post + + content_type = self.content_type + if content_type not in ( + "", + "application/x-www-form-urlencoded", + "multipart/form-data", + ): + self._post = MultiDictProxy(MultiDict()) + return self._post + + out: MultiDict[_Post] = MultiDict() + + if content_type == "multipart/form-data": + multipart = await self.multipart() + max_size = self._client_max_size + + size = 0 + while (field := await multipart.next()) is not None: + field_ct = field.headers.get(hdrs.CONTENT_TYPE) + + if isinstance(field, BodyPartReader): + if field.name is None: + raise ValueError("Multipart field missing name.") + + # Note that according to RFC 7578, the Content-Type header + # is optional, even for files, so we can't assume it's + # present. + # https://tools.ietf.org/html/rfc7578#section-4.4 + if field.filename: + # store file in temp file + tmp = await self._loop.run_in_executor( + None, tempfile.TemporaryFile + ) + while chunk := await field.read_chunk(size=DEFAULT_CHUNK_SIZE): + async for decoded_chunk in field.decode_iter(chunk): + await self._loop.run_in_executor( + None, tmp.write, decoded_chunk + ) + size += len(decoded_chunk) + if 0 < max_size < size: + await self._loop.run_in_executor(None, tmp.close) + raise HTTPRequestEntityTooLarge(max_size) + await self._loop.run_in_executor(None, tmp.seek, 0) + + if field_ct is None: + field_ct = "application/octet-stream" + + ff = FileField( + field.name, + field.filename, + cast(io.BufferedReader, tmp), + field_ct, + field.headers, + ) + out.add(field.name, ff) + else: + # deal with ordinary data + raw_data = bytearray() + while chunk := await field.read_chunk(): + size += len(chunk) + if 0 < max_size < size: + raise HTTPRequestEntityTooLarge(max_size) + raw_data.extend(chunk) + + value = bytearray() + # form-data doesn't support compression, so don't need to check size again. + async for d in field.decode_iter(raw_data): + value.extend(d) + + if field_ct is None or field_ct.startswith("text/"): + charset = field.get_charset(default="utf-8") + out.add(field.name, value.decode(charset)) + else: + out.add(field.name, value) + else: + raise ValueError( + "To decode nested multipart you need to use custom reader", + ) + else: + data = await self.read() + if data: + charset = self.charset or "utf-8" + out.extend( + parse_qsl( + data.rstrip().decode(charset), + keep_blank_values=True, + encoding=charset, + ) + ) + + self._post = MultiDictProxy(out) + return self._post + + def get_extra_info(self, name: str, default: Any = None) -> Any: + """Extra info from protocol transport""" + protocol = self._protocol + if protocol is None: + return default + + transport = protocol.transport + if transport is None: + return default + + return transport.get_extra_info(name, default) + + def __repr__(self) -> str: + ascii_encodable_path = self.path.encode("ascii", "backslashreplace").decode( + "ascii" + ) + return f"<{self.__class__.__name__} {self._method} {ascii_encodable_path} >" + + def __eq__(self, other: object) -> bool: + return id(self) == id(other) + + def __bool__(self) -> bool: + return True + + async def _prepare_hook(self, response: StreamResponse) -> None: + return + + def _cancel(self, exc: BaseException) -> None: + set_exception(self._payload, exc) + + def _finish(self) -> None: + if self._post is None or self.content_type != "multipart/form-data": + return + + # NOTE: Release file descriptors for the + # NOTE: `tempfile.Temporaryfile`-created `_io.BufferedRandom` + # NOTE: instances of files sent within multipart request body + # NOTE: via HTTP POST request. + for file_name, file_field_object in self._post.items(): + if isinstance(file_field_object, FileField): + file_field_object.file.close() + + +class Request(BaseRequest): + + ATTRS = BaseRequest.ATTRS | frozenset(["_match_info"]) + + _match_info: Optional["UrlMappingMatchInfo"] = None + + if DEBUG: + + def __setattr__(self, name: str, val: Any) -> None: + if name not in self.ATTRS: + warnings.warn( + f"Setting custom {self.__class__.__name__}.{name} attribute " + "is discouraged", + DeprecationWarning, + stacklevel=2, + ) + super().__setattr__(name, val) + + def clone( + self, + *, + method: str | _SENTINEL = sentinel, + rel_url: StrOrURL | _SENTINEL = sentinel, + headers: LooseHeaders | _SENTINEL = sentinel, + scheme: str | _SENTINEL = sentinel, + host: str | _SENTINEL = sentinel, + remote: str | _SENTINEL = sentinel, + client_max_size: int | _SENTINEL = sentinel, + ) -> "Request": + ret = super().clone( + method=method, + rel_url=rel_url, + headers=headers, + scheme=scheme, + host=host, + remote=remote, + client_max_size=client_max_size, + ) + new_ret = cast(Request, ret) + new_ret._match_info = self._match_info + return new_ret + + @reify + def match_info(self) -> "UrlMappingMatchInfo": + """Result of route resolving.""" + match_info = self._match_info + assert match_info is not None + return match_info + + @property + def app(self) -> "Application": + """Application instance.""" + match_info = self._match_info + assert match_info is not None + return match_info.current_app + + @property + def config_dict(self) -> ChainMapProxy: + match_info = self._match_info + assert match_info is not None + lst = match_info.apps + app = self.app + idx = lst.index(app) + sublist = list(reversed(lst[: idx + 1])) + return ChainMapProxy(sublist) + + async def _prepare_hook(self, response: StreamResponse) -> None: + match_info = self._match_info + if match_info is None: + return + for app in match_info._apps: + if on_response_prepare := app.on_response_prepare: + await on_response_prepare.send(self, response) diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_response.py b/venv/lib/python3.11/site-packages/aiohttp/web_response.py new file mode 100644 index 0000000..cbe4985 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_response.py @@ -0,0 +1,909 @@ +import asyncio +import collections.abc +import datetime +import enum +import json +import math +import time +import warnings +from collections.abc import Iterator, MutableMapping +from concurrent.futures import Executor +from http import HTTPStatus +from http.cookies import SimpleCookie +from typing import TYPE_CHECKING, Any, Optional, TypeVar, cast, overload + +from multidict import CIMultiDict, istr + +from . import hdrs, payload +from .abc import AbstractStreamWriter +from .compression_utils import MAX_SYNC_CHUNK_SIZE, ZLibCompressor +from .helpers import ( + ETAG_ANY, + QUOTED_ETAG_RE, + ETag, + HeadersMixin, + ResponseKey, + must_be_empty_body, + parse_http_date, + rfc822_formatted_time, + sentinel, + should_remove_content_length, + validate_etag_value, +) +from .http import SERVER_SOFTWARE, HttpVersion10, HttpVersion11 +from .payload import Payload +from .typedefs import JSONBytesEncoder, JSONEncoder, LooseHeaders + +REASON_PHRASES = {http_status.value: http_status.phrase for http_status in HTTPStatus} + +__all__ = ( + "ContentCoding", + "StreamResponse", + "Response", + "json_response", + "json_bytes_response", +) + + +if TYPE_CHECKING: + from .web_request import BaseRequest + + BaseClass = MutableMapping[str, Any] +else: + BaseClass = collections.abc.MutableMapping + + +_T = TypeVar("_T") + + +# TODO(py311): Convert to StrEnum for wider use +class ContentCoding(enum.Enum): + # The content codings that we have support for. + # + # Additional registered codings are listed at: + # https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding + deflate = "deflate" + gzip = "gzip" + identity = "identity" + + +CONTENT_CODINGS = {coding.value: coding for coding in ContentCoding} + +############################################################ +# HTTP Response classes +############################################################ + + +class StreamResponse(MutableMapping[str | ResponseKey[Any], Any], HeadersMixin): + + _body: None | bytes | bytearray | Payload + _length_check = True + _body = None + _keep_alive: bool | None = None + _chunked: bool = False + _compression: bool = False + _compression_strategy: int | None = None + _compression_force: ContentCoding | None = None + _req: Optional["BaseRequest"] = None + _payload_writer: AbstractStreamWriter | None = None + _eof_sent: bool = False + _must_be_empty_body: bool | None = None + _body_length = 0 + _cookies: SimpleCookie | None = None + _send_headers_immediately = True + _seen_str_keys: set[str] = set() + + def __init__( + self, + *, + status: int = 200, + reason: str | None = None, + headers: LooseHeaders | None = None, + _real_headers: CIMultiDict[str] | None = None, + ) -> None: + """Initialize a new stream response object. + + _real_headers is an internal parameter used to pass a pre-populated + headers object. It is used by the `Response` class to avoid copying + the headers when creating a new response object. It is not intended + to be used by external code. + """ + self._state: dict[str | ResponseKey[Any], Any] = {} + + if _real_headers is not None: + self._headers = _real_headers + elif headers is not None: + self._headers: CIMultiDict[str] = CIMultiDict(headers) + else: + self._headers = CIMultiDict() + + self._set_status(status, reason) + + @property + def prepared(self) -> bool: + return self._eof_sent or self._payload_writer is not None + + @property + def task(self) -> "asyncio.Task[None] | None": + if self._req: + return self._req.task + else: + return None + + @property + def status(self) -> int: + return self._status + + @property + def chunked(self) -> bool: + return self._chunked + + @property + def compression(self) -> bool: + return self._compression + + @property + def reason(self) -> str: + return self._reason + + def set_status( + self, + status: int, + reason: str | None = None, + ) -> None: + assert ( + not self.prepared + ), "Cannot change the response status code after the headers have been sent" + self._set_status(status, reason) + + def _set_status(self, status: int, reason: str | None) -> None: + self._status = int(status) + if reason is None: + reason = REASON_PHRASES.get(self._status, "") + elif "\r" in reason or "\n" in reason: + raise ValueError("Reason cannot contain \\r or \\n") + self._reason = reason + + @property + def keep_alive(self) -> bool | None: + return self._keep_alive + + def force_close(self) -> None: + self._keep_alive = False + + @property + def body_length(self) -> int: + return self._body_length + + @property + def output_length(self) -> int: + warnings.warn("output_length is deprecated", DeprecationWarning) + assert self._payload_writer + return self._payload_writer.buffer_size + + def enable_chunked_encoding(self, chunk_size: int | None = None) -> None: + """Enables automatic chunked transfer encoding.""" + if hdrs.CONTENT_LENGTH in self._headers: + raise RuntimeError( + "You can't enable chunked encoding when a content length is set" + ) + if chunk_size is not None: + warnings.warn("Chunk size is deprecated #1615", DeprecationWarning) + self._chunked = True + + def enable_compression( + self, + force: bool | ContentCoding | None = None, + strategy: int | None = None, + ) -> None: + """Enables response compression encoding.""" + # Backwards compatibility for when force was a bool <0.17. + if isinstance(force, bool): + force = ContentCoding.deflate if force else ContentCoding.identity + warnings.warn( + "Using boolean for force is deprecated #3318", DeprecationWarning + ) + elif force is not None: + assert isinstance( + force, ContentCoding + ), "force should one of None, bool or ContentEncoding" + + self._compression = True + self._compression_force = force + self._compression_strategy = strategy + + @property + def headers(self) -> "CIMultiDict[str]": + return self._headers + + @property + def cookies(self) -> SimpleCookie: + if self._cookies is None: + self._cookies = SimpleCookie() + return self._cookies + + def set_cookie( + self, + name: str, + value: str, + *, + expires: str | None = None, + domain: str | None = None, + max_age: int | str | None = None, + path: str = "/", + secure: bool | None = None, + httponly: bool | None = None, + version: str | None = None, + samesite: str | None = None, + partitioned: bool | None = None, + ) -> None: + """Set or update response cookie. + + Sets new cookie or updates existent with new value. + Also updates only those params which are not None. + """ + if self._cookies is None: + self._cookies = SimpleCookie() + + self._cookies[name] = value + c = self._cookies[name] + + if expires is not None: + c["expires"] = expires + elif c.get("expires") == "Thu, 01 Jan 1970 00:00:00 GMT": + del c["expires"] + + if domain is not None: + c["domain"] = domain + + if max_age is not None: + c["max-age"] = str(max_age) + elif "max-age" in c: + del c["max-age"] + + c["path"] = path + + if secure is not None: + c["secure"] = secure + if httponly is not None: + c["httponly"] = httponly + if version is not None: + c["version"] = version + if samesite is not None: + c["samesite"] = samesite + + if partitioned is not None: + c["partitioned"] = partitioned + + def del_cookie( + self, + name: str, + *, + domain: str | None = None, + path: str = "/", + secure: bool | None = None, + httponly: bool | None = None, + samesite: str | None = None, + ) -> None: + """Delete cookie. + + Creates new empty expired cookie. + """ + # TODO: do we need domain/path here? + if self._cookies is not None: + self._cookies.pop(name, None) + self.set_cookie( + name, + "", + max_age=0, + expires="Thu, 01 Jan 1970 00:00:00 GMT", + domain=domain, + path=path, + secure=secure, + httponly=httponly, + samesite=samesite, + ) + + @property + def content_length(self) -> int | None: + # Just a placeholder for adding setter + return super().content_length + + @content_length.setter + def content_length(self, value: int | None) -> None: + if value is not None: + value = int(value) + if self._chunked: + raise RuntimeError( + "You can't set content length when chunked encoding is enable" + ) + self._headers[hdrs.CONTENT_LENGTH] = str(value) + else: + self._headers.pop(hdrs.CONTENT_LENGTH, None) + + @property + def content_type(self) -> str: + # Just a placeholder for adding setter + return super().content_type + + @content_type.setter + def content_type(self, value: str) -> None: + self.content_type # read header values if needed + self._content_type = str(value) + self._generate_content_type_header() + + @property + def charset(self) -> str | None: + # Just a placeholder for adding setter + return super().charset + + @charset.setter + def charset(self, value: str | None) -> None: + ctype = self.content_type # read header values if needed + if ctype == "application/octet-stream": + raise RuntimeError( + "Setting charset for application/octet-stream " + "doesn't make sense, setup content_type first" + ) + assert self._content_dict is not None + if value is None: + self._content_dict.pop("charset", None) + else: + self._content_dict["charset"] = str(value).lower() + self._generate_content_type_header() + + @property + def last_modified(self) -> datetime.datetime | None: + """The value of Last-Modified HTTP header, or None. + + This header is represented as a `datetime` object. + """ + return parse_http_date(self._headers.get(hdrs.LAST_MODIFIED)) + + @last_modified.setter + def last_modified( + self, value: int | float | datetime.datetime | str | None + ) -> None: + if value is None: + self._headers.pop(hdrs.LAST_MODIFIED, None) + elif isinstance(value, (int, float)): + self._headers[hdrs.LAST_MODIFIED] = time.strftime( + "%a, %d %b %Y %H:%M:%S GMT", time.gmtime(math.ceil(value)) + ) + elif isinstance(value, datetime.datetime): + self._headers[hdrs.LAST_MODIFIED] = time.strftime( + "%a, %d %b %Y %H:%M:%S GMT", value.utctimetuple() + ) + elif isinstance(value, str): + self._headers[hdrs.LAST_MODIFIED] = value + else: + msg = f"Unsupported type for last_modified: {type(value).__name__}" + raise TypeError(msg) + + @property + def etag(self) -> ETag | None: + quoted_value = self._headers.get(hdrs.ETAG) + if not quoted_value: + return None + elif quoted_value == ETAG_ANY: + return ETag(value=ETAG_ANY) + match = QUOTED_ETAG_RE.fullmatch(quoted_value) + if not match: + return None + is_weak, value = match.group(1, 2) + return ETag( + is_weak=bool(is_weak), + value=value, + ) + + @etag.setter + def etag(self, value: ETag | str | None) -> None: + if value is None: + self._headers.pop(hdrs.ETAG, None) + elif (isinstance(value, str) and value == ETAG_ANY) or ( + isinstance(value, ETag) and value.value == ETAG_ANY + ): + self._headers[hdrs.ETAG] = ETAG_ANY + elif isinstance(value, str): + validate_etag_value(value) + self._headers[hdrs.ETAG] = f'"{value}"' + elif isinstance(value, ETag) and isinstance(value.value, str): + validate_etag_value(value.value) + hdr_value = f'W/"{value.value}"' if value.is_weak else f'"{value.value}"' + self._headers[hdrs.ETAG] = hdr_value + else: + raise ValueError( + f"Unsupported etag type: {type(value)}. " + f"etag must be str, ETag or None" + ) + + def _generate_content_type_header( + self, CONTENT_TYPE: istr = hdrs.CONTENT_TYPE + ) -> None: + assert self._content_dict is not None + assert self._content_type is not None + params = "; ".join(f"{k}={v}" for k, v in self._content_dict.items()) + if params: + ctype = self._content_type + "; " + params + else: + ctype = self._content_type + self._headers[CONTENT_TYPE] = ctype + + async def _do_start_compression(self, coding: ContentCoding) -> None: + if coding is ContentCoding.identity: + return + assert self._payload_writer is not None + self._headers[hdrs.CONTENT_ENCODING] = coding.value + self._payload_writer.enable_compression( + coding.value, self._compression_strategy + ) + # Compressed payload may have different content length, + # remove the header + self._headers.popall(hdrs.CONTENT_LENGTH, None) + + async def _start_compression(self, request: "BaseRequest") -> None: + if self._compression_force: + await self._do_start_compression(self._compression_force) + return + # Encoding comparisons should be case-insensitive + # https://www.rfc-editor.org/rfc/rfc9110#section-8.4.1 + accept_encoding = request.headers.get(hdrs.ACCEPT_ENCODING, "").lower() + for value, coding in CONTENT_CODINGS.items(): + if value in accept_encoding: + await self._do_start_compression(coding) + return + + async def prepare(self, request: "BaseRequest") -> AbstractStreamWriter | None: + if self._eof_sent: + return None + if self._payload_writer is not None: + return self._payload_writer + self._must_be_empty_body = must_be_empty_body(request.method, self.status) + return await self._start(request) + + async def _start(self, request: "BaseRequest") -> AbstractStreamWriter: + self._req = request + writer = self._payload_writer = request._payload_writer + + await self._prepare_headers() + await request._prepare_hook(self) + await self._write_headers() + + return writer + + async def _prepare_headers(self) -> None: + request = self._req + assert request is not None + writer = self._payload_writer + assert writer is not None + keep_alive = self._keep_alive + if keep_alive is None: + keep_alive = request.keep_alive + self._keep_alive = keep_alive + + version = request.version + + headers = self._headers + if self._cookies: + for cookie in self._cookies.values(): + value = cookie.output(header="")[1:] + headers.add(hdrs.SET_COOKIE, value) + + if self._compression: + await self._start_compression(request) + + if self._chunked: + if version != HttpVersion11: + raise RuntimeError( + "Using chunked encoding is forbidden " + f"for HTTP/{request.version.major}.{request.version.minor}" + ) + if not self._must_be_empty_body: + writer.enable_chunking() + headers[hdrs.TRANSFER_ENCODING] = "chunked" + elif self._length_check: # Disabled for WebSockets + writer.length = self.content_length + if writer.length is None: + if version >= HttpVersion11: + if not self._must_be_empty_body: + writer.enable_chunking() + headers[hdrs.TRANSFER_ENCODING] = "chunked" + elif not self._must_be_empty_body: + keep_alive = False + + # HTTP 1.1: https://tools.ietf.org/html/rfc7230#section-3.3.2 + # HTTP 1.0: https://tools.ietf.org/html/rfc1945#section-10.4 + if self._must_be_empty_body: + if hdrs.CONTENT_LENGTH in headers and should_remove_content_length( + request.method, self.status + ): + del headers[hdrs.CONTENT_LENGTH] + # https://datatracker.ietf.org/doc/html/rfc9112#section-6.1-10 + # https://datatracker.ietf.org/doc/html/rfc9112#section-6.1-13 + if hdrs.TRANSFER_ENCODING in headers: + del headers[hdrs.TRANSFER_ENCODING] + elif (writer.length if self._length_check else self.content_length) != 0: + # https://www.rfc-editor.org/rfc/rfc9110#section-8.3-5 + headers.setdefault(hdrs.CONTENT_TYPE, "application/octet-stream") + headers.setdefault(hdrs.DATE, rfc822_formatted_time()) + headers.setdefault(hdrs.SERVER, SERVER_SOFTWARE) + + # connection header + if hdrs.CONNECTION not in headers: + if keep_alive: + if version == HttpVersion10: + headers[hdrs.CONNECTION] = "keep-alive" + elif version == HttpVersion11: + headers[hdrs.CONNECTION] = "close" + + async def _write_headers(self) -> None: + request = self._req + assert request is not None + writer = self._payload_writer + assert writer is not None + # status line + version = request.version + status_line = f"HTTP/{version[0]}.{version[1]} {self._status} {self._reason}" + await writer.write_headers(status_line, self._headers) + # Send headers immediately if not opted into buffering + if self._send_headers_immediately: + writer.send_headers() + + async def write(self, data: bytes | bytearray | memoryview) -> None: + assert isinstance( + data, (bytes, bytearray, memoryview) + ), "data argument must be byte-ish (%r)" % type(data) + + if self._eof_sent: + raise RuntimeError("Cannot call write() after write_eof()") + if self._payload_writer is None: + raise RuntimeError("Cannot call write() before prepare()") + + await self._payload_writer.write(data) + + async def drain(self) -> None: + assert not self._eof_sent, "EOF has already been sent" + assert self._payload_writer is not None, "Response has not been started" + warnings.warn( + "drain method is deprecated, use await resp.write()", + DeprecationWarning, + stacklevel=2, + ) + await self._payload_writer.drain() + + async def write_eof(self, data: bytes = b"") -> None: + assert isinstance( + data, (bytes, bytearray, memoryview) + ), "data argument must be byte-ish (%r)" % type(data) + + if self._eof_sent: + return + + assert self._payload_writer is not None, "Response has not been started" + + await self._payload_writer.write_eof(data) + self._eof_sent = True + self._req = None + self._body_length = self._payload_writer.output_size + self._payload_writer = None + + def __repr__(self) -> str: + if self._eof_sent: + info = "eof" + elif self.prepared: + assert self._req is not None + info = f"{self._req.method} {self._req.path} " + else: + info = "not prepared" + return f"<{self.__class__.__name__} {self.reason} {info}>" + + @overload # type: ignore[override] + def __getitem__(self, key: ResponseKey[_T]) -> _T: ... + + @overload + def __getitem__(self, key: str) -> Any: ... + + def __getitem__(self, key: str | ResponseKey[_T]) -> Any: + return self._state[key] + + @overload # type: ignore[override] + def __setitem__(self, key: ResponseKey[_T], value: _T) -> None: ... + + @overload + def __setitem__(self, key: str, value: Any) -> None: ... + + def __setitem__(self, key: str | ResponseKey[_T], value: Any) -> None: + if ( + not isinstance(key, ResponseKey) + and key not in StreamResponse._seen_str_keys + ): + # Import here to break circular dependency + from .web_exceptions import NotAppKeyWarning + + StreamResponse._seen_str_keys.add(key) + warnings.warn( + "It is recommended to use web.ResponseKey instances for keys.\n" + + "https://docs.aiohttp.org/en/stable/web_advanced.html" + + "#response-s-storage", + category=NotAppKeyWarning, + stacklevel=2, + ) + self._state[key] = value + + def __delitem__(self, key: str | ResponseKey[_T]) -> None: + del self._state[key] + + def __len__(self) -> int: + return len(self._state) + + def __iter__(self) -> Iterator[str | ResponseKey[Any]]: + return iter(self._state) + + def __hash__(self) -> int: + return hash(id(self)) + + def __eq__(self, other: object) -> bool: + return self is other + + def __bool__(self) -> bool: + return True + + +class Response(StreamResponse): + + _compressed_body: bytes | None = None + _send_headers_immediately = False + + def __init__( + self, + *, + body: Any = None, + status: int = 200, + reason: str | None = None, + text: str | None = None, + headers: LooseHeaders | None = None, + content_type: str | None = None, + charset: str | None = None, + zlib_executor_size: int = MAX_SYNC_CHUNK_SIZE, + zlib_executor: Executor | None = None, + ) -> None: + if body is not None and text is not None: + raise ValueError("body and text are not allowed together") + + if headers is None: + real_headers: CIMultiDict[str] = CIMultiDict() + else: + real_headers = CIMultiDict(headers) + + if content_type is not None and "charset" in content_type: + raise ValueError("charset must not be in content_type argument") + + if text is not None: + if hdrs.CONTENT_TYPE in real_headers: + if content_type or charset: + raise ValueError( + "passing both Content-Type header and " + "content_type or charset params " + "is forbidden" + ) + else: + # fast path for filling headers + if not isinstance(text, str): + raise TypeError("text argument must be str (%r)" % type(text)) + if content_type is None: + content_type = "text/plain" + if charset is None: + charset = "utf-8" + real_headers[hdrs.CONTENT_TYPE] = content_type + "; charset=" + charset + body = text.encode(charset) + text = None + elif hdrs.CONTENT_TYPE in real_headers: + if content_type is not None or charset is not None: + raise ValueError( + "passing both Content-Type header and " + "content_type or charset params " + "is forbidden" + ) + elif content_type is not None: + if charset is not None: + content_type += "; charset=" + charset + real_headers[hdrs.CONTENT_TYPE] = content_type + + super().__init__(status=status, reason=reason, _real_headers=real_headers) + + if text is not None: + self.text = text + else: + self.body = body + + self._zlib_executor_size = zlib_executor_size + self._zlib_executor = zlib_executor + + @property + def body(self) -> bytes | bytearray | Payload | None: + return self._body + + @body.setter + def body(self, body: Any) -> None: + if body is None: + self._body = None + elif isinstance(body, (bytes, bytearray)): + self._body = body + else: + try: + self._body = body = payload.PAYLOAD_REGISTRY.get(body) + except payload.LookupError: + raise ValueError("Unsupported body type %r" % type(body)) + + headers = self._headers + + # set content-type + if hdrs.CONTENT_TYPE not in headers: + headers[hdrs.CONTENT_TYPE] = body.content_type + + # copy payload headers + if body.headers: + for key, value in body.headers.items(): + if key not in headers: + headers[key] = value + + self._compressed_body = None + + @property + def text(self) -> str | None: + if self._body is None: + return None + # Note: When _body is a Payload (e.g. FilePayload), this may do blocking I/O + # This is generally safe as most common payloads (BytesPayload, StringPayload) + # don't do blocking I/O, but be careful with file-based payloads + return self._body.decode(self.charset or "utf-8") + + @text.setter + def text(self, text: str) -> None: + assert text is None or isinstance( + text, str + ), "text argument must be str (%r)" % type(text) + + if self.content_type == "application/octet-stream": + self.content_type = "text/plain" + if self.charset is None: + self.charset = "utf-8" + + self._body = text.encode(self.charset) + self._compressed_body = None + + @property + def content_length(self) -> int | None: + if self._chunked: + return None + + if hdrs.CONTENT_LENGTH in self._headers: + return int(self._headers[hdrs.CONTENT_LENGTH]) + + if self._compressed_body is not None: + # Return length of the compressed body + return len(self._compressed_body) + elif isinstance(self._body, Payload): + # A payload without content length, or a compressed payload + return None + elif self._body is not None: + return len(self._body) + else: + return 0 + + @content_length.setter + def content_length(self, value: int | None) -> None: + raise RuntimeError("Content length is set automatically") + + async def write_eof(self, data: bytes = b"") -> None: + if self._eof_sent: + return + if self._compressed_body is None: + body = self._body + else: + body = self._compressed_body + assert not data, f"data arg is not supported, got {data!r}" + assert self._req is not None + assert self._payload_writer is not None + if body is None or self._must_be_empty_body: + await super().write_eof() + elif isinstance(self._body, Payload): + try: + await self._body.write(self._payload_writer) + finally: + await self._body.close() + await super().write_eof() + else: + await super().write_eof(cast(bytes, body)) + + async def _start(self, request: "BaseRequest") -> AbstractStreamWriter: + if hdrs.CONTENT_LENGTH in self._headers: + if should_remove_content_length(request.method, self.status): + del self._headers[hdrs.CONTENT_LENGTH] + elif not self._chunked: + if isinstance(self._body, Payload): + if (size := self._body.size) is not None: + self._headers[hdrs.CONTENT_LENGTH] = str(size) + else: + body_len = len(self._body) if self._body else "0" + # https://www.rfc-editor.org/rfc/rfc9110.html#section-8.6-7 + if body_len != "0" or ( + self.status != 304 and request.method not in hdrs.METH_HEAD_ALL + ): + self._headers[hdrs.CONTENT_LENGTH] = str(body_len) + + return await super()._start(request) + + async def _do_start_compression(self, coding: ContentCoding) -> None: + if self._chunked or isinstance(self._body, Payload): + return await super()._do_start_compression(coding) + if coding is ContentCoding.identity: + return + # Instead of using _payload_writer.enable_compression, + # compress the whole body + compressor = ZLibCompressor( + encoding=coding.value, + max_sync_chunk_size=self._zlib_executor_size, + executor=self._zlib_executor, + ) + assert self._body is not None + self._compressed_body = ( + await compressor.compress(self._body) + compressor.flush() + ) + self._headers[hdrs.CONTENT_ENCODING] = coding.value + self._headers[hdrs.CONTENT_LENGTH] = str(len(self._compressed_body)) + + +def json_response( + data: Any = sentinel, + *, + text: str | None = None, + body: bytes | None = None, + status: int = 200, + reason: str | None = None, + headers: LooseHeaders | None = None, + content_type: str = "application/json", + dumps: JSONEncoder = json.dumps, +) -> Response: + if data is not sentinel: + if text or body: + raise ValueError("only one of data, text, or body should be specified") + else: + text = dumps(data) + return Response( + text=text, + body=body, + status=status, + reason=reason, + headers=headers, + content_type=content_type, + ) + + +def json_bytes_response( + data: Any = sentinel, + *, + dumps: JSONBytesEncoder, + body: bytes | None = None, + status: int = 200, + reason: str | None = None, + headers: LooseHeaders | None = None, + content_type: str = "application/json", +) -> Response: + """Create a JSON response using a bytes-returning encoder. + + Use this when your JSON encoder (like orjson) returns bytes + instead of str, avoiding the encode/decode overhead. + """ + if data is not sentinel: + if body is not None: + raise ValueError("only one of data or body should be specified") + else: + body = dumps(data) + return Response( + body=body, + status=status, + reason=reason, + headers=headers, + content_type=content_type, + ) diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_routedef.py b/venv/lib/python3.11/site-packages/aiohttp/web_routedef.py new file mode 100644 index 0000000..4c40272 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_routedef.py @@ -0,0 +1,203 @@ +import abc +import os # noqa +from collections.abc import Callable, Iterator, Sequence +from typing import TYPE_CHECKING, Any, Union, overload + +import attr + +from . import hdrs +from .abc import AbstractView +from .typedefs import Handler, PathLike + +if TYPE_CHECKING: + from .web_request import Request + from .web_response import StreamResponse + from .web_urldispatcher import AbstractRoute, UrlDispatcher +else: + Request = StreamResponse = UrlDispatcher = AbstractRoute = None + + +__all__ = ( + "AbstractRouteDef", + "RouteDef", + "StaticDef", + "RouteTableDef", + "head", + "options", + "get", + "post", + "patch", + "put", + "delete", + "route", + "view", + "static", +) + + +class AbstractRouteDef(abc.ABC): + @abc.abstractmethod + def register(self, router: UrlDispatcher) -> list[AbstractRoute]: + pass # pragma: no cover + + +_HandlerType = Union[type[AbstractView], Handler] + + +@attr.s(auto_attribs=True, frozen=True, repr=False, slots=True) +class RouteDef(AbstractRouteDef): + method: str + path: str + handler: _HandlerType + kwargs: dict[str, Any] + + def __repr__(self) -> str: + info = [] + for name, value in sorted(self.kwargs.items()): + info.append(f", {name}={value!r}") + return " {handler.__name__!r}{info}>".format( + method=self.method, path=self.path, handler=self.handler, info="".join(info) + ) + + def register(self, router: UrlDispatcher) -> list[AbstractRoute]: + if self.method in hdrs.METH_ALL: + reg = getattr(router, "add_" + self.method.lower()) + return [reg(self.path, self.handler, **self.kwargs)] + else: + return [ + router.add_route(self.method, self.path, self.handler, **self.kwargs) + ] + + +@attr.s(auto_attribs=True, frozen=True, repr=False, slots=True) +class StaticDef(AbstractRouteDef): + prefix: str + path: PathLike + kwargs: dict[str, Any] + + def __repr__(self) -> str: + info = [] + for name, value in sorted(self.kwargs.items()): + info.append(f", {name}={value!r}") + return " {path}{info}>".format( + prefix=self.prefix, path=self.path, info="".join(info) + ) + + def register(self, router: UrlDispatcher) -> list[AbstractRoute]: + resource = router.add_static(self.prefix, self.path, **self.kwargs) + routes = resource.get_info().get("routes", {}) + return list(routes.values()) + + +def route(method: str, path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return RouteDef(method, path, handler, kwargs) + + +def head(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_HEAD, path, handler, **kwargs) + + +def options(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_OPTIONS, path, handler, **kwargs) + + +def get( + path: str, + handler: _HandlerType, + *, + name: str | None = None, + allow_head: bool = True, + **kwargs: Any, +) -> RouteDef: + return route( + hdrs.METH_GET, path, handler, name=name, allow_head=allow_head, **kwargs + ) + + +def post(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_POST, path, handler, **kwargs) + + +def put(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_PUT, path, handler, **kwargs) + + +def patch(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_PATCH, path, handler, **kwargs) + + +def delete(path: str, handler: _HandlerType, **kwargs: Any) -> RouteDef: + return route(hdrs.METH_DELETE, path, handler, **kwargs) + + +def view(path: str, handler: type[AbstractView], **kwargs: Any) -> RouteDef: + return route(hdrs.METH_ANY, path, handler, **kwargs) + + +def static(prefix: str, path: PathLike, **kwargs: Any) -> StaticDef: + return StaticDef(prefix, path, kwargs) + + +_Deco = Callable[[_HandlerType], _HandlerType] + + +class RouteTableDef(Sequence[AbstractRouteDef]): + """Route definition table""" + + def __init__(self) -> None: + self._items: list[AbstractRouteDef] = [] + + def __repr__(self) -> str: + return f"" + + @overload + def __getitem__(self, index: int) -> AbstractRouteDef: ... + + @overload + def __getitem__(self, index: slice) -> list[AbstractRouteDef]: ... + + def __getitem__(self, index): # type: ignore[no-untyped-def] + return self._items[index] + + def __iter__(self) -> Iterator[AbstractRouteDef]: + return iter(self._items) + + def __len__(self) -> int: + return len(self._items) + + def __contains__(self, item: object) -> bool: + return item in self._items + + def route(self, method: str, path: str, **kwargs: Any) -> _Deco: + def inner(handler: _HandlerType) -> _HandlerType: + self._items.append(RouteDef(method, path, handler, kwargs)) + return handler + + return inner + + def head(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_HEAD, path, **kwargs) + + def get(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_GET, path, **kwargs) + + def post(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_POST, path, **kwargs) + + def put(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_PUT, path, **kwargs) + + def patch(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_PATCH, path, **kwargs) + + def delete(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_DELETE, path, **kwargs) + + def options(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_OPTIONS, path, **kwargs) + + def view(self, path: str, **kwargs: Any) -> _Deco: + return self.route(hdrs.METH_ANY, path, **kwargs) + + def static(self, prefix: str, path: PathLike, **kwargs: Any) -> None: + self._items.append(StaticDef(prefix, path, kwargs)) diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_runner.py b/venv/lib/python3.11/site-packages/aiohttp/web_runner.py new file mode 100644 index 0000000..b2128fa --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_runner.py @@ -0,0 +1,425 @@ +import asyncio +import signal +import socket +import warnings +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any + +from yarl import URL + +from .abc import AbstractAccessLogger +from .typedefs import PathLike +from .web_app import Application +from .web_log import AccessLogger +from .web_server import Server + +if TYPE_CHECKING: + from ssl import SSLContext +else: + try: + from ssl import SSLContext + except ImportError: # pragma: no cover + SSLContext = object # type: ignore[misc,assignment] + +__all__ = ( + "BaseSite", + "TCPSite", + "UnixSite", + "NamedPipeSite", + "SockSite", + "BaseRunner", + "AppRunner", + "ServerRunner", + "GracefulExit", +) + + +class GracefulExit(SystemExit): + code = 1 + + +def _raise_graceful_exit() -> None: + raise GracefulExit() + + +class BaseSite(ABC): + __slots__ = ("_runner", "_ssl_context", "_backlog", "_server") + + def __init__( + self, + runner: "BaseRunner", + *, + shutdown_timeout: float = 60.0, + ssl_context: SSLContext | None = None, + backlog: int = 128, + ) -> None: + if runner.server is None: + raise RuntimeError("Call runner.setup() before making a site") + if shutdown_timeout != 60.0: + msg = "shutdown_timeout should be set on BaseRunner" + warnings.warn(msg, DeprecationWarning, stacklevel=2) + runner._shutdown_timeout = shutdown_timeout + self._runner = runner + self._ssl_context = ssl_context + self._backlog = backlog + self._server: asyncio.AbstractServer | None = None + + @property + @abstractmethod + def name(self) -> str: + pass # pragma: no cover + + @abstractmethod + async def start(self) -> None: + self._runner._reg_site(self) + + async def stop(self) -> None: + self._runner._check_site(self) + if self._server is not None: # Maybe not started yet + self._server.close() + + self._runner._unreg_site(self) + + +class TCPSite(BaseSite): + __slots__ = ("_host", "_port", "_bound_port", "_reuse_address", "_reuse_port") + + def __init__( + self, + runner: "BaseRunner", + host: str | None = None, + port: int | None = None, + *, + shutdown_timeout: float = 60.0, + ssl_context: SSLContext | None = None, + backlog: int = 128, + reuse_address: bool | None = None, + reuse_port: bool | None = None, + ) -> None: + super().__init__( + runner, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + self._host = host + if port is None: + port = 8443 if self._ssl_context else 8080 + self._port = port + self._bound_port: int | None = None + self._reuse_address = reuse_address + self._reuse_port = reuse_port + + @property + def port(self) -> int: + """The port the server is listening on. + + If the server hasn't been started yet, this returns the requested port + (which might be 0 for a dynamic port). + After the server starts, it returns the actual bound port. This is + especially useful when port=0 was requested, as it allows retrieving the + dynamically assigned port after the site has started. + """ + if self._bound_port is not None: + return self._bound_port + return self._port + + @property + def name(self) -> str: + scheme = "https" if self._ssl_context else "http" + host = "0.0.0.0" if not self._host else self._host + return str(URL.build(scheme=scheme, host=host, port=self.port)) + + async def start(self) -> None: + await super().start() + loop = asyncio.get_event_loop() + server = self._runner.server + assert server is not None + self._server = await loop.create_server( + server, + self._host, + self._port, + ssl=self._ssl_context, + backlog=self._backlog, + reuse_address=self._reuse_address, + reuse_port=self._reuse_port, + ) + if self._server.sockets: + self._bound_port = self._server.sockets[0].getsockname()[1] + else: + self._bound_port = self._port + + +class UnixSite(BaseSite): + __slots__ = ("_path",) + + def __init__( + self, + runner: "BaseRunner", + path: PathLike, + *, + shutdown_timeout: float = 60.0, + ssl_context: SSLContext | None = None, + backlog: int = 128, + ) -> None: + super().__init__( + runner, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + self._path = path + + @property + def name(self) -> str: + scheme = "https" if self._ssl_context else "http" + return f"{scheme}://unix:{self._path}:" + + async def start(self) -> None: + await super().start() + loop = asyncio.get_event_loop() + server = self._runner.server + assert server is not None + self._server = await loop.create_unix_server( + server, + self._path, + ssl=self._ssl_context, + backlog=self._backlog, + ) + + +class NamedPipeSite(BaseSite): + __slots__ = ("_path",) + + def __init__( + self, runner: "BaseRunner", path: str, *, shutdown_timeout: float = 60.0 + ) -> None: + loop = asyncio.get_event_loop() + if not isinstance( + loop, asyncio.ProactorEventLoop # type: ignore[attr-defined] + ): + raise RuntimeError( + "Named Pipes only available in proactor loop under windows" + ) + super().__init__(runner, shutdown_timeout=shutdown_timeout) + self._path = path + + @property + def name(self) -> str: + return self._path + + async def start(self) -> None: + await super().start() + loop = asyncio.get_event_loop() + server = self._runner.server + assert server is not None + _server = await loop.start_serving_pipe( # type: ignore[attr-defined] + server, self._path + ) + self._server = _server[0] + + +class SockSite(BaseSite): + __slots__ = ("_sock", "_name") + + def __init__( + self, + runner: "BaseRunner", + sock: socket.socket, + *, + shutdown_timeout: float = 60.0, + ssl_context: SSLContext | None = None, + backlog: int = 128, + ) -> None: + super().__init__( + runner, + shutdown_timeout=shutdown_timeout, + ssl_context=ssl_context, + backlog=backlog, + ) + self._sock = sock + scheme = "https" if self._ssl_context else "http" + if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX: + name = f"{scheme}://unix:{sock.getsockname()}:" + else: + host, port = sock.getsockname()[:2] + name = str(URL.build(scheme=scheme, host=host, port=port)) + self._name = name + + @property + def name(self) -> str: + return self._name + + async def start(self) -> None: + await super().start() + loop = asyncio.get_event_loop() + server = self._runner.server + assert server is not None + self._server = await loop.create_server( + server, sock=self._sock, ssl=self._ssl_context, backlog=self._backlog + ) + + +class BaseRunner(ABC): + __slots__ = ("_handle_signals", "_kwargs", "_server", "_sites", "_shutdown_timeout") + + def __init__( + self, + *, + handle_signals: bool = False, + shutdown_timeout: float = 60.0, + **kwargs: Any, + ) -> None: + self._handle_signals = handle_signals + self._kwargs = kwargs + self._server: Server | None = None + self._sites: list[BaseSite] = [] + self._shutdown_timeout = shutdown_timeout + + @property + def server(self) -> Server | None: + return self._server + + @property + def addresses(self) -> list[Any]: + ret: list[Any] = [] + for site in self._sites: + server = site._server + if server is not None: + sockets = server.sockets # type: ignore[attr-defined] + if sockets is not None: + for sock in sockets: + ret.append(sock.getsockname()) + return ret + + @property + def sites(self) -> set[BaseSite]: + return set(self._sites) + + async def setup(self) -> None: + loop = asyncio.get_event_loop() + + if self._handle_signals: + try: + loop.add_signal_handler(signal.SIGINT, _raise_graceful_exit) + loop.add_signal_handler(signal.SIGTERM, _raise_graceful_exit) + except NotImplementedError: # pragma: no cover + # add_signal_handler is not implemented on Windows + pass + + self._server = await self._make_server() + + @abstractmethod + async def shutdown(self) -> None: + """Call any shutdown hooks to help server close gracefully.""" + + async def cleanup(self) -> None: + # The loop over sites is intentional, an exception on gather() + # leaves self._sites in unpredictable state. + # The loop guaranties that a site is either deleted on success or + # still present on failure + for site in list(self._sites): + await site.stop() + + if self._server: # If setup succeeded + # Yield to event loop to ensure incoming requests prior to stopping the sites + # have all started to be handled before we proceed to close idle connections. + await asyncio.sleep(0) + self._server.pre_shutdown() + await self.shutdown() + await self._server.shutdown(self._shutdown_timeout) + await self._cleanup_server() + + self._server = None + if self._handle_signals: + loop = asyncio.get_running_loop() + try: + loop.remove_signal_handler(signal.SIGINT) + loop.remove_signal_handler(signal.SIGTERM) + except NotImplementedError: # pragma: no cover + # remove_signal_handler is not implemented on Windows + pass + + @abstractmethod + async def _make_server(self) -> Server: + pass # pragma: no cover + + @abstractmethod + async def _cleanup_server(self) -> None: + pass # pragma: no cover + + def _reg_site(self, site: BaseSite) -> None: + if site in self._sites: + raise RuntimeError(f"Site {site} is already registered in runner {self}") + self._sites.append(site) + + def _check_site(self, site: BaseSite) -> None: + if site not in self._sites: + raise RuntimeError(f"Site {site} is not registered in runner {self}") + + def _unreg_site(self, site: BaseSite) -> None: + if site not in self._sites: + raise RuntimeError(f"Site {site} is not registered in runner {self}") + self._sites.remove(site) + + +class ServerRunner(BaseRunner): + """Low-level web server runner""" + + __slots__ = ("_web_server",) + + def __init__( + self, web_server: Server, *, handle_signals: bool = False, **kwargs: Any + ) -> None: + super().__init__(handle_signals=handle_signals, **kwargs) + self._web_server = web_server + + async def shutdown(self) -> None: + pass + + async def _make_server(self) -> Server: + return self._web_server + + async def _cleanup_server(self) -> None: + pass + + +class AppRunner(BaseRunner): + """Web Application runner""" + + __slots__ = ("_app",) + + def __init__( + self, + app: Application, + *, + handle_signals: bool = False, + access_log_class: type[AbstractAccessLogger] = AccessLogger, + **kwargs: Any, + ) -> None: + super().__init__(handle_signals=handle_signals, **kwargs) + if not isinstance(app, Application): + raise TypeError( + f"The first argument should be web.Application instance, got {app!r}" + ) + self._kwargs["access_log_class"] = access_log_class + self._app = app + + @property + def app(self) -> Application: + return self._app + + async def shutdown(self) -> None: + await self._app.shutdown() + + async def _make_server(self) -> Server: + loop = asyncio.get_event_loop() + self._app._set_loop(loop) + self._app.on_startup.freeze() + await self._app.startup() + self._app.freeze() + + return self._app._make_handler(loop=loop, **self._kwargs) + + async def _cleanup_server(self) -> None: + await self._app.cleanup() diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_server.py b/venv/lib/python3.11/site-packages/aiohttp/web_server.py new file mode 100644 index 0000000..97cf2a6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_server.py @@ -0,0 +1,91 @@ +"""Low level HTTP server.""" + +import asyncio +from typing import Any, Awaitable, Callable, Dict, List, Optional # noqa + +from .abc import AbstractStreamWriter +from .http_parser import RawRequestMessage +from .streams import StreamReader +from .web_protocol import RequestHandler, _RequestFactory, _RequestHandler +from .web_request import BaseRequest + +__all__ = ("Server",) + + +class Server: + def __init__( + self, + handler: _RequestHandler, + *, + request_factory: _RequestFactory | None = None, + handler_cancellation: bool = False, + loop: asyncio.AbstractEventLoop | None = None, + **kwargs: Any, + ) -> None: + self._loop = loop or asyncio.get_running_loop() + self._connections: dict[RequestHandler, asyncio.Transport] = {} + self._kwargs = kwargs + # requests_count is the number of requests being processed by the server + # for the lifetime of the server. + self.requests_count = 0 + self.request_handler = handler + self.request_factory = request_factory or self._make_request + self.handler_cancellation = handler_cancellation + + @property + def connections(self) -> list[RequestHandler]: + return list(self._connections.keys()) + + def connection_made( + self, handler: RequestHandler, transport: asyncio.Transport + ) -> None: + self._connections[handler] = transport + + def connection_lost( + self, handler: RequestHandler, exc: BaseException | None = None + ) -> None: + if handler in self._connections: + if handler._task_handler: + handler._task_handler.add_done_callback( + lambda f: self._connections.pop(handler, None) + ) + else: + del self._connections[handler] + + def _make_request( + self, + message: RawRequestMessage, + payload: StreamReader, + protocol: RequestHandler, + writer: AbstractStreamWriter, + task: "asyncio.Task[None]", + ) -> BaseRequest: + return BaseRequest(message, payload, protocol, writer, task, self._loop) + + def pre_shutdown(self) -> None: + for conn in self._connections: + conn.close() + + async def shutdown(self, timeout: float | None = None) -> None: + coros = (conn.shutdown(timeout) for conn in self._connections) + await asyncio.gather(*coros) + self._connections.clear() + + def __call__(self) -> RequestHandler: + try: + return RequestHandler(self, loop=self._loop, **self._kwargs) + except TypeError: + # Failsafe creation: remove all custom handler_args + kwargs = { + k: v + for k, v in self._kwargs.items() + if k in ["debug", "access_log_class"] + } + handler = RequestHandler(self, loop=self._loop, **kwargs) + handler.logger.warning( + "Failed to create request handler with custom kwargs %r, " + "falling back to filtered kwargs. This may indicate a " + "misconfiguration.", + self._kwargs, + ) + return handler diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_urldispatcher.py b/venv/lib/python3.11/site-packages/aiohttp/web_urldispatcher.py new file mode 100644 index 0000000..74351ee --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_urldispatcher.py @@ -0,0 +1,1281 @@ +import abc +import asyncio +import base64 +import functools +import hashlib +import html +import inspect +import keyword +import os +import platform +import re +import sys +import warnings +from collections.abc import ( + Awaitable, + Callable, + Container, + Generator, + Iterable, + Iterator, + Mapping, + Sized, +) +from functools import wraps +from pathlib import Path +from re import Pattern +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, Final, NoReturn, Optional, TypedDict, cast + +from yarl import URL, __version__ as yarl_version + +from . import hdrs +from .abc import AbstractMatchInfo, AbstractRouter, AbstractView +from .helpers import DEBUG, DEFAULT_CHUNK_SIZE +from .http import HttpVersion11 +from .typedefs import Handler, PathLike +from .web_exceptions import ( + HTTPException, + HTTPExpectationFailed, + HTTPForbidden, + HTTPMethodNotAllowed, + HTTPNotFound, +) +from .web_fileresponse import FileResponse +from .web_request import Request +from .web_response import Response, StreamResponse +from .web_routedef import AbstractRouteDef + +__all__ = ( + "UrlDispatcher", + "UrlMappingMatchInfo", + "AbstractResource", + "Resource", + "PlainResource", + "DynamicResource", + "AbstractRoute", + "ResourceRoute", + "StaticResource", + "View", +) + + +if TYPE_CHECKING: + from .web_app import Application + + BaseDict = dict[str, str] +else: + BaseDict = dict + +CIRCULAR_SYMLINK_ERROR = (RuntimeError,) if sys.version_info < (3, 13) else () + +YARL_VERSION: Final[tuple[int, ...]] = tuple(map(int, yarl_version.split(".")[:2])) + +HTTP_METHOD_RE: Final[Pattern[str]] = re.compile( + r"^[0-9A-Za-z!#\$%&'\*\+\-\.\^_`\|~]+$" +) +ROUTE_RE: Final[Pattern[str]] = re.compile( + r"(\{[_a-zA-Z][^{}]*(?:\{[^{}]*\}[^{}]*)*\})" +) +PATH_SEP: Final[str] = re.escape("/") + +IS_WINDOWS: Final[bool] = platform.system() == "Windows" + +_ExpectHandler = Callable[[Request], Awaitable[StreamResponse | None]] +_Resolve = tuple[Optional["UrlMappingMatchInfo"], set[str]] + +html_escape = functools.partial(html.escape, quote=True) + + +class _InfoDict(TypedDict, total=False): + path: str + + formatter: str + pattern: Pattern[str] + + directory: Path + prefix: str + routes: Mapping[str, "AbstractRoute"] + + app: "Application" + + domain: str + + rule: "AbstractRuleMatching" + + http_exception: HTTPException + + +class AbstractResource(Sized, Iterable["AbstractRoute"]): + def __init__(self, *, name: str | None = None) -> None: + self._name = name + + @property + def name(self) -> str | None: + return self._name + + @property + @abc.abstractmethod + def canonical(self) -> str: + """Exposes the resource's canonical path. + + For example '/foo/bar/{name}' + + """ + + @abc.abstractmethod # pragma: no branch + def url_for(self, **kwargs: str) -> URL: + """Construct url for resource with additional params.""" + + @abc.abstractmethod # pragma: no branch + async def resolve(self, request: Request) -> _Resolve: + """Resolve resource. + + Return (UrlMappingMatchInfo, allowed_methods) pair. + """ + + @abc.abstractmethod + def add_prefix(self, prefix: str) -> None: + """Add a prefix to processed URLs. + + Required for subapplications support. + """ + + @abc.abstractmethod + def get_info(self) -> _InfoDict: + """Return a dict with additional info useful for introspection""" + + def freeze(self) -> None: + pass + + @abc.abstractmethod + def raw_match(self, path: str) -> bool: + """Perform a raw match against path""" + + +class AbstractRoute(abc.ABC): + def __init__( + self, + method: str, + handler: Handler | type[AbstractView], + *, + expect_handler: _ExpectHandler | None = None, + resource: AbstractResource | None = None, + ) -> None: + + if expect_handler is None: + expect_handler = _default_expect_handler + + assert inspect.iscoroutinefunction(expect_handler) or ( + sys.version_info < (3, 14) and asyncio.iscoroutinefunction(expect_handler) + ), f"Coroutine is expected, got {expect_handler!r}" + + method = method.upper() + if not HTTP_METHOD_RE.match(method): + raise ValueError(f"{method} is not allowed HTTP method") + + assert callable(handler), handler + if inspect.iscoroutinefunction(handler) or ( + sys.version_info < (3, 14) and asyncio.iscoroutinefunction(handler) + ): + pass + elif inspect.isgeneratorfunction(handler): + if TYPE_CHECKING: + assert False + warnings.warn( + "Bare generators are deprecated, use @coroutine wrapper", + DeprecationWarning, + ) + elif isinstance(handler, type) and issubclass(handler, AbstractView): + pass + else: + warnings.warn( + "Bare functions are deprecated, use async ones", DeprecationWarning + ) + + @wraps(handler) + async def handler_wrapper(request: Request) -> StreamResponse: + result = old_handler(request) # type: ignore[call-arg] + if asyncio.iscoroutine(result): + result = await result + assert isinstance(result, StreamResponse) + return result + + old_handler = handler + handler = handler_wrapper + + self._method = method + self._handler = handler + self._expect_handler = expect_handler + self._resource = resource + + @property + def method(self) -> str: + return self._method + + @property + def handler(self) -> Handler: + return self._handler + + @property + @abc.abstractmethod + def name(self) -> str | None: + """Optional route's name, always equals to resource's name.""" + + @property + def resource(self) -> AbstractResource | None: + return self._resource + + @abc.abstractmethod + def get_info(self) -> _InfoDict: + """Return a dict with additional info useful for introspection""" + + @abc.abstractmethod # pragma: no branch + def url_for(self, *args: str, **kwargs: str) -> URL: + """Construct url for route with additional params.""" + + async def handle_expect_header(self, request: Request) -> StreamResponse | None: + return await self._expect_handler(request) + + +class UrlMappingMatchInfo(BaseDict, AbstractMatchInfo): + + __slots__ = ("_route", "_apps", "_current_app", "_frozen") + + def __init__(self, match_dict: dict[str, str], route: AbstractRoute) -> None: + super().__init__(match_dict) + self._route = route + self._apps: list[Application] = [] + self._current_app: Application | None = None + self._frozen = False + + @property + def handler(self) -> Handler: + return self._route.handler + + @property + def route(self) -> AbstractRoute: + return self._route + + @property + def expect_handler(self) -> _ExpectHandler: + return self._route.handle_expect_header + + @property + def http_exception(self) -> HTTPException | None: + return None + + def get_info(self) -> _InfoDict: # type: ignore[override] + return self._route.get_info() + + @property + def apps(self) -> tuple["Application", ...]: + return tuple(self._apps) + + def add_app(self, app: "Application") -> None: + if self._frozen: + raise RuntimeError("Cannot change apps stack after .freeze() call") + if self._current_app is None: + self._current_app = app + self._apps.insert(0, app) + + @property + def current_app(self) -> "Application": + app = self._current_app + assert app is not None + return app + + @current_app.setter + def current_app(self, app: "Application") -> None: + if DEBUG: # pragma: no cover + if app not in self._apps: + raise RuntimeError( + f"Expected one of the following apps {self._apps!r}, got {app!r}" + ) + self._current_app = app + + def freeze(self) -> None: + self._frozen = True + + def __repr__(self) -> str: + return f"" + + +class MatchInfoError(UrlMappingMatchInfo): + + __slots__ = ("_exception",) + + def __init__(self, http_exception: HTTPException) -> None: + self._exception = http_exception + super().__init__({}, SystemRoute(self._exception)) + + @property + def http_exception(self) -> HTTPException: + return self._exception + + def __repr__(self) -> str: + return f"" + + +async def _default_expect_handler(request: Request) -> None: + """Default handler for Expect header. + + Just send "100 Continue" to client. + raise HTTPExpectationFailed if value of header is not "100-continue" + """ + expect = request.headers.get(hdrs.EXPECT, "") + if request.version == HttpVersion11: + if expect.lower() == "100-continue": + await request.writer.write(b"HTTP/1.1 100 Continue\r\n\r\n") + # Reset output_size as we haven't started the main body yet. + request.writer.output_size = 0 + else: + raise HTTPExpectationFailed(text="Unknown Expect: %s" % expect) + + +class Resource(AbstractResource): + def __init__(self, *, name: str | None = None) -> None: + super().__init__(name=name) + self._routes: dict[str, ResourceRoute] = {} + self._any_route: ResourceRoute | None = None + self._allowed_methods: set[str] = set() + + def add_route( + self, + method: str, + handler: type[AbstractView] | Handler, + *, + expect_handler: _ExpectHandler | None = None, + ) -> "ResourceRoute": + if route := self._routes.get(method, self._any_route): + raise RuntimeError( + "Added route will never be executed, " + f"method {route.method} is already " + "registered" + ) + + route_obj = ResourceRoute(method, handler, self, expect_handler=expect_handler) + self.register_route(route_obj) + return route_obj + + def register_route(self, route: "ResourceRoute") -> None: + assert isinstance( + route, ResourceRoute + ), f"Instance of Route class is required, got {route!r}" + if route.method == hdrs.METH_ANY: + self._any_route = route + self._allowed_methods.add(route.method) + self._routes[route.method] = route + + async def resolve(self, request: Request) -> _Resolve: + if (match_dict := self._match(request.rel_url.path_safe)) is None: + return None, set() + if route := self._routes.get(request.method, self._any_route): + return UrlMappingMatchInfo(match_dict, route), self._allowed_methods + return None, self._allowed_methods + + @abc.abstractmethod + def _match(self, path: str) -> dict[str, str] | None: + pass # pragma: no cover + + def __len__(self) -> int: + return len(self._routes) + + def __iter__(self) -> Iterator["ResourceRoute"]: + return iter(self._routes.values()) + + # TODO: implement all abstract methods + + +class PlainResource(Resource): + def __init__(self, path: str, *, name: str | None = None) -> None: + super().__init__(name=name) + assert not path or path.startswith("/") + self._path = path + + @property + def canonical(self) -> str: + return self._path + + def freeze(self) -> None: + if not self._path: + self._path = "/" + + def add_prefix(self, prefix: str) -> None: + assert prefix.startswith("/") + assert not prefix.endswith("/") + assert len(prefix) > 1 + self._path = prefix + self._path + + def _match(self, path: str) -> dict[str, str] | None: + # string comparison is about 10 times faster than regexp matching + if self._path == path: + return {} + return None + + def raw_match(self, path: str) -> bool: + return self._path == path + + def get_info(self) -> _InfoDict: + return {"path": self._path} + + def url_for(self) -> URL: # type: ignore[override] + return URL.build(path=self._path, encoded=True) + + def __repr__(self) -> str: + name = "'" + self.name + "' " if self.name is not None else "" + return f"" + + +class DynamicResource(Resource): + + DYN = re.compile(r"\{(?P[_a-zA-Z][_a-zA-Z0-9]*)\}") + DYN_WITH_RE = re.compile(r"\{(?P[_a-zA-Z][_a-zA-Z0-9]*):(?P.+)\}") + GOOD = r"[^{}/]+" + + def __init__(self, path: str, *, name: str | None = None) -> None: + super().__init__(name=name) + self._orig_path = path + pattern = "" + formatter = "" + for part in ROUTE_RE.split(path): + match = self.DYN.fullmatch(part) + if match: + pattern += "(?P<{}>{})".format(match.group("var"), self.GOOD) + formatter += "{" + match.group("var") + "}" + continue + + match = self.DYN_WITH_RE.fullmatch(part) + if match: + pattern += "(?P<{var}>{re})".format(**match.groupdict()) + formatter += "{" + match.group("var") + "}" + continue + + if "{" in part or "}" in part: + raise ValueError(f"Invalid path '{path}'['{part}']") + + part = _requote_path(part) + formatter += part + pattern += re.escape(part) + + try: + compiled = re.compile(pattern) + except re.error as exc: + raise ValueError(f"Bad pattern '{pattern}': {exc}") from None + assert compiled.pattern.startswith(PATH_SEP) + assert formatter.startswith("/") + self._pattern = compiled + self._formatter = formatter + + @property + def canonical(self) -> str: + return self._formatter + + def add_prefix(self, prefix: str) -> None: + assert prefix.startswith("/") + assert not prefix.endswith("/") + assert len(prefix) > 1 + self._pattern = re.compile(re.escape(prefix) + self._pattern.pattern) + self._formatter = prefix + self._formatter + + def _match(self, path: str) -> dict[str, str] | None: + match = self._pattern.fullmatch(path) + if match is None: + return None + return { + key: _unquote_path_safe(value) for key, value in match.groupdict().items() + } + + def raw_match(self, path: str) -> bool: + return self._orig_path == path + + def get_info(self) -> _InfoDict: + return {"formatter": self._formatter, "pattern": self._pattern} + + def url_for(self, **parts: str) -> URL: + url = self._formatter.format_map({k: _quote_path(v) for k, v in parts.items()}) + return URL.build(path=url, encoded=True) + + def __repr__(self) -> str: + name = "'" + self.name + "' " if self.name is not None else "" + return f"" + + +class PrefixResource(AbstractResource): + def __init__(self, prefix: str, *, name: str | None = None) -> None: + assert not prefix or prefix.startswith("/"), prefix + assert prefix in ("", "/") or not prefix.endswith("/"), prefix + super().__init__(name=name) + self._prefix = _requote_path(prefix) + self._prefix2 = self._prefix + "/" + + @property + def canonical(self) -> str: + return self._prefix + + def add_prefix(self, prefix: str) -> None: + assert prefix.startswith("/") + assert not prefix.endswith("/") + assert len(prefix) > 1 + self._prefix = prefix + self._prefix + self._prefix2 = self._prefix + "/" + + def raw_match(self, prefix: str) -> bool: + return False + + # TODO: impl missing abstract methods + + +class StaticResource(PrefixResource): + VERSION_KEY = "v" + + def __init__( + self, + prefix: str, + directory: PathLike, + *, + name: str | None = None, + expect_handler: _ExpectHandler | None = None, + chunk_size: int = DEFAULT_CHUNK_SIZE, + show_index: bool = False, + follow_symlinks: bool = False, + append_version: bool = False, + ) -> None: + super().__init__(prefix, name=name) + try: + directory = Path(directory).expanduser().resolve(strict=True) + except FileNotFoundError as error: + raise ValueError(f"'{directory}' does not exist") from error + if not directory.is_dir(): + raise ValueError(f"'{directory}' is not a directory") + self._directory = directory + self._show_index = show_index + self._chunk_size = chunk_size + self._follow_symlinks = follow_symlinks + self._expect_handler = expect_handler + self._append_version = append_version + + self._routes = { + "GET": ResourceRoute( + "GET", self._handle, self, expect_handler=expect_handler + ), + "HEAD": ResourceRoute( + "HEAD", self._handle, self, expect_handler=expect_handler + ), + } + self._allowed_methods = set(self._routes) + + def url_for( # type: ignore[override] + self, + *, + filename: PathLike, + append_version: bool | None = None, + ) -> URL: + if append_version is None: + append_version = self._append_version + filename = str(filename).lstrip("/") + + url = URL.build(path=self._prefix, encoded=True) + # filename is not encoded + if YARL_VERSION < (1, 6): + url = url / filename.replace("%", "%25") + else: + url = url / filename + + if append_version: + unresolved_path = self._directory.joinpath(filename) + try: + if self._follow_symlinks: + normalized_path = Path(os.path.normpath(unresolved_path)) + normalized_path.relative_to(self._directory) + filepath = normalized_path.resolve() + else: + filepath = unresolved_path.resolve() + filepath.relative_to(self._directory) + except (ValueError, FileNotFoundError): + # ValueError for case when path point to symlink + # with follow_symlinks is False + return url # relatively safe + if filepath.is_file(): + # TODO cache file content + # with file watcher for cache invalidation + with filepath.open("rb") as f: + file_bytes = f.read() + h = self._get_file_hash(file_bytes) + url = url.with_query({self.VERSION_KEY: h}) + return url + return url + + @staticmethod + def _get_file_hash(byte_array: bytes) -> str: + m = hashlib.sha256() # todo sha256 can be configurable param + m.update(byte_array) + b64 = base64.urlsafe_b64encode(m.digest()) + return b64.decode("ascii") + + def get_info(self) -> _InfoDict: + return { + "directory": self._directory, + "prefix": self._prefix, + "routes": self._routes, + } + + def set_options_route(self, handler: Handler) -> None: + if "OPTIONS" in self._routes: + raise RuntimeError("OPTIONS route was set already") + self._routes["OPTIONS"] = ResourceRoute( + "OPTIONS", handler, self, expect_handler=self._expect_handler + ) + self._allowed_methods.add("OPTIONS") + + async def resolve(self, request: Request) -> _Resolve: + path = request.rel_url.path_safe + method = request.method + # We normalise here to avoid matches that traverse below the static root. + # e.g. /static/../../../../home/user/webapp/static/ + norm_path = os.path.normpath(path) + if IS_WINDOWS: + norm_path = norm_path.replace("\\", "/") + if not norm_path.startswith(self._prefix2) and norm_path != self._prefix: + return None, set() + + allowed_methods = self._allowed_methods + if method not in allowed_methods: + return None, allowed_methods + + match_dict = {"filename": _unquote_path_safe(path[len(self._prefix) + 1 :])} + return (UrlMappingMatchInfo(match_dict, self._routes[method]), allowed_methods) + + def __len__(self) -> int: + return len(self._routes) + + def __iter__(self) -> Iterator[AbstractRoute]: + return iter(self._routes.values()) + + async def _handle(self, request: Request) -> StreamResponse: + filename = request.match_info["filename"] + if Path(filename).is_absolute(): + # filename is an absolute path e.g. //network/share or D:\path + # which could be a UNC path leading to NTLM credential theft + raise HTTPNotFound() + unresolved_path = self._directory.joinpath(filename) + loop = asyncio.get_running_loop() + return await loop.run_in_executor( + None, self._resolve_path_to_response, unresolved_path + ) + + def _resolve_path_to_response(self, unresolved_path: Path) -> StreamResponse: + """Take the unresolved path and query the file system to form a response.""" + # Check for access outside the root directory. For follow symlinks, URI + # cannot traverse out, but symlinks can. Otherwise, no access outside + # root is permitted. + try: + if self._follow_symlinks: + normalized_path = Path(os.path.normpath(unresolved_path)) + normalized_path.relative_to(self._directory) + file_path = normalized_path.resolve() + else: + file_path = unresolved_path.resolve() + file_path.relative_to(self._directory) + except (ValueError, *CIRCULAR_SYMLINK_ERROR) as error: + # ValueError is raised for the relative check. Circular symlinks + # raise here on resolving for python < 3.13. + raise HTTPNotFound() from error + + # if path is a directory, return the contents if permitted. Note the + # directory check will raise if a segment is not readable. + try: + if file_path.is_dir(): + if self._show_index: + return Response( + text=self._directory_as_html(file_path), + content_type="text/html", + ) + else: + raise HTTPForbidden() + except PermissionError as error: + raise HTTPForbidden() from error + + # Return the file response, which handles all other checks. + return FileResponse(file_path, chunk_size=self._chunk_size) + + def _directory_as_html(self, dir_path: Path) -> str: + """returns directory's index as html.""" + assert dir_path.is_dir() + + relative_path_to_dir = dir_path.relative_to(self._directory).as_posix() + index_of = f"Index of /{html_escape(relative_path_to_dir)}" + h1 = f"

{index_of}

" + + index_list = [] + dir_index = dir_path.iterdir() + for _file in sorted(dir_index): + # show file url as relative to static path + rel_path = _file.relative_to(self._directory).as_posix() + quoted_file_url = _quote_path(f"{self._prefix}/{rel_path}") + + # if file is a directory, add '/' to the end of the name + if _file.is_dir(): + file_name = f"{_file.name}/" + else: + file_name = _file.name + + index_list.append( + f'
  • {html_escape(file_name)}
  • ' + ) + ul = "
      \n{}\n
    ".format("\n".join(index_list)) + body = f"\n{h1}\n{ul}\n" + + head_str = f"\n{index_of}\n" + html = f"\n{head_str}\n{body}\n" + + return html + + def __repr__(self) -> str: + name = "'" + self.name + "'" if self.name is not None else "" + return f" {self._directory!r}>" + + +class PrefixedSubAppResource(PrefixResource): + def __init__(self, prefix: str, app: "Application") -> None: + super().__init__(prefix) + self._app = app + self._add_prefix_to_resources(prefix) + + def add_prefix(self, prefix: str) -> None: + super().add_prefix(prefix) + self._add_prefix_to_resources(prefix) + + def _add_prefix_to_resources(self, prefix: str) -> None: + router = self._app.router + for resource in router.resources(): + # Since the canonical path of a resource is about + # to change, we need to unindex it and then reindex + router.unindex_resource(resource) + resource.add_prefix(prefix) + router.index_resource(resource) + + def url_for(self, *args: str, **kwargs: str) -> URL: + raise RuntimeError(".url_for() is not supported by sub-application root") + + def get_info(self) -> _InfoDict: + return {"app": self._app, "prefix": self._prefix} + + async def resolve(self, request: Request) -> _Resolve: + match_info = await self._app.router.resolve(request) + match_info.add_app(self._app) + if isinstance(match_info.http_exception, HTTPMethodNotAllowed): + methods = match_info.http_exception.allowed_methods + else: + methods = set() + return match_info, methods + + def __len__(self) -> int: + return len(self._app.router.routes()) + + def __iter__(self) -> Iterator[AbstractRoute]: + return iter(self._app.router.routes()) + + def __repr__(self) -> str: + return f" {self._app!r}>" + + +class AbstractRuleMatching(abc.ABC): + @abc.abstractmethod # pragma: no branch + async def match(self, request: Request) -> bool: + """Return bool if the request satisfies the criteria""" + + @abc.abstractmethod # pragma: no branch + def get_info(self) -> _InfoDict: + """Return a dict with additional info useful for introspection""" + + @property + @abc.abstractmethod # pragma: no branch + def canonical(self) -> str: + """Return a str""" + + +class Domain(AbstractRuleMatching): + re_part = re.compile(r"(?!-)[a-z\d-]{1,63}(? None: + super().__init__() + self._domain = self.validation(domain) + + @property + def canonical(self) -> str: + return self._domain + + def validation(self, domain: str) -> str: + if not isinstance(domain, str): + raise TypeError("Domain must be str") + domain = domain.rstrip(".").lower() + if not domain: + raise ValueError("Domain cannot be empty") + elif "://" in domain: + raise ValueError("Scheme not supported") + url = URL("http://" + domain) + assert url.raw_host is not None + if not all(self.re_part.fullmatch(x) for x in url.raw_host.split(".")): + raise ValueError("Domain not valid") + if url.port == 80: + return url.raw_host + return f"{url.raw_host}:{url.port}" + + async def match(self, request: Request) -> bool: + host = request.headers.get(hdrs.HOST) + if not host: + return False + return self.match_domain(host) + + def match_domain(self, host: str) -> bool: + return host.lower() == self._domain + + def get_info(self) -> _InfoDict: + return {"domain": self._domain} + + +class MaskDomain(Domain): + re_part = re.compile(r"(?!-)[a-z\d\*-]{1,63}(? None: + super().__init__(domain) + mask = self._domain.replace(".", r"\.").replace("*", ".*") + self._mask = re.compile(mask) + + @property + def canonical(self) -> str: + return self._mask.pattern + + def match_domain(self, host: str) -> bool: + return self._mask.fullmatch(host) is not None + + +class MatchedSubAppResource(PrefixedSubAppResource): + def __init__(self, rule: AbstractRuleMatching, app: "Application") -> None: + AbstractResource.__init__(self) + self._prefix = "" + self._app = app + self._rule = rule + + @property + def canonical(self) -> str: + return self._rule.canonical + + def get_info(self) -> _InfoDict: + return {"app": self._app, "rule": self._rule} + + async def resolve(self, request: Request) -> _Resolve: + if not await self._rule.match(request): + return None, set() + match_info = await self._app.router.resolve(request) + match_info.add_app(self._app) + if isinstance(match_info.http_exception, HTTPMethodNotAllowed): + methods = match_info.http_exception.allowed_methods + else: + methods = set() + return match_info, methods + + def __repr__(self) -> str: + return f" {self._app!r}>" + + +class ResourceRoute(AbstractRoute): + """A route with resource""" + + def __init__( + self, + method: str, + handler: Handler | type[AbstractView], + resource: AbstractResource, + *, + expect_handler: _ExpectHandler | None = None, + ) -> None: + super().__init__( + method, handler, expect_handler=expect_handler, resource=resource + ) + + def __repr__(self) -> str: + return f" {self.handler!r}" + + @property + def name(self) -> str | None: + if self._resource is None: + return None + return self._resource.name + + def url_for(self, *args: str, **kwargs: str) -> URL: + """Construct url for route with additional params.""" + assert self._resource is not None + return self._resource.url_for(*args, **kwargs) + + def get_info(self) -> _InfoDict: + assert self._resource is not None + return self._resource.get_info() + + +class SystemRoute(AbstractRoute): + def __init__(self, http_exception: HTTPException) -> None: + super().__init__(hdrs.METH_ANY, self._handle) + self._http_exception = http_exception + + def url_for(self, *args: str, **kwargs: str) -> URL: + raise RuntimeError(".url_for() is not allowed for SystemRoute") + + @property + def name(self) -> str | None: + return None + + def get_info(self) -> _InfoDict: + return {"http_exception": self._http_exception} + + async def _handle(self, request: Request) -> StreamResponse: + raise self._http_exception + + @property + def status(self) -> int: + return self._http_exception.status + + @property + def reason(self) -> str: + return self._http_exception.reason + + def __repr__(self) -> str: + return f"" + + +class View(AbstractView): + async def _iter(self) -> StreamResponse: + if self.request.method not in hdrs.METH_ALL: + self._raise_allowed_methods() + method: Callable[[], Awaitable[StreamResponse]] | None + method = getattr(self, self.request.method.lower(), None) + if method is None: + self._raise_allowed_methods() + ret = await method() + assert isinstance(ret, StreamResponse) + return ret + + def __await__(self) -> Generator[None, None, StreamResponse]: + return self._iter().__await__() + + def _raise_allowed_methods(self) -> NoReturn: + allowed_methods = {m for m in hdrs.METH_ALL if hasattr(self, m.lower())} + raise HTTPMethodNotAllowed(self.request.method, allowed_methods) + + +class ResourcesView(Sized, Iterable[AbstractResource], Container[AbstractResource]): + def __init__(self, resources: list[AbstractResource]) -> None: + self._resources = resources + + def __len__(self) -> int: + return len(self._resources) + + def __iter__(self) -> Iterator[AbstractResource]: + yield from self._resources + + def __contains__(self, resource: object) -> bool: + return resource in self._resources + + +class RoutesView(Sized, Iterable[AbstractRoute], Container[AbstractRoute]): + def __init__(self, resources: list[AbstractResource]): + self._routes: list[AbstractRoute] = [] + for resource in resources: + for route in resource: + self._routes.append(route) + + def __len__(self) -> int: + return len(self._routes) + + def __iter__(self) -> Iterator[AbstractRoute]: + yield from self._routes + + def __contains__(self, route: object) -> bool: + return route in self._routes + + +class UrlDispatcher(AbstractRouter, Mapping[str, AbstractResource]): + + NAME_SPLIT_RE = re.compile(r"[.:-]") + + def __init__(self) -> None: + super().__init__() + self._resources: list[AbstractResource] = [] + self._named_resources: dict[str, AbstractResource] = {} + self._resource_index: dict[str, list[AbstractResource]] = {} + self._matched_sub_app_resources: list[MatchedSubAppResource] = [] + + async def resolve(self, request: Request) -> UrlMappingMatchInfo: + resource_index = self._resource_index + allowed_methods: set[str] = set() + + # MatchedSubAppResource is primarily used to match on domain names + # (though custom rules could match on other things). This means that + # the traversal algorithm below can't be applied, and that we likely + # need to check these first so a sub app that defines the same path + # as a parent app will get priority if there's a domain match. + # + # For most cases we do not expect there to be many of these since + # currently they are only added by `.add_domain()`. + for resource in self._matched_sub_app_resources: + match_dict, allowed = await resource.resolve(request) + if match_dict is not None: + return match_dict + else: + allowed_methods |= allowed + + # Walk the url parts looking for candidates. We walk the url backwards + # to ensure the most explicit match is found first. If there are multiple + # candidates for a given url part because there are multiple resources + # registered for the same canonical path, we resolve them in a linear + # fashion to ensure registration order is respected. + url_part = request.rel_url.path_safe + while url_part: + for candidate in resource_index.get(url_part, ()): + match_dict, allowed = await candidate.resolve(request) + if match_dict is not None: + return match_dict + else: + allowed_methods |= allowed + if url_part == "/": + break + url_part = url_part.rpartition("/")[0] or "/" + + if allowed_methods: + return MatchInfoError(HTTPMethodNotAllowed(request.method, allowed_methods)) + + return MatchInfoError(HTTPNotFound()) + + def __iter__(self) -> Iterator[str]: + return iter(self._named_resources) + + def __len__(self) -> int: + return len(self._named_resources) + + def __contains__(self, resource: object) -> bool: + return resource in self._named_resources + + def __getitem__(self, name: str) -> AbstractResource: + return self._named_resources[name] + + def resources(self) -> ResourcesView: + return ResourcesView(self._resources) + + def routes(self) -> RoutesView: + return RoutesView(self._resources) + + def named_resources(self) -> Mapping[str, AbstractResource]: + return MappingProxyType(self._named_resources) + + def register_resource(self, resource: AbstractResource) -> None: + assert isinstance( + resource, AbstractResource + ), f"Instance of AbstractResource class is required, got {resource!r}" + if self.frozen: + raise RuntimeError("Cannot register a resource into frozen router.") + + name = resource.name + + if name is not None: + parts = self.NAME_SPLIT_RE.split(name) + for part in parts: + if keyword.iskeyword(part): + raise ValueError( + f"Incorrect route name {name!r}, " + "python keywords cannot be used " + "for route name" + ) + if not part.isidentifier(): + raise ValueError( + f"Incorrect route name {name!r}, " + "the name should be a sequence of " + "python identifiers separated " + "by dash, dot or column" + ) + if name in self._named_resources: + raise ValueError( + f"Duplicate {name!r}, " + f"already handled by {self._named_resources[name]!r}" + ) + self._named_resources[name] = resource + self._resources.append(resource) + + if isinstance(resource, MatchedSubAppResource): + # We cannot index match sub-app resources because they have match rules + self._matched_sub_app_resources.append(resource) + else: + self.index_resource(resource) + + def _get_resource_index_key(self, resource: AbstractResource) -> str: + """Return a key to index the resource in the resource index.""" + if "{" in (index_key := resource.canonical): + # strip at the first { to allow for variables, and than + # rpartition at / to allow for variable parts in the path + # For example if the canonical path is `/core/locations{tail:.*}` + # the index key will be `/core` since index is based on the + # url parts split by `/` + index_key = index_key.partition("{")[0].rpartition("/")[0] + return index_key.rstrip("/") or "/" + + def index_resource(self, resource: AbstractResource) -> None: + """Add a resource to the resource index.""" + resource_key = self._get_resource_index_key(resource) + # There may be multiple resources for a canonical path + # so we keep them in a list to ensure that registration + # order is respected. + self._resource_index.setdefault(resource_key, []).append(resource) + + def unindex_resource(self, resource: AbstractResource) -> None: + """Remove a resource from the resource index.""" + resource_key = self._get_resource_index_key(resource) + self._resource_index[resource_key].remove(resource) + + def add_resource(self, path: str, *, name: str | None = None) -> Resource: + if path and not path.startswith("/"): + raise ValueError("path should be started with / or be empty") + # Reuse last added resource if path and name are the same + if self._resources: + resource = self._resources[-1] + if resource.name == name and resource.raw_match(path): + return cast(Resource, resource) + if not ("{" in path or "}" in path or ROUTE_RE.search(path)): + resource = PlainResource(path, name=name) + self.register_resource(resource) + return resource + resource = DynamicResource(path, name=name) + self.register_resource(resource) + return resource + + def add_route( + self, + method: str, + path: str, + handler: Handler | type[AbstractView], + *, + name: str | None = None, + expect_handler: _ExpectHandler | None = None, + ) -> AbstractRoute: + resource = self.add_resource(path, name=name) + return resource.add_route(method, handler, expect_handler=expect_handler) + + def add_static( + self, + prefix: str, + path: PathLike, + *, + name: str | None = None, + expect_handler: _ExpectHandler | None = None, + chunk_size: int = DEFAULT_CHUNK_SIZE, + show_index: bool = False, + follow_symlinks: bool = False, + append_version: bool = False, + ) -> AbstractResource: + """Add static files view. + + prefix - url prefix + path - folder with files + + """ + assert prefix.startswith("/") + if prefix.endswith("/"): + prefix = prefix[:-1] + resource = StaticResource( + prefix, + path, + name=name, + expect_handler=expect_handler, + chunk_size=chunk_size, + show_index=show_index, + follow_symlinks=follow_symlinks, + append_version=append_version, + ) + self.register_resource(resource) + return resource + + def add_head(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method HEAD.""" + return self.add_route(hdrs.METH_HEAD, path, handler, **kwargs) + + def add_options(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method OPTIONS.""" + return self.add_route(hdrs.METH_OPTIONS, path, handler, **kwargs) + + def add_get( + self, + path: str, + handler: Handler, + *, + name: str | None = None, + allow_head: bool = True, + **kwargs: Any, + ) -> AbstractRoute: + """Shortcut for add_route with method GET. + + If allow_head is true, another + route is added allowing head requests to the same endpoint. + """ + resource = self.add_resource(path, name=name) + if allow_head: + resource.add_route(hdrs.METH_HEAD, handler, **kwargs) + return resource.add_route(hdrs.METH_GET, handler, **kwargs) + + def add_post(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method POST.""" + return self.add_route(hdrs.METH_POST, path, handler, **kwargs) + + def add_put(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method PUT.""" + return self.add_route(hdrs.METH_PUT, path, handler, **kwargs) + + def add_patch(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method PATCH.""" + return self.add_route(hdrs.METH_PATCH, path, handler, **kwargs) + + def add_delete(self, path: str, handler: Handler, **kwargs: Any) -> AbstractRoute: + """Shortcut for add_route with method DELETE.""" + return self.add_route(hdrs.METH_DELETE, path, handler, **kwargs) + + def add_view( + self, path: str, handler: type[AbstractView], **kwargs: Any + ) -> AbstractRoute: + """Shortcut for add_route with ANY methods for a class-based view.""" + return self.add_route(hdrs.METH_ANY, path, handler, **kwargs) + + def freeze(self) -> None: + super().freeze() + for resource in self._resources: + resource.freeze() + + def add_routes(self, routes: Iterable[AbstractRouteDef]) -> list[AbstractRoute]: + """Append routes to route table. + + Parameter should be a sequence of RouteDef objects. + + Returns a list of registered AbstractRoute instances. + """ + registered_routes = [] + for route_def in routes: + registered_routes.extend(route_def.register(self)) + return registered_routes + + +def _quote_path(value: str) -> str: + if YARL_VERSION < (1, 6): + value = value.replace("%", "%25") + return URL.build(path=value, encoded=False).raw_path + + +def _unquote_path_safe(value: str) -> str: + if "%" not in value: + return value + return value.replace("%2F", "/").replace("%25", "%") + + +def _requote_path(value: str) -> str: + # Quote non-ascii characters and other characters which must be quoted, + # but preserve existing %-sequences. + result = _quote_path(value) + if "%" in value: + result = result.replace("%25", "%") + return result diff --git a/venv/lib/python3.11/site-packages/aiohttp/web_ws.py b/venv/lib/python3.11/site-packages/aiohttp/web_ws.py new file mode 100644 index 0000000..f4cef84 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/web_ws.py @@ -0,0 +1,783 @@ +import asyncio +import base64 +import binascii +import hashlib +import json +import sys +from collections.abc import Callable, Iterable +from typing import Any, Final, Generic, Literal, cast, overload + +import attr +from multidict import CIMultiDict + +from . import hdrs +from ._websocket.reader import WebSocketDataQueue +from .abc import AbstractStreamWriter +from .client_exceptions import WSMessageTypeError +from .helpers import ( + DEFAULT_CHUNK_SIZE, + calculate_timeout_when, + set_exception, + set_result, +) +from .http import ( + WS_CLOSED_MESSAGE, + WS_CLOSING_MESSAGE, + WS_KEY, + WebSocketError, + WebSocketReader, + WebSocketWriter, + WSCloseCode, + WSMessage, + WSMessageDecodeText, + WSMessageNoDecodeText, + WSMsgType as WSMsgType, + ws_ext_gen, + ws_ext_parse, +) +from .http_websocket import _INTERNAL_RECEIVE_TYPES +from .log import ws_logger +from .streams import EofStream +from .typedefs import JSONBytesEncoder, JSONDecoder, JSONEncoder +from .web_exceptions import HTTPBadRequest, HTTPException +from .web_request import BaseRequest +from .web_response import StreamResponse + +if sys.version_info >= (3, 13): + from typing import TypeVar +else: + from typing_extensions import TypeVar + +if sys.version_info >= (3, 12): + from collections.abc import Buffer +else: + from typing import Union + + Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"] + +if sys.version_info >= (3, 11): + import asyncio as async_timeout + from typing import Self +else: + import async_timeout + from typing_extensions import Self + +__all__ = ( + "WebSocketResponse", + "WebSocketReady", + "WSMsgType", +) + +THRESHOLD_CONNLOST_ACCESS: Final[int] = 5 + +# TypeVar for whether text messages are decoded to str (True) or kept as bytes (False) +_DecodeText = TypeVar("_DecodeText", bound=bool, covariant=True, default=Literal[True]) + + +@attr.s(auto_attribs=True, frozen=True, slots=True) +class WebSocketReady: + ok: bool + protocol: str | None + + def __bool__(self) -> bool: + return self.ok + + +class WebSocketResponse(StreamResponse, Generic[_DecodeText]): + + _length_check: bool = False + _ws_protocol: str | None = None + _writer: WebSocketWriter | None = None + _reader: WebSocketDataQueue | None = None + _closed: bool = False + _closing: bool = False + _conn_lost: int = 0 + _close_code: int | None = None + _loop: asyncio.AbstractEventLoop | None = None + _waiting: bool = False + _close_wait: asyncio.Future[None] | None = None + _exception: BaseException | None = None + _heartbeat_when: float = 0.0 + _heartbeat_cb: asyncio.TimerHandle | None = None + _pong_response_cb: asyncio.TimerHandle | None = None + _ping_task: asyncio.Task[None] | None = None + _need_heartbeat_reset: bool = False + _heartbeat_reset_handle: asyncio.Handle | None = None + + def __init__( + self, + *, + timeout: float = 10.0, + receive_timeout: float | None = None, + autoclose: bool = True, + autoping: bool = True, + heartbeat: float | None = None, + protocols: Iterable[str] = (), + compress: bool = True, + max_msg_size: int = 4 * 1024 * 1024, + writer_limit: int = DEFAULT_CHUNK_SIZE, + decode_text: bool = True, + ) -> None: + super().__init__(status=101) + self._protocols = protocols + self._timeout = timeout + self._receive_timeout = receive_timeout + self._autoclose = autoclose + self._autoping = autoping + self._heartbeat = heartbeat + if heartbeat is not None: + self._pong_heartbeat = heartbeat / 2.0 + self._compress: bool | int = compress + self._max_msg_size = max_msg_size + self._writer_limit = writer_limit + self._decode_text = decode_text + self._need_heartbeat_reset = False + self._heartbeat_reset_handle = None + + def _cancel_heartbeat(self) -> None: + self._cancel_pong_response_cb() + if self._heartbeat_reset_handle is not None: + self._heartbeat_reset_handle.cancel() + self._heartbeat_reset_handle = None + self._need_heartbeat_reset = False + if self._heartbeat_cb is not None: + self._heartbeat_cb.cancel() + self._heartbeat_cb = None + if self._ping_task is not None: + self._ping_task.cancel() + self._ping_task = None + + def _cancel_pong_response_cb(self) -> None: + if self._pong_response_cb is not None: + self._pong_response_cb.cancel() + self._pong_response_cb = None + + def _on_data_received(self) -> None: + if self._heartbeat is None or self._need_heartbeat_reset: + return + loop = self._loop + assert loop is not None + # Coalesce multiple chunks received in the same loop tick into a single + # heartbeat reset. Resetting immediately per chunk increases timer churn. + self._need_heartbeat_reset = True + self._heartbeat_reset_handle = loop.call_soon(self._flush_heartbeat_reset) + + def _flush_heartbeat_reset(self) -> None: + self._heartbeat_reset_handle = None + if not self._need_heartbeat_reset: + return + self._reset_heartbeat() + self._need_heartbeat_reset = False + + def _reset_heartbeat(self) -> None: + if self._heartbeat is None: + return + self._cancel_pong_response_cb() + req = self._req + timeout_ceil_threshold = ( + req._protocol._timeout_ceil_threshold if req is not None else 5 + ) + loop = self._loop + assert loop is not None + now = loop.time() + when = calculate_timeout_when(now, self._heartbeat, timeout_ceil_threshold) + self._heartbeat_when = when + if self._heartbeat_cb is None: + # We do not cancel the previous heartbeat_cb here because + # it generates a significant amount of TimerHandle churn + # which causes asyncio to rebuild the heap frequently. + # Instead _send_heartbeat() will reschedule the next + # heartbeat if it fires too early. + self._heartbeat_cb = loop.call_at(when, self._send_heartbeat) + + def _send_heartbeat(self) -> None: + self._heartbeat_cb = None + + # If heartbeat reset is pending (data is being received), skip sending + # the ping and let the reset callback handle rescheduling the heartbeat. + if self._need_heartbeat_reset: + return + + loop = self._loop + assert loop is not None and self._writer is not None + now = loop.time() + if now < self._heartbeat_when: + # Heartbeat fired too early, reschedule + self._heartbeat_cb = loop.call_at( + self._heartbeat_when, self._send_heartbeat + ) + return + + req = self._req + timeout_ceil_threshold = ( + req._protocol._timeout_ceil_threshold if req is not None else 5 + ) + when = calculate_timeout_when(now, self._pong_heartbeat, timeout_ceil_threshold) + self._cancel_pong_response_cb() + self._pong_response_cb = loop.call_at(when, self._pong_not_received) + + coro = self._writer.send_frame(b"", WSMsgType.PING) + if sys.version_info >= (3, 12): + # Optimization for Python 3.12, try to send the ping + # immediately to avoid having to schedule + # the task on the event loop. + ping_task = asyncio.Task(coro, loop=loop, eager_start=True) + else: + ping_task = loop.create_task(coro) + + if not ping_task.done(): + self._ping_task = ping_task + ping_task.add_done_callback(self._ping_task_done) + else: + self._ping_task_done(ping_task) + + def _ping_task_done(self, task: "asyncio.Task[None]") -> None: + """Callback for when the ping task completes.""" + if not task.cancelled() and (exc := task.exception()): + self._handle_ping_pong_exception(exc) + self._ping_task = None + + def _pong_not_received(self) -> None: + if self._req is not None and self._req.transport is not None: + self._handle_ping_pong_exception( + asyncio.TimeoutError( + f"No PONG received after {self._pong_heartbeat} seconds" + ) + ) + + def _handle_ping_pong_exception(self, exc: BaseException) -> None: + """Handle exceptions raised during ping/pong processing.""" + if self._closed: + return + self._set_closed() + self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE) + self._exception = exc + if self._waiting and not self._closing and self._reader is not None: + self._reader.feed_data(WSMessage(WSMsgType.ERROR, exc, None), 0) + + def _set_closed(self) -> None: + """Set the connection to closed. + + Cancel any heartbeat timers and set the closed flag. + """ + self._closed = True + self._cancel_heartbeat() + + async def prepare(self, request: BaseRequest) -> AbstractStreamWriter: + # make pre-check to don't hide it by do_handshake() exceptions + if self._payload_writer is not None: + return self._payload_writer + + protocol, writer = self._pre_start(request) + payload_writer = await super().prepare(request) + assert payload_writer is not None + self._post_start(request, protocol, writer) + await payload_writer.drain() + return payload_writer + + def _handshake( + self, request: BaseRequest + ) -> tuple["CIMultiDict[str]", str | None, int, bool]: + headers = request.headers + if "websocket" != headers.get(hdrs.UPGRADE, "").lower().strip(): + raise HTTPBadRequest( + text=( + f"No WebSocket UPGRADE hdr: {headers.get(hdrs.UPGRADE)}\n Can " + '"Upgrade" only to "WebSocket".' + ) + ) + + if not request._message.upgrade: + raise HTTPBadRequest( + text=f"No CONNECTION upgrade hdr: {headers.get(hdrs.CONNECTION)}" + ) + + # find common sub-protocol between client and server + protocol: str | None = None + if hdrs.SEC_WEBSOCKET_PROTOCOL in headers: + req_protocols = [ + str(proto.strip()) + for proto in headers[hdrs.SEC_WEBSOCKET_PROTOCOL].split(",") + ] + + for proto in req_protocols: + if proto in self._protocols: + protocol = proto + break + else: + # No overlap found: Return no protocol as per spec + ws_logger.warning( + "%s: Client protocols %r don’t overlap server-known ones %r", + request.remote, + req_protocols, + self._protocols, + ) + + # check supported version + version = headers.get(hdrs.SEC_WEBSOCKET_VERSION, "") + if version not in ("13", "8", "7"): + raise HTTPBadRequest(text=f"Unsupported version: {version}") + + # check client handshake for validity + key = headers.get(hdrs.SEC_WEBSOCKET_KEY) + try: + if not key or len(base64.b64decode(key)) != 16: + raise HTTPBadRequest(text=f"Handshake error: {key!r}") + except binascii.Error: + raise HTTPBadRequest(text=f"Handshake error: {key!r}") from None + + accept_val = base64.b64encode( + hashlib.sha1(key.encode() + WS_KEY).digest() + ).decode() + response_headers = CIMultiDict( + { + hdrs.UPGRADE: "websocket", + hdrs.CONNECTION: "upgrade", + hdrs.SEC_WEBSOCKET_ACCEPT: accept_val, + } + ) + + notakeover = False + compress = 0 + if self._compress: + extensions = headers.get(hdrs.SEC_WEBSOCKET_EXTENSIONS) + # Server side always get return with no exception. + # If something happened, just drop compress extension + compress, notakeover = ws_ext_parse(extensions, isserver=True) + if compress: + enabledext = ws_ext_gen( + compress=compress, isserver=True, server_notakeover=notakeover + ) + response_headers[hdrs.SEC_WEBSOCKET_EXTENSIONS] = enabledext + + if protocol: + response_headers[hdrs.SEC_WEBSOCKET_PROTOCOL] = protocol + return ( + response_headers, + protocol, + compress, + notakeover, + ) + + def _pre_start(self, request: BaseRequest) -> tuple[str | None, WebSocketWriter]: + self._loop = request._loop + + headers, protocol, compress, notakeover = self._handshake(request) + + self.set_status(101) + self.headers.update(headers) + self.force_close() + self._compress = compress + transport = request._protocol.transport + if transport is None: + raise ConnectionResetError("Connection lost") + writer = WebSocketWriter( + request._protocol, + transport, + compress=compress, + notakeover=notakeover, + limit=self._writer_limit, + ) + + return protocol, writer + + def _post_start( + self, request: BaseRequest, protocol: str | None, writer: WebSocketWriter + ) -> None: + self._ws_protocol = protocol + self._writer = writer + + self._reset_heartbeat() + + loop = self._loop + assert loop is not None + self._reader = WebSocketDataQueue( + request._protocol, DEFAULT_CHUNK_SIZE, loop=loop + ) + parser = WebSocketReader( + self._reader, + self._max_msg_size, + compress=bool(self._compress), + decode_text=self._decode_text, + ) + cb = None if self._heartbeat is None else self._on_data_received + request.protocol.set_parser(parser, data_received_cb=cb) + # disable HTTP keepalive for WebSocket + request.protocol.keep_alive(False) + + def can_prepare(self, request: BaseRequest) -> WebSocketReady: + if self._writer is not None: + raise RuntimeError("Already started") + try: + _, protocol, _, _ = self._handshake(request) + except HTTPException: + return WebSocketReady(False, None) + else: + return WebSocketReady(True, protocol) + + @property + def prepared(self) -> bool: + return self._writer is not None + + @property + def closed(self) -> bool: + return self._closed + + @property + def close_code(self) -> int | None: + return self._close_code + + @property + def ws_protocol(self) -> str | None: + return self._ws_protocol + + @property + def compress(self) -> int | bool: + return self._compress + + def get_extra_info(self, name: str, default: Any = None) -> Any: + """Get optional transport information. + + If no value associated with ``name`` is found, ``default`` is returned. + """ + writer = self._writer + if writer is None: + return default + transport = writer.transport + if transport is None: + return default + return transport.get_extra_info(name, default) + + def exception(self) -> BaseException | None: + return self._exception + + async def ping(self, message: bytes = b"") -> None: + if self._writer is None: + raise RuntimeError("Call .prepare() first") + await self._writer.send_frame(message, WSMsgType.PING) + + async def pong(self, message: bytes = b"") -> None: + # unsolicited pong + if self._writer is None: + raise RuntimeError("Call .prepare() first") + await self._writer.send_frame(message, WSMsgType.PONG) + + async def send_frame( + self, message: bytes, opcode: WSMsgType, compress: int | None = None + ) -> None: + """Send a frame over the websocket.""" + if self._writer is None: + raise RuntimeError("Call .prepare() first") + await self._writer.send_frame(message, opcode, compress) + + async def send_str(self, data: str, compress: int | None = None) -> None: + if self._writer is None: + raise RuntimeError("Call .prepare() first") + if not isinstance(data, str): + raise TypeError("data argument must be str (%r)" % type(data)) + await self._writer.send_frame( + data.encode("utf-8"), WSMsgType.TEXT, compress=compress + ) + + async def send_bytes(self, data: bytes, compress: int | None = None) -> None: + if self._writer is None: + raise RuntimeError("Call .prepare() first") + if not isinstance(data, (bytes, bytearray, memoryview)): + raise TypeError("data argument must be byte-ish (%r)" % type(data)) + await self._writer.send_frame(data, WSMsgType.BINARY, compress=compress) + + async def send_json( + self, + data: Any, + compress: int | None = None, + *, + dumps: JSONEncoder = json.dumps, + ) -> None: + await self.send_str(dumps(data), compress=compress) + + async def send_json_bytes( + self, + data: Any, + compress: int | None = None, + *, + dumps: JSONBytesEncoder, + ) -> None: + """Send JSON data using a bytes-returning encoder as a binary frame. + + Use this when your JSON encoder (like orjson) returns bytes + instead of str, avoiding the encode/decode overhead. + """ + await self.send_bytes(dumps(data), compress=compress) + + async def write_eof(self) -> None: # type: ignore[override] + if self._eof_sent: + return + if self._payload_writer is None: + raise RuntimeError("Response has not been started") + + await self.close() + self._eof_sent = True + + async def close( + self, *, code: int = WSCloseCode.OK, message: bytes = b"", drain: bool = True + ) -> bool: + """Close websocket connection.""" + if self._writer is None: + raise RuntimeError("Call .prepare() first") + + if self._closed: + return False + self._set_closed() + + try: + await self._writer.close(code, message) + writer = self._payload_writer + assert writer is not None + if drain: + await writer.drain() + except (asyncio.CancelledError, asyncio.TimeoutError): + self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE) + raise + except Exception as exc: + self._exception = exc + self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE) + return True + + reader = self._reader + assert reader is not None + # we need to break `receive()` cycle before we can call + # `reader.read()` as `close()` may be called from different task + if self._waiting: + assert self._loop is not None + assert self._close_wait is None + self._close_wait = self._loop.create_future() + reader.feed_data(WS_CLOSING_MESSAGE, 0) + await self._close_wait + + if self._closing: + self._close_transport() + return True + + try: + async with async_timeout.timeout(self._timeout): + while True: + msg = await reader.read() + if msg.type is WSMsgType.CLOSE: + self._set_code_close_transport(msg.data) + return True + except asyncio.CancelledError: + self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE) + raise + except Exception as exc: + self._exception = exc + self._set_code_close_transport(WSCloseCode.ABNORMAL_CLOSURE) + return True + + def _set_closing(self, code: WSCloseCode) -> None: + """Set the close code and mark the connection as closing.""" + self._closing = True + self._close_code = code + self._cancel_heartbeat() + + def _set_code_close_transport(self, code: WSCloseCode) -> None: + """Set the close code and close the transport.""" + self._close_code = code + self._close_transport() + + def _close_transport(self) -> None: + """Close the transport.""" + if self._req is not None and self._req.transport is not None: + self._req.transport.close() + + @overload + async def receive( + self: "WebSocketResponse[Literal[True]]", timeout: float | None = None + ) -> WSMessageDecodeText: ... + + @overload + async def receive( + self: "WebSocketResponse[Literal[False]]", timeout: float | None = None + ) -> WSMessageNoDecodeText: ... + + @overload + async def receive( + self: "WebSocketResponse[_DecodeText]", timeout: float | None = None + ) -> WSMessageDecodeText | WSMessageNoDecodeText: ... + + async def receive( + self, timeout: float | None = None + ) -> WSMessageDecodeText | WSMessageNoDecodeText: + if self._reader is None: + raise RuntimeError("Call .prepare() first") + + receive_timeout = timeout or self._receive_timeout + while True: + if self._waiting: + raise RuntimeError("Concurrent call to receive() is not allowed") + + if self._closed: + self._conn_lost += 1 + if self._conn_lost >= THRESHOLD_CONNLOST_ACCESS: + raise RuntimeError("WebSocket connection is closed.") + return WS_CLOSED_MESSAGE + elif self._closing: + return WS_CLOSING_MESSAGE + + try: + self._waiting = True + try: + if receive_timeout: + # Entering the context manager and creating + # Timeout() object can take almost 50% of the + # run time in this loop so we avoid it if + # there is no read timeout. + async with async_timeout.timeout(receive_timeout): + msg = await self._reader.read() + else: + msg = await self._reader.read() + finally: + self._waiting = False + if self._close_wait: + set_result(self._close_wait, None) + except asyncio.TimeoutError: + raise + except EofStream: + self._close_code = WSCloseCode.OK + await self.close() + return WSMessage(WSMsgType.CLOSED, None, None) + except WebSocketError as exc: + self._close_code = exc.code + await self.close(code=exc.code) + return WSMessage(WSMsgType.ERROR, exc, None) + except Exception as exc: + self._exception = exc + self._set_closing(WSCloseCode.ABNORMAL_CLOSURE) + await self.close() + return WSMessage(WSMsgType.ERROR, exc, None) + + if msg.type not in _INTERNAL_RECEIVE_TYPES: + # If its not a close/closing/ping/pong message + # we can return it immediately + return msg + + if msg.type is WSMsgType.CLOSE: + self._set_closing(msg.data) + # Could be closed while awaiting reader. + if not self._closed and self._autoclose: + # The client is likely going to close the + # connection out from under us so we do not + # want to drain any pending writes as it will + # likely result writing to a broken pipe. + await self.close(drain=False) + elif msg.type is WSMsgType.CLOSING: + self._set_closing(WSCloseCode.OK) + elif msg.type is WSMsgType.PING and self._autoping: + await self.pong(msg.data) + continue + elif msg.type is WSMsgType.PONG and self._autoping: + continue + + return msg + + @overload + async def receive_str( + self: "WebSocketResponse[Literal[True]]", *, timeout: float | None = None + ) -> str: ... + + @overload + async def receive_str( + self: "WebSocketResponse[Literal[False]]", *, timeout: float | None = None + ) -> bytes: ... + + @overload + async def receive_str( + self: "WebSocketResponse[_DecodeText]", *, timeout: float | None = None + ) -> str | bytes: ... + + async def receive_str(self, *, timeout: float | None = None) -> str | bytes: + """Receive TEXT message. + + Returns str when decode_text=True (default), bytes when decode_text=False. + """ + msg = await self.receive(timeout) + if msg.type is not WSMsgType.TEXT: + raise WSMessageTypeError( + f"Received message {msg.type}:{msg.data!r} is not WSMsgType.TEXT" + ) + return cast(str, msg.data) + + async def receive_bytes(self, *, timeout: float | None = None) -> bytes: + msg = await self.receive(timeout) + if msg.type is not WSMsgType.BINARY: + raise WSMessageTypeError( + f"Received message {msg.type}:{msg.data!r} is not WSMsgType.BINARY" + ) + return cast(bytes, msg.data) + + @overload + async def receive_json( + self: "WebSocketResponse[Literal[True]]", + *, + loads: JSONDecoder = ..., + timeout: float | None = None, + ) -> Any: ... + + @overload + async def receive_json( + self: "WebSocketResponse[Literal[False]]", + *, + loads: Callable[[bytes], Any] = ..., + timeout: float | None = None, + ) -> Any: ... + + @overload + async def receive_json( + self: "WebSocketResponse[_DecodeText]", + *, + loads: JSONDecoder | Callable[[bytes], Any] = ..., + timeout: float | None = None, + ) -> Any: ... + + async def receive_json( + self, + *, + loads: JSONDecoder | Callable[[bytes], Any] = json.loads, + timeout: float | None = None, + ) -> Any: + data = await self.receive_str(timeout=timeout) + return loads(data) # type: ignore[arg-type] + + async def write(self, data: Buffer) -> None: + raise RuntimeError("Cannot call .write() for websocket") + + def __aiter__(self) -> Self: + return self + + @overload + async def __anext__( + self: "WebSocketResponse[Literal[True]]", + ) -> WSMessageDecodeText: ... + + @overload + async def __anext__( + self: "WebSocketResponse[Literal[False]]", + ) -> WSMessageNoDecodeText: ... + + @overload + async def __anext__( + self: "WebSocketResponse[_DecodeText]", + ) -> WSMessageDecodeText | WSMessageNoDecodeText: ... + + async def __anext__(self) -> WSMessageDecodeText | WSMessageNoDecodeText: + msg = await self.receive() + if msg.type in (WSMsgType.CLOSE, WSMsgType.CLOSING, WSMsgType.CLOSED): + raise StopAsyncIteration + return msg + + def _cancel(self, exc: BaseException) -> None: + # web_protocol calls this from connection_lost + # or when the server is shutting down. + self._closing = True + self._cancel_heartbeat() + if self._reader is not None: + set_exception(self._reader, exc) diff --git a/venv/lib/python3.11/site-packages/aiohttp/worker.py b/venv/lib/python3.11/site-packages/aiohttp/worker.py new file mode 100644 index 0000000..a86573b --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiohttp/worker.py @@ -0,0 +1,266 @@ +"""Async gunicorn worker for aiohttp.web""" + +import asyncio +import inspect +import os +import re +import signal +import sys +from types import FrameType +from typing import TYPE_CHECKING, Any, Optional + +from gunicorn.config import AccessLogFormat as GunicornAccessLogFormat +from gunicorn.workers import base + +from aiohttp import web + +from .helpers import set_result +from .web_app import Application +from .web_log import AccessLogger + +if TYPE_CHECKING: + import ssl + + SSLContext = ssl.SSLContext +else: + try: + import ssl + + SSLContext = ssl.SSLContext + except ImportError: # pragma: no cover + ssl = None # type: ignore[assignment] + SSLContext = object # type: ignore[misc,assignment] + + +__all__ = ("GunicornWebWorker", "GunicornUVLoopWebWorker") + + +class GunicornWebWorker(base.Worker): # type: ignore[misc,no-any-unimported] + DEFAULT_AIOHTTP_LOG_FORMAT = AccessLogger.LOG_FORMAT + DEFAULT_GUNICORN_LOG_FORMAT = GunicornAccessLogFormat.default + + def __init__(self, *args: Any, **kw: Any) -> None: # pragma: no cover + super().__init__(*args, **kw) + + self._task: asyncio.Task[None] | None = None + self.exit_code = 0 + self._notify_waiter: asyncio.Future[bool] | None = None + + def init_process(self) -> None: + # create new event_loop after fork + try: + asyncio.get_event_loop().close() + except RuntimeError: + # No loop was running + pass + + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + + super().init_process() + + def run(self) -> None: + self._task = self.loop.create_task(self._run()) + + try: # ignore all finalization problems + self.loop.run_until_complete(self._task) + except Exception: + self.log.exception("Exception in gunicorn worker") + self.loop.run_until_complete(self.loop.shutdown_asyncgens()) + self.loop.close() + + sys.exit(self.exit_code) + + async def _run(self) -> None: + runner = None + if isinstance(self.wsgi, Application): + app = self.wsgi + elif inspect.iscoroutinefunction(self.wsgi) or ( + sys.version_info < (3, 14) and asyncio.iscoroutinefunction(self.wsgi) + ): + wsgi = await self.wsgi() + if isinstance(wsgi, web.AppRunner): + runner = wsgi + app = runner.app + else: + app = wsgi + else: + raise RuntimeError( + "wsgi app should be either Application or " + f"async function returning Application, got {self.wsgi}" + ) + + if runner is None: + access_log = self.log.access_log if self.cfg.accesslog else None + runner = web.AppRunner( + app, + logger=self.log, + keepalive_timeout=self.cfg.keepalive, + access_log=access_log, + access_log_format=self._get_valid_log_format( + self.cfg.access_log_format + ), + shutdown_timeout=self.cfg.graceful_timeout / 100 * 95, + ) + await runner.setup() + + ctx = self._create_ssl_context(self.cfg) if self.cfg.is_ssl else None + + runner = runner + assert runner is not None + server = runner.server + assert server is not None + for sock in self.sockets: + site = web.SockSite( + runner, + sock, + ssl_context=ctx, + ) + await site.start() + + # If our parent changed then we shut down. + pid = os.getpid() + try: + while self.alive: # type: ignore[has-type] + self.notify() + + cnt = server.requests_count + if self.max_requests and cnt > self.max_requests: + self.alive = False + self.log.info("Max requests, shutting down: %s", self) + + elif pid == os.getpid() and self.ppid != os.getppid(): + self.alive = False + self.log.info("Parent changed, shutting down: %s", self) + else: + await self._wait_next_notify() + except Exception: + pass + + await runner.cleanup() + + def _wait_next_notify(self) -> "asyncio.Future[bool]": + self._notify_waiter_done() + + loop = self.loop + assert loop is not None + self._notify_waiter = waiter = loop.create_future() + self.loop.call_later(1.0, self._notify_waiter_done, waiter) + + return waiter + + def _notify_waiter_done( + self, waiter: Optional["asyncio.Future[bool]"] = None + ) -> None: + if waiter is None: + waiter = self._notify_waiter + if waiter is not None: + set_result(waiter, True) + + if waiter is self._notify_waiter: + self._notify_waiter = None + + def init_signals(self) -> None: + # Set up signals through the event loop API. + + self.loop.add_signal_handler( + signal.SIGQUIT, self.handle_quit, signal.SIGQUIT, None + ) + + self.loop.add_signal_handler( + signal.SIGTERM, self.handle_exit, signal.SIGTERM, None + ) + + self.loop.add_signal_handler( + signal.SIGINT, self.handle_quit, signal.SIGINT, None + ) + + self.loop.add_signal_handler( + signal.SIGWINCH, self.handle_winch, signal.SIGWINCH, None + ) + + self.loop.add_signal_handler( + signal.SIGUSR1, self.handle_usr1, signal.SIGUSR1, None + ) + + self.loop.add_signal_handler( + signal.SIGABRT, self.handle_abort, signal.SIGABRT, None + ) + + # Don't let SIGTERM and SIGUSR1 disturb active requests + # by interrupting system calls + signal.siginterrupt(signal.SIGTERM, False) + signal.siginterrupt(signal.SIGUSR1, False) + + # Reset SIGCHLD to default so Gunicorn doesn't swallow subprocess + # return codes. Without this, workers inherit the master arbiter's + # SIGCHLD handler, causing spurious "Worker exited" errors when + # application code spawns subprocesses. + signal.signal(signal.SIGCHLD, signal.SIG_DFL) + + def handle_quit(self, sig: int, frame: FrameType | None) -> None: + self.alive = False + + # worker_int callback + self.cfg.worker_int(self) + + # wakeup closing process + self._notify_waiter_done() + + def handle_abort(self, sig: int, frame: FrameType | None) -> None: + self.alive = False + self.exit_code = 1 + self.cfg.worker_abort(self) + sys.exit(1) + + @staticmethod + def _create_ssl_context(cfg: Any) -> "SSLContext": + """Creates SSLContext instance for usage in asyncio.create_server. + + See ssl.SSLSocket.__init__ for more details. + """ + if ssl is None: # pragma: no cover + raise RuntimeError("SSL is not supported.") + + ctx = ssl.SSLContext(cfg.ssl_version) + ctx.load_cert_chain(cfg.certfile, cfg.keyfile) + ctx.verify_mode = cfg.cert_reqs + if cfg.ca_certs: + ctx.load_verify_locations(cfg.ca_certs) + if cfg.ciphers: + ctx.set_ciphers(cfg.ciphers) + return ctx + + def _get_valid_log_format(self, source_format: str) -> str: + if source_format == self.DEFAULT_GUNICORN_LOG_FORMAT: + return self.DEFAULT_AIOHTTP_LOG_FORMAT + elif re.search(r"%\([^\)]+\)", source_format): + raise ValueError( + "Gunicorn's style options in form of `%(name)s` are not " + "supported for the log formatting. Please use aiohttp's " + "format specification to configure access log formatting: " + "http://docs.aiohttp.org/en/stable/logging.html" + "#format-specification" + ) + else: + return source_format + + +class GunicornUVLoopWebWorker(GunicornWebWorker): + def init_process(self) -> None: + import uvloop + + # Close any existing event loop before setting a + # new policy. + try: + asyncio.get_event_loop().close() + except RuntimeError: + # No loop was running + pass + + # Setup uvloop policy, so that every + # asyncio.get_event_loop() will create an instance + # of uvloop event loop. + asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) + + super().init_process() diff --git a/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/METADATA b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/METADATA new file mode 100644 index 0000000..03a6f0f --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/METADATA @@ -0,0 +1,112 @@ +Metadata-Version: 2.4 +Name: aiosignal +Version: 1.4.0 +Summary: aiosignal: a list of registered asynchronous callbacks +Home-page: https://github.com/aio-libs/aiosignal +Maintainer: aiohttp team +Maintainer-email: team@aiohttp.org +License: Apache 2.0 +Project-URL: Chat: Gitter, https://gitter.im/aio-libs/Lobby +Project-URL: CI: GitHub Actions, https://github.com/aio-libs/aiosignal/actions +Project-URL: Coverage: codecov, https://codecov.io/github/aio-libs/aiosignal +Project-URL: Docs: RTD, https://docs.aiosignal.org +Project-URL: GitHub: issues, https://github.com/aio-libs/aiosignal/issues +Project-URL: GitHub: repo, https://github.com/aio-libs/aiosignal +Classifier: License :: OSI Approved :: Apache Software License +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Development Status :: 5 - Production/Stable +Classifier: Operating System :: POSIX +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Framework :: AsyncIO +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: frozenlist>=1.1.0 +Requires-Dist: typing-extensions>=4.2; python_version < "3.13" +Dynamic: license-file + +========= +aiosignal +========= + +.. image:: https://github.com/aio-libs/aiosignal/workflows/CI/badge.svg + :target: https://github.com/aio-libs/aiosignal/actions?query=workflow%3ACI + :alt: GitHub status for master branch + +.. image:: https://codecov.io/gh/aio-libs/aiosignal/branch/master/graph/badge.svg?flag=pytest + :target: https://codecov.io/gh/aio-libs/aiosignal?flags[0]=pytest + :alt: codecov.io status for master branch + +.. image:: https://badge.fury.io/py/aiosignal.svg + :target: https://pypi.org/project/aiosignal + :alt: Latest PyPI package version + +.. image:: https://readthedocs.org/projects/aiosignal/badge/?version=latest + :target: https://aiosignal.readthedocs.io/ + :alt: Latest Read The Docs + +.. image:: https://img.shields.io/discourse/topics?server=https%3A%2F%2Faio-libs.discourse.group%2F + :target: https://aio-libs.discourse.group/ + :alt: Discourse group for io-libs + +.. image:: https://badges.gitter.im/Join%20Chat.svg + :target: https://gitter.im/aio-libs/Lobby + :alt: Chat on Gitter + +Introduction +============ + +A project to manage callbacks in `asyncio` projects. + +``Signal`` is a list of registered asynchronous callbacks. + +The signal's life-cycle has two stages: after creation its content +could be filled by using standard list operations: ``sig.append()`` +etc. + +After you call ``sig.freeze()`` the signal is *frozen*: adding, removing +and dropping callbacks is forbidden. + +The only available operation is calling the previously registered +callbacks by using ``await sig.send(data)``. + +For concrete usage examples see the `Signals + +section of the `Web Server Advanced +` chapter of the `aiohttp +documentation`_. + + +Installation +------------ + +:: + + $ pip install aiosignal + + +Documentation +============= + +https://aiosignal.readthedocs.io/ + +License +======= + +``aiosignal`` is offered under the Apache 2 license. + +Source code +=========== + +The project is hosted on GitHub_ + +Please file an issue in the `bug tracker +`_ if you have found a bug +or have some suggestions to improve the library. + +.. _GitHub: https://github.com/aio-libs/aiosignal +.. _aiohttp documentation: https://docs.aiohttp.org/ diff --git a/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/RECORD b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/RECORD new file mode 100644 index 0000000..819676e --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/RECORD @@ -0,0 +1,9 @@ +aiosignal-1.4.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +aiosignal-1.4.0.dist-info/METADATA,sha256=CSR-8dqLxpZyjUcTDnAuQwf299EB1sSFv_nzpxznAI0,3662 +aiosignal-1.4.0.dist-info/RECORD,, +aiosignal-1.4.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91 +aiosignal-1.4.0.dist-info/licenses/LICENSE,sha256=b9UkPpLdf5jsacesN3co50kFcJ_1J6W_mNbQJjwE9bY,11332 +aiosignal-1.4.0.dist-info/top_level.txt,sha256=z45aNOKGDdrI1roqZY3BGXQ22kJFPHBmVdwtLYLtXC0,10 +aiosignal/__init__.py,sha256=TIkmUG9HTBt4dfq2nISYBiZiRB2xwvFtEZydLP0HPL4,1537 +aiosignal/__pycache__/__init__.cpython-311.pyc,, +aiosignal/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/WHEEL b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/WHEEL new file mode 100644 index 0000000..e7fa31b --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.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.11/site-packages/aiosignal-1.4.0.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..7082a2d --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiosignal-1.4.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 2013-2019 Nikolay Kim and Andrew Svetlov + + 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.11/site-packages/aiosignal-1.4.0.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/top_level.txt new file mode 100644 index 0000000..ac6df3a --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiosignal-1.4.0.dist-info/top_level.txt @@ -0,0 +1 @@ +aiosignal diff --git a/venv/lib/python3.11/site-packages/aiosignal/__init__.py b/venv/lib/python3.11/site-packages/aiosignal/__init__.py new file mode 100644 index 0000000..5ede009 --- /dev/null +++ b/venv/lib/python3.11/site-packages/aiosignal/__init__.py @@ -0,0 +1,59 @@ +import sys +from typing import Any, Awaitable, Callable, TypeVar + +from frozenlist import FrozenList + +if sys.version_info >= (3, 11): + from typing import Unpack +else: + from typing_extensions import Unpack + +if sys.version_info >= (3, 13): + from typing import TypeVarTuple +else: + from typing_extensions import TypeVarTuple + +_T = TypeVar("_T") +_Ts = TypeVarTuple("_Ts", default=Unpack[tuple[()]]) + +__version__ = "1.4.0" + +__all__ = ("Signal",) + + +class Signal(FrozenList[Callable[[Unpack[_Ts]], Awaitable[object]]]): + """Coroutine-based signal implementation. + + To connect a callback to a signal, use any list method. + + Signals are fired using the send() coroutine, which takes named + arguments. + """ + + __slots__ = ("_owner",) + + def __init__(self, owner: object): + super().__init__() + self._owner = owner + + def __repr__(self) -> str: + return "".format( + self._owner, self.frozen, list(self) + ) + + async def send(self, *args: Unpack[_Ts], **kwargs: Any) -> None: + """ + Sends data to all registered receivers. + """ + if not self.frozen: + raise RuntimeError("Cannot send non-frozen signal.") + + for receiver in self: + await receiver(*args, **kwargs) + + def __call__( + self, func: Callable[[Unpack[_Ts]], Awaitable[_T]] + ) -> Callable[[Unpack[_Ts]], Awaitable[_T]]: + """Decorator to add a function to this Signal.""" + self.append(func) + return func diff --git a/venv/lib/python3.11/site-packages/aiosignal/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/aiosignal/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..fc53a78 Binary files /dev/null and b/venv/lib/python3.11/site-packages/aiosignal/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/aiosignal/py.typed b/venv/lib/python3.11/site-packages/aiosignal/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/METADATA b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/METADATA new file mode 100644 index 0000000..9bf7a9e --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/METADATA @@ -0,0 +1,145 @@ +Metadata-Version: 2.4 +Name: annotated-doc +Version: 0.0.4 +Summary: Document parameters, class attributes, return types, and variables inline, with Annotated. +Author-Email: =?utf-8?q?Sebasti=C3=A1n_Ram=C3=ADrez?= +License-Expression: MIT +License-File: LICENSE +Classifier: Intended Audience :: Information Technology +Classifier: Intended Audience :: System Administrators +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Software Development :: Libraries +Classifier: Topic :: Software Development +Classifier: Typing :: Typed +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +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 +Project-URL: Homepage, https://github.com/fastapi/annotated-doc +Project-URL: Documentation, https://github.com/fastapi/annotated-doc +Project-URL: Repository, https://github.com/fastapi/annotated-doc +Project-URL: Issues, https://github.com/fastapi/annotated-doc/issues +Project-URL: Changelog, https://github.com/fastapi/annotated-doc/release-notes.md +Requires-Python: >=3.8 +Description-Content-Type: text/markdown + +# Annotated Doc + +Document parameters, class attributes, return types, and variables inline, with `Annotated`. + + + Test + + + Coverage + + + Package version + + + Supported Python versions + + +## Installation + +```bash +pip install annotated-doc +``` + +Or with `uv`: + +```Python +uv add annotated-doc +``` + +## Usage + +Import `Doc` and pass a single literal string with the documentation for the specific parameter, class attribute, return type, or variable. + +For example, to document a parameter `name` in a function `hi` you could do: + +```Python +from typing import Annotated + +from annotated_doc import Doc + +def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None: + print(f"Hi, {name}!") +``` + +You can also use it to document class attributes: + +```Python +from typing import Annotated + +from annotated_doc import Doc + +class User: + name: Annotated[str, Doc("The user's name")] + age: Annotated[int, Doc("The user's age")] +``` + +The same way, you could document return types and variables, or anything that could have a type annotation with `Annotated`. + +## Who Uses This + +`annotated-doc` was made for: + +* [FastAPI](https://fastapi.tiangolo.com/) +* [Typer](https://typer.tiangolo.com/) +* [SQLModel](https://sqlmodel.tiangolo.com/) +* [Asyncer](https://asyncer.tiangolo.com/) + +`annotated-doc` is supported by [griffe-typingdoc](https://github.com/mkdocstrings/griffe-typingdoc), which powers reference documentation like the one in the [FastAPI Reference](https://fastapi.tiangolo.com/reference/). + +## Reasons not to use `annotated-doc` + +You are already comfortable with one of the existing docstring formats, like: + +* Sphinx +* numpydoc +* Google +* Keras + +Your team is already comfortable using them. + +You prefer having the documentation about parameters all together in a docstring, separated from the code defining them. + +You care about a specific set of users, using one specific editor, and that editor already has support for the specific docstring format you use. + +## Reasons to use `annotated-doc` + +* No micro-syntax to learn for newcomers, it’s **just Python** syntax. +* **Editing** would be already fully supported by default by any editor (current or future) supporting Python syntax, including syntax errors, syntax highlighting, etc. +* **Rendering** would be relatively straightforward to implement by static tools (tools that don't need runtime execution), as the information can be extracted from the AST they normally already create. +* **Deduplication of information**: the name of a parameter would be defined in a single place, not duplicated inside of a docstring. +* **Elimination** of the possibility of having **inconsistencies** when removing a parameter or class variable and **forgetting to remove** its documentation. +* **Minimization** of the probability of adding a new parameter or class variable and **forgetting to add its documentation**. +* **Elimination** of the possibility of having **inconsistencies** between the **name** of a parameter in the **signature** and the name in the docstring when it is renamed. +* **Access** to the documentation string for each symbol at **runtime**, including existing (older) Python versions. +* A more formalized way to document other symbols, like type aliases, that could use Annotated. +* **Support** for apps using FastAPI, Typer and others. +* **AI Accessibility**: AI tools will have an easier way understanding each parameter as the distance from documentation to parameter is much closer. + +## History + +I ([@tiangolo](https://github.com/tiangolo)) originally wanted for this to be part of the Python standard library (in [PEP 727](https://peps.python.org/pep-0727/)), but the proposal was withdrawn as there was a fair amount of negative feedback and opposition. + +The conclusion was that this was better done as an external effort, in a third-party library. + +So, here it is, with a simpler approach, as a third-party library, in a way that can be used by others, starting with FastAPI and friends. + +## License + +This project is licensed under the terms of the MIT license. diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/RECORD b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/RECORD new file mode 100644 index 0000000..e46a002 --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/RECORD @@ -0,0 +1,11 @@ +annotated_doc-0.0.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +annotated_doc-0.0.4.dist-info/METADATA,sha256=Irm5KJua33dY2qKKAjJ-OhKaVBVIfwFGej_dSe3Z1TU,6566 +annotated_doc-0.0.4.dist-info/RECORD,, +annotated_doc-0.0.4.dist-info/WHEEL,sha256=9P2ygRxDrTJz3gsagc0Z96ukrxjr-LFBGOgv3AuKlCA,90 +annotated_doc-0.0.4.dist-info/entry_points.txt,sha256=6OYgBcLyFCUgeqLgnvMyOJxPCWzgy7se4rLPKtNonMs,34 +annotated_doc-0.0.4.dist-info/licenses/LICENSE,sha256=__Fwd5pqy_ZavbQFwIfxzuF4ZpHkqWpANFF-SlBKDN8,1086 +annotated_doc/__init__.py,sha256=VuyxxUe80kfEyWnOrCx_Bk8hybo3aKo6RYBlkBBYW8k,52 +annotated_doc/__pycache__/__init__.cpython-311.pyc,, +annotated_doc/__pycache__/main.cpython-311.pyc,, +annotated_doc/main.py,sha256=5Zfvxv80SwwLqpRW73AZyZyiM4bWma9QWRbp_cgD20s,1075 +annotated_doc/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/WHEEL b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/WHEEL new file mode 100644 index 0000000..045c8ac --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: pdm-backend (2.4.5) +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/entry_points.txt b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/entry_points.txt new file mode 100644 index 0000000..c3ad472 --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/entry_points.txt @@ -0,0 +1,4 @@ +[console_scripts] + +[gui_scripts] + diff --git a/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/licenses/LICENSE new file mode 100644 index 0000000..7a25446 --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_doc-0.0.4.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2025 Sebastián Ramírez + +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.11/site-packages/annotated_doc/__init__.py b/venv/lib/python3.11/site-packages/annotated_doc/__init__.py new file mode 100644 index 0000000..a0152a7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_doc/__init__.py @@ -0,0 +1,3 @@ +from .main import Doc as Doc + +__version__ = "0.0.4" diff --git a/venv/lib/python3.11/site-packages/annotated_doc/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/annotated_doc/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..dd03762 Binary files /dev/null and b/venv/lib/python3.11/site-packages/annotated_doc/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/annotated_doc/__pycache__/main.cpython-311.pyc b/venv/lib/python3.11/site-packages/annotated_doc/__pycache__/main.cpython-311.pyc new file mode 100644 index 0000000..256aaa9 Binary files /dev/null and b/venv/lib/python3.11/site-packages/annotated_doc/__pycache__/main.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/annotated_doc/main.py b/venv/lib/python3.11/site-packages/annotated_doc/main.py new file mode 100644 index 0000000..7063c59 --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_doc/main.py @@ -0,0 +1,36 @@ +class Doc: + """Define the documentation of a type annotation using `Annotated`, to be + used in class attributes, function and method parameters, return values, + and variables. + + The value should be a positional-only string literal to allow static tools + like editors and documentation generators to use it. + + This complements docstrings. + + The string value passed is available in the attribute `documentation`. + + Example: + + ```Python + from typing import Annotated + from annotated_doc import Doc + + def hi(name: Annotated[str, Doc("Who to say hi to")]) -> None: + print(f"Hi, {name}!") + ``` + """ + + def __init__(self, documentation: str, /) -> None: + self.documentation = documentation + + def __repr__(self) -> str: + return f"Doc({self.documentation!r})" + + def __hash__(self) -> int: + return hash(self.documentation) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Doc): + return NotImplemented + return self.documentation == other.documentation diff --git a/venv/lib/python3.11/site-packages/annotated_doc/py.typed b/venv/lib/python3.11/site-packages/annotated_doc/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/METADATA b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/METADATA new file mode 100644 index 0000000..3ac05cf --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/METADATA @@ -0,0 +1,295 @@ +Metadata-Version: 2.3 +Name: annotated-types +Version: 0.7.0 +Summary: Reusable constraint types to use with typing.Annotated +Project-URL: Homepage, https://github.com/annotated-types/annotated-types +Project-URL: Source, https://github.com/annotated-types/annotated-types +Project-URL: Changelog, https://github.com/annotated-types/annotated-types/releases +Author-email: Adrian Garcia Badaracco <1755071+adriangb@users.noreply.github.com>, Samuel Colvin , Zac Hatfield-Dodds +License-File: LICENSE +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console +Classifier: Environment :: MacOS X +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: Information Technology +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: POSIX :: Linux +Classifier: Operating System :: Unix +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: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Typing :: Typed +Requires-Python: >=3.8 +Requires-Dist: typing-extensions>=4.0.0; python_version < '3.9' +Description-Content-Type: text/markdown + +# annotated-types + +[![CI](https://github.com/annotated-types/annotated-types/workflows/CI/badge.svg?event=push)](https://github.com/annotated-types/annotated-types/actions?query=event%3Apush+branch%3Amain+workflow%3ACI) +[![pypi](https://img.shields.io/pypi/v/annotated-types.svg)](https://pypi.python.org/pypi/annotated-types) +[![versions](https://img.shields.io/pypi/pyversions/annotated-types.svg)](https://github.com/annotated-types/annotated-types) +[![license](https://img.shields.io/github/license/annotated-types/annotated-types.svg)](https://github.com/annotated-types/annotated-types/blob/main/LICENSE) + +[PEP-593](https://peps.python.org/pep-0593/) added `typing.Annotated` as a way of +adding context-specific metadata to existing types, and specifies that +`Annotated[T, x]` _should_ be treated as `T` by any tool or library without special +logic for `x`. + +This package provides metadata objects which can be used to represent common +constraints such as upper and lower bounds on scalar values and collection sizes, +a `Predicate` marker for runtime checks, and +descriptions of how we intend these metadata to be interpreted. In some cases, +we also note alternative representations which do not require this package. + +## Install + +```bash +pip install annotated-types +``` + +## Examples + +```python +from typing import Annotated +from annotated_types import Gt, Len, Predicate + +class MyClass: + age: Annotated[int, Gt(18)] # Valid: 19, 20, ... + # Invalid: 17, 18, "19", 19.0, ... + factors: list[Annotated[int, Predicate(is_prime)]] # Valid: 2, 3, 5, 7, 11, ... + # Invalid: 4, 8, -2, 5.0, "prime", ... + + my_list: Annotated[list[int], Len(0, 10)] # Valid: [], [10, 20, 30, 40, 50] + # Invalid: (1, 2), ["abc"], [0] * 20 +``` + +## Documentation + +_While `annotated-types` avoids runtime checks for performance, users should not +construct invalid combinations such as `MultipleOf("non-numeric")` or `Annotated[int, Len(3)]`. +Downstream implementors may choose to raise an error, emit a warning, silently ignore +a metadata item, etc., if the metadata objects described below are used with an +incompatible type - or for any other reason!_ + +### Gt, Ge, Lt, Le + +Express inclusive and/or exclusive bounds on orderable values - which may be numbers, +dates, times, strings, sets, etc. Note that the boundary value need not be of the +same type that was annotated, so long as they can be compared: `Annotated[int, Gt(1.5)]` +is fine, for example, and implies that the value is an integer x such that `x > 1.5`. + +We suggest that implementors may also interpret `functools.partial(operator.le, 1.5)` +as being equivalent to `Gt(1.5)`, for users who wish to avoid a runtime dependency on +the `annotated-types` package. + +To be explicit, these types have the following meanings: + +* `Gt(x)` - value must be "Greater Than" `x` - equivalent to exclusive minimum +* `Ge(x)` - value must be "Greater than or Equal" to `x` - equivalent to inclusive minimum +* `Lt(x)` - value must be "Less Than" `x` - equivalent to exclusive maximum +* `Le(x)` - value must be "Less than or Equal" to `x` - equivalent to inclusive maximum + +### Interval + +`Interval(gt, ge, lt, le)` allows you to specify an upper and lower bound with a single +metadata object. `None` attributes should be ignored, and non-`None` attributes +treated as per the single bounds above. + +### MultipleOf + +`MultipleOf(multiple_of=x)` might be interpreted in two ways: + +1. Python semantics, implying `value % multiple_of == 0`, or +2. [JSONschema semantics](https://json-schema.org/draft/2020-12/json-schema-validation.html#rfc.section.6.2.1), + where `int(value / multiple_of) == value / multiple_of`. + +We encourage users to be aware of these two common interpretations and their +distinct behaviours, especially since very large or non-integer numbers make +it easy to cause silent data corruption due to floating-point imprecision. + +We encourage libraries to carefully document which interpretation they implement. + +### MinLen, MaxLen, Len + +`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive. + +As well as `Len()` which can optionally include upper and lower bounds, we also +provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)` +and `Len(max_length=y)` respectively. + +`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`. + +Examples of usage: + +* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10))`) - list must have a length of 10 or less +* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less +* `Annotated[list, MinLen(3))` (or `Annotated[list, Len(min_length=3))`) - list must have a length of 3 or more +* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6 +* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8 + +#### Changed in v0.4.0 + +* `min_inclusive` has been renamed to `min_length`, no change in meaning +* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive** +* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic + meaning of the upper bound in slices vs. `Len` + +See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion. + +### Timezone + +`Timezone` can be used with a `datetime` or a `time` to express which timezones +are allowed. `Annotated[datetime, Timezone(None)]` must be a naive datetime. +`Timezone[...]` ([literal ellipsis](https://docs.python.org/3/library/constants.html#Ellipsis)) +expresses that any timezone-aware datetime is allowed. You may also pass a specific +timezone string or [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) +object such as `Timezone(timezone.utc)` or `Timezone("Africa/Abidjan")` to express that you only +allow a specific timezone, though we note that this is often a symptom of fragile design. + +#### Changed in v0.x.x + +* `Timezone` accepts [`tzinfo`](https://docs.python.org/3/library/datetime.html#tzinfo-objects) objects instead of + `timezone`, extending compatibility to [`zoneinfo`](https://docs.python.org/3/library/zoneinfo.html) and third party libraries. + +### Unit + +`Unit(unit: str)` expresses that the annotated numeric value is the magnitude of +a quantity with the specified unit. For example, `Annotated[float, Unit("m/s")]` +would be a float representing a velocity in meters per second. + +Please note that `annotated_types` itself makes no attempt to parse or validate +the unit string in any way. That is left entirely to downstream libraries, +such as [`pint`](https://pint.readthedocs.io) or +[`astropy.units`](https://docs.astropy.org/en/stable/units/). + +An example of how a library might use this metadata: + +```python +from annotated_types import Unit +from typing import Annotated, TypeVar, Callable, Any, get_origin, get_args + +# given a type annotated with a unit: +Meters = Annotated[float, Unit("m")] + + +# you can cast the annotation to a specific unit type with any +# callable that accepts a string and returns the desired type +T = TypeVar("T") +def cast_unit(tp: Any, unit_cls: Callable[[str], T]) -> T | None: + if get_origin(tp) is Annotated: + for arg in get_args(tp): + if isinstance(arg, Unit): + return unit_cls(arg.unit) + return None + + +# using `pint` +import pint +pint_unit = cast_unit(Meters, pint.Unit) + + +# using `astropy.units` +import astropy.units as u +astropy_unit = cast_unit(Meters, u.Unit) +``` + +### Predicate + +`Predicate(func: Callable)` expresses that `func(value)` is truthy for valid values. +Users should prefer the statically inspectable metadata above, but if you need +the full power and flexibility of arbitrary runtime predicates... here it is. + +For some common constraints, we provide generic types: + +* `IsLower = Annotated[T, Predicate(str.islower)]` +* `IsUpper = Annotated[T, Predicate(str.isupper)]` +* `IsDigit = Annotated[T, Predicate(str.isdigit)]` +* `IsFinite = Annotated[T, Predicate(math.isfinite)]` +* `IsNotFinite = Annotated[T, Predicate(Not(math.isfinite))]` +* `IsNan = Annotated[T, Predicate(math.isnan)]` +* `IsNotNan = Annotated[T, Predicate(Not(math.isnan))]` +* `IsInfinite = Annotated[T, Predicate(math.isinf)]` +* `IsNotInfinite = Annotated[T, Predicate(Not(math.isinf))]` + +so that you can write e.g. `x: IsFinite[float] = 2.0` instead of the longer +(but exactly equivalent) `x: Annotated[float, Predicate(math.isfinite)] = 2.0`. + +Some libraries might have special logic to handle known or understandable predicates, +for example by checking for `str.isdigit` and using its presence to both call custom +logic to enforce digit-only strings, and customise some generated external schema. +Users are therefore encouraged to avoid indirection like `lambda s: s.lower()`, in +favor of introspectable methods such as `str.lower` or `re.compile("pattern").search`. + +To enable basic negation of commonly used predicates like `math.isnan` without introducing introspection that makes it impossible for implementers to introspect the predicate we provide a `Not` wrapper that simply negates the predicate in an introspectable manner. Several of the predicates listed above are created in this manner. + +We do not specify what behaviour should be expected for predicates that raise +an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently +skip invalid constraints, or statically raise an error; or it might try calling it +and then propagate or discard the resulting +`TypeError: descriptor 'isdigit' for 'str' objects doesn't apply to a 'int' object` +exception. We encourage libraries to document the behaviour they choose. + +### Doc + +`doc()` can be used to add documentation information in `Annotated`, for function and method parameters, variables, class attributes, return types, and any place where `Annotated` can be used. + +It expects a value that can be statically analyzed, as the main use case is for static analysis, editors, documentation generators, and similar tools. + +It returns a `DocInfo` class with a single attribute `documentation` containing the value passed to `doc()`. + +This is the early adopter's alternative form of the [`typing-doc` proposal](https://github.com/tiangolo/fastapi/blob/typing-doc/typing_doc.md). + +### Integrating downstream types with `GroupedMetadata` + +Implementers may choose to provide a convenience wrapper that groups multiple pieces of metadata. +This can help reduce verbosity and cognitive overhead for users. +For example, an implementer like Pydantic might provide a `Field` or `Meta` type that accepts keyword arguments and transforms these into low-level metadata: + +```python +from dataclasses import dataclass +from typing import Iterator +from annotated_types import GroupedMetadata, Ge + +@dataclass +class Field(GroupedMetadata): + ge: int | None = None + description: str | None = None + + def __iter__(self) -> Iterator[object]: + # Iterating over a GroupedMetadata object should yield annotated-types + # constraint metadata objects which describe it as fully as possible, + # and may include other unknown objects too. + if self.ge is not None: + yield Ge(self.ge) + if self.description is not None: + yield Description(self.description) +``` + +Libraries consuming annotated-types constraints should check for `GroupedMetadata` and unpack it by iterating over the object and treating the results as if they had been "unpacked" in the `Annotated` type. The same logic should be applied to the [PEP 646 `Unpack` type](https://peps.python.org/pep-0646/), so that `Annotated[T, Field(...)]`, `Annotated[T, Unpack[Field(...)]]` and `Annotated[T, *Field(...)]` are all treated consistently. + +Libraries consuming annotated-types should also ignore any metadata they do not recongize that came from unpacking a `GroupedMetadata`, just like they ignore unrecognized metadata in `Annotated` itself. + +Our own `annotated_types.Interval` class is a `GroupedMetadata` which unpacks itself into `Gt`, `Lt`, etc., so this is not an abstract concern. Similarly, `annotated_types.Len` is a `GroupedMetadata` which unpacks itself into `MinLen` (optionally) and `MaxLen`. + +### Consuming metadata + +We intend to not be prescriptive as to _how_ the metadata and constraints are used, but as an example of how one might parse constraints from types annotations see our [implementation in `test_main.py`](https://github.com/annotated-types/annotated-types/blob/f59cf6d1b5255a0fe359b93896759a180bec30ae/tests/test_main.py#L94-L103). + +It is up to the implementer to determine how this metadata is used. +You could use the metadata for runtime type checking, for generating schemas or to generate example data, amongst other use cases. + +## Design & History + +This package was designed at the PyCon 2022 sprints by the maintainers of Pydantic +and Hypothesis, with the goal of making it as easy as possible for end-users to +provide more informative annotations for use by runtime libraries. + +It is deliberately minimal, and following PEP-593 allows considerable downstream +discretion in what (if anything!) they choose to support. Nonetheless, we expect +that staying simple and covering _only_ the most common use-cases will give users +and maintainers the best experience we can. If you'd like more constraints for your +types - follow our lead, by defining them and documenting them downstream! diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/RECORD b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/RECORD new file mode 100644 index 0000000..cb983b0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/RECORD @@ -0,0 +1,10 @@ +annotated_types-0.7.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +annotated_types-0.7.0.dist-info/METADATA,sha256=7ltqxksJJ0wCYFGBNIQCWTlWQGeAH0hRFdnK3CB895E,15046 +annotated_types-0.7.0.dist-info/RECORD,, +annotated_types-0.7.0.dist-info/WHEEL,sha256=zEMcRr9Kr03x1ozGwg5v9NQBKn3kndp6LSoSlVg-jhU,87 +annotated_types-0.7.0.dist-info/licenses/LICENSE,sha256=_hBJiEsaDZNCkB6I4H8ykl0ksxIdmXK2poBfuYJLCV0,1083 +annotated_types/__init__.py,sha256=RynLsRKUEGI0KimXydlD1fZEfEzWwDo0Uon3zOKhG1Q,13819 +annotated_types/__pycache__/__init__.cpython-311.pyc,, +annotated_types/__pycache__/test_cases.cpython-311.pyc,, +annotated_types/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +annotated_types/test_cases.py,sha256=zHFX6EpcMbGJ8FzBYDbO56bPwx_DYIVSKbZM-4B3_lg,6421 diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/WHEEL b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/WHEEL new file mode 100644 index 0000000..516596c --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.24.2 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..d99323a --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_types-0.7.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 the contributors + +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.11/site-packages/annotated_types/__init__.py b/venv/lib/python3.11/site-packages/annotated_types/__init__.py new file mode 100644 index 0000000..74e0dee --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_types/__init__.py @@ -0,0 +1,432 @@ +import math +import sys +import types +from dataclasses import dataclass +from datetime import tzinfo +from typing import TYPE_CHECKING, Any, Callable, Iterator, Optional, SupportsFloat, SupportsIndex, TypeVar, Union + +if sys.version_info < (3, 8): + from typing_extensions import Protocol, runtime_checkable +else: + from typing import Protocol, runtime_checkable + +if sys.version_info < (3, 9): + from typing_extensions import Annotated, Literal +else: + from typing import Annotated, Literal + +if sys.version_info < (3, 10): + EllipsisType = type(Ellipsis) + KW_ONLY = {} + SLOTS = {} +else: + from types import EllipsisType + + KW_ONLY = {"kw_only": True} + SLOTS = {"slots": True} + + +__all__ = ( + 'BaseMetadata', + 'GroupedMetadata', + 'Gt', + 'Ge', + 'Lt', + 'Le', + 'Interval', + 'MultipleOf', + 'MinLen', + 'MaxLen', + 'Len', + 'Timezone', + 'Predicate', + 'LowerCase', + 'UpperCase', + 'IsDigits', + 'IsFinite', + 'IsNotFinite', + 'IsNan', + 'IsNotNan', + 'IsInfinite', + 'IsNotInfinite', + 'doc', + 'DocInfo', + '__version__', +) + +__version__ = '0.7.0' + + +T = TypeVar('T') + + +# arguments that start with __ are considered +# positional only +# see https://peps.python.org/pep-0484/#positional-only-arguments + + +class SupportsGt(Protocol): + def __gt__(self: T, __other: T) -> bool: + ... + + +class SupportsGe(Protocol): + def __ge__(self: T, __other: T) -> bool: + ... + + +class SupportsLt(Protocol): + def __lt__(self: T, __other: T) -> bool: + ... + + +class SupportsLe(Protocol): + def __le__(self: T, __other: T) -> bool: + ... + + +class SupportsMod(Protocol): + def __mod__(self: T, __other: T) -> T: + ... + + +class SupportsDiv(Protocol): + def __div__(self: T, __other: T) -> T: + ... + + +class BaseMetadata: + """Base class for all metadata. + + This exists mainly so that implementers + can do `isinstance(..., BaseMetadata)` while traversing field annotations. + """ + + __slots__ = () + + +@dataclass(frozen=True, **SLOTS) +class Gt(BaseMetadata): + """Gt(gt=x) implies that the value must be greater than x. + + It can be used with any type that supports the ``>`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + gt: SupportsGt + + +@dataclass(frozen=True, **SLOTS) +class Ge(BaseMetadata): + """Ge(ge=x) implies that the value must be greater than or equal to x. + + It can be used with any type that supports the ``>=`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + ge: SupportsGe + + +@dataclass(frozen=True, **SLOTS) +class Lt(BaseMetadata): + """Lt(lt=x) implies that the value must be less than x. + + It can be used with any type that supports the ``<`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + lt: SupportsLt + + +@dataclass(frozen=True, **SLOTS) +class Le(BaseMetadata): + """Le(le=x) implies that the value must be less than or equal to x. + + It can be used with any type that supports the ``<=`` operator, + including numbers, dates and times, strings, sets, and so on. + """ + + le: SupportsLe + + +@runtime_checkable +class GroupedMetadata(Protocol): + """A grouping of multiple objects, like typing.Unpack. + + `GroupedMetadata` on its own is not metadata and has no meaning. + All of the constraints and metadata should be fully expressable + in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`. + + Concrete implementations should override `GroupedMetadata.__iter__()` + to add their own metadata. + For example: + + >>> @dataclass + >>> class Field(GroupedMetadata): + >>> gt: float | None = None + >>> description: str | None = None + ... + >>> def __iter__(self) -> Iterable[object]: + >>> if self.gt is not None: + >>> yield Gt(self.gt) + >>> if self.description is not None: + >>> yield Description(self.gt) + + Also see the implementation of `Interval` below for an example. + + Parsers should recognize this and unpack it so that it can be used + both with and without unpacking: + + - `Annotated[int, Field(...)]` (parser must unpack Field) + - `Annotated[int, *Field(...)]` (PEP-646) + """ # noqa: trailing-whitespace + + @property + def __is_annotated_types_grouped_metadata__(self) -> Literal[True]: + return True + + def __iter__(self) -> Iterator[object]: + ... + + if not TYPE_CHECKING: + __slots__ = () # allow subclasses to use slots + + def __init_subclass__(cls, *args: Any, **kwargs: Any) -> None: + # Basic ABC like functionality without the complexity of an ABC + super().__init_subclass__(*args, **kwargs) + if cls.__iter__ is GroupedMetadata.__iter__: + raise TypeError("Can't subclass GroupedMetadata without implementing __iter__") + + def __iter__(self) -> Iterator[object]: # noqa: F811 + raise NotImplementedError # more helpful than "None has no attribute..." type errors + + +@dataclass(frozen=True, **KW_ONLY, **SLOTS) +class Interval(GroupedMetadata): + """Interval can express inclusive or exclusive bounds with a single object. + + It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which + are interpreted the same way as the single-bound constraints. + """ + + gt: Union[SupportsGt, None] = None + ge: Union[SupportsGe, None] = None + lt: Union[SupportsLt, None] = None + le: Union[SupportsLe, None] = None + + def __iter__(self) -> Iterator[BaseMetadata]: + """Unpack an Interval into zero or more single-bounds.""" + if self.gt is not None: + yield Gt(self.gt) + if self.ge is not None: + yield Ge(self.ge) + if self.lt is not None: + yield Lt(self.lt) + if self.le is not None: + yield Le(self.le) + + +@dataclass(frozen=True, **SLOTS) +class MultipleOf(BaseMetadata): + """MultipleOf(multiple_of=x) might be interpreted in two ways: + + 1. Python semantics, implying ``value % multiple_of == 0``, or + 2. JSONschema semantics, where ``int(value / multiple_of) == value / multiple_of`` + + We encourage users to be aware of these two common interpretations, + and libraries to carefully document which they implement. + """ + + multiple_of: Union[SupportsDiv, SupportsMod] + + +@dataclass(frozen=True, **SLOTS) +class MinLen(BaseMetadata): + """ + MinLen() implies minimum inclusive length, + e.g. ``len(value) >= min_length``. + """ + + min_length: Annotated[int, Ge(0)] + + +@dataclass(frozen=True, **SLOTS) +class MaxLen(BaseMetadata): + """ + MaxLen() implies maximum inclusive length, + e.g. ``len(value) <= max_length``. + """ + + max_length: Annotated[int, Ge(0)] + + +@dataclass(frozen=True, **SLOTS) +class Len(GroupedMetadata): + """ + Len() implies that ``min_length <= len(value) <= max_length``. + + Upper bound may be omitted or ``None`` to indicate no upper length bound. + """ + + min_length: Annotated[int, Ge(0)] = 0 + max_length: Optional[Annotated[int, Ge(0)]] = None + + def __iter__(self) -> Iterator[BaseMetadata]: + """Unpack a Len into zone or more single-bounds.""" + if self.min_length > 0: + yield MinLen(self.min_length) + if self.max_length is not None: + yield MaxLen(self.max_length) + + +@dataclass(frozen=True, **SLOTS) +class Timezone(BaseMetadata): + """Timezone(tz=...) requires a datetime to be aware (or ``tz=None``, naive). + + ``Annotated[datetime, Timezone(None)]`` must be a naive datetime. + ``Timezone[...]`` (the ellipsis literal) expresses that the datetime must be + tz-aware but any timezone is allowed. + + You may also pass a specific timezone string or tzinfo object such as + ``Timezone(timezone.utc)`` or ``Timezone("Africa/Abidjan")`` to express that + you only allow a specific timezone, though we note that this is often + a symptom of poor design. + """ + + tz: Union[str, tzinfo, EllipsisType, None] + + +@dataclass(frozen=True, **SLOTS) +class Unit(BaseMetadata): + """Indicates that the value is a physical quantity with the specified unit. + + It is intended for usage with numeric types, where the value represents the + magnitude of the quantity. For example, ``distance: Annotated[float, Unit('m')]`` + or ``speed: Annotated[float, Unit('m/s')]``. + + Interpretation of the unit string is left to the discretion of the consumer. + It is suggested to follow conventions established by python libraries that work + with physical quantities, such as + + - ``pint`` : + - ``astropy.units``: + + For indicating a quantity with a certain dimensionality but without a specific unit + it is recommended to use square brackets, e.g. `Annotated[float, Unit('[time]')]`. + Note, however, ``annotated_types`` itself makes no use of the unit string. + """ + + unit: str + + +@dataclass(frozen=True, **SLOTS) +class Predicate(BaseMetadata): + """``Predicate(func: Callable)`` implies `func(value)` is truthy for valid values. + + Users should prefer statically inspectable metadata, but if you need the full + power and flexibility of arbitrary runtime predicates... here it is. + + We provide a few predefined predicates for common string constraints: + ``IsLower = Predicate(str.islower)``, ``IsUpper = Predicate(str.isupper)``, and + ``IsDigits = Predicate(str.isdigit)``. Users are encouraged to use methods which + can be given special handling, and avoid indirection like ``lambda s: s.lower()``. + + Some libraries might have special logic to handle certain predicates, e.g. by + checking for `str.isdigit` and using its presence to both call custom logic to + enforce digit-only strings, and customise some generated external schema. + + We do not specify what behaviour should be expected for predicates that raise + an exception. For example `Annotated[int, Predicate(str.isdigit)]` might silently + skip invalid constraints, or statically raise an error; or it might try calling it + and then propagate or discard the resulting exception. + """ + + func: Callable[[Any], bool] + + def __repr__(self) -> str: + if getattr(self.func, "__name__", "") == "": + return f"{self.__class__.__name__}({self.func!r})" + if isinstance(self.func, (types.MethodType, types.BuiltinMethodType)) and ( + namespace := getattr(self.func.__self__, "__name__", None) + ): + return f"{self.__class__.__name__}({namespace}.{self.func.__name__})" + if isinstance(self.func, type(str.isascii)): # method descriptor + return f"{self.__class__.__name__}({self.func.__qualname__})" + return f"{self.__class__.__name__}({self.func.__name__})" + + +@dataclass +class Not: + func: Callable[[Any], bool] + + def __call__(self, __v: Any) -> bool: + return not self.func(__v) + + +_StrType = TypeVar("_StrType", bound=str) + +LowerCase = Annotated[_StrType, Predicate(str.islower)] +""" +Return True if the string is a lowercase string, False otherwise. + +A string is lowercase if all cased characters in the string are lowercase and there is at least one cased character in the string. +""" # noqa: E501 +UpperCase = Annotated[_StrType, Predicate(str.isupper)] +""" +Return True if the string is an uppercase string, False otherwise. + +A string is uppercase if all cased characters in the string are uppercase and there is at least one cased character in the string. +""" # noqa: E501 +IsDigit = Annotated[_StrType, Predicate(str.isdigit)] +IsDigits = IsDigit # type: ignore # plural for backwards compatibility, see #63 +""" +Return True if the string is a digit string, False otherwise. + +A string is a digit string if all characters in the string are digits and there is at least one character in the string. +""" # noqa: E501 +IsAscii = Annotated[_StrType, Predicate(str.isascii)] +""" +Return True if all characters in the string are ASCII, False otherwise. + +ASCII characters have code points in the range U+0000-U+007F. Empty string is ASCII too. +""" + +_NumericType = TypeVar('_NumericType', bound=Union[SupportsFloat, SupportsIndex]) +IsFinite = Annotated[_NumericType, Predicate(math.isfinite)] +"""Return True if x is neither an infinity nor a NaN, and False otherwise.""" +IsNotFinite = Annotated[_NumericType, Predicate(Not(math.isfinite))] +"""Return True if x is one of infinity or NaN, and False otherwise""" +IsNan = Annotated[_NumericType, Predicate(math.isnan)] +"""Return True if x is a NaN (not a number), and False otherwise.""" +IsNotNan = Annotated[_NumericType, Predicate(Not(math.isnan))] +"""Return True if x is anything but NaN (not a number), and False otherwise.""" +IsInfinite = Annotated[_NumericType, Predicate(math.isinf)] +"""Return True if x is a positive or negative infinity, and False otherwise.""" +IsNotInfinite = Annotated[_NumericType, Predicate(Not(math.isinf))] +"""Return True if x is neither a positive or negative infinity, and False otherwise.""" + +try: + from typing_extensions import DocInfo, doc # type: ignore [attr-defined] +except ImportError: + + @dataclass(frozen=True, **SLOTS) + class DocInfo: # type: ignore [no-redef] + """ " + The return value of doc(), mainly to be used by tools that want to extract the + Annotated documentation at runtime. + """ + + documentation: str + """The documentation string passed to doc().""" + + def doc( + documentation: str, + ) -> DocInfo: + """ + Add documentation to a type annotation inside of Annotated. + + For example: + + >>> def hi(name: Annotated[int, doc("The name of the user")]) -> None: ... + """ + return DocInfo(documentation) diff --git a/venv/lib/python3.11/site-packages/annotated_types/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/annotated_types/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..4e478e5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/annotated_types/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/annotated_types/__pycache__/test_cases.cpython-311.pyc b/venv/lib/python3.11/site-packages/annotated_types/__pycache__/test_cases.cpython-311.pyc new file mode 100644 index 0000000..f122bb1 Binary files /dev/null and b/venv/lib/python3.11/site-packages/annotated_types/__pycache__/test_cases.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/annotated_types/py.typed b/venv/lib/python3.11/site-packages/annotated_types/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/annotated_types/test_cases.py b/venv/lib/python3.11/site-packages/annotated_types/test_cases.py new file mode 100644 index 0000000..d9164d6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/annotated_types/test_cases.py @@ -0,0 +1,151 @@ +import math +import sys +from datetime import date, datetime, timedelta, timezone +from decimal import Decimal +from typing import Any, Dict, Iterable, Iterator, List, NamedTuple, Set, Tuple + +if sys.version_info < (3, 9): + from typing_extensions import Annotated +else: + from typing import Annotated + +import annotated_types as at + + +class Case(NamedTuple): + """ + A test case for `annotated_types`. + """ + + annotation: Any + valid_cases: Iterable[Any] + invalid_cases: Iterable[Any] + + +def cases() -> Iterable[Case]: + # Gt, Ge, Lt, Le + yield Case(Annotated[int, at.Gt(4)], (5, 6, 1000), (4, 0, -1)) + yield Case(Annotated[float, at.Gt(0.5)], (0.6, 0.7, 0.8, 0.9), (0.5, 0.0, -0.1)) + yield Case( + Annotated[datetime, at.Gt(datetime(2000, 1, 1))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(2000, 1, 1), datetime(1999, 12, 31)], + ) + yield Case( + Annotated[datetime, at.Gt(date(2000, 1, 1))], + [date(2000, 1, 2), date(2000, 1, 3)], + [date(2000, 1, 1), date(1999, 12, 31)], + ) + yield Case( + Annotated[datetime, at.Gt(Decimal('1.123'))], + [Decimal('1.1231'), Decimal('123')], + [Decimal('1.123'), Decimal('0')], + ) + + yield Case(Annotated[int, at.Ge(4)], (4, 5, 6, 1000, 4), (0, -1)) + yield Case(Annotated[float, at.Ge(0.5)], (0.5, 0.6, 0.7, 0.8, 0.9), (0.4, 0.0, -0.1)) + yield Case( + Annotated[datetime, at.Ge(datetime(2000, 1, 1))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(1998, 1, 1), datetime(1999, 12, 31)], + ) + + yield Case(Annotated[int, at.Lt(4)], (0, -1), (4, 5, 6, 1000, 4)) + yield Case(Annotated[float, at.Lt(0.5)], (0.4, 0.0, -0.1), (0.5, 0.6, 0.7, 0.8, 0.9)) + yield Case( + Annotated[datetime, at.Lt(datetime(2000, 1, 1))], + [datetime(1999, 12, 31), datetime(1999, 12, 31)], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + ) + + yield Case(Annotated[int, at.Le(4)], (4, 0, -1), (5, 6, 1000)) + yield Case(Annotated[float, at.Le(0.5)], (0.5, 0.0, -0.1), (0.6, 0.7, 0.8, 0.9)) + yield Case( + Annotated[datetime, at.Le(datetime(2000, 1, 1))], + [datetime(2000, 1, 1), datetime(1999, 12, 31)], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + ) + + # Interval + yield Case(Annotated[int, at.Interval(gt=4)], (5, 6, 1000), (4, 0, -1)) + yield Case(Annotated[int, at.Interval(gt=4, lt=10)], (5, 6), (4, 10, 1000, 0, -1)) + yield Case(Annotated[float, at.Interval(ge=0.5, le=1)], (0.5, 0.9, 1), (0.49, 1.1)) + yield Case( + Annotated[datetime, at.Interval(gt=datetime(2000, 1, 1), le=datetime(2000, 1, 3))], + [datetime(2000, 1, 2), datetime(2000, 1, 3)], + [datetime(2000, 1, 1), datetime(2000, 1, 4)], + ) + + yield Case(Annotated[int, at.MultipleOf(multiple_of=3)], (0, 3, 9), (1, 2, 4)) + yield Case(Annotated[float, at.MultipleOf(multiple_of=0.5)], (0, 0.5, 1, 1.5), (0.4, 1.1)) + + # lengths + + yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) + yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12')) + yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) + yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2])) + + yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10)) + yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10)) + yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) + yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10)) + + yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10)) + yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234')) + + yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}]) + yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4})) + yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4))) + + # Timezone + + yield Case( + Annotated[datetime, at.Timezone(None)], [datetime(2000, 1, 1)], [datetime(2000, 1, 1, tzinfo=timezone.utc)] + ) + yield Case( + Annotated[datetime, at.Timezone(...)], [datetime(2000, 1, 1, tzinfo=timezone.utc)], [datetime(2000, 1, 1)] + ) + yield Case( + Annotated[datetime, at.Timezone(timezone.utc)], + [datetime(2000, 1, 1, tzinfo=timezone.utc)], + [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], + ) + yield Case( + Annotated[datetime, at.Timezone('Europe/London')], + [datetime(2000, 1, 1, tzinfo=timezone(timedelta(0), name='Europe/London'))], + [datetime(2000, 1, 1), datetime(2000, 1, 1, tzinfo=timezone(timedelta(hours=6)))], + ) + + # Quantity + + yield Case(Annotated[float, at.Unit(unit='m')], (5, 4.2), ('5m', '4.2m')) + + # predicate types + + yield Case(at.LowerCase[str], ['abc', 'foobar'], ['', 'A', 'Boom']) + yield Case(at.UpperCase[str], ['ABC', 'DEFO'], ['', 'a', 'abc', 'AbC']) + yield Case(at.IsDigit[str], ['123'], ['', 'ab', 'a1b2']) + yield Case(at.IsAscii[str], ['123', 'foo bar'], ['£100', '😊', 'whatever 👀']) + + yield Case(Annotated[int, at.Predicate(lambda x: x % 2 == 0)], [0, 2, 4], [1, 3, 5]) + + yield Case(at.IsFinite[float], [1.23], [math.nan, math.inf, -math.inf]) + yield Case(at.IsNotFinite[float], [math.nan, math.inf], [1.23]) + yield Case(at.IsNan[float], [math.nan], [1.23, math.inf]) + yield Case(at.IsNotNan[float], [1.23, math.inf], [math.nan]) + yield Case(at.IsInfinite[float], [math.inf], [math.nan, 1.23]) + yield Case(at.IsNotInfinite[float], [math.nan, 1.23], [math.inf]) + + # check stacked predicates + yield Case(at.IsInfinite[Annotated[float, at.Predicate(lambda x: x > 0)]], [math.inf], [-math.inf, 1.23, math.nan]) + + # doc + yield Case(Annotated[int, at.doc("A number")], [1, 2], []) + + # custom GroupedMetadata + class MyCustomGroupedMetadata(at.GroupedMetadata): + def __iter__(self) -> Iterator[at.Predicate]: + yield at.Predicate(lambda x: float(x).is_integer()) + + yield Case(Annotated[float, MyCustomGroupedMetadata()], [0, 2.0], [0.01, 1.5]) diff --git a/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/METADATA b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/METADATA new file mode 100644 index 0000000..73ee491 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/METADATA @@ -0,0 +1,107 @@ +Metadata-Version: 2.4 +Name: anyio +Version: 4.14.1 +Summary: High-level concurrency and networking framework on top of asyncio or Trio +Author-email: Alex Grönholm +License-Expression: MIT +Project-URL: Documentation, https://anyio.readthedocs.io/en/latest/ +Project-URL: Changelog, https://anyio.readthedocs.io/en/stable/versionhistory.html +Project-URL: Source code, https://github.com/agronholm/anyio +Project-URL: Issue tracker, https://github.com/agronholm/anyio/issues +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Framework :: AnyIO +Classifier: Typing :: Typed +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +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.15 +Requires-Python: >=3.10 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: exceptiongroup>=1.0.2; python_version < "3.11" +Requires-Dist: idna>=2.8 +Requires-Dist: typing_extensions>=4.5; python_version < "3.13" +Provides-Extra: trio +Requires-Dist: trio>=0.32.0; extra == "trio" +Dynamic: license-file + +.. image:: https://github.com/agronholm/anyio/actions/workflows/test.yml/badge.svg + :target: https://github.com/agronholm/anyio/actions/workflows/test.yml + :alt: Build Status +.. image:: https://coveralls.io/repos/github/agronholm/anyio/badge.svg?branch=master + :target: https://coveralls.io/github/agronholm/anyio?branch=master + :alt: Code Coverage +.. image:: https://readthedocs.org/projects/anyio/badge/?version=latest + :target: https://anyio.readthedocs.io/en/latest/?badge=latest + :alt: Documentation +.. image:: https://badges.gitter.im/gitterHQ/gitter.svg + :target: https://gitter.im/python-trio/AnyIO + :alt: Gitter chat +.. image:: https://tidelift.com/badges/package/pypi/anyio + :target: https://tidelift.com/subscription/pkg/pypi-anyio + :alt: Tidelift + +AnyIO is an asynchronous networking and concurrency library that works on top of either asyncio_ or +Trio_. It implements Trio-like `structured concurrency`_ (SC) on top of asyncio and works in harmony +with the native SC of Trio itself. + +Applications and libraries written against AnyIO's API will run unmodified on either asyncio_ or +Trio_. AnyIO can also be adopted into a library or application incrementally – bit by bit, no full +refactoring necessary. It will blend in with the native libraries of your chosen backend. + +To find out why you might want to use AnyIO's APIs instead of asyncio's, you can read about it +`here `_. + +Documentation +------------- + +View full documentation at: https://anyio.readthedocs.io/ + +Features +-------- + +AnyIO offers the following functionality: + +* Task groups (nurseries_ in trio terminology) +* High-level networking (TCP, UDP and UNIX sockets) + + * `Happy eyeballs`_ algorithm for TCP connections (more robust than that of asyncio on Python + 3.8) + * async/await style UDP sockets (unlike asyncio where you still have to use Transports and + Protocols) + +* A versatile API for byte streams and object streams +* Inter-task synchronization and communication (locks, conditions, events, semaphores, object + streams) +* Worker threads +* Subprocesses +* Subinterpreter support for code parallelization (on Python 3.13 and later) +* Asynchronous file I/O (using worker threads) +* Signal handling +* Asynchronous versions of the functools_ and itertools_ modules + +AnyIO also comes with its own pytest_ plugin which also supports asynchronous fixtures. +It even works with the popular Hypothesis_ library. + +.. _asyncio: https://docs.python.org/3/library/asyncio.html +.. _Trio: https://github.com/python-trio/trio +.. _structured concurrency: https://en.wikipedia.org/wiki/Structured_concurrency +.. _nurseries: https://trio.readthedocs.io/en/stable/reference-core.html#nurseries-and-spawning +.. _Happy eyeballs: https://en.wikipedia.org/wiki/Happy_Eyeballs +.. _pytest: https://docs.pytest.org/en/latest/ +.. _functools: https://docs.python.org/3/library/functools.html +.. _itertools: https://docs.python.org/3/library/itertools.html +.. _Hypothesis: https://hypothesis.works/ + +Security contact information +---------------------------- + +To report a security vulnerability, please use the `Tidelift security contact`_. +Tidelift will coordinate the fix and disclosure. + +.. _Tidelift security contact: https://tidelift.com/security diff --git a/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/RECORD b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/RECORD new file mode 100644 index 0000000..1525f9b --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/RECORD @@ -0,0 +1,96 @@ +anyio-4.14.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +anyio-4.14.1.dist-info/METADATA,sha256=bfkjYaZLYPsPI5JV_Gn7HYF65mteyE8nhjaI0ZqC4L4,4645 +anyio-4.14.1.dist-info/RECORD,, +anyio-4.14.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 +anyio-4.14.1.dist-info/entry_points.txt,sha256=_d6Yu6uiaZmNe0CydowirE9Cmg7zUL2g08tQpoS3Qvc,39 +anyio-4.14.1.dist-info/licenses/LICENSE,sha256=U2GsncWPLvX9LpsJxoKXwX8ElQkJu8gCO9uC6s8iwrA,1081 +anyio-4.14.1.dist-info/scm_file_list.json,sha256=wDSXGv8Ehn5ZW5BhB-RlaAc16zY_OfO27qrlMfMMZy8,3654 +anyio-4.14.1.dist-info/scm_version.json,sha256=gw22Q2aBbdiYhyMbObTYNN7BN-wSpzOCktNiAuulRN8,161 +anyio-4.14.1.dist-info/top_level.txt,sha256=QglSMiWX8_5dpoVAEIHdEYzvqFMdSYWmCj6tYw2ITkQ,6 +anyio/__init__.py,sha256=HitUIfzvAojSeaHVmJ9rFn8k_yI63G6s_jUL2QChf4U,6405 +anyio/__pycache__/__init__.cpython-311.pyc,, +anyio/__pycache__/from_thread.cpython-311.pyc,, +anyio/__pycache__/functools.cpython-311.pyc,, +anyio/__pycache__/itertools.cpython-311.pyc,, +anyio/__pycache__/lowlevel.cpython-311.pyc,, +anyio/__pycache__/pytest_plugin.cpython-311.pyc,, +anyio/__pycache__/to_interpreter.cpython-311.pyc,, +anyio/__pycache__/to_process.cpython-311.pyc,, +anyio/__pycache__/to_thread.cpython-311.pyc,, +anyio/_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/_backends/__pycache__/__init__.cpython-311.pyc,, +anyio/_backends/__pycache__/_asyncio.cpython-311.pyc,, +anyio/_backends/__pycache__/_trio.cpython-311.pyc,, +anyio/_backends/_asyncio.py,sha256=-q-5gUYg_r5SsN-OYbQnF_lvtW0v51-dFlsU8_gduWA,102077 +anyio/_backends/_trio.py,sha256=vR0ZgxVnOo4AHhHcHVG0worMc-3ZNpAZ6Vxh0m0ZZC0,45189 +anyio/_core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/_core/__pycache__/__init__.cpython-311.pyc,, +anyio/_core/__pycache__/_asyncio_selector_thread.cpython-311.pyc,, +anyio/_core/__pycache__/_contextmanagers.cpython-311.pyc,, +anyio/_core/__pycache__/_eventloop.cpython-311.pyc,, +anyio/_core/__pycache__/_exceptions.cpython-311.pyc,, +anyio/_core/__pycache__/_fileio.cpython-311.pyc,, +anyio/_core/__pycache__/_resources.cpython-311.pyc,, +anyio/_core/__pycache__/_signals.cpython-311.pyc,, +anyio/_core/__pycache__/_sockets.cpython-311.pyc,, +anyio/_core/__pycache__/_streams.cpython-311.pyc,, +anyio/_core/__pycache__/_subprocesses.cpython-311.pyc,, +anyio/_core/__pycache__/_synchronization.cpython-311.pyc,, +anyio/_core/__pycache__/_tasks.cpython-311.pyc,, +anyio/_core/__pycache__/_tempfile.cpython-311.pyc,, +anyio/_core/__pycache__/_testing.cpython-311.pyc,, +anyio/_core/__pycache__/_typedattr.cpython-311.pyc,, +anyio/_core/_asyncio_selector_thread.py,sha256=2PdxFM3cs02Kp6BSppbvmRT7q7asreTW5FgBxEsflBo,5626 +anyio/_core/_contextmanagers.py,sha256=YInBCabiEeS-UaP_Jdxa1CaFC71ETPW8HZTHIM8Rsc8,7215 +anyio/_core/_eventloop.py,sha256=ByZUeJD9alMfcyTseRo5IzTO0IltEul_Gyq9iqSjqDk,6658 +anyio/_core/_exceptions.py,sha256=OfzLO4Z3Hog1TnipbIn72YNtkoYxS4lHW9MqKDeGc88,4936 +anyio/_core/_fileio.py,sha256=hHfyV0bXDL-R2ZNnInwse3nmTAd36AIz1cBxgmAwzAQ,31358 +anyio/_core/_resources.py,sha256=NbmU5O5UX3xEyACnkmYX28Fmwdl-f-ny0tHym26e0w0,435 +anyio/_core/_signals.py,sha256=mjTBB2hTKNPRlU0IhnijeQedpWOGERDiMjSlJQsFrug,1016 +anyio/_core/_sockets.py,sha256=9FU423j52XBBfGVr6MdzPTdyw8bGrzApZ5m338-AtsY,35286 +anyio/_core/_streams.py,sha256=FczFwIgDpnkK0bODWJXMpsUJYdvAD04kaUaGzJU8DK0,1806 +anyio/_core/_subprocesses.py,sha256=tkmkPKEkEaiMD8C9WRZBlmgjOYRDRbZdte6e-unay2E,7916 +anyio/_core/_synchronization.py,sha256=jn2nIbTRlBAUXL-mx_a3I_VnasF8GbVFpBRp2-YwCx0,21591 +anyio/_core/_tasks.py,sha256=ELL2jscaSW0Jw_xA6MtQlm3xwvFEzjTbc1u9Tteyt0I,13244 +anyio/_core/_tempfile.py,sha256=jE2w59FRF3yRo4vjkjfZF2YcqsBZvc66VWRwrJGDYGk,19624 +anyio/_core/_testing.py,sha256=u7MPqGXwpTxqI7hclSdNA30z2GH1Nw258uwKvy_RfBg,2340 +anyio/_core/_typedattr.py,sha256=P4ozZikn3-DbpoYcvyghS_FOYAgbmUxeoU8-L_07pZM,2508 +anyio/abc/__init__.py,sha256=6mWhcl_pGXhrgZVHP_TCfMvIXIOp9mroEFM90fYCU_U,2869 +anyio/abc/__pycache__/__init__.cpython-311.pyc,, +anyio/abc/__pycache__/_eventloop.cpython-311.pyc,, +anyio/abc/__pycache__/_resources.cpython-311.pyc,, +anyio/abc/__pycache__/_sockets.cpython-311.pyc,, +anyio/abc/__pycache__/_streams.cpython-311.pyc,, +anyio/abc/__pycache__/_subprocesses.cpython-311.pyc,, +anyio/abc/__pycache__/_tasks.cpython-311.pyc,, +anyio/abc/__pycache__/_testing.cpython-311.pyc,, +anyio/abc/_eventloop.py,sha256=OqWYSEj0TmwL_xniCJt3_jHFWsuMk9THk8tCTGsKapI,10681 +anyio/abc/_resources.py,sha256=DrYvkNN1hH6Uvv5_5uKySvDsnknGVDe8FCKfko0VtN8,783 +anyio/abc/_sockets.py,sha256=OmVDrfemVvF9c5K1tpBgQyV6fn5v0XyCExLAqBOGz9o,13124 +anyio/abc/_streams.py,sha256=HYvna1iZbWcwLROTO6IhLX79RTRLPShZMWe0sG1q54I,7481 +anyio/abc/_subprocesses.py,sha256=cumAPJTktOQtw63IqG0lDpyZqu_l1EElvQHMiwJgL08,2067 +anyio/abc/_tasks.py,sha256=m-FtE4phxeNIELSG7A3H7VUz3jA2Ib5J2JIew8-PS6o,6642 +anyio/abc/_testing.py,sha256=9YYM2AXsYFvf4PLjUEr6yRxDiUeB5QbY_gOg0X_C6lY,2034 +anyio/from_thread.py,sha256=JYsbaCaIB_Iit6kNhtXSteJGt4PcQ7ncq0nIpcelIrg,19265 +anyio/functools.py,sha256=T4JS8IXq-x1S0Lbo2owF8l9fza2KypO147QLeyz4cjs,11797 +anyio/itertools.py,sha256=QV-9mnRCr2yBph8g01QFvN-bQ_Yle-8Sl13YSydBlMI,16168 +anyio/lowlevel.py,sha256=WPtppHfI2qs1nokzjn8elL8LvyqI05AK5Zslhlo71A4,6242 +anyio/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/pytest_plugin.py,sha256=paMpI_VMNQf2bir0LfvgMpXSiYJoHDzWdKUVTyoHmvQ,13609 +anyio/streams/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +anyio/streams/__pycache__/__init__.cpython-311.pyc,, +anyio/streams/__pycache__/buffered.cpython-311.pyc,, +anyio/streams/__pycache__/file.cpython-311.pyc,, +anyio/streams/__pycache__/memory.cpython-311.pyc,, +anyio/streams/__pycache__/stapled.cpython-311.pyc,, +anyio/streams/__pycache__/text.cpython-311.pyc,, +anyio/streams/__pycache__/tls.cpython-311.pyc,, +anyio/streams/buffered.py,sha256=v3xKtjFHgNV41g2SvMAkA_qd2t9WYlCI1_lNGCAatw0,6650 +anyio/streams/file.py,sha256=msnrotVKGMQomUu_Rj2qz9MvIdUp6d3JGr7MOEO8kV4,4428 +anyio/streams/memory.py,sha256=F0zwzvFJKAhX_LRZGoKzzqDC2oMM-f-yyTBrEYEGOaU,10740 +anyio/streams/stapled.py,sha256=T8Xqwf8K6EgURPxbt1N4i7A8BAk-gScv-GRhjLXIf_o,4390 +anyio/streams/text.py,sha256=BcVAGJw1VRvtIqnv-o0Rb0pwH7p8vwlvl21xHq522ag,5765 +anyio/streams/tls.py,sha256=DQVkXUvsTEYKkBO8dlVU7j_5H8QOtLy4sGi1Wrjqevo,15303 +anyio/to_interpreter.py,sha256=_mLngrMy97TMR6VbW4Y6YzDUk9ZuPcQMPlkuyRh3C9k,7100 +anyio/to_process.py,sha256=68qhLfce7MeXysid4fOpmhfWkgdo7Z7-9BC0VyUciIE,9809 +anyio/to_thread.py,sha256=f6h_k2d743GBv9FhAnhM_YpTvWgIrzBy9cOE0eJ1UJw,2693 diff --git a/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/WHEEL b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/WHEEL new file mode 100644 index 0000000..14a883f --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/entry_points.txt b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/entry_points.txt new file mode 100644 index 0000000..44dd9bd --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[pytest11] +anyio = anyio.pytest_plugin diff --git a/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/licenses/LICENSE new file mode 100644 index 0000000..104eebf --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/licenses/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2018 Alex Grönholm + +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.11/site-packages/anyio-4.14.1.dist-info/scm_file_list.json b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/scm_file_list.json new file mode 100644 index 0000000..72a4814 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/scm_file_list.json @@ -0,0 +1,119 @@ +{ + "files": [ + ".pre-commit-config.yaml", + "LICENSE", + "pyproject.toml", + "AGENTS.md", + "README.rst", + "CLAUDE.md", + ".readthedocs.yml", + ".gitignore", + "docs/tempfile.rst", + "docs/signals.rst", + "docs/synchronization.rst", + "docs/contextmanagers.rst", + "docs/testing.rst", + "docs/networking.rst", + "docs/contributing.rst", + "docs/index.rst", + "docs/versionhistory.rst", + "docs/threads.rst", + "docs/api.rst", + "docs/typedattrs.rst", + "docs/basics.rst", + "docs/fileio.rst", + "docs/cancellation.rst", + "docs/support.rst", + "docs/streams.rst", + "docs/why.rst", + "docs/tasks.rst", + "docs/migration.rst", + "docs/conf.py", + "docs/subprocesses.rst", + "docs/faq.rst", + "docs/subinterpreters.rst", + "src/anyio/functools.py", + "src/anyio/py.typed", + "src/anyio/__init__.py", + "src/anyio/pytest_plugin.py", + "src/anyio/itertools.py", + "src/anyio/to_interpreter.py", + "src/anyio/from_thread.py", + "src/anyio/to_process.py", + "src/anyio/to_thread.py", + "src/anyio/lowlevel.py", + "src/anyio/_backends/_trio.py", + "src/anyio/_backends/__init__.py", + "src/anyio/_backends/_asyncio.py", + "src/anyio/streams/memory.py", + "src/anyio/streams/__init__.py", + "src/anyio/streams/tls.py", + "src/anyio/streams/file.py", + "src/anyio/streams/text.py", + "src/anyio/streams/stapled.py", + "src/anyio/streams/buffered.py", + "src/anyio/abc/_eventloop.py", + "src/anyio/abc/__init__.py", + "src/anyio/abc/_sockets.py", + "src/anyio/abc/_tasks.py", + "src/anyio/abc/_subprocesses.py", + "src/anyio/abc/_resources.py", + "src/anyio/abc/_streams.py", + "src/anyio/abc/_testing.py", + "src/anyio/_core/_typedattr.py", + "src/anyio/_core/_eventloop.py", + "src/anyio/_core/__init__.py", + "src/anyio/_core/_tempfile.py", + "src/anyio/_core/_sockets.py", + "src/anyio/_core/_tasks.py", + "src/anyio/_core/_fileio.py", + "src/anyio/_core/_synchronization.py", + "src/anyio/_core/_subprocesses.py", + "src/anyio/_core/_resources.py", + "src/anyio/_core/_contextmanagers.py", + "src/anyio/_core/_exceptions.py", + "src/anyio/_core/_streams.py", + "src/anyio/_core/_signals.py", + "src/anyio/_core/_asyncio_selector_thread.py", + "src/anyio/_core/_testing.py", + "tests/test_itertools.py", + "tests/test_functools.py", + "tests/test_eventloop.py", + "tests/__init__.py", + "tests/test_to_thread.py", + "tests/test_from_thread.py", + "tests/test_lowlevel.py", + "tests/test_to_interpreter.py", + "tests/test_sockets.py", + "tests/test_typedattr.py", + "tests/test_to_process.py", + "tests/test_all_attributes.py", + "tests/test_synchronization.py", + "tests/test_debugging.py", + "tests/test_contextmanagers.py", + "tests/test_fileio.py", + "tests/conftest.py", + "tests/test_signals.py", + "tests/test_deprecations.py", + "tests/test_tempfile.py", + "tests/test_taskgroups.py", + "tests/test_pytest_plugin.py", + "tests/test_subprocesses.py", + "tests/streams/test_text.py", + "tests/streams/test_memory.py", + "tests/streams/__init__.py", + "tests/streams/test_file.py", + "tests/streams/test_stapled.py", + "tests/streams/test_tls.py", + "tests/streams/test_buffered.py", + ".github/pull_request_template.md", + ".github/dependabot.yml", + ".github/FUNDING.yml", + ".github/ISSUE_TEMPLATE/features_request.yaml", + ".github/ISSUE_TEMPLATE/bug_report.yaml", + ".github/ISSUE_TEMPLATE/config.yml", + ".github/workflows/test.yml", + ".github/workflows/test-downstream.yml", + ".github/workflows/publish.yml" + ] +} diff --git a/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/scm_version.json b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/scm_version.json new file mode 100644 index 0000000..17978b3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/scm_version.json @@ -0,0 +1,8 @@ +{ + "tag": "4.14.1", + "distance": 0, + "node": "g149b9e907618fadf6840a4d3cebad533b0c7d033", + "dirty": false, + "branch": "HEAD", + "node_date": "2026-06-24" +} diff --git a/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/top_level.txt new file mode 100644 index 0000000..c77c069 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio-4.14.1.dist-info/top_level.txt @@ -0,0 +1 @@ +anyio diff --git a/venv/lib/python3.11/site-packages/anyio/__init__.py b/venv/lib/python3.11/site-packages/anyio/__init__.py new file mode 100644 index 0000000..2502c76 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/__init__.py @@ -0,0 +1,115 @@ +from __future__ import annotations + +from ._core._contextmanagers import AsyncContextManagerMixin as AsyncContextManagerMixin +from ._core._contextmanagers import ContextManagerMixin as ContextManagerMixin +from ._core._eventloop import current_time as current_time +from ._core._eventloop import get_all_backends as get_all_backends +from ._core._eventloop import get_available_backends as get_available_backends +from ._core._eventloop import get_cancelled_exc_class as get_cancelled_exc_class +from ._core._eventloop import run as run +from ._core._eventloop import sleep as sleep +from ._core._eventloop import sleep_forever as sleep_forever +from ._core._eventloop import sleep_until as sleep_until +from ._core._exceptions import BrokenResourceError as BrokenResourceError +from ._core._exceptions import BrokenWorkerInterpreter as BrokenWorkerInterpreter +from ._core._exceptions import BrokenWorkerProcess as BrokenWorkerProcess +from ._core._exceptions import BusyResourceError as BusyResourceError +from ._core._exceptions import ClosedResourceError as ClosedResourceError +from ._core._exceptions import ConnectionFailed as ConnectionFailed +from ._core._exceptions import DelimiterNotFound as DelimiterNotFound +from ._core._exceptions import EndOfStream as EndOfStream +from ._core._exceptions import IncompleteRead as IncompleteRead +from ._core._exceptions import NoEventLoopError as NoEventLoopError +from ._core._exceptions import RunFinishedError as RunFinishedError +from ._core._exceptions import TaskCancelled as TaskCancelled +from ._core._exceptions import TaskFailed as TaskFailed +from ._core._exceptions import TaskNotFinished as TaskNotFinished +from ._core._exceptions import TypedAttributeLookupError as TypedAttributeLookupError +from ._core._exceptions import WouldBlock as WouldBlock +from ._core._fileio import AsyncFile as AsyncFile +from ._core._fileio import Path as Path +from ._core._fileio import open_file as open_file +from ._core._fileio import wrap_file as wrap_file +from ._core._resources import aclose_forcefully as aclose_forcefully +from ._core._signals import open_signal_receiver as open_signal_receiver +from ._core._sockets import TCPConnectable as TCPConnectable +from ._core._sockets import UNIXConnectable as UNIXConnectable +from ._core._sockets import as_connectable as as_connectable +from ._core._sockets import connect_tcp as connect_tcp +from ._core._sockets import connect_unix as connect_unix +from ._core._sockets import create_connected_udp_socket as create_connected_udp_socket +from ._core._sockets import ( + create_connected_unix_datagram_socket as create_connected_unix_datagram_socket, +) +from ._core._sockets import create_tcp_listener as create_tcp_listener +from ._core._sockets import create_udp_socket as create_udp_socket +from ._core._sockets import create_unix_datagram_socket as create_unix_datagram_socket +from ._core._sockets import create_unix_listener as create_unix_listener +from ._core._sockets import getaddrinfo as getaddrinfo +from ._core._sockets import getnameinfo as getnameinfo +from ._core._sockets import notify_closing as notify_closing +from ._core._sockets import wait_readable as wait_readable +from ._core._sockets import wait_socket_readable as wait_socket_readable +from ._core._sockets import wait_socket_writable as wait_socket_writable +from ._core._sockets import wait_writable as wait_writable +from ._core._streams import create_memory_object_stream as create_memory_object_stream +from ._core._subprocesses import open_process as open_process +from ._core._subprocesses import run_process as run_process +from ._core._synchronization import CapacityLimiter as CapacityLimiter +from ._core._synchronization import ( + CapacityLimiterStatistics as CapacityLimiterStatistics, +) +from ._core._synchronization import Condition as Condition +from ._core._synchronization import ConditionStatistics as ConditionStatistics +from ._core._synchronization import Event as Event +from ._core._synchronization import EventStatistics as EventStatistics +from ._core._synchronization import Lock as Lock +from ._core._synchronization import LockStatistics as LockStatistics +from ._core._synchronization import ResourceGuard as ResourceGuard +from ._core._synchronization import Semaphore as Semaphore +from ._core._synchronization import SemaphoreStatistics as SemaphoreStatistics +from ._core._tasks import TASK_STATUS_IGNORED as TASK_STATUS_IGNORED +from ._core._tasks import CancelScope as CancelScope +from ._core._tasks import TaskHandle as TaskHandle +from ._core._tasks import create_task_group as create_task_group +from ._core._tasks import current_effective_deadline as current_effective_deadline +from ._core._tasks import fail_after as fail_after +from ._core._tasks import move_on_after as move_on_after +from ._core._tempfile import NamedTemporaryFile as NamedTemporaryFile +from ._core._tempfile import SpooledTemporaryFile as SpooledTemporaryFile +from ._core._tempfile import TemporaryDirectory as TemporaryDirectory +from ._core._tempfile import TemporaryFile as TemporaryFile +from ._core._tempfile import gettempdir as gettempdir +from ._core._tempfile import gettempdirb as gettempdirb +from ._core._tempfile import mkdtemp as mkdtemp +from ._core._tempfile import mkstemp as mkstemp +from ._core._testing import TaskInfo as TaskInfo +from ._core._testing import get_current_task as get_current_task +from ._core._testing import get_running_tasks as get_running_tasks +from ._core._testing import wait_all_tasks_blocked as wait_all_tasks_blocked +from ._core._typedattr import TypedAttributeProvider as TypedAttributeProvider +from ._core._typedattr import TypedAttributeSet as TypedAttributeSet +from ._core._typedattr import typed_attribute as typed_attribute + +# Re-export imports so they look like they live directly in this package +for __value in list(locals().values()): + if getattr(__value, "__module__", "").startswith("anyio."): + __value.__module__ = __name__ + + +del __value + + +def __getattr__(attr: str) -> type[BrokenWorkerInterpreter]: + """Support deprecated aliases.""" + if attr == "BrokenWorkerIntepreter": + import warnings + + warnings.warn( + "The 'BrokenWorkerIntepreter' alias is deprecated, use 'BrokenWorkerInterpreter' instead.", + DeprecationWarning, + stacklevel=2, + ) + return BrokenWorkerInterpreter + + raise AttributeError(f"module {__name__!r} has no attribute {attr!r}") diff --git a/venv/lib/python3.11/site-packages/anyio/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..eb851de Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/__pycache__/from_thread.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/__pycache__/from_thread.cpython-311.pyc new file mode 100644 index 0000000..5fb22d1 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/__pycache__/from_thread.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/__pycache__/functools.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/__pycache__/functools.cpython-311.pyc new file mode 100644 index 0000000..1d5d869 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/__pycache__/functools.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/__pycache__/itertools.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/__pycache__/itertools.cpython-311.pyc new file mode 100644 index 0000000..f386e15 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/__pycache__/itertools.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/__pycache__/lowlevel.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/__pycache__/lowlevel.cpython-311.pyc new file mode 100644 index 0000000..cef7f36 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/__pycache__/lowlevel.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/__pycache__/pytest_plugin.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/__pycache__/pytest_plugin.cpython-311.pyc new file mode 100644 index 0000000..ff0b91a Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/__pycache__/pytest_plugin.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/__pycache__/to_interpreter.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/__pycache__/to_interpreter.cpython-311.pyc new file mode 100644 index 0000000..dc382b0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/__pycache__/to_interpreter.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/__pycache__/to_process.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/__pycache__/to_process.cpython-311.pyc new file mode 100644 index 0000000..e36e468 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/__pycache__/to_process.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/__pycache__/to_thread.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/__pycache__/to_thread.cpython-311.pyc new file mode 100644 index 0000000..b543a17 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/__pycache__/to_thread.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_backends/__init__.py b/venv/lib/python3.11/site-packages/anyio/_backends/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..612c242 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-311.pyc new file mode 100644 index 0000000..8c64c38 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/_asyncio.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/_trio.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/_trio.cpython-311.pyc new file mode 100644 index 0000000..a93c1c8 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_backends/__pycache__/_trio.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py b/venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py new file mode 100644 index 0000000..e6fd955 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_backends/_asyncio.py @@ -0,0 +1,3077 @@ +from __future__ import annotations + +import array +import asyncio +import concurrent.futures +import contextvars +import math +import os +import socket +import sys +import threading +import weakref +from asyncio import ( + AbstractEventLoop, + CancelledError, + all_tasks, + create_task, + current_task, + get_running_loop, + sleep, +) +from asyncio.base_events import _run_until_complete_cb # type: ignore[attr-defined] +from collections import OrderedDict, deque +from collections.abc import ( + AsyncGenerator, + AsyncIterator, + Awaitable, + Callable, + Collection, + Coroutine, + Iterable, + Sequence, +) +from concurrent.futures import Future +from contextlib import AbstractContextManager +from contextvars import Context, copy_context +from dataclasses import dataclass, field +from functools import partial, wraps +from inspect import ( + CORO_RUNNING, + CORO_SUSPENDED, + getcoroutinestate, +) +from io import IOBase +from os import PathLike +from queue import Queue +from signal import Signals +from socket import AddressFamily, SocketKind +from threading import Thread +from types import CodeType, TracebackType +from typing import ( + IO, + TYPE_CHECKING, + Any, + Literal, + ParamSpec, + TypeVar, + cast, +) +from weakref import WeakKeyDictionary + +from .. import ( + CapacityLimiterStatistics, + EventStatistics, + LockStatistics, + TaskInfo, + abc, +) +from .._core._eventloop import ( + claim_worker_thread, + set_current_async_library, + threadlocals, +) +from .._core._exceptions import ( + BrokenResourceError, + BusyResourceError, + ClosedResourceError, + EndOfStream, + RunFinishedError, + WouldBlock, +) +from .._core._sockets import convert_ipv6_sockaddr +from .._core._streams import create_memory_object_stream +from .._core._synchronization import ( + CapacityLimiter as BaseCapacityLimiter, +) +from .._core._synchronization import Event as BaseEvent +from .._core._synchronization import Lock as BaseLock +from .._core._synchronization import ( + ResourceGuard, + SemaphoreStatistics, +) +from .._core._synchronization import Semaphore as BaseSemaphore +from .._core._tasks import CancelScope as BaseCancelScope +from .._core._tasks import TaskHandle +from ..abc import ( + AsyncBackend, + IPSockAddrType, + SocketListener, + UDPPacketType, + UNIXDatagramPacketType, +) +from ..abc._eventloop import StrOrBytesPath +from ..abc._tasks import call_for_coroutine, get_callable_name +from ..lowlevel import RunVar +from ..streams.memory import MemoryObjectReceiveStream, MemoryObjectSendStream + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike +else: + FileDescriptorLike = object + +if sys.version_info >= (3, 11): + from asyncio import Runner + from typing import TypeVarTuple, Unpack +else: + import contextvars + import enum + import signal + from asyncio import coroutines, events, exceptions, tasks + + from exceptiongroup import BaseExceptionGroup + from typing_extensions import TypeVarTuple, Unpack + + class _State(enum.Enum): + CREATED = "created" + INITIALIZED = "initialized" + CLOSED = "closed" + + class Runner: + # Copied from CPython 3.11 + def __init__( + self, + *, + debug: bool | None = None, + loop_factory: Callable[[], AbstractEventLoop] | None = None, + ): + self._state = _State.CREATED + self._debug = debug + self._loop_factory = loop_factory + self._loop: AbstractEventLoop | None = None + self._context = None + self._interrupt_count = 0 + self._set_event_loop = False + + def __enter__(self) -> Runner: + self._lazy_init() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def close(self) -> None: + """Shutdown and close event loop.""" + loop = self._loop + if self._state is not _State.INITIALIZED or loop is None: + return + try: + _cancel_all_tasks(loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + if hasattr(loop, "shutdown_default_executor"): + loop.run_until_complete(loop.shutdown_default_executor()) + else: + loop.run_until_complete(_shutdown_default_executor(loop)) + finally: + if self._set_event_loop: + events.set_event_loop(None) + loop.close() + self._loop = None + self._state = _State.CLOSED + + def get_loop(self) -> AbstractEventLoop: + """Return embedded event loop.""" + self._lazy_init() + return self._loop + + def run(self, coro: Coroutine[T_Retval], *, context=None) -> T_Retval: + """Run a coroutine inside the embedded event loop.""" + if not coroutines.iscoroutine(coro): + raise ValueError(f"a coroutine was expected, got {coro!r}") + + if events._get_running_loop() is not None: + # fail fast with short traceback + raise RuntimeError( + "Runner.run() cannot be called from a running event loop" + ) + + self._lazy_init() + + if context is None: + context = self._context + task = context.run(self._loop.create_task, coro) + + if ( + threading.current_thread() is threading.main_thread() + and signal.getsignal(signal.SIGINT) is signal.default_int_handler + ): + sigint_handler = partial(self._on_sigint, main_task=task) + try: + signal.signal(signal.SIGINT, sigint_handler) + except ValueError: + # `signal.signal` may throw if `threading.main_thread` does + # not support signals (e.g. embedded interpreter with signals + # not registered - see gh-91880) + sigint_handler = None + else: + sigint_handler = None + + self._interrupt_count = 0 + try: + return self._loop.run_until_complete(task) + except exceptions.CancelledError: + if self._interrupt_count > 0: + uncancel = getattr(task, "uncancel", None) + if uncancel is not None and uncancel() == 0: + raise KeyboardInterrupt # noqa: B904 + raise # CancelledError + finally: + if ( + sigint_handler is not None + and signal.getsignal(signal.SIGINT) is sigint_handler + ): + signal.signal(signal.SIGINT, signal.default_int_handler) + + def _lazy_init(self) -> None: + if self._state is _State.CLOSED: + raise RuntimeError("Runner is closed") + if self._state is _State.INITIALIZED: + return + if self._loop_factory is None: + self._loop = events.new_event_loop() + if not self._set_event_loop: + # Call set_event_loop only once to avoid calling + # attach_loop multiple times on child watchers + events.set_event_loop(self._loop) + self._set_event_loop = True + else: + self._loop = self._loop_factory() + if self._debug is not None: + self._loop.set_debug(self._debug) + self._context = contextvars.copy_context() + self._state = _State.INITIALIZED + + def _on_sigint(self, signum, frame, main_task: asyncio.Task) -> None: + self._interrupt_count += 1 + if self._interrupt_count == 1 and not main_task.done(): + main_task.cancel() + # wakeup loop if it is blocked by select() with long timeout + self._loop.call_soon_threadsafe(lambda: None) + return + raise KeyboardInterrupt() + + def _cancel_all_tasks(loop: AbstractEventLoop) -> None: + to_cancel = tasks.all_tasks(loop) + if not to_cancel: + return + + for task in to_cancel: + task.cancel() + + loop.run_until_complete(tasks.gather(*to_cancel, return_exceptions=True)) + + for task in to_cancel: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "unhandled exception during asyncio.run() shutdown", + "exception": task.exception(), + "task": task, + } + ) + + async def _shutdown_default_executor(loop: AbstractEventLoop) -> None: + """Schedule the shutdown of the default executor.""" + + def _do_shutdown(future: asyncio.futures.Future) -> None: + try: + loop._default_executor.shutdown(wait=True) # type: ignore[attr-defined] + loop.call_soon_threadsafe(future.set_result, None) + except Exception as ex: + loop.call_soon_threadsafe(future.set_exception, ex) + + loop._executor_shutdown_called = True + if loop._default_executor is None: + return + future = loop.create_future() + thread = threading.Thread(target=_do_shutdown, args=(future,)) + thread.start() + try: + await future + finally: + thread.join() + + +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) +PosArgsT = TypeVarTuple("PosArgsT") +P = ParamSpec("P") + +_root_task: RunVar[asyncio.Task | None] = RunVar("_root_task") + + +def find_root_task() -> asyncio.Task: + root_task = _root_task.get(None) + if root_task is not None and not root_task.done(): + return root_task + + # Look for a task that has been started via run_until_complete() + for task in all_tasks(): + if task._callbacks and not task.done(): + callbacks = [cb for cb, context in task._callbacks] + for cb in callbacks: + if ( + cb is _run_until_complete_cb + or getattr(cb, "__module__", None) == "uvloop.loop" + ): + _root_task.set(task) + return task + + # Look up the topmost task in the AnyIO task tree, if possible + task = cast(asyncio.Task, current_task()) + state = _task_states.get(task) + if state: + cancel_scope = state.cancel_scope + while cancel_scope and cancel_scope._parent_scope is not None: + cancel_scope = cancel_scope._parent_scope + + if cancel_scope is not None: + return cast(asyncio.Task, cancel_scope._host_task) + + return task + + +# +# Event loop +# + +_run_vars: WeakKeyDictionary[asyncio.AbstractEventLoop, Any] = WeakKeyDictionary() + + +def _task_started(task: asyncio.Task) -> bool: + """Return ``True`` if the task has been started and has not finished.""" + # The task coro should never be None here, as we never add finished tasks to the + # task list + coro = task.get_coro() + assert coro is not None + return getcoroutinestate(coro) in (CORO_RUNNING, CORO_SUSPENDED) + + +# +# Timeouts and cancellation +# + + +def is_anyio_cancellation(exc: CancelledError) -> bool: + # Sometimes third party frameworks catch a CancelledError and raise a new one, so as + # a workaround we have to look at the previous ones in __context__ too for a + # matching cancel message + while True: + if ( + exc.args + and isinstance(exc.args[0], str) + and exc.args[0].startswith("Cancelled via cancel scope ") + ): + return True + + if isinstance(exc.__context__, CancelledError): + exc = exc.__context__ + continue + + return False + + +class CancelScope(BaseCancelScope): + __slots__ = ( + "_active", + "_cancel_called", + "_cancel_handle", + "_cancel_reason", + "_cancelled_caught", + "_child_scopes", + "_deadline", + "_host_task", + "_parent_scope", + "_pending_uncancellations", + "_shield", + "_tasks", + "_timeout_handle", + ) + + def __new__( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + return object.__new__(cls) + + def __init__(self, deadline: float = math.inf, shield: bool = False): + self._deadline = deadline + self._shield = shield + self._parent_scope: CancelScope | None = None + self._child_scopes: set[CancelScope] = set() + self._cancel_called = False + self._cancel_reason: str | None = None + self._cancelled_caught = False + self._active = False + self._timeout_handle: asyncio.TimerHandle | None = None + self._cancel_handle: asyncio.Handle | None = None + self._tasks: set[asyncio.Task] = set() + self._host_task: asyncio.Task | None = None + if sys.version_info >= (3, 11): + self._pending_uncancellations: int | None = 0 + else: + self._pending_uncancellations = None + + def __enter__(self) -> CancelScope: + if self._active: + raise RuntimeError( + "Each CancelScope may only be used for a single 'with' block" + ) + + self._host_task = host_task = cast(asyncio.Task, current_task()) + self._tasks.add(host_task) + try: + task_state = _task_states[host_task] + except KeyError: + task_state = TaskState(None, self) + _task_states[host_task] = task_state + else: + self._parent_scope = task_state.cancel_scope + task_state.cancel_scope = self + if self._parent_scope is not None: + # If using an eager task factory, the parent scope may not even contain + # the host task + self._parent_scope._child_scopes.add(self) + self._parent_scope._tasks.discard(host_task) + + self._timeout() + self._active = True + + # Start cancelling the host task if the scope was cancelled before entering + if self._cancel_called: + self._deliver_cancellation(self) + + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + del exc_tb + + if not self._active: + raise RuntimeError("This cancel scope is not active") + if current_task() is not self._host_task: + raise RuntimeError( + "Attempted to exit cancel scope in a different task than it was " + "entered in" + ) + + assert self._host_task is not None + host_task_state = _task_states.get(self._host_task) + if host_task_state is None or host_task_state.cancel_scope is not self: + raise RuntimeError( + "Attempted to exit a cancel scope that isn't the current tasks's " + "current cancel scope" + ) + + try: + self._active = False + if self._timeout_handle: + self._timeout_handle.cancel() + self._timeout_handle = None + + self._tasks.remove(self._host_task) + if self._parent_scope is not None: + self._parent_scope._child_scopes.remove(self) + self._parent_scope._tasks.add(self._host_task) + + host_task_state.cancel_scope = self._parent_scope + + # Restart the cancellation effort in the closest visible, cancelled parent + # scope if necessary + self._restart_cancellation_in_parent() + + # We only swallow the exception iff it was an AnyIO CancelledError, either + # directly as exc_val or inside an exception group and there are no cancelled + # parent cancel scopes visible to us here + if self._cancel_called and not self._parent_cancellation_is_visible_to_us: + # For each level-cancel() call made on the host task, call uncancel() + while self._pending_uncancellations: + self._host_task.uncancel() + self._pending_uncancellations -= 1 + + # Update cancelled_caught and check for exceptions we must not swallow + if isinstance(exc_val, BaseExceptionGroup): + cancelleds_caught, remaining = exc_val.split( + lambda exc: ( + isinstance(exc, CancelledError) + and is_anyio_cancellation(exc) + ) + ) + + if cancelleds_caught is None: + return False + + self._cancelled_caught = True + + if remaining is None: + return True + + context = remaining.__context__ + try: + # Preserve __cause__ and __suppress_context__ by avoiding `raise + # ... from ...` + raise remaining + finally: + # Preserve __context__ + remaining.__context__ = context + del context + else: + if isinstance(exc_val, CancelledError) and is_anyio_cancellation( + exc_val + ): + self._cancelled_caught = True + return True + else: + return False + else: + if self._pending_uncancellations: + assert self._parent_scope is not None + assert self._parent_scope._pending_uncancellations is not None + self._parent_scope._pending_uncancellations += ( + self._pending_uncancellations + ) + self._pending_uncancellations = 0 + + return False + finally: + self._host_task = None + del exc_val + + @property + def _effectively_cancelled(self) -> bool: + cancel_scope: CancelScope | None = self + while cancel_scope is not None: + if cancel_scope._cancel_called: + return True + + if cancel_scope.shield: + return False + + cancel_scope = cancel_scope._parent_scope + + return False + + @property + def _parent_cancellation_is_visible_to_us(self) -> bool: + return ( + self._parent_scope is not None + and not self.shield + and self._parent_scope._effectively_cancelled + ) + + def _timeout(self) -> None: + if self._deadline != math.inf: + loop = get_running_loop() + if loop.time() >= self._deadline: + self.cancel("deadline exceeded") + else: + self._timeout_handle = loop.call_at(self._deadline, self._timeout) + + def _deliver_cancellation(self, origin: CancelScope) -> bool: + """ + Deliver cancellation to directly contained tasks and nested cancel scopes. + + Schedule another run at the end if we still have tasks eligible for + cancellation. + + :param origin: the cancel scope that originated the cancellation + :return: ``True`` if the delivery needs to be retried on the next cycle + + """ + should_retry = False + current = current_task() + for task in self._tasks: + should_retry = True + if task._must_cancel: # type: ignore[attr-defined] + continue + + # The task is eligible for cancellation if it has started + if task is not current and (task is self._host_task or _task_started(task)): + waiter = task._fut_waiter # type: ignore[attr-defined] + if not isinstance(waiter, asyncio.Future) or not waiter.done(): + task.cancel(origin._cancel_reason) + if ( + task is origin._host_task + and origin._pending_uncancellations is not None + ): + origin._pending_uncancellations += 1 + + # Deliver cancellation to child scopes that aren't shielded or running their own + # cancellation callbacks + for scope in self._child_scopes: + if not scope._shield and not scope.cancel_called: + should_retry = scope._deliver_cancellation(origin) or should_retry + + # Schedule another callback if there are still tasks left + if origin is self: + if should_retry: + self._cancel_handle = get_running_loop().call_soon( + self._deliver_cancellation, origin + ) + else: + self._cancel_handle = None + + return should_retry + + def _restart_cancellation_in_parent(self) -> None: + """ + Restart the cancellation effort in the closest directly cancelled parent scope. + + """ + scope = self._parent_scope + while scope is not None: + if scope._cancel_called: + if scope._cancel_handle is None: + scope._deliver_cancellation(scope) + + break + + # No point in looking beyond any shielded scope + if scope._shield: + break + + scope = scope._parent_scope + + def cancel(self, reason: str | None = None) -> None: + if not self._cancel_called: + if self._timeout_handle: + self._timeout_handle.cancel() + self._timeout_handle = None + + self._cancel_called = True + self._cancel_reason = f"Cancelled via cancel scope {id(self):x}" + if task := current_task(): + self._cancel_reason += f" by {task}" + + if reason: + self._cancel_reason += f"; reason: {reason}" + + if self._host_task is not None: + self._deliver_cancellation(self) + + @property + def deadline(self) -> float: + return self._deadline + + @deadline.setter + def deadline(self, value: float) -> None: + self._deadline = float(value) + if self._timeout_handle is not None: + self._timeout_handle.cancel() + self._timeout_handle = None + + if self._active and not self._cancel_called: + self._timeout() + + @property + def cancel_called(self) -> bool: + return self._cancel_called + + @property + def cancelled_caught(self) -> bool: + return self._cancelled_caught + + @property + def shield(self) -> bool: + return self._shield + + @shield.setter + def shield(self, value: bool) -> None: + if self._shield != value: + self._shield = value + if not value: + self._restart_cancellation_in_parent() + + +# +# Task states +# + + +class TaskState: + """ + Encapsulates auxiliary task information that cannot be added to the Task instance + itself because there are no guarantees about its implementation. + """ + + __slots__ = "parent_id", "cancel_scope", "__weakref__" + + def __init__(self, parent_id: int | None, cancel_scope: CancelScope | None): + self.parent_id = parent_id + self.cancel_scope = cancel_scope + + +_task_states: WeakKeyDictionary[asyncio.Task, TaskState] = WeakKeyDictionary() + + +# +# Task groups +# + + +class _AsyncioTaskStatus(abc.TaskStatus): + def __init__(self, future: asyncio.Future, parent_id: int): + self._future = future + self._parent_id = parent_id + + def started(self, value: T_contra | None = None) -> None: + try: + self._future.set_result(value) + except asyncio.InvalidStateError: + if not self._future.cancelled(): + raise RuntimeError( + "called 'started' twice on the same task status" + ) from None + + task = cast(asyncio.Task, current_task()) + _task_states[task].parent_id = self._parent_id + + +if sys.version_info >= (3, 12): + _eager_task_factory_code: CodeType | None = asyncio.eager_task_factory.__code__ +else: + _eager_task_factory_code = None + + +class TaskGroup(abc.TaskGroup): + def __init__(self) -> None: + self.cancel_scope: CancelScope = CancelScope() + self._entered = False + self._exceptions: list[BaseException] = [] + self._tasks: set[asyncio.Task] = set() + self._on_completed_fut: asyncio.Future[None] | None = None + + async def __aenter__(self) -> TaskGroup: + if self._entered: + raise RuntimeError("TaskGroup cannot be entered more than once") + + self._entered = True + + self.cancel_scope.__enter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + try: + if exc_val is not None: + self.cancel_scope.cancel() + if not isinstance(exc_val, CancelledError): + self._exceptions.append(exc_val) + + loop = get_running_loop() + try: + if self._tasks: + with CancelScope() as wait_scope: + while self._tasks: + self._on_completed_fut = loop.create_future() + + try: + await self._on_completed_fut + except CancelledError as exc: + # Shield the scope against further cancellation attempts, + # as they're not productive (#695) + wait_scope.shield = True + self.cancel_scope.cancel() + + # Set exc_val from the cancellation exception if it was + # previously unset. However, we should not replace a native + # cancellation exception with one raise by a cancel scope. + if exc_val is None or ( + isinstance(exc_val, CancelledError) + and not is_anyio_cancellation(exc) + ): + exc_val = exc + + self._on_completed_fut = None + else: + # If there are no child tasks to wait on, run at least one checkpoint + # anyway + await AsyncIOBackend.cancel_shielded_checkpoint() + + if self._exceptions: + # The exception that got us here should already have been + # added to self._exceptions so it's ok to break exception + # chaining and avoid adding a "During handling of above..." + # for each nesting level. + raise BaseExceptionGroup( + "unhandled errors in a TaskGroup", self._exceptions + ) from None + elif exc_val: + raise exc_val + except BaseException as exc: + if self.cancel_scope.__exit__(type(exc), exc, exc.__traceback__): + return True + + raise + + return self.cancel_scope.__exit__(exc_type, exc_val, exc_tb) + finally: + del exc_val, exc_tb, self._exceptions + + def _spawn( + self, + coro: Coroutine[Any, Any, T_co], + name: object, + task_status_future: asyncio.Future | None = None, + ) -> TaskHandle[T_co]: + def task_done(_task: asyncio.Task) -> None: + if sys.version_info >= (3, 14) and self.cancel_scope._host_task is not None: + asyncio.future_discard_from_awaited_by( + _task, self.cancel_scope._host_task + ) + + task_state = _task_states[_task] + assert task_state.cancel_scope is not None + assert _task in task_state.cancel_scope._tasks + task_state.cancel_scope._tasks.remove(_task) + self._tasks.remove(task) + del _task_states[_task] + + if self._on_completed_fut is not None and not self._tasks: + try: + self._on_completed_fut.set_result(None) + except asyncio.InvalidStateError: + pass + + try: + exc = _task.exception() + except CancelledError as e: + while isinstance(e.__context__, CancelledError): + e = e.__context__ + + exc = e + + if exc is not None: + # The future can only be in the cancelled state if the host task was + # cancelled, so return immediately instead of adding one more + # CancelledError to the exceptions list + if task_status_future is not None and task_status_future.cancelled(): + return + + if task_status_future is None or task_status_future.done(): + if not isinstance(exc, CancelledError): + self._exceptions.append(exc) + + if not self.cancel_scope._effectively_cancelled: + self.cancel_scope.cancel() + else: + task_status_future.set_exception(exc) + elif task_status_future is not None and not task_status_future.done(): + task_status_future.set_exception( + RuntimeError("Child exited without calling task_status.started()") + ) + + if task_status_future: + parent_id = id(current_task()) + else: + parent_id = id(self.cancel_scope._host_task) + + handle = TaskHandle(coro, name) + loop = asyncio.get_running_loop() + wrapper_coro = handle._run_coro() + if ( + (factory := loop.get_task_factory()) + and getattr(factory, "__code__", None) is _eager_task_factory_code + and (closure := getattr(factory, "__closure__", None)) + ): + custom_task_constructor = closure[0].cell_contents + task = custom_task_constructor(wrapper_coro, loop=loop, name=handle.name) + else: + task = loop.create_task(wrapper_coro, name=handle.name) + + # Make the spawned task inherit the task group's cancel scope + _task_states[task] = TaskState( + parent_id=parent_id, cancel_scope=self.cancel_scope + ) + self.cancel_scope._tasks.add(task) + self._tasks.add(task) + if sys.version_info >= (3, 14) and self.cancel_scope._host_task is not None: + asyncio.future_add_to_awaited_by(task, self.cancel_scope._host_task) + + task.add_done_callback(task_done) + return handle + + def create_task( + self, + coro: Coroutine[Any, Any, T_co], + *, + name: object = None, + context: Context | None = None, + ) -> TaskHandle[T_co]: + if not isinstance(coro, Coroutine): + raise TypeError(f"expected a coroutine, got {coro.__class__.__qualname__}") + + if not self._entered or not self.cancel_scope._active: + coro.close() + raise RuntimeError( + "This task group is not active; no new tasks can be started." + ) + + if context is not None: + return context.run(self._spawn, coro, name=name) + else: + return self._spawn(coro, name=name) + + async def start( + self, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + name: object = None, + return_handle: Literal[False] | Literal[True] = False, + ) -> Any: + if not self._entered or not self.cancel_scope._active: + raise RuntimeError( + "This task group is not active; no new tasks can be started." + ) + + future: asyncio.Future = asyncio.Future() + final_name = get_callable_name(func, name) + task_status = _AsyncioTaskStatus(future, id(self.cancel_scope._host_task)) + coro = call_for_coroutine(func, args, task_status=task_status) + handle = self._spawn(coro, final_name, future) + + # If the task raises an exception after sending a start value without a switch + # point between, the task group is cancelled and this method never proceeds to + # process the completed future. That's why we have to have a shielded cancel + # scope here. + try: + await future + except BaseException: + if handle.status is TaskHandle.Status.PENDING: + # Cancel the task and wait for it to exit before returning + handle.cancel() + with CancelScope(shield=True): + await handle.wait() + + raise + + if return_handle: + handle._start_value = future.result() + return handle + else: + return future.result() + + +# +# Threads +# + +_Retval_Queue_Type = tuple[T_Retval | None, BaseException | None] + + +class WorkerThread(Thread): + MAX_IDLE_TIME = 10 # seconds + + def __init__( + self, + root_task: asyncio.Task, + workers: set[WorkerThread], + idle_workers: deque[WorkerThread], + ): + super().__init__(name="AnyIO worker thread") + self.root_task = root_task + self.workers = workers + self.idle_workers = idle_workers + self.loop = root_task._loop + self.queue: Queue[ + tuple[Context, Callable, tuple, asyncio.Future, CancelScope] | None + ] = Queue(2) + self.idle_since = AsyncIOBackend.current_time() + self.stopping = False + + def _report_result( + self, future: asyncio.Future, result: Any, exc: BaseException | None + ) -> None: + self.idle_since = AsyncIOBackend.current_time() + if not self.stopping: + self.idle_workers.append(self) + + if not future.cancelled(): + if exc is not None: + if isinstance(exc, StopIteration): + new_exc = RuntimeError("coroutine raised StopIteration") + new_exc.__cause__ = exc + exc = new_exc + + future.set_exception(exc) + else: + future.set_result(result) + + def run(self) -> None: + with claim_worker_thread(AsyncIOBackend, self.loop): + while True: + item = self.queue.get() + if item is None: + # Shutdown command received + return + + context, func, args, future, cancel_scope = item + if not future.cancelled(): + result = None + exception: BaseException | None = None + threadlocals.current_cancel_scope = cancel_scope + try: + result = context.run(func, *args) + except BaseException as exc: + exception = exc + finally: + del threadlocals.current_cancel_scope + + if not self.loop.is_closed(): + self.loop.call_soon_threadsafe( + self._report_result, future, result, exception + ) + + del result, exception + + self.queue.task_done() + del item, context, func, args, future, cancel_scope + + def stop(self, f: asyncio.Task | None = None) -> None: + self.stopping = True + self.queue.put_nowait(None) + self.workers.discard(self) + try: + self.idle_workers.remove(self) + except ValueError: + pass + + +_threadpool_idle_workers: RunVar[deque[WorkerThread]] = RunVar( + "_threadpool_idle_workers" +) +_threadpool_workers: RunVar[set[WorkerThread]] = RunVar("_threadpool_workers") + + +# +# Subprocesses +# + + +@dataclass(eq=False) +class StreamReaderWrapper(abc.ByteReceiveStream): + _stream: asyncio.StreamReader + + async def receive(self, max_bytes: int = 65536) -> bytes: + data = await self._stream.read(max_bytes) + if data: + return data + else: + raise EndOfStream + + async def aclose(self) -> None: + self._stream.set_exception(ClosedResourceError()) + await AsyncIOBackend.checkpoint() + + +@dataclass(eq=False) +class StreamWriterWrapper(abc.ByteSendStream): + _stream: asyncio.StreamWriter + _closed: bool = field(init=False, default=False) + + async def send(self, item: bytes) -> None: + await AsyncIOBackend.checkpoint_if_cancelled() + stream_paused = self._stream._protocol._paused # type: ignore[attr-defined] + try: + self._stream.write(item) + await self._stream.drain() + except (ConnectionResetError, BrokenPipeError, RuntimeError) as exc: + # If closed by us and/or the peer: + # * on stdlib, drain() raises ConnectionResetError or BrokenPipeError + # * on uvloop and Winloop, write() eventually starts raising RuntimeError + if self._closed: + raise ClosedResourceError from exc + elif self._stream.is_closing(): + raise BrokenResourceError from exc + + raise + + if not stream_paused: + await AsyncIOBackend.cancel_shielded_checkpoint() + + async def aclose(self) -> None: + self._closed = True + self._stream.close() + await AsyncIOBackend.checkpoint() + + +@dataclass(eq=False) +class Process(abc.Process): + _process: asyncio.subprocess.Process + _stdin: StreamWriterWrapper | None + _stdout: StreamReaderWrapper | None + _stderr: StreamReaderWrapper | None + + async def aclose(self) -> None: + with CancelScope(shield=True) as scope: + if self._stdin: + await self._stdin.aclose() + if self._stdout: + await self._stdout.aclose() + if self._stderr: + await self._stderr.aclose() + + scope.shield = False + try: + await self.wait() + except BaseException: + scope.shield = True + self.kill() + await self.wait() + raise + + async def wait(self) -> int: + return await self._process.wait() + + def terminate(self) -> None: + self._process.terminate() + + def kill(self) -> None: + self._process.kill() + + def send_signal(self, signal: int) -> None: + self._process.send_signal(signal) + + @property + def pid(self) -> int: + return self._process.pid + + @property + def returncode(self) -> int | None: + return self._process.returncode + + @property + def stdin(self) -> abc.ByteSendStream | None: + return self._stdin + + @property + def stdout(self) -> abc.ByteReceiveStream | None: + return self._stdout + + @property + def stderr(self) -> abc.ByteReceiveStream | None: + return self._stderr + + +def _forcibly_shutdown_process_pool_on_exit( + workers: set[Process], _task: object +) -> None: + """ + Forcibly shuts down worker processes belonging to this event loop.""" + child_watcher: asyncio.AbstractChildWatcher | None = None # type: ignore[name-defined] + if sys.version_info < (3, 12): + try: + child_watcher = asyncio.get_event_loop_policy().get_child_watcher() + except NotImplementedError: + pass + + # Close as much as possible (w/o async/await) to avoid warnings + for process in workers.copy(): + if process.returncode is not None: + continue + + process._stdin._stream._transport.close() # type: ignore[union-attr] + process._stdout._stream._transport.close() # type: ignore[union-attr] + process._stderr._stream._transport.close() # type: ignore[union-attr] + process.kill() + if child_watcher: + child_watcher.remove_child_handler(process.pid) + + +async def _shutdown_process_pool_on_exit(workers: set[abc.Process]) -> None: + """ + Shuts down worker processes belonging to this event loop. + + NOTE: this only works when the event loop was started using asyncio.run() or + anyio.run(). + + """ + process: abc.Process + try: + await sleep(math.inf) + except asyncio.CancelledError: + workers = workers.copy() + for process in workers: + if process.returncode is None: + process.kill() + + for process in workers: + await process.aclose() + + +# +# Sockets and networking +# + + +class StreamProtocol(asyncio.Protocol): + read_queue: deque[bytes] + read_event: asyncio.Event + write_event: asyncio.Event + exception: Exception | None = None + is_at_eof: bool = False + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + self.read_queue = deque() + self.read_event = asyncio.Event() + self.write_event = asyncio.Event() + self.write_event.set() + cast(asyncio.Transport, transport).set_write_buffer_limits(0) + + def connection_lost(self, exc: Exception | None) -> None: + if exc: + self.exception = exc + + self.read_event.set() + self.write_event.set() + + def data_received(self, data: bytes) -> None: + # ProactorEventloop sometimes sends bytearray instead of bytes + self.read_queue.append(bytes(data)) + self.read_event.set() + + def eof_received(self) -> bool | None: + self.is_at_eof = True + self.read_event.set() + return True + + def pause_writing(self) -> None: + self.write_event = asyncio.Event() + + def resume_writing(self) -> None: + self.write_event.set() + + +class DatagramProtocol(asyncio.DatagramProtocol): + read_queue: deque[tuple[bytes, IPSockAddrType]] + read_event: asyncio.Event + write_event: asyncio.Event + closed_event: asyncio.Event + exception: Exception | None = None + + def connection_made(self, transport: asyncio.BaseTransport) -> None: + self.read_queue = deque(maxlen=100) # arbitrary value + self.read_event = asyncio.Event() + self.write_event = asyncio.Event() + self.closed_event = asyncio.Event() + self.write_event.set() + + def connection_lost(self, exc: Exception | None) -> None: + self.read_event.set() + self.write_event.set() + self.closed_event.set() + + def datagram_received(self, data: bytes, addr: IPSockAddrType) -> None: + addr = convert_ipv6_sockaddr(addr) + self.read_queue.append((data, addr)) + self.read_event.set() + + def error_received(self, exc: Exception) -> None: + self.exception = exc + + def pause_writing(self) -> None: + self.write_event.clear() + + def resume_writing(self) -> None: + self.write_event.set() + + +class SocketStream(abc.SocketStream): + def __init__(self, transport: asyncio.Transport, protocol: StreamProtocol): + self._transport = transport + self._protocol = protocol + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + self._closed = False + + @property + def _raw_socket(self) -> socket.socket: + return self._transport.get_extra_info("socket") + + async def receive(self, max_bytes: int = 65536) -> bytes: + with self._receive_guard: + if ( + not self._protocol.read_event.is_set() + and not self._transport.is_closing() + and not self._protocol.is_at_eof + ): + self._transport.resume_reading() + await self._protocol.read_event.wait() + self._transport.pause_reading() + else: + await AsyncIOBackend.checkpoint() + + try: + chunk = self._protocol.read_queue.popleft() + except IndexError: + if self._closed: + raise ClosedResourceError from None + elif self._protocol.exception: + raise BrokenResourceError from self._protocol.exception + else: + raise EndOfStream from None + + if len(chunk) > max_bytes: + # Split the oversized chunk + chunk, leftover = chunk[:max_bytes], chunk[max_bytes:] + self._protocol.read_queue.appendleft(leftover) + + # If the read queue is empty, clear the flag so that the next call will + # block until data is available + if not self._protocol.read_queue: + self._protocol.read_event.clear() + + return chunk + + async def send(self, item: bytes) -> None: + with self._send_guard: + await AsyncIOBackend.checkpoint() + + if self._closed: + raise ClosedResourceError + elif self._protocol.exception is not None: + raise BrokenResourceError from self._protocol.exception + + try: + self._transport.write(item) + except RuntimeError as exc: + if self._transport.is_closing(): + raise BrokenResourceError from exc + else: + raise + + await self._protocol.write_event.wait() + + async def send_eof(self) -> None: + try: + self._transport.write_eof() + except OSError: + pass + + async def aclose(self) -> None: + self._closed = True + if not self._transport.is_closing(): + try: + self._transport.write_eof() + except OSError: + pass + + self._transport.close() + await sleep(0) + self._transport.abort() + + +class _RawSocketMixin: + _receive_future: asyncio.Future | None = None + _send_future: asyncio.Future | None = None + _closing = False + + def __init__(self, raw_socket: socket.socket): + self.__raw_socket = raw_socket + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + @property + def _raw_socket(self) -> socket.socket: + return self.__raw_socket + + def _wait_until_readable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: + def callback(f: object) -> None: + del self._receive_future + loop.remove_reader(self.__raw_socket) + + f = self._receive_future = asyncio.Future() + loop.add_reader(self.__raw_socket, f.set_result, None) + f.add_done_callback(callback) + return f + + def _wait_until_writable(self, loop: asyncio.AbstractEventLoop) -> asyncio.Future: + def callback(f: object) -> None: + del self._send_future + loop.remove_writer(self.__raw_socket) + + f = self._send_future = asyncio.Future() + loop.add_writer(self.__raw_socket, f.set_result, None) + f.add_done_callback(callback) + return f + + async def aclose(self) -> None: + if not self._closing: + self._closing = True + if self.__raw_socket.fileno() != -1: + self.__raw_socket.close() + + if self._receive_future: + self._receive_future.set_result(None) + if self._send_future: + self._send_future.set_result(None) + + +class UNIXSocketStream(_RawSocketMixin, abc.UNIXSocketStream): + async def send_eof(self) -> None: + with self._send_guard: + self._raw_socket.shutdown(socket.SHUT_WR) + + async def receive(self, max_bytes: int = 65536) -> bytes: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + data = self._raw_socket.recv(max_bytes) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + if not data: + raise EndOfStream + + return data + + async def send(self, item: bytes) -> None: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._send_guard: + view = memoryview(item) + while view: + try: + bytes_sent = self._raw_socket.send(view) + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + view = view[bytes_sent:] + + async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: + if not isinstance(msglen, int) or msglen < 0: + raise ValueError("msglen must be a non-negative integer") + if not isinstance(maxfds, int) or maxfds < 1: + raise ValueError("maxfds must be a positive integer") + + loop = get_running_loop() + fds = array.array("i") + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + message, ancdata, flags, addr = self._raw_socket.recvmsg( + msglen, socket.CMSG_LEN(maxfds * fds.itemsize) + ) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + if not message and not ancdata: + raise EndOfStream + + break + + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: + raise RuntimeError( + f"Received unexpected ancillary data; message = {message!r}, " + f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" + ) + + fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) + + return message, list(fds) + + async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: + if not message: + raise ValueError("message must not be empty") + if not fds: + raise ValueError("fds must not be empty") + + loop = get_running_loop() + filenos: list[int] = [] + for fd in fds: + if isinstance(fd, int): + filenos.append(fd) + elif isinstance(fd, IOBase): + filenos.append(fd.fileno()) + + fdarray = array.array("i", filenos) + await AsyncIOBackend.checkpoint() + with self._send_guard: + while True: + try: + # The ignore can be removed after mypy picks up + # https://github.com/python/typeshed/pull/5545 + self._raw_socket.sendmsg( + [message], [(socket.SOL_SOCKET, socket.SCM_RIGHTS, fdarray)] + ) + break + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + + +class TCPSocketListener(abc.SocketListener): + _accept_scope: CancelScope | None = None + _closed = False + + def __init__(self, raw_socket: socket.socket): + self.__raw_socket = raw_socket + self._loop = cast(asyncio.BaseEventLoop, get_running_loop()) + self._accept_guard = ResourceGuard("accepting connections from") + + @property + def _raw_socket(self) -> socket.socket: + return self.__raw_socket + + async def accept(self) -> abc.SocketStream: + if self._closed: + raise ClosedResourceError + + with self._accept_guard: + await AsyncIOBackend.checkpoint() + with CancelScope() as self._accept_scope: + try: + client_sock, _addr = await self._loop.sock_accept(self._raw_socket) + except asyncio.CancelledError: + # Workaround for https://bugs.python.org/issue41317 + try: + self._loop.remove_reader(self._raw_socket) + except (ValueError, NotImplementedError): + pass + + if self._closed: + raise ClosedResourceError from None + + raise + finally: + self._accept_scope = None + + client_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + transport, protocol = await self._loop.connect_accepted_socket( + StreamProtocol, client_sock + ) + return SocketStream(transport, protocol) + + async def aclose(self) -> None: + if self._closed: + return + + self._closed = True + if self._accept_scope: + # Workaround for https://bugs.python.org/issue41317 + try: + self._loop.remove_reader(self._raw_socket) + except (ValueError, NotImplementedError): + pass + + self._accept_scope.cancel() + await sleep(0) + + self._raw_socket.close() + + +class UNIXSocketListener(abc.SocketListener): + def __init__(self, raw_socket: socket.socket): + self.__raw_socket = raw_socket + self._loop = get_running_loop() + self._accept_guard = ResourceGuard("accepting connections from") + self._closed = False + + async def accept(self) -> abc.SocketStream: + await AsyncIOBackend.checkpoint() + with self._accept_guard: + while True: + try: + client_sock, _ = self.__raw_socket.accept() + client_sock.setblocking(False) + return UNIXSocketStream(client_sock) + except BlockingIOError: + f: asyncio.Future = asyncio.Future() + self._loop.add_reader(self.__raw_socket, f.set_result, None) + f.add_done_callback( + lambda _: self._loop.remove_reader(self.__raw_socket) + ) + await f + except OSError as exc: + if self._closed: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + + async def aclose(self) -> None: + self._closed = True + self.__raw_socket.close() + + @property + def _raw_socket(self) -> socket.socket: + return self.__raw_socket + + +class UDPSocket(abc.UDPSocket): + def __init__( + self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol + ): + self._transport = transport + self._protocol = protocol + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + self._closed = False + + @property + def _raw_socket(self) -> socket.socket: + return self._transport.get_extra_info("socket") + + async def aclose(self) -> None: + self._closed = True + if not self._transport.is_closing(): + self._transport.close() + + await self._protocol.closed_event.wait() + + async def receive(self) -> tuple[bytes, IPSockAddrType]: + with self._receive_guard: + await AsyncIOBackend.checkpoint() + + # If the buffer is empty, ask for more data + if not self._protocol.read_queue and not self._transport.is_closing(): + self._protocol.read_event.clear() + await self._protocol.read_event.wait() + + try: + return self._protocol.read_queue.popleft() + except IndexError: + if self._closed: + raise ClosedResourceError from None + else: + raise BrokenResourceError from None + + async def send(self, item: UDPPacketType) -> None: + with self._send_guard: + await AsyncIOBackend.checkpoint() + await self._protocol.write_event.wait() + if self._closed: + raise ClosedResourceError + elif self._transport.is_closing(): + raise BrokenResourceError + else: + self._transport.sendto(*item) + + +class ConnectedUDPSocket(abc.ConnectedUDPSocket): + def __init__( + self, transport: asyncio.DatagramTransport, protocol: DatagramProtocol + ): + self._transport = transport + self._protocol = protocol + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + self._closed = False + + @property + def _raw_socket(self) -> socket.socket: + return self._transport.get_extra_info("socket") + + async def aclose(self) -> None: + self._closed = True + if not self._transport.is_closing(): + self._transport.close() + + await self._protocol.closed_event.wait() + + async def receive(self) -> bytes: + with self._receive_guard: + await AsyncIOBackend.checkpoint() + + # If the buffer is empty, ask for more data + if not self._protocol.read_queue and not self._transport.is_closing(): + self._protocol.read_event.clear() + await self._protocol.read_event.wait() + + try: + packet = self._protocol.read_queue.popleft() + except IndexError: + if self._closed: + raise ClosedResourceError from None + else: + raise BrokenResourceError from None + + return packet[0] + + async def send(self, item: bytes) -> None: + with self._send_guard: + await AsyncIOBackend.checkpoint() + await self._protocol.write_event.wait() + if self._closed: + raise ClosedResourceError + elif self._transport.is_closing(): + raise BrokenResourceError + else: + self._transport.sendto(item) + + +class UNIXDatagramSocket(_RawSocketMixin, abc.UNIXDatagramSocket): + async def receive(self) -> UNIXDatagramPacketType: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + data = self._raw_socket.recvfrom(65536) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return data + + async def send(self, item: UNIXDatagramPacketType) -> None: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._send_guard: + while True: + try: + self._raw_socket.sendto(*item) + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return + + +class ConnectedUNIXDatagramSocket(_RawSocketMixin, abc.ConnectedUNIXDatagramSocket): + async def receive(self) -> bytes: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._receive_guard: + while True: + try: + data = self._raw_socket.recv(65536) + except BlockingIOError: + await self._wait_until_readable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return data + + async def send(self, item: bytes) -> None: + loop = get_running_loop() + await AsyncIOBackend.checkpoint() + with self._send_guard: + while True: + try: + self._raw_socket.send(item) + except BlockingIOError: + await self._wait_until_writable(loop) + except OSError as exc: + if self._closing: + raise ClosedResourceError from None + else: + raise BrokenResourceError from exc + else: + return + + +_read_events: RunVar[dict[int, asyncio.Future[bool]]] = RunVar("read_events") +_write_events: RunVar[dict[int, asyncio.Future[bool]]] = RunVar("write_events") + + +# +# Synchronization +# + + +class Event(BaseEvent): + __slots__ = ("_event",) + + def __new__(cls) -> Event: + return object.__new__(cls) + + def __init__(self) -> None: + self._event = asyncio.Event() + + def set(self) -> None: + self._event.set() + + def is_set(self) -> bool: + return self._event.is_set() + + async def wait(self) -> None: + if self.is_set(): + await AsyncIOBackend.checkpoint() + else: + await self._event.wait() + + def statistics(self) -> EventStatistics: + return EventStatistics(len(self._event._waiters)) + + +class Lock(BaseLock): + __slots__ = "_fast_acquire", "_owner_task", "_waiters" + + def __new__(cls, *, fast_acquire: bool = False) -> Lock: + return object.__new__(cls) + + def __init__(self, *, fast_acquire: bool = False) -> None: + self._fast_acquire = fast_acquire + self._owner_task: asyncio.Task | None = None + self._waiters: deque[tuple[asyncio.Task, asyncio.Future]] = deque() + + async def acquire(self) -> None: + task = cast(asyncio.Task, current_task()) + if self._owner_task is None and not self._waiters: + await AsyncIOBackend.checkpoint_if_cancelled() + self._owner_task = task + + # Unless on the "fast path", yield control of the event loop so that other + # tasks can run too + if not self._fast_acquire: + try: + await AsyncIOBackend.cancel_shielded_checkpoint() + except CancelledError: + self.release() + raise + + return + + if self._owner_task == task: + raise RuntimeError("Attempted to acquire an already held Lock") + + fut: asyncio.Future[None] = asyncio.Future() + item = task, fut + self._waiters.append(item) + try: + await fut + except CancelledError: + if fut.cancelled(): + try: + self._waiters.remove(item) + except ValueError: + pass + else: + self.release() + + raise + + def acquire_nowait(self) -> None: + task = cast(asyncio.Task, current_task()) + if self._owner_task is None and not self._waiters: + self._owner_task = task + return + + if self._owner_task is task: + raise RuntimeError("Attempted to acquire an already held Lock") + + raise WouldBlock + + def locked(self) -> bool: + return self._owner_task is not None + + def release(self) -> None: + if self._owner_task != current_task(): + raise RuntimeError("The current task is not holding this lock") + + # A cancelled waiter that already received ownership removes itself from + # _waiters before calling release(); any cancelled waiter still queued here + # was cancelled before being woken, so drop it. + while self._waiters: + task, fut = self._waiters.popleft() + if fut.cancelled(): + continue + + self._owner_task = task + fut.set_result(None) + return + + self._owner_task = None + + def statistics(self) -> LockStatistics: + task_info = AsyncIOTaskInfo(self._owner_task) if self._owner_task else None + return LockStatistics(self.locked(), task_info, len(self._waiters)) + + +class Semaphore(BaseSemaphore): + __slots__ = "_value", "_max_value", "_fast_acquire", "_waiters" + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Semaphore: + return object.__new__(cls) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ): + super().__init__(initial_value, max_value=max_value) + self._value = initial_value + self._max_value = max_value + self._fast_acquire = fast_acquire + self._waiters: deque[asyncio.Future[None]] = deque() + + async def acquire(self) -> None: + if self._value > 0 and not self._waiters: + await AsyncIOBackend.checkpoint_if_cancelled() + self._value -= 1 + + # Unless on the "fast path", yield control of the event loop so that other + # tasks can run too + if not self._fast_acquire: + try: + await AsyncIOBackend.cancel_shielded_checkpoint() + except CancelledError: + self.release() + raise + + return + + fut: asyncio.Future[None] = asyncio.Future() + self._waiters.append(fut) + try: + await fut + except CancelledError: + if fut.cancelled(): + try: + self._waiters.remove(fut) + except ValueError: + pass + else: + self.release() + + raise + + def acquire_nowait(self) -> None: + if self._value == 0: + raise WouldBlock + + self._value -= 1 + + def release(self) -> None: + if self._max_value is not None and self._value == self._max_value: + raise ValueError("semaphore released too many times") + + while self._waiters: + fut = self._waiters.popleft() + if fut.cancelled(): + continue + + fut.set_result(None) + return + + self._value += 1 + + @property + def value(self) -> int: + return self._value + + @property + def max_value(self) -> int | None: + return self._max_value + + def statistics(self) -> SemaphoreStatistics: + return SemaphoreStatistics(len(self._waiters)) + + +class CapacityLimiter(BaseCapacityLimiter): + __slots__ = "_total_tokens", "_borrowers", "_wait_queue" + + def __new__(cls, total_tokens: float) -> CapacityLimiter: + return object.__new__(cls) + + def __init__(self, total_tokens: float): + self._total_tokens: float = 0 + self._borrowers: set[Any] = set() + self._wait_queue: OrderedDict[Any, asyncio.Event] = OrderedDict() + self.total_tokens = total_tokens + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + @property + def total_tokens(self) -> float: + return self._total_tokens + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + if not isinstance(value, int) and not math.isinf(value): + raise TypeError("total_tokens must be an int or math.inf") + + if value < 0: + raise ValueError("total_tokens must be >= 0") + + waiters_to_notify = max(value - self._total_tokens, 0) + self._total_tokens = value + + # Notify waiting tasks that they have acquired the limiter + while self._wait_queue and waiters_to_notify: + event = self._wait_queue.popitem(last=False)[1] + event.set() + waiters_to_notify -= 1 + + @property + def borrowed_tokens(self) -> int: + return len(self._borrowers) + + @property + def available_tokens(self) -> float: + return self._total_tokens - len(self._borrowers) + + def _notify_next_waiter(self) -> None: + """Notify the next task in line if this limiter has free capacity now.""" + if self._wait_queue and len(self._borrowers) < self._total_tokens: + event = self._wait_queue.popitem(last=False)[1] + event.set() + + def acquire_nowait(self) -> None: + self.acquire_on_behalf_of_nowait(current_task()) + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + if borrower in self._borrowers: + raise RuntimeError( + "this borrower is already holding one of this CapacityLimiter's tokens" + ) + + if self._wait_queue or len(self._borrowers) >= self._total_tokens: + raise WouldBlock + + self._borrowers.add(borrower) + + async def acquire(self) -> None: + return await self.acquire_on_behalf_of(current_task()) + + async def acquire_on_behalf_of(self, borrower: object) -> None: + await AsyncIOBackend.checkpoint_if_cancelled() + try: + self.acquire_on_behalf_of_nowait(borrower) + except WouldBlock: + event = asyncio.Event() + self._wait_queue[borrower] = event + try: + await event.wait() + except BaseException: + self._wait_queue.pop(borrower, None) + if event.is_set(): + self._notify_next_waiter() + + raise + + self._borrowers.add(borrower) + else: + try: + await AsyncIOBackend.cancel_shielded_checkpoint() + except BaseException: + self.release() + raise + + def release(self) -> None: + self.release_on_behalf_of(current_task()) + + def release_on_behalf_of(self, borrower: object) -> None: + try: + self._borrowers.remove(borrower) + except KeyError: + raise RuntimeError( + "this borrower isn't holding any of this CapacityLimiter's tokens" + ) from None + + self._notify_next_waiter() + + def statistics(self) -> CapacityLimiterStatistics: + return CapacityLimiterStatistics( + self.borrowed_tokens, + self.total_tokens, + tuple(self._borrowers), + len(self._wait_queue), + ) + + +_default_thread_limiter: RunVar[CapacityLimiter] = RunVar("_default_thread_limiter") + + +# +# Operating system signals +# + + +class _SignalReceiver: + def __init__(self, signals: tuple[Signals, ...]): + self._signals = signals + self._loop = get_running_loop() + self._signal_queue: deque[Signals] = deque() + self._future: asyncio.Future = asyncio.Future() + self._handled_signals: set[Signals] = set() + + def _deliver(self, signum: Signals) -> None: + self._signal_queue.append(signum) + if not self._future.done(): + self._future.set_result(None) + + def __enter__(self) -> _SignalReceiver: + for sig in set(self._signals): + self._loop.add_signal_handler(sig, self._deliver, sig) + self._handled_signals.add(sig) + + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + for sig in self._handled_signals: + self._loop.remove_signal_handler(sig) + + def __aiter__(self) -> _SignalReceiver: + return self + + async def __anext__(self) -> Signals: + await AsyncIOBackend.checkpoint() + if not self._signal_queue: + self._future = asyncio.Future() + await self._future + + return self._signal_queue.popleft() + + +# +# Testing and debugging +# + + +class AsyncIOTaskInfo(TaskInfo): + def __init__(self, task: asyncio.Task): + task_state = _task_states.get(task) + if task_state is None: + parent_id = None + else: + parent_id = task_state.parent_id + + coro = task.get_coro() + assert coro is not None, "created TaskInfo from a completed Task" + super().__init__(id(task), parent_id, task.get_name(), coro) + self._task = weakref.ref(task) + + def has_pending_cancellation(self) -> bool: + if not (task := self._task()): + # If the task isn't around anymore, it won't have a pending cancellation + return False + + if task._must_cancel: # type: ignore[attr-defined] + return True + elif ( + isinstance(task._fut_waiter, asyncio.Future) # type: ignore[attr-defined] + and task._fut_waiter.cancelled() # type: ignore[attr-defined] + ): + return True + + if task_state := _task_states.get(task): + if cancel_scope := task_state.cancel_scope: + return cancel_scope._effectively_cancelled + + return False + + +class TestRunner(abc.TestRunner): + _send_stream: MemoryObjectSendStream[tuple[Awaitable[Any], asyncio.Future[Any]]] + + def __init__( + self, + *, + debug: bool | None = None, + use_uvloop: bool = False, + loop_factory: Callable[[], AbstractEventLoop] | None = None, + ) -> None: + if use_uvloop and loop_factory is None: + if sys.platform != "win32": + import uvloop + + loop_factory = uvloop.new_event_loop + else: + import winloop + + loop_factory = winloop.new_event_loop + + self._runner = Runner(debug=debug, loop_factory=loop_factory) + self._exceptions: list[BaseException] = [] + self._runner_task: asyncio.Task | None = None + + def __enter__(self) -> TestRunner: + self._runner.__enter__() + self.get_loop().set_exception_handler(self._exception_handler) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._runner.__exit__(exc_type, exc_val, exc_tb) + + def get_loop(self) -> AbstractEventLoop: + return self._runner.get_loop() + + def is_running(self) -> bool: + try: + asyncio.get_running_loop() + return True + except RuntimeError: + return False + + def _exception_handler( + self, loop: asyncio.AbstractEventLoop, context: dict[str, Any] + ) -> None: + if isinstance(context.get("exception"), Exception): + self._exceptions.append(context["exception"]) + else: + loop.default_exception_handler(context) + + def _raise_async_exceptions(self) -> None: + # Re-raise any exceptions raised in asynchronous callbacks + if self._exceptions: + exceptions, self._exceptions = self._exceptions, [] + if len(exceptions) == 1: + raise exceptions[0] + elif exceptions: + raise BaseExceptionGroup( + "Multiple exceptions occurred in asynchronous callbacks", exceptions + ) + + async def _run_tests_and_fixtures( + self, + receive_stream: MemoryObjectReceiveStream[ + tuple[Awaitable[T_Retval], asyncio.Future[T_Retval]] + ], + ) -> None: + from _pytest.outcomes import OutcomeException + + with receive_stream, self._send_stream: + async for coro, future in receive_stream: + try: + retval = await coro + except CancelledError as exc: + if not future.cancelled(): + future.cancel(*exc.args) + + raise + except BaseException as exc: + if not future.cancelled(): + future.set_exception(exc) + + if not isinstance(exc, (Exception, OutcomeException)): + raise + else: + if not future.cancelled(): + future.set_result(retval) + + async def _call_in_runner_task( + self, + func: Callable[P, Awaitable[T_Retval]], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> T_Retval: + if not self._runner_task: + self._send_stream, receive_stream = create_memory_object_stream[ + tuple[Awaitable[Any], asyncio.Future] + ](1) + self._runner_task = self.get_loop().create_task( + self._run_tests_and_fixtures(receive_stream) + ) + + coro = func(*args, **kwargs) + future: asyncio.Future[T_Retval] = self.get_loop().create_future() + self._send_stream.send_nowait((coro, future)) + return await future + + def run_asyncgen_fixture( + self, + fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], + kwargs: dict[str, Any], + ) -> Iterable[T_Retval]: + asyncgen = fixture_func(**kwargs) + fixturevalue: T_Retval = self.get_loop().run_until_complete( + self._call_in_runner_task(asyncgen.asend, None) + ) + self._raise_async_exceptions() + + yield fixturevalue + + try: + self.get_loop().run_until_complete( + self._call_in_runner_task(asyncgen.asend, None) + ) + except StopAsyncIteration: + self._raise_async_exceptions() + else: + self.get_loop().run_until_complete(asyncgen.aclose()) + raise RuntimeError("Async generator fixture did not stop") + + def run_fixture( + self, + fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], + kwargs: dict[str, Any], + ) -> T_Retval: + retval = self.get_loop().run_until_complete( + self._call_in_runner_task(fixture_func, **kwargs) + ) + self._raise_async_exceptions() + return retval + + def run_test( + self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] + ) -> None: + from _pytest.outcomes import OutcomeException + + try: + self.get_loop().run_until_complete( + self._call_in_runner_task(test_func, **kwargs) + ) + except Exception as exc: + self._exceptions.append(exc) + except OutcomeException: + raise + except BaseException: + # A BaseException (e.g. KeyboardInterrupt, SystemExit) interrupted the event loop before + # the test completed. Cancel _runner_task so it does not resume when the event + # loop is re-entered during async generator fixture teardown. + if self._runner_task is not None and not self._runner_task.done(): + self._runner_task.cancel() + self._send_stream.close() + try: + self.get_loop().run_until_complete(self._runner_task) + except CancelledError: + pass + finally: + self._runner_task = None + raise + self._raise_async_exceptions() + + +class AsyncIOBackend(AsyncBackend): + @classmethod + def run( + cls, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + options: dict[str, Any], + ) -> T_Retval: + @wraps(func) + async def wrapper() -> T_Retval: + task = cast(asyncio.Task, current_task()) + task.set_name(get_callable_name(func)) + _task_states[task] = TaskState(None, None) + + try: + return await func(*args) + finally: + del _task_states[task] + + debug = options.get("debug", None) + loop_factory = options.get("loop_factory", None) + if loop_factory is None and options.get("use_uvloop", False): + if sys.platform != "win32": + import uvloop + + loop_factory = uvloop.new_event_loop + else: + import winloop + + loop_factory = winloop.new_event_loop + + with Runner(debug=debug, loop_factory=loop_factory) as runner: + return runner.run(wrapper()) + + @classmethod + def current_token(cls) -> object: + return get_running_loop() + + @classmethod + def current_time(cls) -> float: + return get_running_loop().time() + + @classmethod + def cancelled_exception_class(cls) -> type[BaseException]: + return CancelledError + + @classmethod + async def checkpoint(cls) -> None: + await sleep(0) + + @classmethod + async def checkpoint_if_cancelled(cls) -> None: + task = current_task() + if task is None: + return + + try: + cancel_scope = _task_states[task].cancel_scope + except KeyError: + return + + while cancel_scope: + if cancel_scope.cancel_called: + await sleep(0) + elif cancel_scope.shield: + break + else: + cancel_scope = cancel_scope._parent_scope + + @classmethod + async def cancel_shielded_checkpoint(cls) -> None: + with CancelScope(shield=True): + await sleep(0) + + @classmethod + async def sleep(cls, delay: float) -> None: + await sleep(delay) + + @classmethod + def create_cancel_scope( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + return CancelScope(deadline=deadline, shield=shield) + + @classmethod + def current_effective_deadline(cls) -> float: + if (task := current_task()) is None: + return math.inf + + try: + cancel_scope = _task_states[task].cancel_scope + except KeyError: + return math.inf + + deadline = math.inf + while cancel_scope: + deadline = min(deadline, cancel_scope.deadline) + if cancel_scope._cancel_called: + deadline = -math.inf + break + elif cancel_scope.shield: + break + else: + cancel_scope = cancel_scope._parent_scope + + return deadline + + @classmethod + def create_task_group(cls) -> abc.TaskGroup: + return TaskGroup() + + @classmethod + def create_event(cls) -> abc.Event: + return Event() + + @classmethod + def create_lock(cls, *, fast_acquire: bool) -> abc.Lock: + return Lock(fast_acquire=fast_acquire) + + @classmethod + def create_semaphore( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> abc.Semaphore: + return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire) + + @classmethod + def create_capacity_limiter(cls, total_tokens: float) -> abc.CapacityLimiter: + return CapacityLimiter(total_tokens) + + @classmethod + async def run_sync_in_worker_thread( # type: ignore[return] + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + abandon_on_cancel: bool = False, + limiter: abc.CapacityLimiter | None = None, + ) -> T_Retval: + await cls.checkpoint() + + # If this is the first run in this event loop thread, set up the necessary + # variables + try: + idle_workers = _threadpool_idle_workers.get() + workers = _threadpool_workers.get() + except LookupError: + idle_workers = deque() + workers = set() + _threadpool_idle_workers.set(idle_workers) + _threadpool_workers.set(workers) + + async with limiter or cls.current_default_thread_limiter(): + with CancelScope(shield=not abandon_on_cancel) as scope: + future = asyncio.Future[T_Retval]() + root_task = find_root_task() + if not idle_workers: + worker = WorkerThread(root_task, workers, idle_workers) + worker.start() + workers.add(worker) + root_task.add_done_callback( + worker.stop, context=contextvars.Context() + ) + else: + worker = idle_workers.pop() + + # Prune any other workers that have been idle for MAX_IDLE_TIME + # seconds or longer + now = cls.current_time() + while idle_workers: + if ( + now - idle_workers[0].idle_since + < WorkerThread.MAX_IDLE_TIME + ): + break + + expired_worker = idle_workers.popleft() + expired_worker.root_task.remove_done_callback( + expired_worker.stop + ) + expired_worker.stop() + + context = copy_context() + context.run(set_current_async_library, None) + if abandon_on_cancel or scope._parent_scope is None: + worker_scope = scope + else: + worker_scope = scope._parent_scope + + worker.queue.put_nowait((context, func, args, future, worker_scope)) + return await future + + @classmethod + def check_cancelled(cls) -> None: + scope: CancelScope | None = threadlocals.current_cancel_scope + while scope is not None: + if scope.cancel_called: + raise CancelledError(f"Cancelled via cancel scope {id(scope):x}") + + if scope.shield: + return + + scope = scope._parent_scope + + @classmethod + def run_async_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_co: + async def task_wrapper() -> T_co: + __tracebackhide__ = True + if scope is not None: + task = cast(asyncio.Task, current_task()) + _task_states[task] = TaskState(None, scope) + scope._tasks.add(task) + try: + return await func(*args) + except CancelledError as exc: + raise concurrent.futures.CancelledError(str(exc)) from None + finally: + if scope is not None: + scope._tasks.discard(task) + + loop = cast( + "AbstractEventLoop", token or threadlocals.current_token.native_token + ) + if loop.is_closed(): + raise RunFinishedError + + context = copy_context() + context.run(set_current_async_library, "asyncio") + scope = getattr(threadlocals, "current_cancel_scope", None) + f: concurrent.futures.Future[T_co] = context.run( + asyncio.run_coroutine_threadsafe, task_wrapper(), loop=loop + ) + return f.result() + + @classmethod + def run_sync_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_Retval: + @wraps(func) + def wrapper() -> None: + try: + set_current_async_library("asyncio") + f.set_result(func(*args)) + except BaseException as exc: + f.set_exception(exc) + if not isinstance(exc, Exception): + raise + + loop = cast( + "AbstractEventLoop", token or threadlocals.current_token.native_token + ) + if loop.is_closed(): + raise RunFinishedError + + f: concurrent.futures.Future[T_Retval] = Future() + loop.call_soon_threadsafe(wrapper) + return f.result() + + @classmethod + async def open_process( + cls, + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None, + stdout: int | IO[Any] | None, + stderr: int | IO[Any] | None, + **kwargs: Any, + ) -> Process: + await cls.checkpoint() + if isinstance(command, PathLike): + command = os.fspath(command) + + if isinstance(command, (str, bytes)): + process = await asyncio.create_subprocess_shell( + command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + **kwargs, + ) + else: + process = await asyncio.create_subprocess_exec( + *command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + **kwargs, + ) + + stdin_stream = StreamWriterWrapper(process.stdin) if process.stdin else None + stdout_stream = StreamReaderWrapper(process.stdout) if process.stdout else None + stderr_stream = StreamReaderWrapper(process.stderr) if process.stderr else None + return Process(process, stdin_stream, stdout_stream, stderr_stream) + + @classmethod + def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: + create_task( + _shutdown_process_pool_on_exit(workers), + name="AnyIO process pool shutdown task", + ) + find_root_task().add_done_callback( + partial(_forcibly_shutdown_process_pool_on_exit, workers) # type:ignore[arg-type] + ) + + @classmethod + async def connect_tcp( + cls, host: str, port: int, local_address: IPSockAddrType | None = None + ) -> abc.SocketStream: + transport, protocol = cast( + tuple[asyncio.Transport, StreamProtocol], + await get_running_loop().create_connection( + StreamProtocol, host, port, local_addr=local_address + ), + ) + transport.pause_reading() + return SocketStream(transport, protocol) + + @classmethod + async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: + await cls.checkpoint() + loop = get_running_loop() + raw_socket = socket.socket(socket.AF_UNIX) + raw_socket.setblocking(False) + while True: + try: + raw_socket.connect(path) + except BlockingIOError: + f: asyncio.Future = asyncio.Future() + loop.add_writer(raw_socket, f.set_result, None) + f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) + await f + except BaseException: + raw_socket.close() + raise + else: + return UNIXSocketStream(raw_socket) + + @classmethod + def create_tcp_listener(cls, sock: socket.socket) -> SocketListener: + return TCPSocketListener(sock) + + @classmethod + def create_unix_listener(cls, sock: socket.socket) -> SocketListener: + return UNIXSocketListener(sock) + + @classmethod + async def create_udp_socket( + cls, + family: AddressFamily, + local_address: IPSockAddrType | None, + remote_address: IPSockAddrType | None, + reuse_port: bool, + ) -> UDPSocket | ConnectedUDPSocket: + transport, protocol = await get_running_loop().create_datagram_endpoint( + DatagramProtocol, + local_addr=local_address, + remote_addr=remote_address, + family=family, + reuse_port=reuse_port, + ) + if protocol.exception: + transport.close() + raise protocol.exception + + if not remote_address: + return UDPSocket(transport, protocol) + else: + return ConnectedUDPSocket(transport, protocol) + + @classmethod + async def create_unix_datagram_socket( # type: ignore[override] + cls, raw_socket: socket.socket, remote_path: str | bytes | None + ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: + await cls.checkpoint() + loop = get_running_loop() + + if remote_path: + while True: + try: + raw_socket.connect(remote_path) + except BlockingIOError: + f: asyncio.Future = asyncio.Future() + loop.add_writer(raw_socket, f.set_result, None) + f.add_done_callback(lambda _: loop.remove_writer(raw_socket)) + await f + except BaseException: + raw_socket.close() + raise + else: + return ConnectedUNIXDatagramSocket(raw_socket) + else: + return UNIXDatagramSocket(raw_socket) + + @classmethod + async def getaddrinfo( + cls, + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, + ) -> Sequence[ + tuple[ + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], + ] + ]: + return await get_running_loop().getaddrinfo( + host, port, family=family, type=type, proto=proto, flags=flags + ) + + @classmethod + async def getnameinfo( + cls, sockaddr: IPSockAddrType, flags: int = 0 + ) -> tuple[str, str]: + return await get_running_loop().getnameinfo(sockaddr, flags) + + @classmethod + async def wait_readable(cls, obj: FileDescriptorLike) -> None: + try: + read_events = _read_events.get() + except LookupError: + read_events = {} + _read_events.set(read_events) + + fd = obj if isinstance(obj, int) else obj.fileno() + if read_events.get(fd): + raise BusyResourceError("reading from") + + loop = get_running_loop() + fut: asyncio.Future[bool] = loop.create_future() + + def cb() -> None: + try: + del read_events[fd] + except KeyError: + pass + else: + remove_reader(fd) + + try: + fut.set_result(True) + except asyncio.InvalidStateError: + pass + + try: + loop.add_reader(fd, cb) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + selector = get_selector() + selector.add_reader(fd, cb) + remove_reader = selector.remove_reader + else: + remove_reader = loop.remove_reader + + read_events[fd] = fut + try: + success = await fut + finally: + try: + del read_events[fd] + except KeyError: + pass + else: + remove_reader(fd) + + if not success: + raise ClosedResourceError + + @classmethod + async def wait_writable(cls, obj: FileDescriptorLike) -> None: + try: + write_events = _write_events.get() + except LookupError: + write_events = {} + _write_events.set(write_events) + + fd = obj if isinstance(obj, int) else obj.fileno() + if write_events.get(fd): + raise BusyResourceError("writing to") + + loop = get_running_loop() + fut: asyncio.Future[bool] = loop.create_future() + + def cb() -> None: + try: + del write_events[fd] + except KeyError: + pass + else: + remove_writer(fd) + + try: + fut.set_result(True) + except asyncio.InvalidStateError: + pass + + try: + loop.add_writer(fd, cb) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + selector = get_selector() + selector.add_writer(fd, cb) + remove_writer = selector.remove_writer + else: + remove_writer = loop.remove_writer + + write_events[fd] = fut + try: + success = await fut + finally: + try: + del write_events[fd] + except KeyError: + pass + else: + remove_writer(fd) + + if not success: + raise ClosedResourceError + + @classmethod + def notify_closing(cls, obj: FileDescriptorLike) -> None: + fd = obj if isinstance(obj, int) else obj.fileno() + loop = get_running_loop() + + try: + write_events = _write_events.get() + except LookupError: + pass + else: + try: + fut = write_events.pop(fd) + except KeyError: + pass + else: + try: + fut.set_result(False) + except asyncio.InvalidStateError: + pass + + try: + loop.remove_writer(fd) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + get_selector().remove_writer(fd) + + try: + read_events = _read_events.get() + except LookupError: + pass + else: + try: + fut = read_events.pop(fd) + except KeyError: + pass + else: + try: + fut.set_result(False) + except asyncio.InvalidStateError: + pass + + try: + loop.remove_reader(fd) + except NotImplementedError: + from anyio._core._asyncio_selector_thread import get_selector + + get_selector().remove_reader(fd) + + @classmethod + async def wrap_listener_socket(cls, sock: socket.socket) -> SocketListener: + if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX: + return UNIXSocketListener(sock) + + return TCPSocketListener(sock) + + @classmethod + async def wrap_stream_socket(cls, sock: socket.socket) -> SocketStream: + transport, protocol = await get_running_loop().create_connection( + StreamProtocol, sock=sock + ) + return SocketStream(transport, protocol) + + @classmethod + async def wrap_unix_stream_socket(cls, sock: socket.socket) -> UNIXSocketStream: + return UNIXSocketStream(sock) + + @classmethod + async def wrap_udp_socket(cls, sock: socket.socket) -> UDPSocket: + transport, protocol = await get_running_loop().create_datagram_endpoint( + DatagramProtocol, sock=sock + ) + return UDPSocket(transport, protocol) + + @classmethod + async def wrap_connected_udp_socket(cls, sock: socket.socket) -> ConnectedUDPSocket: + transport, protocol = await get_running_loop().create_datagram_endpoint( + DatagramProtocol, sock=sock + ) + return ConnectedUDPSocket(transport, protocol) + + @classmethod + async def wrap_unix_datagram_socket(cls, sock: socket.socket) -> UNIXDatagramSocket: + return UNIXDatagramSocket(sock) + + @classmethod + async def wrap_connected_unix_datagram_socket( + cls, sock: socket.socket + ) -> ConnectedUNIXDatagramSocket: + return ConnectedUNIXDatagramSocket(sock) + + @classmethod + def current_default_thread_limiter(cls) -> CapacityLimiter: + try: + return _default_thread_limiter.get() + except LookupError: + limiter = CapacityLimiter(40) + _default_thread_limiter.set(limiter) + return limiter + + @classmethod + def open_signal_receiver( + cls, *signals: Signals + ) -> AbstractContextManager[AsyncIterator[Signals]]: + return _SignalReceiver(signals) + + @classmethod + def get_current_task(cls) -> TaskInfo: + return AsyncIOTaskInfo(current_task()) # type: ignore[arg-type] + + @classmethod + def get_running_tasks(cls) -> Sequence[TaskInfo]: + return [AsyncIOTaskInfo(task) for task in all_tasks() if not task.done()] + + @classmethod + async def wait_all_tasks_blocked(cls) -> None: + await cls.checkpoint() + this_task = current_task() + while True: + for task in all_tasks(): + if task is this_task: + continue + + waiter = task._fut_waiter # type: ignore[attr-defined] + if waiter is None or waiter.done(): + await sleep(0.1) + break + else: + return + + @classmethod + def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: + return TestRunner(**options) + + +backend_class = AsyncIOBackend diff --git a/venv/lib/python3.11/site-packages/anyio/_backends/_trio.py b/venv/lib/python3.11/site-packages/anyio/_backends/_trio.py new file mode 100644 index 0000000..091c78c --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_backends/_trio.py @@ -0,0 +1,1456 @@ +from __future__ import annotations + +import array +import math +import os +import socket +import sys +import types +import weakref +from collections.abc import ( + AsyncGenerator, + AsyncIterator, + Awaitable, + Callable, + Collection, + Coroutine, + Iterable, + Sequence, +) +from contextlib import AbstractContextManager +from contextvars import Context +from dataclasses import dataclass +from functools import partial, wraps +from io import IOBase +from os import PathLike +from signal import Signals +from socket import AddressFamily, SocketKind +from types import TracebackType +from typing import ( + IO, + TYPE_CHECKING, + Any, + Generic, + Literal, + NoReturn, + ParamSpec, + TypeVar, + cast, + overload, +) + +import trio.from_thread +import trio.lowlevel +from outcome import Error, Outcome, Value +from trio.lowlevel import ( + current_root_task, + current_task, + notify_closing, + wait_readable, + wait_writable, +) +from trio.socket import SocketType as TrioSocketType +from trio.to_thread import run_sync + +from .. import ( + CapacityLimiterStatistics, + EventStatistics, + LockStatistics, + RunFinishedError, + TaskInfo, + WouldBlock, + abc, +) +from .._core._eventloop import claim_worker_thread +from .._core._exceptions import ( + BrokenResourceError, + BusyResourceError, + ClosedResourceError, + EndOfStream, +) +from .._core._sockets import convert_ipv6_sockaddr +from .._core._streams import create_memory_object_stream +from .._core._synchronization import ( + CapacityLimiter as BaseCapacityLimiter, +) +from .._core._synchronization import Event as BaseEvent +from .._core._synchronization import Lock as BaseLock +from .._core._synchronization import ( + ResourceGuard, + SemaphoreStatistics, +) +from .._core._synchronization import Semaphore as BaseSemaphore +from .._core._tasks import CancelScope as BaseCancelScope +from .._core._tasks import TaskHandle +from ..abc import IPSockAddrType, UDPPacketType, UNIXDatagramPacketType +from ..abc._eventloop import AsyncBackend, StrOrBytesPath +from ..abc._tasks import T_contra, call_for_coroutine, get_callable_name +from ..streams.memory import MemoryObjectSendStream + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from exceptiongroup import BaseExceptionGroup + from typing_extensions import TypeVarTuple, Unpack + +T = TypeVar("T") +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +T_SockAddr = TypeVar("T_SockAddr", str, IPSockAddrType) +PosArgsT = TypeVarTuple("PosArgsT") +P = ParamSpec("P") + + +def ensure_returns_coro( + func: Callable[P, Awaitable[T_Retval]], +) -> Callable[P, Coroutine[Any, Any, T_Retval]]: + @wraps(func) + def wrapper(*args: P.args, **kwargs: P.kwargs) -> Coroutine[Any, Any, T_Retval]: + awaitable = func(*args, **kwargs) + # Check the common case first. + if isinstance(awaitable, Coroutine): + return awaitable + elif not isinstance(awaitable, Awaitable): + # The user violated the type annotations. Still, we should pass this on to + # Trio so it can raise with an appropriate message. + return awaitable + else: + + @wraps(func) + async def inner_wrapper() -> T_Retval: + return await awaitable + + return inner_wrapper() + + return wrapper + + +# +# Event loop +# + +RunVar = trio.lowlevel.RunVar + + +# +# Timeouts and cancellation +# + + +class CancelScope(BaseCancelScope): + __slots__ = ("__original",) + + def __new__( + cls, original: trio.CancelScope | None = None, **kwargs: object + ) -> CancelScope: + return object.__new__(cls) + + def __init__(self, original: trio.CancelScope | None = None, **kwargs: Any) -> None: + self.__original = original or trio.CancelScope(**kwargs) + + def __enter__(self) -> CancelScope: + self.__original.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + return self.__original.__exit__(exc_type, exc_val, exc_tb) + + def cancel(self, reason: str | None = None) -> None: + self.__original.cancel(reason) + + @property + def deadline(self) -> float: + return self.__original.deadline + + @deadline.setter + def deadline(self, value: float) -> None: + self.__original.deadline = value + + @property + def cancel_called(self) -> bool: + return self.__original.cancel_called + + @property + def cancelled_caught(self) -> bool: + return self.__original.cancelled_caught + + @property + def shield(self) -> bool: + return self.__original.shield + + @shield.setter + def shield(self, value: bool) -> None: + self.__original.shield = value + + +# +# Task groups +# + +empty_start_value = object() + + +class _TrioTaskStatus(Generic[T_contra], abc.TaskStatus[T_contra]): + early_start_value: T_contra | object = empty_start_value + real_task_status: trio.TaskStatus[T_contra | None] | None = None + + def started(self, value: T_contra | None = None) -> None: + if self.real_task_status is None: + if self.early_start_value is not empty_start_value: + raise RuntimeError("called 'started' twice on the same task status") + + self.early_start_value = value + else: + self.real_task_status.started(value) + + +class TaskGroup(abc.TaskGroup): + def __init__(self) -> None: + self._entered = False + self._active = False + self._nursery_manager = trio.open_nursery(strict_exception_groups=True) + self.cancel_scope = None # type: ignore[assignment] + + async def __aenter__(self) -> TaskGroup: + if self._entered: + raise RuntimeError("TaskGroup cannot be entered more than once") + + self._entered = True + self._active = True + self._nursery = await self._nursery_manager.__aenter__() + self.cancel_scope = CancelScope(self._nursery.cancel_scope) + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + try: + # trio.Nursery.__exit__ returns bool; .open_nursery has wrong type + return await self._nursery_manager.__aexit__(exc_type, exc_val, exc_tb) # type: ignore[return-value] + except BaseExceptionGroup as exc: + if not exc.split(trio.Cancelled)[1]: + raise trio.Cancelled._create() from exc + + raise + finally: + del exc_val, exc_tb + self._active = False + + def _check_active(self, coro: Coroutine | None = None) -> None: + if not self._active: + if coro is not None: + coro.close() + + raise RuntimeError( + "This task group is not active; no new tasks can be started." + ) + + def create_task( + self, + coro: Coroutine[Any, Any, T_co], + *, + name: object = None, + context: Context | None = None, + ) -> TaskHandle[T_co]: + if not isinstance(coro, Coroutine): + raise TypeError(f"expected a coroutine, got {coro.__class__.__qualname__}") + + self._check_active(coro) + handle = TaskHandle(coro, name) + if context is not None: + context.run( + partial(self._nursery.start_soon, handle._run_coro, name=handle.name) + ) + else: + self._nursery.start_soon(handle._run_coro, name=handle.name) + + return handle + + async def start( + self, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + name: object = None, + return_handle: Literal[False] | Literal[True] = False, + ) -> Any: + handle: TaskHandle[T_co] + + async def run_coro_with_task_status( + *, task_status: trio.TaskStatus[Any] + ) -> None: + nonlocal handle + wrapper_task_status = _TrioTaskStatus() + coro = call_for_coroutine(func, args, task_status=wrapper_task_status) + if wrapper_task_status.early_start_value is not empty_start_value: + task_status.started(wrapper_task_status.early_start_value) + else: + wrapper_task_status.real_task_status = task_status + + handle = TaskHandle(coro, name) + await handle._run_coro() + + self._check_active() + final_name = get_callable_name(func, name) + start_value = await self._nursery.start( + run_coro_with_task_status, name=final_name + ) + if return_handle: + handle._start_value = start_value + return handle + else: + return start_value + + +# +# Subprocesses +# + + +@dataclass(eq=False) +class ReceiveStreamWrapper(abc.ByteReceiveStream): + _stream: trio.abc.ReceiveStream + + async def receive(self, max_bytes: int | None = None) -> bytes: + try: + data = await self._stream.receive_some(max_bytes) + except trio.ClosedResourceError as exc: + raise ClosedResourceError from exc.__cause__ + except trio.BrokenResourceError as exc: + raise BrokenResourceError from exc.__cause__ + + if data: + return bytes(data) + else: + raise EndOfStream + + async def aclose(self) -> None: + await self._stream.aclose() + + +@dataclass(eq=False) +class SendStreamWrapper(abc.ByteSendStream): + _stream: trio.abc.SendStream + + async def send(self, item: bytes) -> None: + try: + await self._stream.send_all(item) + except trio.ClosedResourceError as exc: + raise ClosedResourceError from exc.__cause__ + except trio.BrokenResourceError as exc: + raise BrokenResourceError from exc.__cause__ + + async def aclose(self) -> None: + await self._stream.aclose() + + +@dataclass(eq=False) +class Process(abc.Process): + _process: trio.Process + _stdin: abc.ByteSendStream | None + _stdout: abc.ByteReceiveStream | None + _stderr: abc.ByteReceiveStream | None + + async def aclose(self) -> None: + with CancelScope(shield=True): + if self._stdin: + await self._stdin.aclose() + if self._stdout: + await self._stdout.aclose() + if self._stderr: + await self._stderr.aclose() + + try: + await self.wait() + except BaseException: + self.kill() + with CancelScope(shield=True): + await self.wait() + raise + + async def wait(self) -> int: + return await self._process.wait() + + def terminate(self) -> None: + self._process.terminate() + + def kill(self) -> None: + self._process.kill() + + def send_signal(self, signal: Signals) -> None: + self._process.send_signal(signal) + + @property + def pid(self) -> int: + return self._process.pid + + @property + def returncode(self) -> int | None: + return self._process.returncode + + @property + def stdin(self) -> abc.ByteSendStream | None: + return self._stdin + + @property + def stdout(self) -> abc.ByteReceiveStream | None: + return self._stdout + + @property + def stderr(self) -> abc.ByteReceiveStream | None: + return self._stderr + + +class _ProcessPoolShutdownInstrument(trio.abc.Instrument): + def after_run(self) -> None: + super().after_run() + + +current_default_worker_process_limiter: trio.lowlevel.RunVar = RunVar( + "current_default_worker_process_limiter" +) + + +async def _shutdown_process_pool(workers: set[abc.Process]) -> None: + try: + await trio.sleep(math.inf) + except trio.Cancelled: + for process in workers: + if process.returncode is None: + process.kill() + + with CancelScope(shield=True): + for process in workers: + await process.aclose() + + +# +# Sockets and networking +# + + +class _TrioSocketMixin(Generic[T_SockAddr]): + def __init__(self, trio_socket: TrioSocketType) -> None: + self._trio_socket = trio_socket + self._closed = False + + def _check_closed(self) -> None: + if self._closed: + raise ClosedResourceError + if self._trio_socket.fileno() < 0: + raise BrokenResourceError + + @property + def _raw_socket(self) -> socket.socket: + return self._trio_socket._sock # type: ignore[attr-defined] + + async def aclose(self) -> None: + if self._trio_socket.fileno() >= 0: + self._closed = True + self._trio_socket.close() + + def _convert_socket_error(self, exc: BaseException) -> NoReturn: + if isinstance(exc, trio.ClosedResourceError): + raise ClosedResourceError from exc + elif self._trio_socket.fileno() < 0 and self._closed: + raise ClosedResourceError from None + elif isinstance(exc, OSError): + raise BrokenResourceError from exc + else: + raise exc + + +class SocketStream(_TrioSocketMixin, abc.SocketStream): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self, max_bytes: int = 65536) -> bytes: + with self._receive_guard: + try: + data = await self._trio_socket.recv(max_bytes) + except BaseException as exc: + self._convert_socket_error(exc) + + if data: + return data + else: + raise EndOfStream + + async def send(self, item: bytes) -> None: + with self._send_guard: + view = memoryview(item) + while view: + try: + bytes_sent = await self._trio_socket.send(view) + except BaseException as exc: + self._convert_socket_error(exc) + + view = view[bytes_sent:] + + async def send_eof(self) -> None: + self._trio_socket.shutdown(socket.SHUT_WR) + + +class UNIXSocketStream(SocketStream, abc.UNIXSocketStream): + async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: + if not isinstance(msglen, int) or msglen < 0: + raise ValueError("msglen must be a non-negative integer") + if not isinstance(maxfds, int) or maxfds < 1: + raise ValueError("maxfds must be a positive integer") + + fds = array.array("i") + await trio.lowlevel.checkpoint() + with self._receive_guard: + while True: + try: + message, ancdata, flags, addr = await self._trio_socket.recvmsg( + msglen, socket.CMSG_LEN(maxfds * fds.itemsize) + ) + except BaseException as exc: + self._convert_socket_error(exc) + else: + if not message and not ancdata: + raise EndOfStream + + break + + for cmsg_level, cmsg_type, cmsg_data in ancdata: + if cmsg_level != socket.SOL_SOCKET or cmsg_type != socket.SCM_RIGHTS: + raise RuntimeError( + f"Received unexpected ancillary data; message = {message!r}, " + f"cmsg_level = {cmsg_level}, cmsg_type = {cmsg_type}" + ) + + fds.frombytes(cmsg_data[: len(cmsg_data) - (len(cmsg_data) % fds.itemsize)]) + + return message, list(fds) + + async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: + if not message: + raise ValueError("message must not be empty") + if not fds: + raise ValueError("fds must not be empty") + + filenos: list[int] = [] + for fd in fds: + if isinstance(fd, int): + filenos.append(fd) + elif isinstance(fd, IOBase): + filenos.append(fd.fileno()) + + fdarray = array.array("i", filenos) + await trio.lowlevel.checkpoint() + with self._send_guard: + while True: + try: + await self._trio_socket.sendmsg( + [message], + [ + ( + socket.SOL_SOCKET, + socket.SCM_RIGHTS, + fdarray, + ) + ], + ) + break + except BaseException as exc: + self._convert_socket_error(exc) + + +class TCPSocketListener(_TrioSocketMixin, abc.SocketListener): + def __init__(self, raw_socket: socket.socket): + super().__init__(trio.socket.from_stdlib_socket(raw_socket)) + self._accept_guard = ResourceGuard("accepting connections from") + + async def accept(self) -> SocketStream: + with self._accept_guard: + try: + trio_socket, _addr = await self._trio_socket.accept() + except BaseException as exc: + self._convert_socket_error(exc) + + trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + return SocketStream(trio_socket) + + +class UNIXSocketListener(_TrioSocketMixin, abc.SocketListener): + def __init__(self, raw_socket: socket.socket): + super().__init__(trio.socket.from_stdlib_socket(raw_socket)) + self._accept_guard = ResourceGuard("accepting connections from") + + async def accept(self) -> UNIXSocketStream: + with self._accept_guard: + try: + trio_socket, _addr = await self._trio_socket.accept() + except BaseException as exc: + self._convert_socket_error(exc) + + return UNIXSocketStream(trio_socket) + + +class UDPSocket(_TrioSocketMixin[IPSockAddrType], abc.UDPSocket): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> tuple[bytes, IPSockAddrType]: + with self._receive_guard: + try: + data, addr = await self._trio_socket.recvfrom(65536) + return data, convert_ipv6_sockaddr(addr) + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: UDPPacketType) -> None: + with self._send_guard: + try: + await self._trio_socket.sendto(*item) + except BaseException as exc: + self._convert_socket_error(exc) + + +class ConnectedUDPSocket(_TrioSocketMixin[IPSockAddrType], abc.ConnectedUDPSocket): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> bytes: + with self._receive_guard: + try: + return await self._trio_socket.recv(65536) + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: bytes) -> None: + with self._send_guard: + try: + await self._trio_socket.send(item) + except BaseException as exc: + self._convert_socket_error(exc) + + +class UNIXDatagramSocket(_TrioSocketMixin[str], abc.UNIXDatagramSocket): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> UNIXDatagramPacketType: + with self._receive_guard: + try: + data, addr = await self._trio_socket.recvfrom(65536) + return data, addr + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: UNIXDatagramPacketType) -> None: + with self._send_guard: + try: + await self._trio_socket.sendto(*item) + except BaseException as exc: + self._convert_socket_error(exc) + + +class ConnectedUNIXDatagramSocket( + _TrioSocketMixin[str], abc.ConnectedUNIXDatagramSocket +): + def __init__(self, trio_socket: TrioSocketType) -> None: + super().__init__(trio_socket) + self._receive_guard = ResourceGuard("reading from") + self._send_guard = ResourceGuard("writing to") + + async def receive(self) -> bytes: + with self._receive_guard: + try: + return await self._trio_socket.recv(65536) + except BaseException as exc: + self._convert_socket_error(exc) + + async def send(self, item: bytes) -> None: + with self._send_guard: + try: + await self._trio_socket.send(item) + except BaseException as exc: + self._convert_socket_error(exc) + + +# +# Synchronization +# + + +class Event(BaseEvent): + __slots__ = ("__original",) + + def __new__(cls) -> Event: + return object.__new__(cls) + + def __init__(self) -> None: + self.__original = trio.Event() + + def is_set(self) -> bool: + return self.__original.is_set() + + async def wait(self) -> None: + return await self.__original.wait() + + def statistics(self) -> EventStatistics: + orig_statistics = self.__original.statistics() + return EventStatistics(tasks_waiting=orig_statistics.tasks_waiting) + + def set(self) -> None: + self.__original.set() + + +class Lock(BaseLock): + __slots__ = "_fast_acquire", "__original" + + def __new__(cls, *, fast_acquire: bool = False) -> Lock: + return object.__new__(cls) + + def __init__(self, *, fast_acquire: bool = False) -> None: + self._fast_acquire = fast_acquire + self.__original = trio.Lock() + + @staticmethod + def _convert_runtime_error_msg(exc: RuntimeError) -> None: + if exc.args == ("attempt to re-acquire an already held Lock",): + exc.args = ("Attempted to acquire an already held Lock",) + + async def acquire(self) -> None: + if not self._fast_acquire: + try: + await self.__original.acquire() + except RuntimeError as exc: + self._convert_runtime_error_msg(exc) + raise + + return + + # This is the "fast path" where we don't let other tasks run + await trio.lowlevel.checkpoint_if_cancelled() + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + await self.__original._lot.park() + except RuntimeError as exc: + self._convert_runtime_error_msg(exc) + raise + + def acquire_nowait(self) -> None: + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + raise WouldBlock from None + except RuntimeError as exc: + self._convert_runtime_error_msg(exc) + raise + + def locked(self) -> bool: + return self.__original.locked() + + def release(self) -> None: + self.__original.release() + + def statistics(self) -> LockStatistics: + orig_statistics = self.__original.statistics() + owner = TrioTaskInfo(orig_statistics.owner) if orig_statistics.owner else None + return LockStatistics( + orig_statistics.locked, owner, orig_statistics.tasks_waiting + ) + + +class Semaphore(BaseSemaphore): + __slots__ = ("__original",) + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Semaphore: + return object.__new__(cls) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> None: + super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire) + self.__original = trio.Semaphore(initial_value, max_value=max_value) + + async def acquire(self) -> None: + if not self._fast_acquire: + await self.__original.acquire() + return + + # This is the "fast path" where we don't let other tasks run + await trio.lowlevel.checkpoint_if_cancelled() + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + await self.__original._lot.park() + + def acquire_nowait(self) -> None: + try: + self.__original.acquire_nowait() + except trio.WouldBlock: + raise WouldBlock from None + + @property + def max_value(self) -> int | None: + return self.__original.max_value + + @property + def value(self) -> int: + return self.__original.value + + def release(self) -> None: + self.__original.release() + + def statistics(self) -> SemaphoreStatistics: + orig_statistics = self.__original.statistics() + return SemaphoreStatistics(orig_statistics.tasks_waiting) + + +class CapacityLimiter(BaseCapacityLimiter): + __slots__ = ("__original",) + + def __new__( + cls, + total_tokens: float | None = None, + *, + original: trio.CapacityLimiter | None = None, + ) -> CapacityLimiter: + return object.__new__(cls) + + def __init__( + self, + total_tokens: float | None = None, + *, + original: trio.CapacityLimiter | None = None, + ) -> None: + if original is not None: + self.__original = original + else: + assert total_tokens is not None + self.__original = trio.CapacityLimiter(total_tokens) + + async def __aenter__(self) -> None: + return await self.__original.__aenter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.__original.__aexit__(exc_type, exc_val, exc_tb) + + @property + def total_tokens(self) -> float: + return self.__original.total_tokens + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + self.__original.total_tokens = value + + @property + def borrowed_tokens(self) -> int: + return self.__original.borrowed_tokens + + @property + def available_tokens(self) -> float: + return self.__original.available_tokens + + def acquire_nowait(self) -> None: + self.__original.acquire_nowait() + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + self.__original.acquire_on_behalf_of_nowait(borrower) + + async def acquire(self) -> None: + await self.__original.acquire() + + async def acquire_on_behalf_of(self, borrower: object) -> None: + await self.__original.acquire_on_behalf_of(borrower) + + def release(self) -> None: + return self.__original.release() + + def release_on_behalf_of(self, borrower: object) -> None: + return self.__original.release_on_behalf_of(borrower) + + def statistics(self) -> CapacityLimiterStatistics: + orig = self.__original.statistics() + return CapacityLimiterStatistics( + borrowed_tokens=orig.borrowed_tokens, + total_tokens=orig.total_tokens, + borrowers=tuple(orig.borrowers), + tasks_waiting=orig.tasks_waiting, + ) + + +_capacity_limiter_wrapper: trio.lowlevel.RunVar = RunVar("_capacity_limiter_wrapper") + + +# +# Signal handling +# + + +class _SignalReceiver: + _iterator: AsyncIterator[int] + + def __init__(self, signals: tuple[Signals, ...]): + self._signals = signals + + def __enter__(self) -> _SignalReceiver: + self._cm = trio.open_signal_receiver(*self._signals) + self._iterator = self._cm.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool | None: + return self._cm.__exit__(exc_type, exc_val, exc_tb) + + def __aiter__(self) -> _SignalReceiver: + return self + + async def __anext__(self) -> Signals: + signum = await self._iterator.__anext__() + return Signals(signum) + + +# +# Testing and debugging +# + + +class TestRunner(abc.TestRunner): + def __init__(self, **options: Any) -> None: + from queue import Queue + + self._call_queue: Queue[Callable[[], object]] = Queue() + self._send_stream: ( + MemoryObjectSendStream[tuple[Awaitable[Any], list[Outcome]]] | None + ) = None + self._options = options + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> None: + if self._send_stream: + self._send_stream.close() + while self._send_stream is not None: + self._call_queue.get()() + + def is_running(self) -> bool: + return trio.lowlevel.in_trio_task() + + async def _run_tests_and_fixtures(self) -> None: + self._send_stream, receive_stream = create_memory_object_stream[ + tuple[Awaitable[Any], list[Outcome]] + ](1) + with receive_stream: + async for awaitable, outcome_holder in receive_stream: + try: + retval = await awaitable + except BaseException as exc: + outcome_holder.append(Error(exc)) + else: + outcome_holder.append(Value(retval)) + + def _main_task_finished(self, outcome: object) -> None: + self._send_stream = None + + def _call_in_runner_task( + self, + func: Callable[P, Awaitable[T_Retval]], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> T_Retval: + if self._send_stream is None: + trio.lowlevel.start_guest_run( + self._run_tests_and_fixtures, + run_sync_soon_threadsafe=self._call_queue.put, + done_callback=self._main_task_finished, + **self._options, + ) + while self._send_stream is None: + self._call_queue.get()() + + outcome_holder: list[Outcome] = [] + self._send_stream.send_nowait((func(*args, **kwargs), outcome_holder)) + while not outcome_holder: + self._call_queue.get()() + + return outcome_holder[0].unwrap() + + def run_asyncgen_fixture( + self, + fixture_func: Callable[..., AsyncGenerator[T_Retval, Any]], + kwargs: dict[str, Any], + ) -> Iterable[T_Retval]: + asyncgen = fixture_func(**kwargs) + fixturevalue: T_Retval = self._call_in_runner_task(asyncgen.asend, None) + + yield fixturevalue + + try: + self._call_in_runner_task(asyncgen.asend, None) + except StopAsyncIteration: + pass + else: + self._call_in_runner_task(asyncgen.aclose) + raise RuntimeError("Async generator fixture did not stop") + + def run_fixture( + self, + fixture_func: Callable[..., Coroutine[Any, Any, T_Retval]], + kwargs: dict[str, Any], + ) -> T_Retval: + return self._call_in_runner_task(fixture_func, **kwargs) + + def run_test( + self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] + ) -> None: + self._call_in_runner_task(test_func, **kwargs) + + +class TrioTaskInfo(TaskInfo): + def __init__(self, task: trio.lowlevel.Task): + parent_id = None + if task.parent_nursery and task.parent_nursery.parent_task: + parent_id = id(task.parent_nursery.parent_task) + + super().__init__(id(task), parent_id, task.name, task.coro) + self._task = weakref.proxy(task) + + def has_pending_cancellation(self) -> bool: + try: + return self._task._cancel_status.effectively_cancelled + except ReferenceError: + # If the task is no longer around, it surely doesn't have a cancellation + # pending + return False + + +class TrioBackend(AsyncBackend): + @classmethod + def run( + cls, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + options: dict[str, Any], + ) -> T_Retval: + assert not kwargs, "unreachable, and not supported by Trio" + return trio.run(ensure_returns_coro(func), *args, **options) + + @classmethod + def current_token(cls) -> object: + return trio.lowlevel.current_trio_token() + + @classmethod + def current_time(cls) -> float: + return trio.current_time() + + @classmethod + def cancelled_exception_class(cls) -> type[BaseException]: + return trio.Cancelled + + @classmethod + async def checkpoint(cls) -> None: + await trio.lowlevel.checkpoint() + + @classmethod + async def checkpoint_if_cancelled(cls) -> None: + await trio.lowlevel.checkpoint_if_cancelled() + + @classmethod + async def cancel_shielded_checkpoint(cls) -> None: + await trio.lowlevel.cancel_shielded_checkpoint() + + @classmethod + async def sleep(cls, delay: float) -> None: + await trio.sleep(delay) + + @classmethod + def create_cancel_scope( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> abc.CancelScope: + return CancelScope(deadline=deadline, shield=shield) + + @classmethod + def current_effective_deadline(cls) -> float: + return trio.current_effective_deadline() + + @classmethod + def create_task_group(cls) -> abc.TaskGroup: + return TaskGroup() + + @classmethod + def create_event(cls) -> abc.Event: + return Event() + + @classmethod + def create_lock(cls, *, fast_acquire: bool) -> Lock: + return Lock(fast_acquire=fast_acquire) + + @classmethod + def create_semaphore( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> abc.Semaphore: + return Semaphore(initial_value, max_value=max_value, fast_acquire=fast_acquire) + + @classmethod + def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: + return CapacityLimiter(total_tokens) + + @classmethod + async def run_sync_in_worker_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + abandon_on_cancel: bool = False, + limiter: abc.CapacityLimiter | None = None, + ) -> T_Retval: + def wrapper() -> T_Retval: + with claim_worker_thread(TrioBackend, token): + return func(*args) + + token = TrioBackend.current_token() + return await run_sync( + wrapper, + abandon_on_cancel=abandon_on_cancel, + limiter=cast(trio.CapacityLimiter, limiter), + ) + + @classmethod + def check_cancelled(cls) -> None: + trio.from_thread.check_cancelled() + + @classmethod + def run_async_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_co: + trio_token = cast("trio.lowlevel.TrioToken | None", token) + try: + return trio.from_thread.run(func, *args, trio_token=trio_token) + except trio.RunFinishedError: + raise RunFinishedError from None + + @classmethod + def run_sync_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_Retval: + trio_token = cast("trio.lowlevel.TrioToken | None", token) + try: + return trio.from_thread.run_sync(func, *args, trio_token=trio_token) + except trio.RunFinishedError: + raise RunFinishedError from None + + @classmethod + async def open_process( + cls, + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None, + stdout: int | IO[Any] | None, + stderr: int | IO[Any] | None, + **kwargs: Any, + ) -> Process: + def convert_item(item: StrOrBytesPath) -> str: + str_or_bytes = os.fspath(item) + if isinstance(str_or_bytes, str): + return str_or_bytes + else: + return os.fsdecode(str_or_bytes) + + if isinstance(command, (str, bytes, PathLike)): + process = await trio.lowlevel.open_process( + convert_item(command), + stdin=stdin, + stdout=stdout, + stderr=stderr, + shell=True, + **kwargs, + ) + else: + process = await trio.lowlevel.open_process( + [convert_item(item) for item in command], + stdin=stdin, + stdout=stdout, + stderr=stderr, + shell=False, + **kwargs, + ) + + stdin_stream = SendStreamWrapper(process.stdin) if process.stdin else None + stdout_stream = ReceiveStreamWrapper(process.stdout) if process.stdout else None + stderr_stream = ReceiveStreamWrapper(process.stderr) if process.stderr else None + return Process(process, stdin_stream, stdout_stream, stderr_stream) + + @classmethod + def setup_process_pool_exit_at_shutdown(cls, workers: set[abc.Process]) -> None: + trio.lowlevel.spawn_system_task(_shutdown_process_pool, workers) + + @classmethod + async def connect_tcp( + cls, host: str, port: int, local_address: IPSockAddrType | None = None + ) -> SocketStream: + family = socket.AF_INET6 if ":" in host else socket.AF_INET + trio_socket = trio.socket.socket(family) + trio_socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + if local_address: + await trio_socket.bind(local_address) + + try: + await trio_socket.connect((host, port)) + except BaseException: + trio_socket.close() + raise + + return SocketStream(trio_socket) + + @classmethod + async def connect_unix(cls, path: str | bytes) -> abc.UNIXSocketStream: + trio_socket = trio.socket.socket(socket.AF_UNIX) + try: + await trio_socket.connect(path) + except BaseException: + trio_socket.close() + raise + + return UNIXSocketStream(trio_socket) + + @classmethod + def create_tcp_listener(cls, sock: socket.socket) -> abc.SocketListener: + return TCPSocketListener(sock) + + @classmethod + def create_unix_listener(cls, sock: socket.socket) -> abc.SocketListener: + return UNIXSocketListener(sock) + + @classmethod + async def create_udp_socket( + cls, + family: socket.AddressFamily, + local_address: IPSockAddrType | None, + remote_address: IPSockAddrType | None, + reuse_port: bool, + ) -> UDPSocket | ConnectedUDPSocket: + trio_socket = trio.socket.socket(family=family, type=socket.SOCK_DGRAM) + + if reuse_port: + trio_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + + if local_address: + await trio_socket.bind(local_address) + + if remote_address: + await trio_socket.connect(remote_address) + return ConnectedUDPSocket(trio_socket) + else: + return UDPSocket(trio_socket) + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket.socket, remote_path: None + ) -> abc.UNIXDatagramSocket: ... + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket.socket, remote_path: str | bytes + ) -> abc.ConnectedUNIXDatagramSocket: ... + + @classmethod + async def create_unix_datagram_socket( + cls, raw_socket: socket.socket, remote_path: str | bytes | None + ) -> abc.UNIXDatagramSocket | abc.ConnectedUNIXDatagramSocket: + trio_socket = trio.socket.from_stdlib_socket(raw_socket) + + if remote_path: + await trio_socket.connect(remote_path) + return ConnectedUNIXDatagramSocket(trio_socket) + else: + return UNIXDatagramSocket(trio_socket) + + @classmethod + async def getaddrinfo( + cls, + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, + ) -> Sequence[ + tuple[ + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], + ] + ]: + return await trio.socket.getaddrinfo(host, port, family, type, proto, flags) + + @classmethod + async def getnameinfo( + cls, sockaddr: IPSockAddrType, flags: int = 0 + ) -> tuple[str, str]: + return await trio.socket.getnameinfo(sockaddr, flags) + + @classmethod + async def wait_readable(cls, obj: FileDescriptorLike) -> None: + try: + await wait_readable(obj) + except trio.ClosedResourceError as exc: + raise ClosedResourceError().with_traceback(exc.__traceback__) from None + except trio.BusyResourceError: + raise BusyResourceError("reading from") from None + + @classmethod + async def wait_writable(cls, obj: FileDescriptorLike) -> None: + try: + await wait_writable(obj) + except trio.ClosedResourceError as exc: + raise ClosedResourceError().with_traceback(exc.__traceback__) from None + except trio.BusyResourceError: + raise BusyResourceError("writing to") from None + + @classmethod + def notify_closing(cls, obj: FileDescriptorLike) -> None: + notify_closing(obj) + + @classmethod + async def wrap_listener_socket(cls, sock: socket.socket) -> abc.SocketListener: + if hasattr(socket, "AF_UNIX") and sock.family == socket.AF_UNIX: + return UNIXSocketListener(sock) + + return TCPSocketListener(sock) + + @classmethod + async def wrap_stream_socket(cls, sock: socket.socket) -> SocketStream: + trio_sock = trio.socket.from_stdlib_socket(sock) + return SocketStream(trio_sock) + + @classmethod + async def wrap_unix_stream_socket(cls, sock: socket.socket) -> UNIXSocketStream: + trio_sock = trio.socket.from_stdlib_socket(sock) + return UNIXSocketStream(trio_sock) + + @classmethod + async def wrap_udp_socket(cls, sock: socket.socket) -> UDPSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return UDPSocket(trio_sock) + + @classmethod + async def wrap_connected_udp_socket(cls, sock: socket.socket) -> ConnectedUDPSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return ConnectedUDPSocket(trio_sock) + + @classmethod + async def wrap_unix_datagram_socket(cls, sock: socket.socket) -> UNIXDatagramSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return UNIXDatagramSocket(trio_sock) + + @classmethod + async def wrap_connected_unix_datagram_socket( + cls, sock: socket.socket + ) -> ConnectedUNIXDatagramSocket: + trio_sock = trio.socket.from_stdlib_socket(sock) + return ConnectedUNIXDatagramSocket(trio_sock) + + @classmethod + def current_default_thread_limiter(cls) -> CapacityLimiter: + try: + return _capacity_limiter_wrapper.get() + except LookupError: + limiter = CapacityLimiter( + original=trio.to_thread.current_default_thread_limiter() + ) + _capacity_limiter_wrapper.set(limiter) + return limiter + + @classmethod + def open_signal_receiver( + cls, *signals: Signals + ) -> AbstractContextManager[AsyncIterator[Signals]]: + return _SignalReceiver(signals) + + @classmethod + def get_current_task(cls) -> TaskInfo: + task = current_task() + return TrioTaskInfo(task) + + @classmethod + def get_running_tasks(cls) -> Sequence[TaskInfo]: + root_task = current_root_task() + assert root_task + task_infos = [TrioTaskInfo(root_task)] + nurseries = root_task.child_nurseries + while nurseries: + new_nurseries: list[trio.Nursery] = [] + for nursery in nurseries: + for task in nursery.child_tasks: + task_infos.append(TrioTaskInfo(task)) + new_nurseries.extend(task.child_nurseries) + + nurseries = new_nurseries + + return task_infos + + @classmethod + async def wait_all_tasks_blocked(cls) -> None: + from trio.testing import wait_all_tasks_blocked + + await wait_all_tasks_blocked() + + @classmethod + def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: + return TestRunner(**options) + + +backend_class = TrioBackend diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__init__.py b/venv/lib/python3.11/site-packages/anyio/_core/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..b2cec8d Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_asyncio_selector_thread.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_asyncio_selector_thread.cpython-311.pyc new file mode 100644 index 0000000..2f9a4f6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_asyncio_selector_thread.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_contextmanagers.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_contextmanagers.cpython-311.pyc new file mode 100644 index 0000000..d787f95 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_contextmanagers.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_eventloop.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_eventloop.cpython-311.pyc new file mode 100644 index 0000000..fb9fa82 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_eventloop.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_exceptions.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_exceptions.cpython-311.pyc new file mode 100644 index 0000000..3a195fe Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_exceptions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_fileio.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_fileio.cpython-311.pyc new file mode 100644 index 0000000..869b08c Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_fileio.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_resources.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_resources.cpython-311.pyc new file mode 100644 index 0000000..f544153 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_resources.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_signals.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_signals.cpython-311.pyc new file mode 100644 index 0000000..dbdc906 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_signals.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_sockets.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_sockets.cpython-311.pyc new file mode 100644 index 0000000..c0478ec Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_sockets.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_streams.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_streams.cpython-311.pyc new file mode 100644 index 0000000..3fec8fc Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_streams.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-311.pyc new file mode 100644 index 0000000..047ebf5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_subprocesses.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_synchronization.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_synchronization.cpython-311.pyc new file mode 100644 index 0000000..f12a9de Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_synchronization.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_tasks.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_tasks.cpython-311.pyc new file mode 100644 index 0000000..3d12bb7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_tasks.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_tempfile.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_tempfile.cpython-311.pyc new file mode 100644 index 0000000..f71f707 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_tempfile.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_testing.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_testing.cpython-311.pyc new file mode 100644 index 0000000..c95a11a Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_testing.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_typedattr.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_typedattr.cpython-311.pyc new file mode 100644 index 0000000..72587b4 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/_core/__pycache__/_typedattr.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_asyncio_selector_thread.py b/venv/lib/python3.11/site-packages/anyio/_core/_asyncio_selector_thread.py new file mode 100644 index 0000000..9f35bae --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_asyncio_selector_thread.py @@ -0,0 +1,167 @@ +from __future__ import annotations + +import asyncio +import socket +import threading +from collections.abc import Callable +from selectors import EVENT_READ, EVENT_WRITE, DefaultSelector +from typing import TYPE_CHECKING, Any + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + +_selector_lock = threading.Lock() +_selector: Selector | None = None + + +class Selector: + def __init__(self) -> None: + self._thread = threading.Thread(target=self.run, name="AnyIO socket selector") + self._selector = DefaultSelector() + self._send, self._receive = socket.socketpair() + self._send.setblocking(False) + self._receive.setblocking(False) + # This somewhat reduces the amount of memory wasted queueing up data + # for wakeups. With these settings, maximum number of 1-byte sends + # before getting BlockingIOError: + # Linux 4.8: 6 + # macOS (darwin 15.5): 1 + # Windows 10: 525347 + # Windows you're weird. (And on Windows setting SNDBUF to 0 makes send + # blocking, even on non-blocking sockets, so don't do that.) + self._receive.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1) + self._send.setsockopt(socket.SOL_SOCKET, socket.SO_SNDBUF, 1) + # On Windows this is a TCP socket so this might matter. On other + # platforms this fails b/c AF_UNIX sockets aren't actually TCP. + try: + self._send.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + except OSError: + pass + + self._selector.register(self._receive, EVENT_READ) + self._closed = False + + def start(self) -> None: + self._thread.start() + threading._register_atexit(self._stop) # type: ignore[attr-defined] + + def _stop(self) -> None: + global _selector + self._closed = True + self._notify_self() + self._send.close() + self._thread.join() + self._selector.unregister(self._receive) + self._receive.close() + self._selector.close() + _selector = None + assert not self._selector.get_map(), ( + "selector still has registered file descriptors after shutdown" + ) + + def _notify_self(self) -> None: + try: + self._send.send(b"\x00") + except BlockingIOError: + pass + + def add_reader(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: + loop = asyncio.get_running_loop() + try: + key = self._selector.get_key(fd) + except KeyError: + self._selector.register(fd, EVENT_READ, {EVENT_READ: (loop, callback)}) + else: + if EVENT_READ in key.data: + raise ValueError( + "this file descriptor is already registered for reading" + ) + + key.data[EVENT_READ] = loop, callback + self._selector.modify(fd, key.events | EVENT_READ, key.data) + + self._notify_self() + + def add_writer(self, fd: FileDescriptorLike, callback: Callable[[], Any]) -> None: + loop = asyncio.get_running_loop() + try: + key = self._selector.get_key(fd) + except KeyError: + self._selector.register(fd, EVENT_WRITE, {EVENT_WRITE: (loop, callback)}) + else: + if EVENT_WRITE in key.data: + raise ValueError( + "this file descriptor is already registered for writing" + ) + + key.data[EVENT_WRITE] = loop, callback + self._selector.modify(fd, key.events | EVENT_WRITE, key.data) + + self._notify_self() + + def remove_reader(self, fd: FileDescriptorLike) -> bool: + try: + key = self._selector.get_key(fd) + except KeyError: + return False + + if new_events := key.events ^ EVENT_READ: + del key.data[EVENT_READ] + self._selector.modify(fd, new_events, key.data) + else: + self._selector.unregister(fd) + + return True + + def remove_writer(self, fd: FileDescriptorLike) -> bool: + try: + key = self._selector.get_key(fd) + except KeyError: + return False + + if new_events := key.events ^ EVENT_WRITE: + del key.data[EVENT_WRITE] + self._selector.modify(fd, new_events, key.data) + else: + self._selector.unregister(fd) + + return True + + def run(self) -> None: + while not self._closed: + for key, events in self._selector.select(): + if key.fileobj is self._receive: + try: + while self._receive.recv(4096): + pass + except BlockingIOError: + pass + + continue + + if events & EVENT_READ: + loop, callback = key.data[EVENT_READ] + self.remove_reader(key.fd) + try: + loop.call_soon_threadsafe(callback) + except RuntimeError: + pass # the loop was already closed + + if events & EVENT_WRITE: + loop, callback = key.data[EVENT_WRITE] + self.remove_writer(key.fd) + try: + loop.call_soon_threadsafe(callback) + except RuntimeError: + pass # the loop was already closed + + +def get_selector() -> Selector: + global _selector + + with _selector_lock: + if _selector is None: + _selector = Selector() + _selector.start() + + return _selector diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_contextmanagers.py b/venv/lib/python3.11/site-packages/anyio/_core/_contextmanagers.py new file mode 100644 index 0000000..302f32b --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_contextmanagers.py @@ -0,0 +1,200 @@ +from __future__ import annotations + +from abc import abstractmethod +from contextlib import AbstractAsyncContextManager, AbstractContextManager +from inspect import isasyncgen, iscoroutine, isgenerator +from types import TracebackType +from typing import Protocol, TypeVar, cast, final + +_T_co = TypeVar("_T_co", covariant=True) +_ExitT_co = TypeVar("_ExitT_co", covariant=True, bound="bool | None") + + +class _SupportsCtxMgr(Protocol[_T_co, _ExitT_co]): + def __contextmanager__(self) -> AbstractContextManager[_T_co, _ExitT_co]: ... + + +class _SupportsAsyncCtxMgr(Protocol[_T_co, _ExitT_co]): + def __asynccontextmanager__( + self, + ) -> AbstractAsyncContextManager[_T_co, _ExitT_co]: ... + + +class ContextManagerMixin: + """ + Mixin class providing context manager functionality via a generator-based + implementation. + + This class allows you to implement a context manager via :meth:`__contextmanager__` + which should return a generator. The mechanics are meant to mirror those of + :func:`@contextmanager `. + + .. note:: Classes using this mix-in are not reentrant as context managers, meaning + that once you enter it, you can't re-enter before first exiting it. + + .. seealso:: :doc:`contextmanagers` + """ + + __cm: AbstractContextManager[object, bool | None] | None = None + + @final + def __enter__(self: _SupportsCtxMgr[_T_co, bool | None]) -> _T_co: + # Needed for mypy to assume self still has the __cm member + assert isinstance(self, ContextManagerMixin) + if self.__cm is not None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has already been entered" + ) + + cm = self.__contextmanager__() + if not isinstance(cm, AbstractContextManager): + if isgenerator(cm): + raise TypeError( + "__contextmanager__() returned a generator object instead of " + "a context manager. Did you forget to add the @contextmanager " + "decorator?" + ) + + raise TypeError( + f"__contextmanager__() did not return a context manager object, " + f"but {cm.__class__!r}" + ) + + if cm is self: + raise TypeError( + f"{self.__class__.__qualname__}.__contextmanager__() returned " + f"self. Did you forget to add the @contextmanager decorator and a " + f"'yield' statement?" + ) + + value = cm.__enter__() + self.__cm = cm + return value + + @final + def __exit__( + self: _SupportsCtxMgr[object, _ExitT_co], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> _ExitT_co: + # Needed for mypy to assume self still has the __cm member + assert isinstance(self, ContextManagerMixin) + if self.__cm is None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has not been entered yet" + ) + + # Prevent circular references + cm = self.__cm + del self.__cm + + return cast(_ExitT_co, cm.__exit__(exc_type, exc_val, exc_tb)) + + @abstractmethod + def __contextmanager__(self) -> AbstractContextManager[object, bool | None]: + """ + Implement your context manager logic here. + + This method **must** be decorated with + :func:`@contextmanager `. + + .. note:: Remember that the ``yield`` will raise any exception raised in the + enclosed context block, so use a ``finally:`` block to clean up resources! + + :return: a context manager object + """ + + +class AsyncContextManagerMixin: + """ + Mixin class providing async context manager functionality via a generator-based + implementation. + + This class allows you to implement a context manager via + :meth:`__asynccontextmanager__`. The mechanics are meant to mirror those of + :func:`@asynccontextmanager `. + + .. note:: Classes using this mix-in are not reentrant as context managers, meaning + that once you enter it, you can't re-enter before first exiting it. + + .. seealso:: :doc:`contextmanagers` + """ + + __cm: AbstractAsyncContextManager[object, bool | None] | None = None + + @final + async def __aenter__(self: _SupportsAsyncCtxMgr[_T_co, bool | None]) -> _T_co: + # Needed for mypy to assume self still has the __cm member + assert isinstance(self, AsyncContextManagerMixin) + if self.__cm is not None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has already been entered" + ) + + cm = self.__asynccontextmanager__() + if not isinstance(cm, AbstractAsyncContextManager): + if isasyncgen(cm): + raise TypeError( + "__asynccontextmanager__() returned an async generator instead of " + "an async context manager. Did you forget to add the " + "@asynccontextmanager decorator?" + ) + elif iscoroutine(cm): + cm.close() + raise TypeError( + "__asynccontextmanager__() returned a coroutine object instead of " + "an async context manager. Did you forget to add the " + "@asynccontextmanager decorator and a 'yield' statement?" + ) + + raise TypeError( + f"__asynccontextmanager__() did not return an async context manager, " + f"but {cm.__class__!r}" + ) + + if cm is self: + raise TypeError( + f"{self.__class__.__qualname__}.__asynccontextmanager__() returned " + f"self. Did you forget to add the @asynccontextmanager decorator and a " + f"'yield' statement?" + ) + + value = await cm.__aenter__() + self.__cm = cm + return value + + @final + async def __aexit__( + self: _SupportsAsyncCtxMgr[object, _ExitT_co], + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> _ExitT_co: + assert isinstance(self, AsyncContextManagerMixin) + if self.__cm is None: + raise RuntimeError( + f"this {self.__class__.__qualname__} has not been entered yet" + ) + + # Prevent circular references + cm = self.__cm + del self.__cm + + return cast(_ExitT_co, await cm.__aexit__(exc_type, exc_val, exc_tb)) + + @abstractmethod + def __asynccontextmanager__( + self, + ) -> AbstractAsyncContextManager[object, bool | None]: + """ + Implement your async context manager logic here. + + This method **must** be decorated with + :func:`@asynccontextmanager `. + + .. note:: Remember that the ``yield`` will raise any exception raised in the + enclosed context block, so use a ``finally:`` block to clean up resources! + + :return: an async context manager object + """ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_eventloop.py b/venv/lib/python3.11/site-packages/anyio/_core/_eventloop.py new file mode 100644 index 0000000..a3e2ab1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_eventloop.py @@ -0,0 +1,240 @@ +from __future__ import annotations + +import math +import sys +import threading +from collections.abc import Awaitable, Callable, Generator +from contextlib import contextmanager +from contextvars import Token +from importlib import import_module +from typing import TYPE_CHECKING, Any, TypeVar + +from ._exceptions import NoEventLoopError + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +sniffio: Any +try: + import sniffio +except ModuleNotFoundError: + sniffio = None + +if TYPE_CHECKING: + from ..abc import AsyncBackend + +# This must be updated when new backends are introduced +BACKENDS = "asyncio", "trio" + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + +threadlocals = threading.local() +loaded_backends: dict[str, type[AsyncBackend]] = {} + + +def run( + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + *args: Unpack[PosArgsT], + backend: str = "asyncio", + backend_options: dict[str, Any] | None = None, +) -> T_Retval: + """ + Run the given coroutine function in an asynchronous event loop. + + The current thread must not be already running an event loop. + + :param func: a coroutine function + :param args: positional arguments to ``func`` + :param backend: name of the asynchronous event loop implementation – currently + either ``asyncio`` or ``trio`` + :param backend_options: keyword arguments to call the backend ``run()`` + implementation with (documented :ref:`here `) + :return: the return value of the coroutine function + :raises RuntimeError: if an asynchronous event loop is already running in this + thread + :raises LookupError: if the named backend is not found + + """ + if asynclib_name := current_async_library(): + raise RuntimeError(f"Already running {asynclib_name} in this thread") + + try: + async_backend = get_async_backend(backend) + except ImportError as exc: + if backend in BACKENDS: + raise LookupError( + f"Backend {backend!r} is not available. " + f"Install it with: pip install anyio[{backend}]" + ) from exc + + raise LookupError(f"No such backend: {backend}") from exc + + token = None + if asynclib_name is None: + # Since we're in control of the event loop, we can cache the name of the async + # library + token = set_current_async_library(backend) + + try: + backend_options = backend_options or {} + return async_backend.run(func, args, {}, backend_options) + finally: + reset_current_async_library(token) + + +async def sleep(delay: float) -> None: + """ + Pause the current task for the specified duration. + + :param delay: the duration, in seconds + + """ + return await get_async_backend().sleep(delay) + + +async def sleep_forever() -> None: + """ + Pause the current task until it's cancelled. + + This is a shortcut for ``sleep(math.inf)``. + + .. versionadded:: 3.1 + + """ + await sleep(math.inf) + + +async def sleep_until(deadline: float) -> None: + """ + Pause the current task until the given time. + + :param deadline: the absolute time to wake up at (according to the internal + monotonic clock of the event loop) + + .. versionadded:: 3.1 + + """ + now = current_time() + await sleep(max(deadline - now, 0)) + + +def current_time() -> float: + """ + Return the current value of the event loop's internal clock. + + :return: the clock value (seconds) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().current_time() + + +def get_all_backends() -> tuple[str, ...]: + """Return a tuple of the names of all built-in backends.""" + return BACKENDS + + +def get_available_backends() -> tuple[str, ...]: + """ + Test for the availability of built-in backends. + + :return a tuple of the built-in backend names that were successfully imported + + .. versionadded:: 4.12 + + """ + available_backends: list[str] = [] + for backend_name in get_all_backends(): + try: + get_async_backend(backend_name) + except ImportError: + continue + + available_backends.append(backend_name) + + return tuple(available_backends) + + +def get_cancelled_exc_class() -> type[BaseException]: + """ + Return the current async library's cancellation exception class. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().cancelled_exception_class() + + +# +# Private API +# + + +@contextmanager +def claim_worker_thread( + backend_class: type[AsyncBackend], token: object +) -> Generator[Any, None, None]: + from ..lowlevel import EventLoopToken + + threadlocals.current_token = EventLoopToken(backend_class, token) + try: + yield + finally: + del threadlocals.current_token + + +def get_async_backend(asynclib_name: str | None = None) -> type[AsyncBackend]: + if asynclib_name is None: + asynclib_name = current_async_library() + if not asynclib_name: + raise NoEventLoopError( + f"Not currently running on any asynchronous event loop. " + f"Available async backends: {', '.join(get_all_backends())}" + ) + + # We use our own dict instead of sys.modules to get the already imported back-end + # class because the appropriate modules in sys.modules could potentially be only + # partially initialized + try: + return loaded_backends[asynclib_name] + except KeyError: + module = import_module(f"anyio._backends._{asynclib_name}") + loaded_backends[asynclib_name] = module.backend_class + return module.backend_class + + +def current_async_library() -> str | None: + if sniffio is None: + # If sniffio is not installed, we assume we're either running asyncio or nothing + import asyncio + + try: + asyncio.get_running_loop() + return "asyncio" + except RuntimeError: + pass + else: + try: + return sniffio.current_async_library() + except sniffio.AsyncLibraryNotFoundError: + pass + + return None + + +def set_current_async_library(asynclib_name: str | None) -> Token | None: + # no-op if sniffio is not installed + if sniffio is None: + return None + + return sniffio.current_async_library_cvar.set(asynclib_name) + + +def reset_current_async_library(token: Token | None) -> None: + if token is not None: + sniffio.current_async_library_cvar.reset(token) diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_exceptions.py b/venv/lib/python3.11/site-packages/anyio/_core/_exceptions.py new file mode 100644 index 0000000..cd6eb9b --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_exceptions.py @@ -0,0 +1,177 @@ +from __future__ import annotations + +import sys +from collections.abc import Generator +from textwrap import dedent +from typing import Any + +if sys.version_info < (3, 11): + from exceptiongroup import BaseExceptionGroup + + +class BrokenResourceError(Exception): + """ + Raised when trying to use a resource that has been rendered unusable due to external + causes (e.g. a send stream whose peer has disconnected). + """ + + +class BrokenWorkerProcess(Exception): + """ + Raised by :meth:`~anyio.to_process.run_sync` if the worker process terminates abruptly or + otherwise misbehaves. + """ + + +class BrokenWorkerInterpreter(Exception): + """ + Raised by :meth:`~anyio.to_interpreter.run_sync` if an unexpected exception is + raised in the subinterpreter. + """ + + def __init__(self, excinfo: Any): + # This was adapted from concurrent.futures.interpreter.ExecutionFailed + msg = excinfo.formatted + if not msg: + if excinfo.type and excinfo.msg: + msg = f"{excinfo.type.__name__}: {excinfo.msg}" + else: + msg = excinfo.type.__name__ or excinfo.msg + + super().__init__(msg) + self.excinfo = excinfo + + def __str__(self) -> str: + try: + formatted = self.excinfo.errdisplay + except Exception: + return super().__str__() + else: + return dedent( + f""" + {super().__str__()} + + Uncaught in the interpreter: + + {formatted} + """.strip() + ) + + +class BusyResourceError(Exception): + """ + Raised when two tasks are trying to read from or write to the same resource + concurrently. + """ + + def __init__(self, action: str): + super().__init__(f"Another task is already {action} this resource") + + +class ClosedResourceError(Exception): + """Raised when trying to use a resource that has been closed.""" + + +class ConnectionFailed(OSError): + """ + Raised when a connection attempt fails. + + .. note:: This class inherits from :exc:`OSError` for backwards compatibility. + """ + + +def iterate_exceptions( + exception: BaseException, +) -> Generator[BaseException, None, None]: + if isinstance(exception, BaseExceptionGroup): + for exc in exception.exceptions: + yield from iterate_exceptions(exc) + else: + yield exception + + +class DelimiterNotFound(Exception): + """ + Raised during + :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the + maximum number of bytes has been read without the delimiter being found. + """ + + def __init__(self, max_bytes: int) -> None: + super().__init__( + f"The delimiter was not found among the first {max_bytes} bytes" + ) + + +class EndOfStream(Exception): + """ + Raised when trying to read from a stream that has been closed from the other end. + """ + + +class IncompleteRead(Exception): + """ + Raised during + :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_exactly` or + :meth:`~anyio.streams.buffered.BufferedByteReceiveStream.receive_until` if the + connection is closed before the requested amount of bytes has been read. + """ + + def __init__(self) -> None: + super().__init__( + "The stream was closed before the read operation could be completed" + ) + + +class TypedAttributeLookupError(LookupError): + """ + Raised by :meth:`~anyio.TypedAttributeProvider.extra` when the given typed attribute + is not found and no default value has been given. + """ + + +class WouldBlock(Exception): + """Raised by ``X_nowait`` functions if ``X()`` would block.""" + + +class NoEventLoopError(RuntimeError): + """ + Raised by several functions that require an event loop to be running in the current + thread when there is no running event loop. + + This is also raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` + if not calling from an AnyIO worker thread, and no ``token`` was passed. + """ + + +class RunFinishedError(RuntimeError): + """ + Raised by :func:`.from_thread.run` and :func:`.from_thread.run_sync` if the event + loop associated with the explicitly passed token has already finished. + """ + + def __init__(self) -> None: + super().__init__( + "The event loop associated with the given token has already finished" + ) + + +class TaskFailed(Exception): + """ + Raised when awaiting on, or attempting to access the return value of, a + :class:`.TaskHandle` that raised an exception. + """ + + +class TaskCancelled(TaskFailed): + """ + Raised when awaiting on, or attempting to access the return value of, a + :class:`.TaskHandle` that was cancelled. + """ + + +class TaskNotFinished(Exception): + """ + Raised when attempting to access the return value or exception of a + :class:`.TaskHandle` that is still pending completion. + """ diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_fileio.py b/venv/lib/python3.11/site-packages/anyio/_core/_fileio.py new file mode 100644 index 0000000..692c754 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_fileio.py @@ -0,0 +1,960 @@ +from __future__ import annotations + +import os +import pathlib +import sys +from collections.abc import ( + AsyncIterator, + Callable, + Iterable, + Iterator, + Sequence, +) +from dataclasses import dataclass +from functools import partial +from os import PathLike +from typing import ( + IO, + TYPE_CHECKING, + Any, + AnyStr, + ClassVar, + Final, + Generic, + TypeVar, + overload, +) + +from .. import to_thread +from ..abc import AsyncResource +from ._synchronization import CapacityLimiter + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +if sys.version_info >= (3, 14): + from pathlib.types import PathInfo + +if TYPE_CHECKING: + from types import ModuleType + + from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer +else: + ReadableBuffer = OpenBinaryMode = OpenTextMode = WriteableBuffer = object + + +T = TypeVar("T", bound="Path") + + +class AsyncFile(AsyncResource, Generic[AnyStr]): + """ + An asynchronous file object. + + This class wraps a standard file object and provides async friendly versions of the + following blocking methods (where available on the original file object): + + * read + * read1 + * readline + * readlines + * readinto + * readinto1 + * write + * writelines + * truncate + * seek + * tell + * flush + + All other methods are directly passed through. + + This class supports the asynchronous context manager protocol which closes the + underlying file at the end of the context block. + + This class also supports asynchronous iteration:: + + async with await open_file(...) as f: + async for line in f: + print(line) + """ + + def __init__( + self, fp: IO[AnyStr], *, limiter: CapacityLimiter | None = None + ) -> None: + if limiter is not None and not isinstance(limiter, CapacityLimiter): + raise TypeError( + f"limiter must be a CapacityLimiter or None, not " + f"{limiter.__class__.__name__}" + ) + + self._fp: Any = fp + self._limiter = limiter + + def __getattr__(self, name: str) -> object: + return getattr(self._fp, name) + + @property + def limiter(self) -> CapacityLimiter | None: + """The capacity limiter used by this file object, if not the global limiter.""" + return self._limiter + + @property + def wrapped(self) -> IO[AnyStr]: + """The wrapped file object.""" + return self._fp + + async def __aiter__(self) -> AsyncIterator[AnyStr]: + while True: + line = await self.readline() + if line: + yield line + else: + break + + async def aclose(self) -> None: + return await to_thread.run_sync(self._fp.close, limiter=self._limiter) + + async def read(self, size: int = -1) -> AnyStr: + return await to_thread.run_sync(self._fp.read, size, limiter=self._limiter) + + async def read1(self: AsyncFile[bytes], size: int = -1) -> bytes: + return await to_thread.run_sync(self._fp.read1, size, limiter=self._limiter) + + async def readline(self) -> AnyStr: + return await to_thread.run_sync(self._fp.readline, limiter=self._limiter) + + async def readlines(self) -> list[AnyStr]: + return await to_thread.run_sync(self._fp.readlines, limiter=self._limiter) + + async def readinto(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + return await to_thread.run_sync(self._fp.readinto, b, limiter=self._limiter) + + async def readinto1(self: AsyncFile[bytes], b: WriteableBuffer) -> int: + return await to_thread.run_sync(self._fp.readinto1, b, limiter=self._limiter) + + @overload + async def write(self: AsyncFile[bytes], b: ReadableBuffer) -> int: ... + + @overload + async def write(self: AsyncFile[str], b: str) -> int: ... + + async def write(self, b: ReadableBuffer | str) -> int: + return await to_thread.run_sync(self._fp.write, b, limiter=self._limiter) + + @overload + async def writelines( + self: AsyncFile[bytes], lines: Iterable[ReadableBuffer] + ) -> None: ... + + @overload + async def writelines(self: AsyncFile[str], lines: Iterable[str]) -> None: ... + + async def writelines(self, lines: Iterable[ReadableBuffer] | Iterable[str]) -> None: + return await to_thread.run_sync( + self._fp.writelines, lines, limiter=self._limiter + ) + + async def truncate(self, size: int | None = None) -> int: + return await to_thread.run_sync(self._fp.truncate, size, limiter=self._limiter) + + async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: + return await to_thread.run_sync( + self._fp.seek, offset, whence, limiter=self._limiter + ) + + async def tell(self) -> int: + return await to_thread.run_sync(self._fp.tell, limiter=self._limiter) + + async def flush(self) -> None: + return await to_thread.run_sync(self._fp.flush, limiter=self._limiter) + + +@overload +async def open_file( + file: str | PathLike[str] | int, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] | None = ..., + *, + limiter: CapacityLimiter | None = ..., +) -> AsyncFile[bytes]: ... + + +@overload +async def open_file( + file: str | PathLike[str] | int, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + closefd: bool = ..., + opener: Callable[[str, int], int] | None = ..., + *, + limiter: CapacityLimiter | None = ..., +) -> AsyncFile[str]: ... + + +async def open_file( + file: str | PathLike[str] | int, + mode: str = "r", + buffering: int = -1, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + closefd: bool = True, + opener: Callable[[str, int], int] | None = None, + *, + limiter: CapacityLimiter | None = None, +) -> AsyncFile[Any]: + """ + Open a file asynchronously. + + Except for ``limiter``, the arguments are exactly the same as for the builtin :func:`open`. + + :param limiter: an optional capacity limiter to use with the file + instead of the default one + :return: an asynchronous file object + + .. versionchanged:: 4.14.0 + Added the ``limiter`` keyword argument. + + """ + fp = await to_thread.run_sync( + open, + file, + mode, + buffering, + encoding, + errors, + newline, + closefd, + opener, + limiter=limiter, + ) + return AsyncFile(fp, limiter=limiter) + + +def wrap_file( + file: IO[AnyStr], *, limiter: CapacityLimiter | None = None +) -> AsyncFile[AnyStr]: + """ + Wrap an existing file as an asynchronous file. + + :param file: an existing file-like object + :param limiter: an optional capacity limiter to use with the file + instead of the default one + :return: an asynchronous file object + + .. versionchanged:: 4.14.0 + Added the ``limiter`` keyword argument. + + """ + return AsyncFile(file, limiter=limiter) + + +@dataclass(eq=False) +class _PathIterator(AsyncIterator[T]): + iterator: Iterator[PathLike[str]] + limiter: CapacityLimiter | None + # This was added to ensure that iterating over a subclass of Path yields instances + # of that subclass rather than the base Path class. + path_cls: type[T] + + async def __anext__(self) -> T: + nextval = await to_thread.run_sync( + next, self.iterator, None, abandon_on_cancel=True, limiter=self.limiter + ) + if nextval is None: + raise StopAsyncIteration from None + + return self.path_cls(nextval, limiter=self.limiter) + + +class Path: + """ + An asynchronous version of :class:`pathlib.Path`. + + This class cannot be substituted for :class:`pathlib.Path` or + :class:`pathlib.PurePath`, but it is compatible with the :class:`os.PathLike` + interface. + + It implements the Python 3.10 version of :class:`pathlib.Path` interface, except for + the deprecated :meth:`~pathlib.Path.link_to` method. + + Some methods may be unavailable or have limited functionality, based on the Python + version: + + * :meth:`~pathlib.Path.copy` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.copy_into` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.from_uri` (available on Python 3.13 or later) + * :meth:`~pathlib.PurePath.full_match` (available on Python 3.13 or later) + * :attr:`~pathlib.Path.info` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.is_junction` (available on Python 3.12 or later) + * :meth:`~pathlib.PurePath.match` (the ``case_sensitive`` parameter is only + available on Python 3.13 or later) + * :meth:`~pathlib.Path.move` (available on Python 3.14 or later) + * :meth:`~pathlib.Path.move_into` (available on Python 3.14 or later) + * :meth:`~pathlib.PurePath.relative_to` (the ``walk_up`` parameter is only available + on Python 3.12 or later) + * :meth:`~pathlib.Path.walk` (available on Python 3.12 or later) + + Any methods that do disk I/O need to be awaited on. These methods are: + + * :meth:`~pathlib.Path.absolute` + * :meth:`~pathlib.Path.chmod` + * :meth:`~pathlib.Path.cwd` + * :meth:`~pathlib.Path.exists` + * :meth:`~pathlib.Path.expanduser` + * :meth:`~pathlib.Path.group` + * :meth:`~pathlib.Path.hardlink_to` + * :meth:`~pathlib.Path.home` + * :meth:`~pathlib.Path.is_block_device` + * :meth:`~pathlib.Path.is_char_device` + * :meth:`~pathlib.Path.is_dir` + * :meth:`~pathlib.Path.is_fifo` + * :meth:`~pathlib.Path.is_file` + * :meth:`~pathlib.Path.is_junction` + * :meth:`~pathlib.Path.is_mount` + * :meth:`~pathlib.Path.is_socket` + * :meth:`~pathlib.Path.is_symlink` + * :meth:`~pathlib.Path.lchmod` + * :meth:`~pathlib.Path.lstat` + * :meth:`~pathlib.Path.mkdir` + * :meth:`~pathlib.Path.open` + * :meth:`~pathlib.Path.owner` + * :meth:`~pathlib.Path.read_bytes` + * :meth:`~pathlib.Path.read_text` + * :meth:`~pathlib.Path.readlink` + * :meth:`~pathlib.Path.rename` + * :meth:`~pathlib.Path.replace` + * :meth:`~pathlib.Path.resolve` + * :meth:`~pathlib.Path.rmdir` + * :meth:`~pathlib.Path.samefile` + * :meth:`~pathlib.Path.stat` + * :meth:`~pathlib.Path.symlink_to` + * :meth:`~pathlib.Path.touch` + * :meth:`~pathlib.Path.unlink` + * :meth:`~pathlib.Path.walk` + * :meth:`~pathlib.Path.write_bytes` + * :meth:`~pathlib.Path.write_text` + + Additionally, the following methods return an async iterator yielding + :class:`~.Path` objects: + + * :meth:`~pathlib.Path.glob` + * :meth:`~pathlib.Path.iterdir` + * :meth:`~pathlib.Path.rglob` + + .. versionchanged:: 4.14.0 + Added the ``limiter`` keyword argument. + """ + + __slots__ = "_path", "_limiter", "__weakref__" + + __weakref__: Any + + def __init__( + self, *args: str | PathLike[str], limiter: CapacityLimiter | None = None + ) -> None: + if limiter is not None and not isinstance(limiter, CapacityLimiter): + raise TypeError( + f"limiter must be a CapacityLimiter or None, not " + f"{limiter.__class__.__name__}" + ) + + self._path: Final[pathlib.Path] = pathlib.Path(*args) + self._limiter = limiter + + def __fspath__(self) -> str: + return self._path.__fspath__() + + if sys.version_info >= (3, 15): + + def __vfspath__(self) -> str: + return self._path.__vfspath__() + + def __str__(self) -> str: + return self._path.__str__() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}({self.as_posix()!r})" + + def __bytes__(self) -> bytes: + return self._path.__bytes__() + + def __hash__(self) -> int: + return self._path.__hash__() + + def __eq__(self, other: object) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__eq__(target) + + def __lt__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__lt__(target) + + def __le__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__le__(target) + + def __gt__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__gt__(target) + + def __ge__(self, other: pathlib.PurePath | Path) -> bool: + target = other._path if isinstance(other, Path) else other + return self._path.__ge__(target) + + def __truediv__(self, other: str | PathLike[str]) -> Self: + return type(self)(self._path / other, limiter=self._limiter) + + def __rtruediv__(self, other: str | PathLike[str]) -> Self: + return type(self)(other, limiter=self._limiter) / self + + @property + def limiter(self) -> CapacityLimiter | None: + """The capacity limiter used by this path, if not the global limiter.""" + return self._limiter + + @property + def parts(self) -> tuple[str, ...]: + return self._path.parts + + @property + def drive(self) -> str: + return self._path.drive + + @property + def root(self) -> str: + return self._path.root + + @property + def anchor(self) -> str: + return self._path.anchor + + @property + def parents(self) -> Sequence[Self]: + return tuple(type(self)(p, limiter=self._limiter) for p in self._path.parents) + + @property + def parent(self) -> Self: + return type(self)(self._path.parent, limiter=self._limiter) + + @property + def name(self) -> str: + return self._path.name + + @property + def suffix(self) -> str: + return self._path.suffix + + @property + def suffixes(self) -> list[str]: + return self._path.suffixes + + @property + def stem(self) -> str: + return self._path.stem + + async def absolute(self) -> Self: + path = await to_thread.run_sync(self._path.absolute, limiter=self._limiter) + return type(self)(path, limiter=self._limiter) + + def as_posix(self) -> str: + return self._path.as_posix() + + def as_uri(self) -> str: + return self._path.as_uri() + + if sys.version_info >= (3, 13): + parser: ClassVar[ModuleType] = pathlib.Path.parser + + @classmethod + def from_uri(cls, uri: str, *, limiter: CapacityLimiter | None = None) -> Self: + return cls(pathlib.Path.from_uri(uri), limiter=limiter) + + def full_match( + self, path_pattern: str, *, case_sensitive: bool | None = None + ) -> bool: + return self._path.full_match(path_pattern, case_sensitive=case_sensitive) + + def match( + self, path_pattern: str, *, case_sensitive: bool | None = None + ) -> bool: + return self._path.match(path_pattern, case_sensitive=case_sensitive) + else: + + def match(self, path_pattern: str) -> bool: + return self._path.match(path_pattern) + + if sys.version_info >= (3, 14): + + @property + def info(self) -> PathInfo: + return self._path.info + + async def copy( + self, + target: str | os.PathLike[str], + *, + follow_symlinks: bool = True, + preserve_metadata: bool = False, + ) -> Self: + func = partial( + self._path.copy, + follow_symlinks=follow_symlinks, + preserve_metadata=preserve_metadata, + ) + return type(self)( + await to_thread.run_sync( + func, pathlib.Path(target), limiter=self._limiter + ), + limiter=self._limiter, + ) + + async def copy_into( + self, + target_dir: str | os.PathLike[str], + *, + follow_symlinks: bool = True, + preserve_metadata: bool = False, + ) -> Self: + func = partial( + self._path.copy_into, + follow_symlinks=follow_symlinks, + preserve_metadata=preserve_metadata, + ) + return type(self)( + await to_thread.run_sync( + func, pathlib.Path(target_dir), limiter=self._limiter + ), + limiter=self._limiter, + ) + + async def move(self, target: str | os.PathLike[str]) -> Self: + # Upstream does not handle anyio.Path properly as a PathLike + target = pathlib.Path(target) + return type(self)( + await to_thread.run_sync( + self._path.move, target, limiter=self._limiter + ), + limiter=self._limiter, + ) + + async def move_into( + self, + target_dir: str | os.PathLike[str], + ) -> Self: + return type(self)( + await to_thread.run_sync( + self._path.move_into, target_dir, limiter=self._limiter + ), + limiter=self._limiter, + ) + + def is_relative_to(self, other: str | PathLike[str]) -> bool: + try: + self.relative_to(other) + return True + except ValueError: + return False + + async def chmod(self, mode: int, *, follow_symlinks: bool = True) -> None: + func = partial(os.chmod, follow_symlinks=follow_symlinks) + return await to_thread.run_sync(func, self._path, mode, limiter=self._limiter) + + @classmethod + async def cwd(cls, *, limiter: CapacityLimiter | None = None) -> Self: + path = await to_thread.run_sync(pathlib.Path.cwd, limiter=limiter) + return cls(path, limiter=limiter) + + async def exists(self) -> bool: + return await to_thread.run_sync( + self._path.exists, abandon_on_cancel=True, limiter=self._limiter + ) + + async def expanduser(self) -> Self: + return type(self)( + await to_thread.run_sync( + self._path.expanduser, abandon_on_cancel=True, limiter=self._limiter + ), + limiter=self._limiter, + ) + + if sys.version_info < (3, 12): + # Python 3.11 and earlier + def glob(self, pattern: str) -> AsyncIterator[Self]: + gen = self._path.glob(pattern) + return _PathIterator(gen, self._limiter, type(self)) + elif (3, 12) <= sys.version_info < (3, 13): + # changed in Python 3.12: + # - The case_sensitive parameter was added. + def glob( + self, + pattern: str, + *, + case_sensitive: bool | None = None, + ) -> AsyncIterator[Self]: + gen = self._path.glob(pattern, case_sensitive=case_sensitive) + return _PathIterator(gen, self._limiter, type(self)) + elif sys.version_info >= (3, 13): + # Changed in Python 3.13: + # - The recurse_symlinks parameter was added. + # - The pattern parameter accepts a path-like object. + def glob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block + self, + pattern: str | PathLike[str], + *, + case_sensitive: bool | None = None, + recurse_symlinks: bool = False, + ) -> AsyncIterator[Self]: + gen = self._path.glob( + pattern, # type: ignore[arg-type] + case_sensitive=case_sensitive, + recurse_symlinks=recurse_symlinks, + ) + return _PathIterator(gen, self._limiter, type(self)) + + async def group(self) -> str: + return await to_thread.run_sync( + self._path.group, abandon_on_cancel=True, limiter=self._limiter + ) + + async def hardlink_to( + self, target: str | bytes | PathLike[str] | PathLike[bytes] + ) -> None: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync(os.link, target, self, limiter=self._limiter) + + @classmethod + async def home(cls, *, limiter: CapacityLimiter | None = None) -> Self: + home_path = await to_thread.run_sync(pathlib.Path.home, limiter=limiter) + return cls(home_path, limiter=limiter) + + def is_absolute(self) -> bool: + return self._path.is_absolute() + + async def is_block_device(self) -> bool: + return await to_thread.run_sync( + self._path.is_block_device, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_char_device(self) -> bool: + return await to_thread.run_sync( + self._path.is_char_device, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_dir(self) -> bool: + return await to_thread.run_sync( + self._path.is_dir, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_fifo(self) -> bool: + return await to_thread.run_sync( + self._path.is_fifo, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_file(self) -> bool: + return await to_thread.run_sync( + self._path.is_file, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info >= (3, 12): + + async def is_junction(self) -> bool: + return await to_thread.run_sync( + self._path.is_junction, limiter=self._limiter + ) + + async def is_mount(self) -> bool: + return await to_thread.run_sync( + os.path.ismount, self._path, abandon_on_cancel=True, limiter=self._limiter + ) + + if sys.version_info < (3, 15): + + def is_reserved(self) -> bool: + return self._path.is_reserved() + + async def is_socket(self) -> bool: + return await to_thread.run_sync( + self._path.is_socket, abandon_on_cancel=True, limiter=self._limiter + ) + + async def is_symlink(self) -> bool: + return await to_thread.run_sync( + self._path.is_symlink, abandon_on_cancel=True, limiter=self._limiter + ) + + async def iterdir(self) -> AsyncIterator[Self]: + gen = ( + self._path.iterdir() + if sys.version_info < (3, 13) + else await to_thread.run_sync( + self._path.iterdir, abandon_on_cancel=True, limiter=self._limiter + ) + ) + async for path in _PathIterator(gen, self._limiter, type(self)): + yield path + + def joinpath(self, *args: str | PathLike[str]) -> Self: + return type(self)(self._path.joinpath(*args), limiter=self._limiter) + + async def lchmod(self, mode: int) -> None: + await to_thread.run_sync(self._path.lchmod, mode, limiter=self._limiter) + + async def lstat(self) -> os.stat_result: + return await to_thread.run_sync( + self._path.lstat, abandon_on_cancel=True, limiter=self._limiter + ) + + async def mkdir( + self, mode: int = 0o777, parents: bool = False, exist_ok: bool = False + ) -> None: + await to_thread.run_sync( + self._path.mkdir, mode, parents, exist_ok, limiter=self._limiter + ) + + @overload + async def open( + self, + mode: OpenBinaryMode, + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + ) -> AsyncFile[bytes]: ... + + @overload + async def open( + self, + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + errors: str | None = ..., + newline: str | None = ..., + ) -> AsyncFile[str]: ... + + async def open( + self, + mode: str = "r", + buffering: int = -1, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> AsyncFile[Any]: + fp = await to_thread.run_sync( + self._path.open, + mode, + buffering, + encoding, + errors, + newline, + limiter=self._limiter, + ) + return AsyncFile(fp, limiter=self._limiter) + + async def owner(self) -> str: + return await to_thread.run_sync( + self._path.owner, abandon_on_cancel=True, limiter=self._limiter + ) + + async def read_bytes(self) -> bytes: + return await to_thread.run_sync(self._path.read_bytes, limiter=self._limiter) + + async def read_text( + self, encoding: str | None = None, errors: str | None = None + ) -> str: + return await to_thread.run_sync( + self._path.read_text, encoding, errors, limiter=self._limiter + ) + + if sys.version_info >= (3, 12): + + def relative_to( + self, *other: str | PathLike[str], walk_up: bool = False + ) -> Self: + # relative_to() should work with any PathLike but it doesn't + others = [pathlib.Path(other) for other in other] + return type(self)( + self._path.relative_to(*others, walk_up=walk_up), limiter=self._limiter + ) + + else: + + def relative_to(self, *other: str | PathLike[str]) -> Self: + return type(self)(self._path.relative_to(*other), limiter=self._limiter) + + async def readlink(self) -> Self: + target = await to_thread.run_sync( + os.readlink, self._path, limiter=self._limiter + ) + return type(self)(target, limiter=self._limiter) + + async def rename(self, target: str | pathlib.PurePath | Path) -> Self: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync(self._path.rename, target, limiter=self._limiter) + return type(self)(target, limiter=self._limiter) + + async def replace(self, target: str | pathlib.PurePath | Path) -> Self: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync(self._path.replace, target, limiter=self._limiter) + return type(self)(target, limiter=self._limiter) + + async def resolve(self, strict: bool = False) -> Self: + func = partial(self._path.resolve, strict=strict) + return type(self)( + await to_thread.run_sync( + func, abandon_on_cancel=True, limiter=self._limiter + ), + limiter=self._limiter, + ) + + if sys.version_info < (3, 12): + # Pre Python 3.12 + def rglob(self, pattern: str) -> AsyncIterator[Self]: + gen = self._path.rglob(pattern) + return _PathIterator(gen, self._limiter, type(self)) + elif (3, 12) <= sys.version_info < (3, 13): + # Changed in Python 3.12: + # - The case_sensitive parameter was added. + def rglob( + self, pattern: str, *, case_sensitive: bool | None = None + ) -> AsyncIterator[Self]: + gen = self._path.rglob(pattern, case_sensitive=case_sensitive) + return _PathIterator(gen, self._limiter, type(self)) + elif sys.version_info >= (3, 13): + # Changed in Python 3.13: + # - The recurse_symlinks parameter was added. + # - The pattern parameter accepts a path-like object. + def rglob( # type: ignore[misc] # mypy doesn't allow for differing signatures in a conditional block + self, + pattern: str | PathLike[str], + *, + case_sensitive: bool | None = None, + recurse_symlinks: bool = False, + ) -> AsyncIterator[Self]: + gen = self._path.rglob( + pattern, # type: ignore[arg-type] + case_sensitive=case_sensitive, + recurse_symlinks=recurse_symlinks, + ) + return _PathIterator(gen, self._limiter, type(self)) + + async def rmdir(self) -> None: + await to_thread.run_sync(self._path.rmdir, limiter=self._limiter) + + async def samefile(self, other_path: str | PathLike[str]) -> bool: + if isinstance(other_path, Path): + other_path = other_path._path + + return await to_thread.run_sync( + self._path.samefile, + other_path, + abandon_on_cancel=True, + limiter=self._limiter, + ) + + async def stat(self, *, follow_symlinks: bool = True) -> os.stat_result: + func = partial(os.stat, follow_symlinks=follow_symlinks) + return await to_thread.run_sync( + func, self._path, abandon_on_cancel=True, limiter=self._limiter + ) + + async def symlink_to( + self, + target: str | bytes | PathLike[str] | PathLike[bytes], + target_is_directory: bool = False, + ) -> None: + if isinstance(target, Path): + target = target._path + + await to_thread.run_sync( + self._path.symlink_to, target, target_is_directory, limiter=self._limiter + ) + + async def touch(self, mode: int = 0o666, exist_ok: bool = True) -> None: + await to_thread.run_sync( + self._path.touch, mode, exist_ok, limiter=self._limiter + ) + + async def unlink(self, missing_ok: bool = False) -> None: + try: + await to_thread.run_sync(self._path.unlink, limiter=self._limiter) + except FileNotFoundError: + if not missing_ok: + raise + + if sys.version_info >= (3, 12): + + async def walk( + self, + top_down: bool = True, + on_error: Callable[[OSError], object] | None = None, + follow_symlinks: bool = False, + ) -> AsyncIterator[tuple[Self, list[str], list[str]]]: + def get_next_value() -> tuple[pathlib.Path, list[str], list[str]] | None: + try: + return next(gen) + except StopIteration: + return None + + gen = self._path.walk(top_down, on_error, follow_symlinks) + while True: + value = await to_thread.run_sync(get_next_value, limiter=self._limiter) + if value is None: + return + + root, dirs, paths = value + yield type(self)(root, limiter=self._limiter), dirs, paths + + def with_name(self, name: str) -> Self: + return type(self)(self._path.with_name(name), limiter=self._limiter) + + def with_stem(self, stem: str) -> Self: + return type(self)( + self._path.with_name(stem + self._path.suffix), limiter=self._limiter + ) + + def with_suffix(self, suffix: str) -> Self: + return type(self)(self._path.with_suffix(suffix), limiter=self._limiter) + + def with_segments(self, *pathsegments: str | PathLike[str]) -> Self: + return type(self)(*pathsegments, limiter=self._limiter) + + async def write_bytes(self, data: ReadableBuffer) -> int: + return await to_thread.run_sync( + self._path.write_bytes, data, limiter=self._limiter + ) + + async def write_text( + self, + data: str, + encoding: str | None = None, + errors: str | None = None, + newline: str | None = None, + ) -> int: + return await to_thread.run_sync( + self._path.write_text, + data, + encoding, + errors, + newline, + limiter=self._limiter, + ) + + +PathLike.register(Path) diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_resources.py b/venv/lib/python3.11/site-packages/anyio/_core/_resources.py new file mode 100644 index 0000000..b9a5344 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_resources.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +from ..abc import AsyncResource +from ._tasks import CancelScope + + +async def aclose_forcefully(resource: AsyncResource) -> None: + """ + Close an asynchronous resource in a cancelled scope. + + Doing this closes the resource without waiting on anything. + + :param resource: the resource to close + + """ + with CancelScope() as scope: + scope.cancel() + await resource.aclose() diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_signals.py b/venv/lib/python3.11/site-packages/anyio/_core/_signals.py new file mode 100644 index 0000000..e24c79e --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_signals.py @@ -0,0 +1,29 @@ +from __future__ import annotations + +from collections.abc import AsyncIterator +from contextlib import AbstractContextManager +from signal import Signals + +from ._eventloop import get_async_backend + + +def open_signal_receiver( + *signals: Signals, +) -> AbstractContextManager[AsyncIterator[Signals]]: + """ + Start receiving operating system signals. + + :param signals: signals to receive (e.g. ``signal.SIGINT``) + :return: an asynchronous context manager for an asynchronous iterator which yields + signal numbers + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. warning:: Windows does not support signals natively so it is best to avoid + relying on this in cross-platform applications. + + .. warning:: On asyncio, this permanently replaces any previous signal handler for + the given signals, as set via :meth:`~asyncio.loop.add_signal_handler`. + + """ + return get_async_backend().open_signal_receiver(*signals) diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_sockets.py b/venv/lib/python3.11/site-packages/anyio/_core/_sockets.py new file mode 100644 index 0000000..29f7332 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_sockets.py @@ -0,0 +1,1011 @@ +from __future__ import annotations + +import errno +import os +import socket +import ssl +import stat +import sys +from collections.abc import Awaitable +from dataclasses import dataclass +from ipaddress import IPv4Address, IPv6Address, ip_address +from os import PathLike, chmod +from socket import AddressFamily, SocketKind +from typing import TYPE_CHECKING, Any, Literal, cast, overload + +from .. import ConnectionFailed, to_thread +from ..abc import ( + ByteStreamConnectable, + ConnectedUDPSocket, + ConnectedUNIXDatagramSocket, + IPAddressType, + IPSockAddrType, + SocketListener, + SocketStream, + UDPSocket, + UNIXDatagramSocket, + UNIXSocketStream, +) +from ..streams.stapled import MultiListener +from ..streams.tls import TLSConnectable, TLSStream +from ._eventloop import get_async_backend +from ._resources import aclose_forcefully +from ._synchronization import Event +from ._tasks import create_task_group, move_on_after + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike +else: + FileDescriptorLike = object + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +if sys.version_info < (3, 13): + from typing_extensions import deprecated +else: + from warnings import deprecated + +IPPROTO_IPV6 = getattr(socket, "IPPROTO_IPV6", 41) # https://bugs.python.org/issue29515 + +AnyIPAddressFamily = Literal[ + AddressFamily.AF_UNSPEC, AddressFamily.AF_INET, AddressFamily.AF_INET6 +] +IPAddressFamily = Literal[AddressFamily.AF_INET, AddressFamily.AF_INET6] + + +# tls_hostname given +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + ssl_context: ssl.SSLContext | None = ..., + tls_standard_compatible: bool = ..., + tls_hostname: str, + happy_eyeballs_delay: float = ..., +) -> TLSStream: ... + + +# ssl_context given +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + ssl_context: ssl.SSLContext, + tls_standard_compatible: bool = ..., + tls_hostname: str | None = ..., + happy_eyeballs_delay: float = ..., +) -> TLSStream: ... + + +# tls=True +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + tls: Literal[True], + ssl_context: ssl.SSLContext | None = ..., + tls_standard_compatible: bool = ..., + tls_hostname: str | None = ..., + happy_eyeballs_delay: float = ..., +) -> TLSStream: ... + + +# tls=False +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + tls: Literal[False], + ssl_context: ssl.SSLContext | None = ..., + tls_standard_compatible: bool = ..., + tls_hostname: str | None = ..., + happy_eyeballs_delay: float = ..., +) -> SocketStream: ... + + +# No TLS arguments +@overload +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = ..., + local_port: int | None = ..., + happy_eyeballs_delay: float = ..., +) -> SocketStream: ... + + +async def connect_tcp( + remote_host: IPAddressType, + remote_port: int, + *, + local_host: IPAddressType | None = None, + local_port: int | None = None, + tls: bool = False, + ssl_context: ssl.SSLContext | None = None, + tls_standard_compatible: bool = True, + tls_hostname: str | None = None, + happy_eyeballs_delay: float = 0.25, +) -> SocketStream | TLSStream: + """ + Connect to a host using the TCP protocol. + + This function implements the stateless version of the Happy Eyeballs algorithm (RFC + 6555). If ``remote_host`` is a host name that resolves to multiple IP addresses, + each one is tried until one connection attempt succeeds. If the first attempt does + not connected within 250 milliseconds, a second attempt is started using the next + address in the list, and so on. On IPv6 enabled systems, an IPv6 address (if + available) is tried first. + + When the connection has been established, a TLS handshake will be done if either + ``ssl_context`` or ``tls_hostname`` is not ``None``, or if ``tls`` is ``True``. + + :param remote_host: the IP address or host name to connect to + :param remote_port: port on the target host to connect to + :param local_host: the interface address or name to bind the socket to before + connecting + :param local_port: the local port to bind to (requires ``local_host`` to also be + set) + :param tls: ``True`` to do a TLS handshake with the connected stream and return a + :class:`~anyio.streams.tls.TLSStream` instead + :param ssl_context: the SSL context object to use (if omitted, a default context is + created) + :param tls_standard_compatible: If ``True``, performs the TLS shutdown handshake + before closing the stream and requires that the server does this as well. + Otherwise, :exc:`~ssl.SSLEOFError` may be raised during reads from the stream. + Some protocols, such as HTTP, require this option to be ``False``. + See :meth:`~ssl.SSLContext.wrap_socket` for details. + :param tls_hostname: host name to check the server certificate against (defaults to + the value of ``remote_host``) + :param happy_eyeballs_delay: delay (in seconds) before starting the next connection + attempt + :return: a socket stream object if no TLS handshake was done, otherwise a TLS stream + :raises ConnectionFailed: if the connection fails + + """ + # Placed here due to https://github.com/python/mypy/issues/7057 + connected_stream: SocketStream | None = None + + async def try_connect(remote_host: str, event: Event) -> None: + nonlocal connected_stream + try: + stream = await asynclib.connect_tcp(remote_host, remote_port, local_address) + except OSError as exc: + oserrors.append(exc) + return + else: + if connected_stream is None: + connected_stream = stream + tg.cancel_scope.cancel() + else: + await stream.aclose() + finally: + event.set() + + asynclib = get_async_backend() + local_address: IPSockAddrType | None = None + family = socket.AF_UNSPEC + if local_host: + gai_res = await getaddrinfo(str(local_host), local_port) + family, *_, local_address = gai_res[0] + + target_host = str(remote_host) + try: + addr_obj = ip_address(remote_host) + except ValueError: + addr_obj = None + + if addr_obj is not None: + if isinstance(addr_obj, IPv6Address): + target_addrs = [(socket.AF_INET6, addr_obj.compressed)] + else: + target_addrs = [(socket.AF_INET, addr_obj.compressed)] + else: + # getaddrinfo() will raise an exception if name resolution fails + gai_res = await getaddrinfo( + target_host, remote_port, family=family, type=socket.SOCK_STREAM + ) + + # Organize the list so that the first address is an IPv6 address (if available) + # and the second one is an IPv4 addresses. The rest can be in whatever order. + v6_found = v4_found = False + target_addrs = [] + for af, *_, sa in gai_res: + if af == socket.AF_INET6 and not v6_found: + v6_found = True + target_addrs.insert(0, (af, sa[0])) + elif af == socket.AF_INET and not v4_found and v6_found: + v4_found = True + target_addrs.insert(1, (af, sa[0])) + else: + target_addrs.append((af, sa[0])) + + oserrors: list[OSError] = [] + try: + async with create_task_group() as tg: + for _af, addr in target_addrs: + event = Event() + tg.start_soon(try_connect, addr, event) + with move_on_after(happy_eyeballs_delay): + await event.wait() + + if connected_stream is None: + cause = ( + oserrors[0] + if len(oserrors) == 1 + else ExceptionGroup("multiple connection attempts failed", oserrors) + ) + raise OSError("All connection attempts failed") from cause + finally: + oserrors.clear() + + if tls or tls_hostname or ssl_context: + try: + return await TLSStream.wrap( + connected_stream, + server_side=False, + hostname=tls_hostname or str(remote_host), + ssl_context=ssl_context, + standard_compatible=tls_standard_compatible, + ) + except BaseException: + await aclose_forcefully(connected_stream) + raise + + return connected_stream + + +async def connect_unix(path: str | bytes | PathLike[Any]) -> UNIXSocketStream: + """ + Connect to the given UNIX socket. + + Not available on Windows. + + :param path: path to the socket + :return: a socket stream object + :raises ConnectionFailed: if the connection fails + + """ + path = os.fspath(path) + return await get_async_backend().connect_unix(path) + + +async def create_tcp_listener( + *, + local_host: IPAddressType | None = None, + local_port: int = 0, + family: AnyIPAddressFamily = socket.AddressFamily.AF_UNSPEC, + backlog: int = 65536, + reuse_port: bool = False, +) -> MultiListener[SocketStream]: + """ + Create a TCP socket listener. + + :param local_port: port number to listen on + :param local_host: IP address of the interface to listen on. If omitted, listen on + all IPv4 and IPv6 interfaces. To listen on all interfaces on a specific address + family, use ``0.0.0.0`` for IPv4 or ``::`` for IPv6. + :param family: address family (used if ``local_host`` was omitted) + :param backlog: maximum number of queued incoming connections (up to a maximum of + 2**16, or 65536) + :param reuse_port: ``True`` to allow multiple sockets to bind to the same + address/port (not supported on Windows) + :return: a multi-listener object containing one or more socket listeners + :raises OSError: if there's an error creating a socket, or binding to one or more + interfaces failed + + """ + asynclib = get_async_backend() + backlog = min(backlog, 65536) + local_host = str(local_host) if local_host is not None else None + + def setup_raw_socket( + fam: AddressFamily, + bind_addr: tuple[str, int] | tuple[str, int, int, int], + *, + v6only: bool = True, + ) -> socket.socket: + sock = socket.socket(fam) + try: + sock.setblocking(False) + + if fam == AddressFamily.AF_INET6: + sock.setsockopt(IPPROTO_IPV6, socket.IPV6_V6ONLY, v6only) + + # For Windows, enable exclusive address use. For others, enable address + # reuse. + if sys.platform == "win32": + sock.setsockopt(socket.SOL_SOCKET, socket.SO_EXCLUSIVEADDRUSE, 1) + else: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + + if reuse_port: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + + # Workaround for #554 + if fam == socket.AF_INET6 and "%" in bind_addr[0]: + addr, scope_id = bind_addr[0].split("%", 1) + bind_addr = (addr, bind_addr[1], 0, int(scope_id)) + + sock.bind(bind_addr) + sock.listen(backlog) + except BaseException: + sock.close() + raise + + return sock + + # We passing type=0 on non-Windows platforms as a workaround for a uvloop bug + # where we don't get the correct scope ID for IPv6 link-local addresses when passing + # type=socket.SOCK_STREAM to getaddrinfo(): + # https://github.com/MagicStack/uvloop/issues/539 + gai_res = await getaddrinfo( + local_host, + local_port, + family=family, + type=socket.SOCK_STREAM if sys.platform == "win32" else 0, + flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, + ) + + # The set comprehension is here to work around a glibc bug: + # https://sourceware.org/bugzilla/show_bug.cgi?id=14969 + sockaddrs = sorted({res for res in gai_res if res[1] == SocketKind.SOCK_STREAM}) + + # Special case for dual-stack binding on the "any" interface + if ( + local_host is None + and family == AddressFamily.AF_UNSPEC + and socket.has_dualstack_ipv6() + and any(fam == AddressFamily.AF_INET6 for fam, *_ in gai_res) + ): + raw_socket = setup_raw_socket( + AddressFamily.AF_INET6, ("::", local_port), v6only=False + ) + listener = asynclib.create_tcp_listener(raw_socket) + return MultiListener([listener]) + + errors: list[OSError] = [] + try: + for _ in range(len(sockaddrs)): + listeners: list[SocketListener] = [] + bound_ephemeral_port = local_port + try: + for fam, *_, sockaddr in sockaddrs: + sockaddr = sockaddr[0], bound_ephemeral_port, *sockaddr[2:] + raw_socket = setup_raw_socket(fam, sockaddr) + + # Store the assigned port if an ephemeral port was requested, so + # we'll bind to the same port on all interfaces + if local_port == 0 and len(gai_res) > 1: + bound_ephemeral_port = raw_socket.getsockname()[1] + + listeners.append(asynclib.create_tcp_listener(raw_socket)) + except BaseException as exc: + for listener in listeners: + await listener.aclose() + + # If an ephemeral port was requested but binding the assigned port + # failed for another interface, rotate the address list and try again + if ( + isinstance(exc, OSError) + and exc.errno == errno.EADDRINUSE + and local_port == 0 + and bound_ephemeral_port + ): + errors.append(exc) + sockaddrs.append(sockaddrs.pop(0)) + continue + + raise + + return MultiListener(listeners) + + raise OSError( + f"Could not create {len(sockaddrs)} listeners with a consistent port" + ) from ExceptionGroup("Several bind attempts failed", errors) + finally: + del errors # Prevent reference cycles + + +async def create_unix_listener( + path: str | bytes | PathLike[Any], + *, + mode: int | None = None, + backlog: int = 65536, +) -> SocketListener: + """ + Create a UNIX socket listener. + + Not available on Windows. + + :param path: path of the socket + :param mode: permissions to set on the socket + :param backlog: maximum number of queued incoming connections (up to a maximum of + 2**16, or 65536) + :return: a listener object + + .. versionchanged:: 3.0 + If a socket already exists on the file system in the given path, it will be + removed first. + + """ + backlog = min(backlog, 65536) + raw_socket = await setup_unix_local_socket(path, mode, socket.SOCK_STREAM) + try: + raw_socket.listen(backlog) + return get_async_backend().create_unix_listener(raw_socket) + except BaseException: + raw_socket.close() + raise + + +async def create_udp_socket( + family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, + *, + local_host: IPAddressType | None = None, + local_port: int = 0, + reuse_port: bool = False, +) -> UDPSocket: + """ + Create a UDP socket. + + If ``port`` has been given, the socket will be bound to this port on the local + machine, making this socket suitable for providing UDP based services. + + :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically + determined from ``local_host`` if omitted + :param local_host: IP address or host name of the local interface to bind to + :param local_port: local port to bind to + :param reuse_port: ``True`` to allow multiple sockets to bind to the same + address/port (not supported on Windows) + :return: a UDP socket + + """ + if family is AddressFamily.AF_UNSPEC and not local_host: + raise ValueError('Either "family" or "local_host" must be given') + + if local_host: + gai_res = await getaddrinfo( + str(local_host), + local_port, + family=family, + type=socket.SOCK_DGRAM, + flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, + ) + family = cast(AnyIPAddressFamily, gai_res[0][0]) + local_address = gai_res[0][-1] + elif family is AddressFamily.AF_INET6: + local_address = ("::", 0) + else: + local_address = ("0.0.0.0", 0) + + sock = await get_async_backend().create_udp_socket( + family, local_address, None, reuse_port + ) + return cast(UDPSocket, sock) + + +async def create_connected_udp_socket( + remote_host: IPAddressType, + remote_port: int, + *, + family: AnyIPAddressFamily = AddressFamily.AF_UNSPEC, + local_host: IPAddressType | None = None, + local_port: int = 0, + reuse_port: bool = False, +) -> ConnectedUDPSocket: + """ + Create a connected UDP socket. + + Connected UDP sockets can only communicate with the specified remote host/port, an + any packets sent from other sources are dropped. + + :param remote_host: remote host to set as the default target + :param remote_port: port on the remote host to set as the default target + :param family: address family (``AF_INET`` or ``AF_INET6``) – automatically + determined from ``local_host`` or ``remote_host`` if omitted + :param local_host: IP address or host name of the local interface to bind to + :param local_port: local port to bind to + :param reuse_port: ``True`` to allow multiple sockets to bind to the same + address/port (not supported on Windows) + :return: a connected UDP socket + + """ + local_address = None + if local_host: + gai_res = await getaddrinfo( + str(local_host), + local_port, + family=family, + type=socket.SOCK_DGRAM, + flags=socket.AI_PASSIVE | socket.AI_ADDRCONFIG, + ) + family = cast(AnyIPAddressFamily, gai_res[0][0]) + local_address = gai_res[0][-1] + + gai_res = await getaddrinfo( + str(remote_host), remote_port, family=family, type=socket.SOCK_DGRAM + ) + family = cast(AnyIPAddressFamily, gai_res[0][0]) + remote_address = gai_res[0][-1] + + sock = await get_async_backend().create_udp_socket( + family, local_address, remote_address, reuse_port + ) + return cast(ConnectedUDPSocket, sock) + + +async def create_unix_datagram_socket( + *, + local_path: None | str | bytes | PathLike[Any] = None, + local_mode: int | None = None, +) -> UNIXDatagramSocket: + """ + Create a UNIX datagram socket. + + Not available on Windows. + + If ``local_path`` has been given, the socket will be bound to this path, making this + socket suitable for receiving datagrams from other processes. Other processes can + send datagrams to this socket only if ``local_path`` is set. + + If a socket already exists on the file system in the ``local_path``, it will be + removed first. + + :param local_path: the path on which to bind to + :param local_mode: permissions to set on the local socket + :return: a UNIX datagram socket + + """ + raw_socket = await setup_unix_local_socket( + local_path, local_mode, socket.SOCK_DGRAM + ) + return await get_async_backend().create_unix_datagram_socket(raw_socket, None) + + +async def create_connected_unix_datagram_socket( + remote_path: str | bytes | PathLike[Any], + *, + local_path: None | str | bytes | PathLike[Any] = None, + local_mode: int | None = None, +) -> ConnectedUNIXDatagramSocket: + """ + Create a connected UNIX datagram socket. + + Connected datagram sockets can only communicate with the specified remote path. + + If ``local_path`` has been given, the socket will be bound to this path, making + this socket suitable for receiving datagrams from other processes. Other processes + can send datagrams to this socket only if ``local_path`` is set. + + If a socket already exists on the file system in the ``local_path``, it will be + removed first. + + :param remote_path: the path to set as the default target + :param local_path: the path on which to bind to + :param local_mode: permissions to set on the local socket + :return: a connected UNIX datagram socket + + """ + remote_path = os.fspath(remote_path) + raw_socket = await setup_unix_local_socket( + local_path, local_mode, socket.SOCK_DGRAM + ) + return await get_async_backend().create_unix_datagram_socket( + raw_socket, remote_path + ) + + +async def getaddrinfo( + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, +) -> list[tuple[AddressFamily, SocketKind, int, str, tuple[str, int]]]: + """ + Look up a numeric IP address given a host name. + + Internationalized domain names are translated according to the (non-transitional) + IDNA 2008 standard. + + .. note:: 4-tuple IPv6 socket addresses are automatically converted to 2-tuples of + (host, port), unlike what :func:`socket.getaddrinfo` does. + + :param host: host name + :param port: port number + :param family: socket family (`'AF_INET``, ...) + :param type: socket type (``SOCK_STREAM``, ...) + :param proto: protocol number + :param flags: flags to pass to upstream ``getaddrinfo()`` + :return: list of tuples containing (family, type, proto, canonname, sockaddr) + + .. seealso:: :func:`socket.getaddrinfo` + + """ + # Handle unicode hostnames + if isinstance(host, str): + try: + encoded_host: bytes | None = host.encode("ascii") + except UnicodeEncodeError: + import idna + + encoded_host = idna.encode(host, uts46=True) + else: + encoded_host = host + + gai_res = await get_async_backend().getaddrinfo( + encoded_host, port, family=family, type=type, proto=proto, flags=flags + ) + return [ + (family, type, proto, canonname, convert_ipv6_sockaddr(sockaddr)) + for family, type, proto, canonname, sockaddr in gai_res + # filter out IPv6 results when IPv6 is disabled + if not isinstance(sockaddr[0], int) + ] + + +def getnameinfo(sockaddr: IPSockAddrType, flags: int = 0) -> Awaitable[tuple[str, str]]: + """ + Look up the host name of an IP address. + + :param sockaddr: socket address (e.g. (ipaddress, port) for IPv4) + :param flags: flags to pass to upstream ``getnameinfo()`` + :return: a tuple of (host name, service name) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. seealso:: :func:`socket.getnameinfo` + + """ + return get_async_backend().getnameinfo(sockaddr, flags) + + +@deprecated("This function is deprecated; use `wait_readable` instead") +def wait_socket_readable(sock: socket.socket) -> Awaitable[None]: + """ + .. deprecated:: 4.7.0 + Use :func:`wait_readable` instead. + + Wait until the given socket has data to be read. + + .. warning:: Only use this on raw sockets that have not been wrapped by any higher + level constructs like socket streams! + + :param sock: a socket object + :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the + socket to become readable + :raises ~anyio.BusyResourceError: if another task is already waiting for the socket + to become readable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().wait_readable(sock.fileno()) + + +@deprecated("This function is deprecated; use `wait_writable` instead") +def wait_socket_writable(sock: socket.socket) -> Awaitable[None]: + """ + .. deprecated:: 4.7.0 + Use :func:`wait_writable` instead. + + Wait until the given socket can be written to. + + This does **NOT** work on Windows when using the asyncio backend with a proactor + event loop (default on py3.8+). + + .. warning:: Only use this on raw sockets that have not been wrapped by any higher + level constructs like socket streams! + + :param sock: a socket object + :raises ~anyio.ClosedResourceError: if the socket was closed while waiting for the + socket to become writable + :raises ~anyio.BusyResourceError: if another task is already waiting for the socket + to become writable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().wait_writable(sock.fileno()) + + +def wait_readable(obj: FileDescriptorLike) -> Awaitable[None]: + """ + Wait until the given object has data to be read. + + On Unix systems, ``obj`` must either be an integer file descriptor, or else an + object with a ``.fileno()`` method which returns an integer file descriptor. Any + kind of file descriptor can be passed, though the exact semantics will depend on + your kernel. For example, this probably won't do anything useful for on-disk files. + + On Windows systems, ``obj`` must either be an integer ``SOCKET`` handle, or else an + object with a ``.fileno()`` method which returns an integer ``SOCKET`` handle. File + descriptors aren't supported, and neither are handles that refer to anything besides + a ``SOCKET``. + + On backends where this functionality is not natively provided (asyncio + ``ProactorEventLoop`` on Windows), it is provided using a separate selector thread + which is set to shut down when the interpreter shuts down. + + .. warning:: Don't use this on raw sockets that have been wrapped by any higher + level constructs like socket streams! + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the + object to become readable + :raises ~anyio.BusyResourceError: if another task is already waiting for the object + to become readable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().wait_readable(obj) + + +def wait_writable(obj: FileDescriptorLike) -> Awaitable[None]: + """ + Wait until the given object can be written to. + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises ~anyio.ClosedResourceError: if the object was closed while waiting for the + object to become writable + :raises ~anyio.BusyResourceError: if another task is already waiting for the object + to become writable + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. seealso:: See the documentation of :func:`wait_readable` for the definition of + ``obj`` and notes on backend compatibility. + + .. warning:: Don't use this on raw sockets that have been wrapped by any higher + level constructs like socket streams! + + """ + return get_async_backend().wait_writable(obj) + + +def notify_closing(obj: FileDescriptorLike) -> None: + """ + Call this before closing a file descriptor (on Unix) or socket (on + Windows). This will cause any `wait_readable` or `wait_writable` + calls on the given object to immediately wake up and raise + `~anyio.ClosedResourceError`. + + This doesn't actually close the object – you still have to do that + yourself afterwards. Also, you want to be careful to make sure no + new tasks start waiting on the object in between when you call this + and when it's actually closed. So to close something properly, you + usually want to do these steps in order: + + 1. Explicitly mark the object as closed, so that any new attempts + to use it will abort before they start. + 2. Call `notify_closing` to wake up any already-existing users. + 3. Actually close the object. + + It's also possible to do them in a different order if that's more + convenient, *but only if* you make sure not to have any checkpoints in + between the steps. This way they all happen in a single atomic + step, so other tasks won't be able to tell what order they happened + in anyway. + + :param obj: an object with a ``.fileno()`` method or an integer handle + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + get_async_backend().notify_closing(obj) + + +# +# Private API +# + + +def convert_ipv6_sockaddr( + sockaddr: tuple[str, int, int, int] | tuple[str, int], +) -> tuple[str, int]: + """ + Convert a 4-tuple IPv6 socket address to a 2-tuple (address, port) format. + + If the scope ID is nonzero, it is added to the address, separated with ``%``. + Otherwise the flow id and scope id are simply cut off from the tuple. + Any other kinds of socket addresses are returned as-is. + + :param sockaddr: the result of :meth:`~socket.socket.getsockname` + :return: the converted socket address + + """ + # This is more complicated than it should be because of MyPy + if isinstance(sockaddr, tuple) and len(sockaddr) == 4: + host, port, flowinfo, scope_id = sockaddr + if scope_id: + # PyPy (as of v7.3.11) leaves the interface name in the result, so + # we discard it and only get the scope ID from the end + # (https://foss.heptapod.net/pypy/pypy/-/issues/3938) + host = host.split("%")[0] + + # Add scope_id to the address + return f"{host}%{scope_id}", port + else: + return host, port + else: + return sockaddr + + +async def setup_unix_local_socket( + path: None | str | bytes | PathLike[Any], + mode: int | None, + socktype: int, +) -> socket.socket: + """ + Create a UNIX local socket object, deleting the socket at the given path if it + exists. + + Not available on Windows. + + :param path: path of the socket + :param mode: permissions to set on the socket + :param socktype: socket.SOCK_STREAM or socket.SOCK_DGRAM + + """ + path_str: str | None + if path is not None: + path_str = os.fsdecode(path) + + # Linux abstract namespace sockets aren't backed by a concrete file so skip stat call + if not path_str.startswith("\0"): + # Copied from pathlib... + try: + stat_result = os.stat(path) + except OSError as e: + if e.errno not in ( + errno.ENOENT, + errno.ENOTDIR, + errno.EBADF, + errno.ELOOP, + ): + raise + else: + if stat.S_ISSOCK(stat_result.st_mode): + os.unlink(path) + else: + path_str = None + + raw_socket = socket.socket(socket.AF_UNIX, socktype) + raw_socket.setblocking(False) + + if path_str is not None: + try: + await to_thread.run_sync(raw_socket.bind, path_str, abandon_on_cancel=True) + if mode is not None: + await to_thread.run_sync(chmod, path_str, mode, abandon_on_cancel=True) + except BaseException: + raw_socket.close() + raise + + return raw_socket + + +@dataclass +class TCPConnectable(ByteStreamConnectable): + """ + Connects to a TCP server at the given host and port. + + :param host: host name or IP address of the server + :param port: TCP port number of the server + """ + + host: str | IPv4Address | IPv6Address + port: int + + def __post_init__(self) -> None: + if self.port < 1 or self.port > 65535: + raise ValueError("TCP port number out of range") + + @override + async def connect(self) -> SocketStream: + try: + return await connect_tcp(self.host, self.port) + except OSError as exc: + raise ConnectionFailed( + f"error connecting to {self.host}:{self.port}: {exc}" + ) from exc + + +@dataclass +class UNIXConnectable(ByteStreamConnectable): + """ + Connects to a UNIX domain socket at the given path. + + :param path: the file system path of the socket + """ + + path: str | bytes | PathLike[str] | PathLike[bytes] + + @override + async def connect(self) -> UNIXSocketStream: + try: + return await connect_unix(self.path) + except OSError as exc: + raise ConnectionFailed(f"error connecting to {self.path!r}: {exc}") from exc + + +def as_connectable( + remote: ByteStreamConnectable + | tuple[str | IPv4Address | IPv6Address, int] + | str + | bytes + | PathLike[str], + /, + *, + tls: bool = False, + ssl_context: ssl.SSLContext | None = None, + tls_hostname: str | None = None, + tls_standard_compatible: bool = True, +) -> ByteStreamConnectable: + """ + Return a byte stream connectable from the given object. + + If a bytestream connectable is given, it is returned unchanged. + If a tuple of (host, port) is given, a TCP connectable is returned. + If a string or bytes path is given, a UNIX connectable is returned. + + If ``tls=True``, the connectable will be wrapped in a + :class:`~.streams.tls.TLSConnectable`. + + :param remote: a connectable, a tuple of (host, port) or a path to a UNIX socket + :param tls: if ``True``, wrap the plaintext connectable in a + :class:`~.streams.tls.TLSConnectable`, using the provided TLS settings) + :param ssl_context: if ``tls=True``, the SSLContext object to use (if not provided, + a secure default will be created) + :param tls_hostname: if ``tls=True``, host name of the server to use for checking + the server certificate (defaults to the host portion of the address for TCP + connectables) + :param tls_standard_compatible: if ``False`` and ``tls=True``, makes the TLS stream + skip the closing handshake when closing the connection, so it won't raise an + exception if the server does the same + + """ + connectable: TCPConnectable | UNIXConnectable | TLSConnectable + if isinstance(remote, ByteStreamConnectable): + return remote + elif isinstance(remote, tuple) and len(remote) == 2: + connectable = TCPConnectable(*remote) + elif isinstance(remote, (str, bytes, PathLike)): + connectable = UNIXConnectable(remote) + else: + raise TypeError(f"cannot convert {remote!r} to a connectable") + + if tls: + if not tls_hostname and isinstance(connectable, TCPConnectable): + tls_hostname = str(connectable.host) + + connectable = TLSConnectable( + connectable, + ssl_context=ssl_context, + hostname=tls_hostname, + standard_compatible=tls_standard_compatible, + ) + + return connectable diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_streams.py b/venv/lib/python3.11/site-packages/anyio/_core/_streams.py new file mode 100644 index 0000000..2b9c7df --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_streams.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import math +from typing import TypeVar +from warnings import warn + +from ..streams.memory import ( + MemoryObjectReceiveStream, + MemoryObjectSendStream, + _MemoryObjectStreamState, +) + +T_Item = TypeVar("T_Item") + + +class create_memory_object_stream( + tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]], +): + """ + Create a memory object stream. + + The stream's item type can be annotated like + :func:`create_memory_object_stream[T_Item]`. + + :param max_buffer_size: number of items held in the buffer until ``send()`` starts + blocking + :param item_type: old way of marking the streams with the right generic type for + static typing (does nothing on AnyIO 4) + + .. deprecated:: 4.0 + Use ``create_memory_object_stream[YourItemType](...)`` instead. + :return: a tuple of (send stream, receive stream) + + """ + + def __new__( # type: ignore[misc] + cls, max_buffer_size: float = 0, item_type: object = None + ) -> tuple[MemoryObjectSendStream[T_Item], MemoryObjectReceiveStream[T_Item]]: + if max_buffer_size != math.inf and not isinstance(max_buffer_size, int): + raise ValueError("max_buffer_size must be either an integer or math.inf") + if max_buffer_size < 0: + raise ValueError("max_buffer_size cannot be negative") + if item_type is not None: + warn( + "The item_type argument has been deprecated in AnyIO 4.0. " + "Use create_memory_object_stream[YourItemType](...) instead.", + DeprecationWarning, + stacklevel=2, + ) + + state = _MemoryObjectStreamState[T_Item](max_buffer_size) + return (MemoryObjectSendStream(state), MemoryObjectReceiveStream(state)) diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_subprocesses.py b/venv/lib/python3.11/site-packages/anyio/_core/_subprocesses.py new file mode 100644 index 0000000..9796f8b --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_subprocesses.py @@ -0,0 +1,196 @@ +from __future__ import annotations + +from collections.abc import AsyncIterable, Iterable, Mapping, Sequence +from io import BytesIO +from os import PathLike +from subprocess import PIPE, CalledProcessError, CompletedProcess +from typing import IO, Any, TypeAlias, cast + +from ..abc import Process +from ._eventloop import get_async_backend +from ._tasks import create_task_group + +StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes] + + +async def run_process( + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + input: bytes | None = None, + stdin: int | IO[Any] | None = None, + stdout: int | IO[Any] | None = PIPE, + stderr: int | IO[Any] | None = PIPE, + check: bool = True, + cwd: StrOrBytesPath | None = None, + env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, + start_new_session: bool = False, + pass_fds: Sequence[int] = (), + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, +) -> CompletedProcess[bytes]: + """ + Run an external command in a subprocess and wait until it completes. + + .. seealso:: :func:`subprocess.run` + + :param command: either a string to pass to the shell, or an iterable of strings + containing the executable name or path and its arguments + :param input: bytes passed to the standard input of the subprocess + :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + a file-like object, or `None`; ``input`` overrides this + :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + a file-like object, or `None` + :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + :data:`subprocess.STDOUT`, a file-like object, or `None` + :param check: if ``True``, raise :exc:`~subprocess.CalledProcessError` if the + process terminates with a return code other than 0 + :param cwd: If not ``None``, change the working directory to this before running the + command + :param env: if not ``None``, this mapping replaces the inherited environment + variables from the parent process + :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used + to specify process startup parameters (Windows only) + :param creationflags: flags that can be used to control the creation of the + subprocess (see :class:`subprocess.Popen` for the specifics) + :param start_new_session: if ``true`` the setsid() system call will be made in the + child process prior to the execution of the subprocess. (POSIX only) + :param pass_fds: sequence of file descriptors to keep open between the parent and + child processes. (POSIX only) + :param user: effective user to run the process as (Python >= 3.9, POSIX only) + :param group: effective group to run the process as (Python >= 3.9, POSIX only) + :param extra_groups: supplementary groups to set in the subprocess (Python >= 3.9, + POSIX only) + :param umask: if not negative, this umask is applied in the child process before + running the given command (Python >= 3.9, POSIX only) + :return: an object representing the completed process + :raises ~subprocess.CalledProcessError: if ``check`` is ``True`` and the process + exits with a nonzero return code + + """ + + async def drain_stream(stream: AsyncIterable[bytes], index: int) -> None: + buffer = BytesIO() + async for chunk in stream: + buffer.write(chunk) + + stream_contents[index] = buffer.getvalue() + + if stdin is not None and input is not None: + raise ValueError("only one of stdin and input is allowed") + + async with await open_process( + command, + stdin=PIPE if input else stdin, + stdout=stdout, + stderr=stderr, + cwd=cwd, + env=env, + startupinfo=startupinfo, + creationflags=creationflags, + start_new_session=start_new_session, + pass_fds=pass_fds, + user=user, + group=group, + extra_groups=extra_groups, + umask=umask, + ) as process: + stream_contents: list[bytes | None] = [None, None] + async with create_task_group() as tg: + if process.stdout: + tg.start_soon(drain_stream, process.stdout, 0) + + if process.stderr: + tg.start_soon(drain_stream, process.stderr, 1) + + if process.stdin and input: + await process.stdin.send(input) + await process.stdin.aclose() + + await process.wait() + + output, errors = stream_contents + if check and process.returncode != 0: + raise CalledProcessError(cast(int, process.returncode), command, output, errors) + + return CompletedProcess(command, cast(int, process.returncode), output, errors) + + +async def open_process( + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None = PIPE, + stdout: int | IO[Any] | None = PIPE, + stderr: int | IO[Any] | None = PIPE, + cwd: StrOrBytesPath | None = None, + env: Mapping[str, str] | None = None, + startupinfo: Any = None, + creationflags: int = 0, + start_new_session: bool = False, + pass_fds: Sequence[int] = (), + user: str | int | None = None, + group: str | int | None = None, + extra_groups: Iterable[str | int] | None = None, + umask: int = -1, +) -> Process: + """ + Start an external command in a subprocess. + + .. seealso:: :class:`subprocess.Popen` + + :param command: either a string to pass to the shell, or an iterable of strings + containing the executable name or path and its arguments + :param stdin: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, a + file-like object, or ``None`` + :param stdout: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + a file-like object, or ``None`` + :param stderr: one of :data:`subprocess.PIPE`, :data:`subprocess.DEVNULL`, + :data:`subprocess.STDOUT`, a file-like object, or ``None`` + :param cwd: If not ``None``, the working directory is changed before executing + :param env: If env is not ``None``, it must be a mapping that defines the + environment variables for the new process + :param creationflags: flags that can be used to control the creation of the + subprocess (see :class:`subprocess.Popen` for the specifics) + :param startupinfo: an instance of :class:`subprocess.STARTUPINFO` that can be used + to specify process startup parameters (Windows only) + :param start_new_session: if ``true`` the setsid() system call will be made in the + child process prior to the execution of the subprocess. (POSIX only) + :param pass_fds: sequence of file descriptors to keep open between the parent and + child processes. (POSIX only) + :param user: effective user to run the process as (POSIX only) + :param group: effective group to run the process as (POSIX only) + :param extra_groups: supplementary groups to set in the subprocess (POSIX only) + :param umask: if not negative, this umask is applied in the child process before + running the given command (POSIX only) + :return: an asynchronous process object + + """ + kwargs: dict[str, Any] = {} + if user is not None: + kwargs["user"] = user + + if group is not None: + kwargs["group"] = group + + if extra_groups is not None: + kwargs["extra_groups"] = group + + if umask >= 0: + kwargs["umask"] = umask + + return await get_async_backend().open_process( + command, + stdin=stdin, + stdout=stdout, + stderr=stderr, + cwd=cwd, + env=env, + startupinfo=startupinfo, + creationflags=creationflags, + start_new_session=start_new_session, + pass_fds=pass_fds, + **kwargs, + ) diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_synchronization.py b/venv/lib/python3.11/site-packages/anyio/_core/_synchronization.py new file mode 100644 index 0000000..9098bee --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_synchronization.py @@ -0,0 +1,772 @@ +from __future__ import annotations + +import math +from collections import deque +from collections.abc import Callable +from dataclasses import dataclass +from types import TracebackType +from typing import TypeVar + +from ..lowlevel import checkpoint_if_cancelled +from ._eventloop import get_async_backend +from ._exceptions import BusyResourceError, NoEventLoopError +from ._tasks import CancelScope +from ._testing import TaskInfo, get_current_task + +T = TypeVar("T") + + +@dataclass(frozen=True) +class EventStatistics: + """ + :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Event.wait` + """ + + tasks_waiting: int + + +@dataclass(frozen=True) +class CapacityLimiterStatistics: + """ + :ivar int borrowed_tokens: number of tokens currently borrowed by tasks + :ivar float total_tokens: total number of available tokens + :ivar tuple borrowers: tasks or other objects currently holding tokens borrowed from + this limiter + :ivar int tasks_waiting: number of tasks waiting on + :meth:`~.CapacityLimiter.acquire` or + :meth:`~.CapacityLimiter.acquire_on_behalf_of` + """ + + borrowed_tokens: int + total_tokens: float + borrowers: tuple[object, ...] + tasks_waiting: int + + +@dataclass(frozen=True) +class LockStatistics: + """ + :ivar bool locked: flag indicating if this lock is locked or not + :ivar ~anyio.TaskInfo owner: task currently holding the lock (or ``None`` if the + lock is not held by any task) + :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Lock.acquire` + """ + + locked: bool + owner: TaskInfo | None + tasks_waiting: int + + +@dataclass(frozen=True) +class ConditionStatistics: + """ + :ivar int tasks_waiting: number of tasks blocked on :meth:`~.Condition.wait` + :ivar ~anyio.LockStatistics lock_statistics: statistics of the underlying + :class:`~.Lock` + """ + + tasks_waiting: int + lock_statistics: LockStatistics + + +@dataclass(frozen=True) +class SemaphoreStatistics: + """ + :ivar int tasks_waiting: number of tasks waiting on :meth:`~.Semaphore.acquire` + + """ + + tasks_waiting: int + + +class Event: + __slots__ = ("__weakref__",) + + def __new__(cls) -> Event: + try: + return get_async_backend().create_event() + except NoEventLoopError: + return EventAdapter() + + def set(self) -> None: + """Set the flag, notifying all listeners.""" + raise NotImplementedError + + def is_set(self) -> bool: + """Return ``True`` if the flag is set, ``False`` if not.""" + raise NotImplementedError + + async def wait(self) -> None: + """ + Wait until the flag has been set. + + If the flag has already been set when this method is called, it returns + immediately. + + """ + raise NotImplementedError + + def statistics(self) -> EventStatistics: + """Return statistics about the current state of this event.""" + raise NotImplementedError + + +class EventAdapter(Event): + __slots__ = "_internal_event", "_is_set" + + def __new__(cls) -> EventAdapter: + return object.__new__(cls) + + def __init__(self) -> None: + self._internal_event: Event | None = None + self._is_set = False + + @property + def _event(self) -> Event: + if self._internal_event is None: + self._internal_event = get_async_backend().create_event() + if self._is_set: + self._internal_event.set() + + return self._internal_event + + def set(self) -> None: + if self._internal_event is None: + self._is_set = True + else: + self._event.set() + + def is_set(self) -> bool: + if self._internal_event is None: + return self._is_set + + return self._internal_event.is_set() + + async def wait(self) -> None: + await self._event.wait() + + def statistics(self) -> EventStatistics: + if self._internal_event is None: + return EventStatistics(tasks_waiting=0) + + return self._internal_event.statistics() + + +class Lock: + __slots__ = ("__weakref__",) + + def __new__(cls, *, fast_acquire: bool = False) -> Lock: + try: + return get_async_backend().create_lock(fast_acquire=fast_acquire) + except NoEventLoopError: + return LockAdapter(fast_acquire=fast_acquire) + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + async def acquire(self) -> None: + """Acquire the lock.""" + raise NotImplementedError + + def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + raise NotImplementedError + + def release(self) -> None: + """Release the lock.""" + raise NotImplementedError + + def locked(self) -> bool: + """Return True if the lock is currently held.""" + raise NotImplementedError + + def statistics(self) -> LockStatistics: + """ + Return statistics about the current state of this lock. + + .. versionadded:: 3.0 + """ + raise NotImplementedError + + +class LockAdapter(Lock): + __slots__ = "_internal_lock", "_fast_acquire" + + def __new__(cls, *, fast_acquire: bool = False) -> LockAdapter: + return object.__new__(cls) + + def __init__(self, *, fast_acquire: bool = False): + self._internal_lock: Lock | None = None + self._fast_acquire = fast_acquire + + @property + def _lock(self) -> Lock: + if self._internal_lock is None: + self._internal_lock = get_async_backend().create_lock( + fast_acquire=self._fast_acquire + ) + + return self._internal_lock + + async def __aenter__(self) -> None: + await self._lock.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if self._internal_lock is not None: + self._internal_lock.release() + + async def acquire(self) -> None: + """Acquire the lock.""" + await self._lock.acquire() + + def acquire_nowait(self) -> None: + """ + Acquire the lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + self._lock.acquire_nowait() + + def release(self) -> None: + """Release the lock.""" + self._lock.release() + + def locked(self) -> bool: + """Return True if the lock is currently held.""" + return self._lock.locked() + + def statistics(self) -> LockStatistics: + """ + Return statistics about the current state of this lock. + + .. versionadded:: 3.0 + + """ + if self._internal_lock is None: + return LockStatistics(False, None, 0) + + return self._internal_lock.statistics() + + +class Condition: + __slots__ = "__weakref__", "_owner_task", "_lock", "_waiters" + + def __init__(self, lock: Lock | None = None): + self._owner_task: TaskInfo | None = None + self._lock = lock or Lock() + self._waiters: deque[Event] = deque() + + async def __aenter__(self) -> None: + await self.acquire() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + def _check_acquired(self) -> None: + if self._owner_task != get_current_task(): + raise RuntimeError("The current task is not holding the underlying lock") + + async def acquire(self) -> None: + """Acquire the underlying lock.""" + await self._lock.acquire() + self._owner_task = get_current_task() + + def acquire_nowait(self) -> None: + """ + Acquire the underlying lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + self._lock.acquire_nowait() + self._owner_task = get_current_task() + + def release(self) -> None: + """Release the underlying lock.""" + self._lock.release() + + def locked(self) -> bool: + """Return True if the lock is set.""" + return self._lock.locked() + + def notify(self, n: int = 1) -> None: + """Notify exactly n listeners.""" + self._check_acquired() + for _ in range(n): + try: + event = self._waiters.popleft() + except IndexError: + break + + event.set() + + def notify_all(self) -> None: + """Notify all the listeners.""" + self._check_acquired() + for event in self._waiters: + event.set() + + self._waiters.clear() + + async def wait(self) -> None: + """Wait for a notification.""" + await checkpoint_if_cancelled() + self._check_acquired() + event = Event() + self._waiters.append(event) + self.release() + try: + await event.wait() + except BaseException: + if not event.is_set(): + self._waiters.remove(event) + elif self._waiters: + # This task was notified by could not act on it, so pass + # it on to the next task + self._waiters.popleft().set() + + raise + finally: + with CancelScope(shield=True): + await self.acquire() + + async def wait_for(self, predicate: Callable[[], T]) -> T: + """ + Wait until a predicate becomes true. + + :param predicate: a callable that returns a truthy value when the condition is + met + :return: the result of the predicate + + .. versionadded:: 4.11.0 + + """ + while not (result := predicate()): + await self.wait() + + return result + + def statistics(self) -> ConditionStatistics: + """ + Return statistics about the current state of this condition. + + .. versionadded:: 3.0 + """ + return ConditionStatistics(len(self._waiters), self._lock.statistics()) + + +class Semaphore: + __slots__ = "__weakref__", "_fast_acquire" + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Semaphore: + try: + return get_async_backend().create_semaphore( + initial_value, max_value=max_value, fast_acquire=fast_acquire + ) + except NoEventLoopError: + return SemaphoreAdapter(initial_value, max_value=max_value) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ): + if not isinstance(initial_value, int): + raise TypeError("initial_value must be an integer") + if initial_value < 0: + raise ValueError("initial_value must be >= 0") + if max_value is not None: + if not isinstance(max_value, int): + raise TypeError("max_value must be an integer or None") + if max_value < initial_value: + raise ValueError( + "max_value must be equal to or higher than initial_value" + ) + + self._fast_acquire = fast_acquire + + async def __aenter__(self) -> Semaphore: + await self.acquire() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.release() + + async def acquire(self) -> None: + """Decrement the semaphore value, blocking if necessary.""" + raise NotImplementedError + + def acquire_nowait(self) -> None: + """ + Acquire the underlying lock, without blocking. + + :raises ~anyio.WouldBlock: if the operation would block + + """ + raise NotImplementedError + + def release(self) -> None: + """Increment the semaphore value.""" + raise NotImplementedError + + @property + def value(self) -> int: + """The current value of the semaphore.""" + raise NotImplementedError + + @property + def max_value(self) -> int | None: + """The maximum value of the semaphore.""" + raise NotImplementedError + + def statistics(self) -> SemaphoreStatistics: + """ + Return statistics about the current state of this semaphore. + + .. versionadded:: 3.0 + """ + raise NotImplementedError + + +class SemaphoreAdapter(Semaphore): + __slots__ = "_internal_semaphore", "_initial_value", "_max_value" + + def __new__( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> SemaphoreAdapter: + return object.__new__(cls) + + def __init__( + self, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> None: + super().__init__(initial_value, max_value=max_value, fast_acquire=fast_acquire) + self._internal_semaphore: Semaphore | None = None + self._initial_value = initial_value + self._max_value = max_value + + @property + def _semaphore(self) -> Semaphore: + if self._internal_semaphore is None: + self._internal_semaphore = get_async_backend().create_semaphore( + self._initial_value, max_value=self._max_value + ) + + return self._internal_semaphore + + async def acquire(self) -> None: + await self._semaphore.acquire() + + def acquire_nowait(self) -> None: + self._semaphore.acquire_nowait() + + def release(self) -> None: + self._semaphore.release() + + @property + def value(self) -> int: + if self._internal_semaphore is None: + return self._initial_value + + return self._semaphore.value + + @property + def max_value(self) -> int | None: + return self._max_value + + def statistics(self) -> SemaphoreStatistics: + if self._internal_semaphore is None: + return SemaphoreStatistics(tasks_waiting=0) + + return self._semaphore.statistics() + + +class CapacityLimiter: + __slots__ = ("__weakref__",) + + def __new__(cls, total_tokens: float) -> CapacityLimiter: + try: + return get_async_backend().create_capacity_limiter(total_tokens) + except NoEventLoopError: + return CapacityLimiterAdapter(total_tokens) + + async def __aenter__(self) -> None: + raise NotImplementedError + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + raise NotImplementedError + + @property + def total_tokens(self) -> float: + """ + The total number of tokens available for borrowing. + + This is a read-write property. If the total number of tokens is increased, the + proportionate number of tasks waiting on this limiter will be granted their + tokens. + + .. versionchanged:: 3.0 + The property is now writable. + .. versionchanged:: 4.12 + The value can now be set to 0. + + """ + raise NotImplementedError + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + raise NotImplementedError + + @property + def borrowed_tokens(self) -> int: + """The number of tokens that have currently been borrowed.""" + raise NotImplementedError + + @property + def available_tokens(self) -> float: + """The number of tokens currently available to be borrowed""" + raise NotImplementedError + + def acquire_nowait(self) -> None: + """ + Acquire a token for the current task without waiting for one to become + available. + + :raises ~anyio.WouldBlock: if there are no tokens available for borrowing + + """ + raise NotImplementedError + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + """ + Acquire a token without waiting for one to become available. + + :param borrower: the entity borrowing a token + :raises ~anyio.WouldBlock: if there are no tokens available for borrowing + + """ + raise NotImplementedError + + async def acquire(self) -> None: + """ + Acquire a token for the current task, waiting if necessary for one to become + available. + + """ + raise NotImplementedError + + async def acquire_on_behalf_of(self, borrower: object) -> None: + """ + Acquire a token, waiting if necessary for one to become available. + + :param borrower: the entity borrowing a token + + """ + raise NotImplementedError + + def release(self) -> None: + """ + Release the token held by the current task. + + :raises RuntimeError: if the current task has not borrowed a token from this + limiter. + + """ + raise NotImplementedError + + def release_on_behalf_of(self, borrower: object) -> None: + """ + Release the token held by the given borrower. + + :raises RuntimeError: if the borrower has not borrowed a token from this + limiter. + + """ + raise NotImplementedError + + def statistics(self) -> CapacityLimiterStatistics: + """ + Return statistics about the current state of this limiter. + + .. versionadded:: 3.0 + + """ + raise NotImplementedError + + +class CapacityLimiterAdapter(CapacityLimiter): + __slots__ = "_internal_limiter", "_total_tokens" + + def __new__(cls, total_tokens: float) -> CapacityLimiterAdapter: + return object.__new__(cls) + + def __init__(self, total_tokens: float) -> None: + self._internal_limiter: CapacityLimiter | None = None + self.total_tokens = total_tokens + + @property + def _limiter(self) -> CapacityLimiter: + if self._internal_limiter is None: + self._internal_limiter = get_async_backend().create_capacity_limiter( + self._total_tokens + ) + + return self._internal_limiter + + async def __aenter__(self) -> None: + await self._limiter.__aenter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + return await self._limiter.__aexit__(exc_type, exc_val, exc_tb) + + @property + def total_tokens(self) -> float: + if self._internal_limiter is None: + return self._total_tokens + + return self._internal_limiter.total_tokens + + @total_tokens.setter + def total_tokens(self, value: float) -> None: + if not isinstance(value, int) and value is not math.inf: + raise TypeError("total_tokens must be an int or math.inf") + elif value < 0: + raise ValueError("total_tokens must be >= 0") + + if self._internal_limiter is None: + self._total_tokens = value + return + + self._limiter.total_tokens = value + + @property + def borrowed_tokens(self) -> int: + if self._internal_limiter is None: + return 0 + + return self._internal_limiter.borrowed_tokens + + @property + def available_tokens(self) -> float: + if self._internal_limiter is None: + return self._total_tokens + + return self._internal_limiter.available_tokens + + def acquire_nowait(self) -> None: + self._limiter.acquire_nowait() + + def acquire_on_behalf_of_nowait(self, borrower: object) -> None: + self._limiter.acquire_on_behalf_of_nowait(borrower) + + async def acquire(self) -> None: + await self._limiter.acquire() + + async def acquire_on_behalf_of(self, borrower: object) -> None: + await self._limiter.acquire_on_behalf_of(borrower) + + def release(self) -> None: + self._limiter.release() + + def release_on_behalf_of(self, borrower: object) -> None: + self._limiter.release_on_behalf_of(borrower) + + def statistics(self) -> CapacityLimiterStatistics: + if self._internal_limiter is None: + return CapacityLimiterStatistics( + borrowed_tokens=0, + total_tokens=self.total_tokens, + borrowers=(), + tasks_waiting=0, + ) + + return self._internal_limiter.statistics() + + +class ResourceGuard: + """ + A context manager for ensuring that a resource is only used by a single task at a + time. + + Entering this context manager while the previous has not exited it yet will trigger + :exc:`BusyResourceError`. + + :param action: the action to guard against (visible in the :exc:`BusyResourceError` + when triggered, e.g. "Another task is already {action} this resource") + + .. versionadded:: 4.1 + """ + + __slots__ = "__weakref__", "action", "_guarded" + + def __init__(self, action: str = "using"): + self.action: str = action + self._guarded = False + + def __enter__(self) -> None: + if self._guarded: + raise BusyResourceError(self.action) + + self._guarded = True + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._guarded = False diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_tasks.py b/venv/lib/python3.11/site-packages/anyio/_core/_tasks.py new file mode 100644 index 0000000..108393e --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_tasks.py @@ -0,0 +1,415 @@ +from __future__ import annotations + +import math +import sys +from collections.abc import ( + Coroutine, + Generator, +) +from contextlib import ( + contextmanager, +) +from contextvars import ContextVar +from enum import Enum, auto +from inspect import iscoroutine +from types import TracebackType +from typing import Any, Generic, final + +from ..abc import TaskGroup, TaskStatus +from ._eventloop import get_async_backend, get_cancelled_exc_class +from ._exceptions import TaskCancelled, TaskFailed, TaskNotFinished + +if sys.version_info >= (3, 13): + from typing import TypeVar +else: + from typing_extensions import TypeVar + +if sys.version_info >= (3, 11): + from typing import Never, TypeVarTuple +else: + from typing_extensions import Never, TypeVarTuple + +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) +T_startval = TypeVar("T_startval", covariant=True, default=Never) +PosArgsT = TypeVarTuple("PosArgsT") + +_current_task_handle: ContextVar[TaskHandle] = ContextVar("_current_task_handle") + + +class _IgnoredTaskStatus(TaskStatus[object]): + def started(self, value: object = None) -> None: + pass + + +TASK_STATUS_IGNORED = _IgnoredTaskStatus() + + +class CancelScope: + """ + Wraps a unit of work that can be made separately cancellable. + + :param deadline: The time (clock value) when this scope is cancelled automatically + :param shield: ``True`` to shield the cancel scope from external cancellation + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + """ + + __slots__ = ("__weakref__",) + + def __new__( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + return get_async_backend().create_cancel_scope(shield=shield, deadline=deadline) + + def cancel(self, reason: str | None = None) -> None: + """ + Cancel this scope immediately. + + :param reason: a message describing the reason for the cancellation + + """ + raise NotImplementedError + + @property + def deadline(self) -> float: + """ + The time (clock value) when this scope is cancelled automatically. + + Will be ``float('inf')`` if no timeout has been set. + + """ + raise NotImplementedError + + @deadline.setter + def deadline(self, value: float) -> None: + raise NotImplementedError + + @property + def cancel_called(self) -> bool: + """``True`` if :meth:`cancel` has been called.""" + raise NotImplementedError + + @property + def cancelled_caught(self) -> bool: + """ + ``True`` if this scope suppressed a cancellation exception it itself raised. + + This is typically used to check if any work was interrupted, or to see if the + scope was cancelled due to its deadline being reached. The value will, however, + only be ``True`` if the cancellation was triggered by the scope itself (and not + an outer scope). + + """ + raise NotImplementedError + + @property + def shield(self) -> bool: + """ + ``True`` if this scope is shielded from external cancellation. + + While a scope is shielded, it will not receive cancellations from outside. + + """ + raise NotImplementedError + + @shield.setter + def shield(self, value: bool) -> None: + raise NotImplementedError + + def __enter__(self) -> CancelScope: + raise NotImplementedError + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + raise NotImplementedError + + +@contextmanager +def fail_after( + delay: float | None, shield: bool = False +) -> Generator[CancelScope, None, None]: + """ + Create a context manager which raises a :class:`TimeoutError` if does not finish in + time. + + :param delay: maximum allowed time (in seconds) before raising the exception, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :return: a context manager that yields a cancel scope + :rtype: :class:`~typing.ContextManager`\\[:class:`~anyio.CancelScope`\\] + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + current_time = get_async_backend().current_time + deadline = (current_time() + delay) if delay is not None else math.inf + with get_async_backend().create_cancel_scope( + deadline=deadline, shield=shield + ) as cancel_scope: + yield cancel_scope + + if cancel_scope.cancelled_caught and current_time() >= cancel_scope.deadline: + raise TimeoutError + + +def move_on_after(delay: float | None, shield: bool = False) -> CancelScope: + """ + Create a cancel scope with a deadline that expires after the given delay. + + :param delay: maximum allowed time (in seconds) before exiting the context block, or + ``None`` to disable the timeout + :param shield: ``True`` to shield the cancel scope from external cancellation + :return: a cancel scope + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + deadline = ( + (get_async_backend().current_time() + delay) if delay is not None else math.inf + ) + return get_async_backend().create_cancel_scope(deadline=deadline, shield=shield) + + +def current_effective_deadline() -> float: + """ + Return the nearest deadline among all the cancel scopes effective for the current + task. + + :return: a clock value from the event loop's internal clock (or ``float('inf')`` if + there is no deadline in effect, or ``float('-inf')`` if the current scope has + been cancelled) + :rtype: float + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().current_effective_deadline() + + +def create_task_group() -> TaskGroup: + """ + Create a task group. + + :return: a task group + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().create_task_group() + + +@final +class TaskHandle(Generic[T_co, T_startval]): + """ + Returned from the task-spawning methods of :class:`TaskGroup`. Can be awaited on to + get the return value of the task (or the raised exception). If the task was + terminated by a :exc:`BaseException`, :exc:`TaskFailed` will be raised (or its + subclass :exc:`TaskCancelled` if the task was cancelled). + + .. versionadded:: 4.14.0 + """ + + class Status(Enum): + """ + The status of a task handle. + + .. attribute:: PENDING + + The task has not finished yet. + .. attribute:: FINISHED + + The task has finished with a return value. + .. attribute:: CANCELLING + + The task has been cancelled but has not finished yet. + .. attribute:: CANCELLED + + The task was cancelled and has finished since. + .. attribute:: FAILED + + The task raised an exception. + """ + + PENDING = auto() + FINISHED = auto() + CANCELLING = auto() + CANCELLED = auto() + FAILED = auto() + + __slots__ = ( + "__weakref__", + "_coro", + "_name", + "_cancel_scope", + "_finished_event", + "_return_value", + "_start_value", + "_exception", + ) + + _return_value: T_co + _start_value: T_startval + + def __init__(self, coro: Coroutine[Any, Any, T_co], name: object) -> None: + from ._synchronization import Event + + self._coro = coro + self._cancel_scope = CancelScope() + self._finished_event = Event() + self._exception: BaseException | None = None + + if name is not None: + self._name = str(name) + elif iscoroutine(coro): + self._name = coro.__qualname__ + else: + self._name = str(coro) # coroutine-like object (e.g. asend() objects) + + async def _run_coro(self) -> None: + __tracebackhide__ = True + + with self._cancel_scope: + try: + retval = await self._coro + except BaseException as exc: + self._exception = exc + raise + else: + self._return_value = retval + finally: + self._finished_event.set() + del self # Break the reference cycle + + def cancel(self) -> None: + """ + Set the task to a cancelled state. + + This will interrupt any interruptible asynchronous operation, and will cause + any further awaits on this task to get immediately cancelled, unless done in + a shielded cancel scope. + + If the task has already finished, this method has no effect. + """ + if not self._finished_event.is_set(): + self._cancel_scope.cancel() + + @property + def coro(self) -> Coroutine[Any, Any, T_co]: + """ + The coroutine object that was passed to one of the task-spawning methods in + :class:`TaskGroup`. + """ + return self._coro + + @property + def status(self) -> TaskHandle.Status: + """ + The current status of the task. + + Every task starts in the :attr:`~TaskHandle.Status.PENDING` state. + If a task is cancelled while in this state, it will transition to the + :attr:`~TaskHandle.Status.CANCELLING` state. When the task finishes, it will + transition to one of the three final states ( + :attr:`~TaskHandle.Status.FINISHED`, :attr:`~TaskHandle.Status.FAILED`, or + :attr:`~TaskHandle.Status.CANCELLING`) depending on the exception the task + raised, if any. No other status transitions will happen. + """ + if not self._finished_event.is_set(): + if self._cancel_scope.cancel_called: + return TaskHandle.Status.CANCELLING + else: + return TaskHandle.Status.PENDING + elif self._exception is not None: + if isinstance(self._exception, get_cancelled_exc_class()): + return TaskHandle.Status.CANCELLED + else: + return TaskHandle.Status.FAILED + else: + return TaskHandle.Status.FINISHED + + @property + def name(self) -> str: + """The name of the task.""" + return self._name + + @property + def exception(self) -> BaseException | None: + """ + The exception raised by the task, or ``None`` if it finished without raising. + + :raises TaskNotFinished: if the task has not finished yet + :raises TaskCancelled: if the task was cancelled + + """ + match self.status: + case TaskHandle.Status.PENDING: + raise TaskNotFinished("the task has not finished yet") + case TaskHandle.Status.FINISHED: + return None + case TaskHandle.Status.CANCELLING: + raise TaskCancelled("the task was cancelled") + case TaskHandle.Status.CANCELLED: + raise TaskCancelled("the task was cancelled") from self._exception + case TaskHandle.Status.FAILED: + return self._exception + + @property + def return_value(self) -> T_co: + """ + The return value of the task. + + :raises TaskNotFinished: if the task has not finished yet + :raises TaskCancelled: if the task was cancelled + :raises TaskFailed: if the task raised an exception + + """ + match self.status: + case TaskHandle.Status.PENDING: + raise TaskNotFinished("the task has not finished yet") + case TaskHandle.Status.FINISHED: + return self._return_value + case TaskHandle.Status.CANCELLING: + raise TaskCancelled("the task was cancelled") + case TaskHandle.Status.CANCELLED: + raise TaskCancelled("the task was cancelled") from self._exception + case TaskHandle.Status.FAILED: + raise TaskFailed("the task raised an exception") from self._exception + + @property + def start_value(self) -> T_startval: + """ + The value passed to :meth:`task_status.started() <.abc.TaskStatus.started>`, + + :raises RuntimeError: if the task was not started with :meth:`TaskGroup.start() + <.abc.TaskGroup.start>` + """ + try: + return self._start_value + except AttributeError: + raise RuntimeError( + "the task was not started with TaskGroup.start()" + ) from None + + async def wait(self) -> None: + """ + Wait for the task to finish. + + This method will return as soon as the task has finished, no matter how it + happened. + """ + await self._finished_event.wait() + + def __await__(self) -> Generator[Any, Any, T_co]: + yield from self._finished_event.wait().__await__() + return self.return_value + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} {self.status.name.lower()} " + f"name={self._name!r} coro={self._coro!r}>" + ) diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_tempfile.py b/venv/lib/python3.11/site-packages/anyio/_core/_tempfile.py new file mode 100644 index 0000000..75a09f7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_tempfile.py @@ -0,0 +1,613 @@ +from __future__ import annotations + +import os +import sys +import tempfile +from collections.abc import Iterable +from io import BytesIO, TextIOWrapper +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + AnyStr, + Generic, + overload, +) + +from .. import to_thread +from .._core._fileio import AsyncFile +from ..lowlevel import checkpoint_if_cancelled + +if TYPE_CHECKING: + from _typeshed import OpenBinaryMode, OpenTextMode, ReadableBuffer, WriteableBuffer + + +class TemporaryFile(Generic[AnyStr]): + """ + An asynchronous temporary file that is automatically created and cleaned up. + + This class provides an asynchronous context manager interface to a temporary file. + The file is created using Python's standard `tempfile.TemporaryFile` function in a + background thread, and is wrapped as an asynchronous file using `AsyncFile`. + + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file. Only applicable in + text mode. + :param newline: Controls how universal newlines mode works (only applicable in text + mode). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param errors: The error handling scheme used for encoding/decoding errors. + """ + + _async_file: AsyncFile[AnyStr] + + @overload + def __init__( + self: TemporaryFile[bytes], + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + @overload + def __init__( + self: TemporaryFile[str], + mode: OpenTextMode, + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + + def __init__( + self, + mode: OpenTextMode | OpenBinaryMode = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + *, + errors: str | None = None, + ) -> None: + self.mode = mode + self.buffering = buffering + self.encoding = encoding + self.newline = newline + self.suffix: str | None = suffix + self.prefix: str | None = prefix + self.dir: str | None = dir + self.errors = errors + + async def __aenter__(self) -> AsyncFile[AnyStr]: + fp = await to_thread.run_sync( + lambda: tempfile.TemporaryFile( + self.mode, + self.buffering, + self.encoding, + self.newline, + self.suffix, + self.prefix, + self.dir, + errors=self.errors, + ) + ) + self._async_file = AsyncFile(fp) + return self._async_file + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self._async_file.aclose() + + +class NamedTemporaryFile(Generic[AnyStr]): + """ + An asynchronous named temporary file that is automatically created and cleaned up. + + This class provides an asynchronous context manager for a temporary file with a + visible name in the file system. It uses Python's standard + :func:`~tempfile.NamedTemporaryFile` function and wraps the file object with + :class:`AsyncFile` for asynchronous operations. + + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file. Only applicable in + text mode. + :param newline: Controls how universal newlines mode works (only applicable in text + mode). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param delete: Whether to delete the file when it is closed. + :param errors: The error handling scheme used for encoding/decoding errors. + :param delete_on_close: (Python 3.12+) Whether to delete the file on close. + """ + + _async_file: AsyncFile[AnyStr] + + @overload + def __init__( + self: NamedTemporaryFile[bytes], + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + delete: bool = ..., + *, + errors: str | None = ..., + delete_on_close: bool = ..., + ): ... + @overload + def __init__( + self: NamedTemporaryFile[str], + mode: OpenTextMode, + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + delete: bool = ..., + *, + errors: str | None = ..., + delete_on_close: bool = ..., + ): ... + + def __init__( + self, + mode: OpenBinaryMode | OpenTextMode = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + delete: bool = True, + *, + errors: str | None = None, + delete_on_close: bool = True, + ) -> None: + self._params: dict[str, Any] = { + "mode": mode, + "buffering": buffering, + "encoding": encoding, + "newline": newline, + "suffix": suffix, + "prefix": prefix, + "dir": dir, + "delete": delete, + "errors": errors, + } + if sys.version_info >= (3, 12): + self._params["delete_on_close"] = delete_on_close + + async def __aenter__(self) -> AsyncFile[AnyStr]: + fp = await to_thread.run_sync( + lambda: tempfile.NamedTemporaryFile(**self._params) + ) + self._async_file = AsyncFile(fp) + return self._async_file + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + await self._async_file.aclose() + + +class SpooledTemporaryFile(AsyncFile[AnyStr]): + """ + An asynchronous spooled temporary file that starts in memory and is spooled to disk. + + This class provides an asynchronous interface to a spooled temporary file, much like + Python's standard :class:`~tempfile.SpooledTemporaryFile`. It supports asynchronous + write operations and provides a method to force a rollover to disk. + + :param max_size: Maximum size in bytes before the file is rolled over to disk. + :param mode: The mode in which the file is opened. Defaults to "w+b". + :param buffering: The buffering policy (-1 means the default buffering). + :param encoding: The encoding used to decode or encode the file (text mode only). + :param newline: Controls how universal newlines mode works (text mode only). + :param suffix: The suffix for the temporary file name. + :param prefix: The prefix for the temporary file name. + :param dir: The directory in which the temporary file is created. + :param errors: The error handling scheme used for encoding/decoding errors. + """ + + _rolled: bool = False + + @overload + def __init__( + self: SpooledTemporaryFile[bytes], + max_size: int = ..., + mode: OpenBinaryMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + @overload + def __init__( + self: SpooledTemporaryFile[str], + max_size: int = ..., + mode: OpenTextMode = ..., + buffering: int = ..., + encoding: str | None = ..., + newline: str | None = ..., + suffix: str | None = ..., + prefix: str | None = ..., + dir: str | None = ..., + *, + errors: str | None = ..., + ): ... + + def __init__( + self, + max_size: int = 0, + mode: OpenBinaryMode | OpenTextMode = "w+b", + buffering: int = -1, + encoding: str | None = None, + newline: str | None = None, + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + *, + errors: str | None = None, + ) -> None: + self._tempfile_params: dict[str, Any] = { + "mode": mode, + "buffering": buffering, + "encoding": encoding, + "newline": newline, + "suffix": suffix, + "prefix": prefix, + "dir": dir, + "errors": errors, + } + self._max_size = max_size + if "b" in mode: + super().__init__(BytesIO()) # type: ignore[arg-type] + else: + super().__init__( + TextIOWrapper( # type: ignore[arg-type] + BytesIO(), + encoding=encoding, + errors=errors, + newline=newline, + write_through=True, + ) + ) + + async def aclose(self) -> None: + if not self._rolled: + self._fp.close() + return + + await super().aclose() + + async def _check(self) -> None: + if self._rolled or self._fp.tell() <= self._max_size: + return + + await self.rollover() + + async def rollover(self) -> None: + if self._rolled: + return + + self._rolled = True + buffer = self._fp + buffer.seek(0) + self._fp = await to_thread.run_sync( + lambda: tempfile.TemporaryFile(**self._tempfile_params) + ) + await self.write(buffer.read()) + buffer.close() + + @property + def closed(self) -> bool: + return self._fp.closed + + async def read(self, size: int = -1) -> AnyStr: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.read(size) + + return await super().read(size) # type: ignore[return-value] + + async def read1(self: SpooledTemporaryFile[bytes], size: int = -1) -> bytes: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.read1(size) + + return await super().read1(size) + + async def readline(self) -> AnyStr: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readline() + + return await super().readline() # type: ignore[return-value] + + async def readlines(self) -> list[AnyStr]: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.readlines() + + return await super().readlines() # type: ignore[return-value] + + async def readinto(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + self._fp.readinto(b) + + return await super().readinto(b) + + async def readinto1(self: SpooledTemporaryFile[bytes], b: WriteableBuffer) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + self._fp.readinto(b) + + return await super().readinto1(b) + + async def seek(self, offset: int, whence: int | None = os.SEEK_SET) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.seek(offset, whence) + + return await super().seek(offset, whence) + + async def tell(self) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.tell() + + return await super().tell() + + async def truncate(self, size: int | None = None) -> int: + if not self._rolled: + await checkpoint_if_cancelled() + return self._fp.truncate(size) + + return await super().truncate(size) + + @overload + async def write(self: SpooledTemporaryFile[bytes], b: ReadableBuffer) -> int: ... + @overload + async def write(self: SpooledTemporaryFile[str], b: str) -> int: ... + + async def write(self, b: ReadableBuffer | str) -> int: + """ + Asynchronously write data to the spooled temporary file. + + If the file has not yet been rolled over, the data is written synchronously, + and a rollover is triggered if the size exceeds the maximum size. + + :param s: The data to write. + :return: The number of bytes written. + :raises RuntimeError: If the underlying file is not initialized. + + """ + if not self._rolled: + await checkpoint_if_cancelled() + result = self._fp.write(b) + await self._check() + return result + + return await super().write(b) # type: ignore[misc] + + @overload + async def writelines( + self: SpooledTemporaryFile[bytes], lines: Iterable[ReadableBuffer] + ) -> None: ... + @overload + async def writelines( + self: SpooledTemporaryFile[str], lines: Iterable[str] + ) -> None: ... + + async def writelines(self, lines: Iterable[str] | Iterable[ReadableBuffer]) -> None: + """ + Asynchronously write a list of lines to the spooled temporary file. + + If the file has not yet been rolled over, the lines are written synchronously, + and a rollover is triggered if the size exceeds the maximum size. + + :param lines: An iterable of lines to write. + :raises RuntimeError: If the underlying file is not initialized. + + """ + if not self._rolled: + await checkpoint_if_cancelled() + result = self._fp.writelines(lines) + await self._check() + return result + + return await super().writelines(lines) # type: ignore[misc] + + +class TemporaryDirectory(Generic[AnyStr]): + """ + An asynchronous temporary directory that is created and cleaned up automatically. + + This class provides an asynchronous context manager for creating a temporary + directory. It wraps Python's standard :class:`~tempfile.TemporaryDirectory` to + perform directory creation and cleanup operations in a background thread. + + :param suffix: Suffix to be added to the temporary directory name. + :param prefix: Prefix to be added to the temporary directory name. + :param dir: The parent directory where the temporary directory is created. + :param ignore_cleanup_errors: Whether to ignore errors during cleanup + :param delete: Whether to delete the directory upon closing (Python 3.12+). + """ + + def __init__( + self, + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + *, + ignore_cleanup_errors: bool = False, + delete: bool = True, + ) -> None: + self.suffix: AnyStr | None = suffix + self.prefix: AnyStr | None = prefix + self.dir: AnyStr | None = dir + self.ignore_cleanup_errors = ignore_cleanup_errors + self.delete = delete + + self._tempdir: tempfile.TemporaryDirectory | None = None + + async def __aenter__(self) -> str: + params: dict[str, Any] = { + "suffix": self.suffix, + "prefix": self.prefix, + "dir": self.dir, + "ignore_cleanup_errors": self.ignore_cleanup_errors, + } + if sys.version_info >= (3, 12): + params["delete"] = self.delete + + self._tempdir = await to_thread.run_sync( + lambda: tempfile.TemporaryDirectory(**params) + ) + return await to_thread.run_sync(self._tempdir.__enter__) + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + if self._tempdir is not None: + await to_thread.run_sync( + self._tempdir.__exit__, exc_type, exc_value, traceback + ) + + async def cleanup(self) -> None: + if self._tempdir is not None: + await to_thread.run_sync(self._tempdir.cleanup) + + +@overload +async def mkstemp( + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, + text: bool = False, +) -> tuple[int, str]: ... + + +@overload +async def mkstemp( + suffix: bytes | None = None, + prefix: bytes | None = None, + dir: bytes | None = None, + text: bool = False, +) -> tuple[int, bytes]: ... + + +async def mkstemp( + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, + text: bool = False, +) -> tuple[int, str | bytes]: + """ + Asynchronously create a temporary file and return an OS-level handle and the file + name. + + This function wraps `tempfile.mkstemp` and executes it in a background thread. + + :param suffix: Suffix to be added to the file name. + :param prefix: Prefix to be added to the file name. + :param dir: Directory in which the temporary file is created. + :param text: Whether the file is opened in text mode. + :return: A tuple containing the file descriptor and the file name. + + """ + return await to_thread.run_sync(tempfile.mkstemp, suffix, prefix, dir, text) + + +@overload +async def mkdtemp( + suffix: str | None = None, + prefix: str | None = None, + dir: str | None = None, +) -> str: ... + + +@overload +async def mkdtemp( + suffix: bytes | None = None, + prefix: bytes | None = None, + dir: bytes | None = None, +) -> bytes: ... + + +async def mkdtemp( + suffix: AnyStr | None = None, + prefix: AnyStr | None = None, + dir: AnyStr | None = None, +) -> str | bytes: + """ + Asynchronously create a temporary directory and return its path. + + This function wraps `tempfile.mkdtemp` and executes it in a background thread. + + :param suffix: Suffix to be added to the directory name. + :param prefix: Prefix to be added to the directory name. + :param dir: Parent directory where the temporary directory is created. + :return: The path of the created temporary directory. + + """ + return await to_thread.run_sync(tempfile.mkdtemp, suffix, prefix, dir) + + +async def gettempdir() -> str: + """ + Asynchronously return the name of the directory used for temporary files. + + This function wraps `tempfile.gettempdir` and executes it in a background thread. + + :return: The path of the temporary directory as a string. + + """ + return await to_thread.run_sync(tempfile.gettempdir) + + +async def gettempdirb() -> bytes: + """ + Asynchronously return the name of the directory used for temporary files in bytes. + + This function wraps `tempfile.gettempdirb` and executes it in a background thread. + + :return: The path of the temporary directory as bytes. + + """ + return await to_thread.run_sync(tempfile.gettempdirb) diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_testing.py b/venv/lib/python3.11/site-packages/anyio/_core/_testing.py new file mode 100644 index 0000000..369e65c --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_testing.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Generator +from typing import Any, cast + +from ._eventloop import get_async_backend + + +class TaskInfo: + """ + Represents an asynchronous task. + + :ivar int id: the unique identifier of the task + :ivar parent_id: the identifier of the parent task, if any + :vartype parent_id: Optional[int] + :ivar str name: the description of the task (if any) + :ivar ~collections.abc.Coroutine coro: the coroutine object of the task + """ + + __slots__ = "_name", "id", "parent_id", "name", "coro" + + def __init__( + self, + id: int, + parent_id: int | None, + name: str | None, + coro: Generator[Any, Any, Any] | Awaitable[Any], + ): + func = get_current_task + self._name = f"{func.__module__}.{func.__qualname__}" + self.id: int = id + self.parent_id: int | None = parent_id + self.name: str | None = name + self.coro: Generator[Any, Any, Any] | Awaitable[Any] = coro + + def __eq__(self, other: object) -> bool: + if isinstance(other, TaskInfo): + return self.id == other.id + + return NotImplemented + + def __hash__(self) -> int: + return hash(self.id) + + def __repr__(self) -> str: + return f"{self.__class__.__name__}(id={self.id!r}, name={self.name!r})" + + def has_pending_cancellation(self) -> bool: + """ + Return ``True`` if the task has a cancellation pending, ``False`` otherwise. + + """ + return False + + +def get_current_task() -> TaskInfo: + """ + Return the current task. + + :return: a representation of the current task + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().get_current_task() + + +def get_running_tasks() -> list[TaskInfo]: + """ + Return a list of running tasks in the current event loop. + + :return: a list of task info objects + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return cast("list[TaskInfo]", get_async_backend().get_running_tasks()) + + +async def wait_all_tasks_blocked() -> None: + """Wait until all other tasks are waiting for something.""" + await get_async_backend().wait_all_tasks_blocked() diff --git a/venv/lib/python3.11/site-packages/anyio/_core/_typedattr.py b/venv/lib/python3.11/site-packages/anyio/_core/_typedattr.py new file mode 100644 index 0000000..f358a44 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/_core/_typedattr.py @@ -0,0 +1,81 @@ +from __future__ import annotations + +from collections.abc import Callable, Mapping +from typing import Any, TypeVar, final, overload + +from ._exceptions import TypedAttributeLookupError + +T_Attr = TypeVar("T_Attr") +T_Default = TypeVar("T_Default") +undefined = object() + + +def typed_attribute() -> Any: + """Return a unique object, used to mark typed attributes.""" + return object() + + +class TypedAttributeSet: + """ + Superclass for typed attribute collections. + + Checks that every public attribute of every subclass has a type annotation. + """ + + def __init_subclass__(cls) -> None: + annotations: dict[str, Any] = getattr(cls, "__annotations__", {}) + for attrname in dir(cls): + if not attrname.startswith("_") and attrname not in annotations: + raise TypeError( + f"Attribute {attrname!r} is missing its type annotation" + ) + + super().__init_subclass__() + + +class TypedAttributeProvider: + """Base class for classes that wish to provide typed extra attributes.""" + + @property + def extra_attributes(self) -> Mapping[T_Attr, Callable[[], T_Attr]]: + """ + A mapping of the extra attributes to callables that return the corresponding + values. + + If the provider wraps another provider, the attributes from that wrapper should + also be included in the returned mapping (but the wrapper may override the + callables from the wrapped instance). + + """ + return {} + + @overload + def extra(self, attribute: T_Attr) -> T_Attr: ... + + @overload + def extra(self, attribute: T_Attr, default: T_Default) -> T_Attr | T_Default: ... + + @final + def extra(self, attribute: Any, default: object = undefined) -> object: + """ + extra(attribute, default=undefined) + + Return the value of the given typed extra attribute. + + :param attribute: the attribute (member of a :class:`~TypedAttributeSet`) to + look for + :param default: the value that should be returned if no value is found for the + attribute + :raises ~anyio.TypedAttributeLookupError: if the search failed and no default + value was given + + """ + try: + getter = self.extra_attributes[attribute] + except KeyError: + if default is undefined: + raise TypedAttributeLookupError("Attribute not found") from None + else: + return default + + return getter() diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__init__.py b/venv/lib/python3.11/site-packages/anyio/abc/__init__.py new file mode 100644 index 0000000..d560ce3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/abc/__init__.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from ._eventloop import AsyncBackend as AsyncBackend +from ._resources import AsyncResource as AsyncResource +from ._sockets import ConnectedUDPSocket as ConnectedUDPSocket +from ._sockets import ConnectedUNIXDatagramSocket as ConnectedUNIXDatagramSocket +from ._sockets import IPAddressType as IPAddressType +from ._sockets import IPSockAddrType as IPSockAddrType +from ._sockets import SocketAttribute as SocketAttribute +from ._sockets import SocketListener as SocketListener +from ._sockets import SocketStream as SocketStream +from ._sockets import UDPPacketType as UDPPacketType +from ._sockets import UDPSocket as UDPSocket +from ._sockets import UNIXDatagramPacketType as UNIXDatagramPacketType +from ._sockets import UNIXDatagramSocket as UNIXDatagramSocket +from ._sockets import UNIXSocketStream as UNIXSocketStream +from ._streams import AnyByteReceiveStream as AnyByteReceiveStream +from ._streams import AnyByteSendStream as AnyByteSendStream +from ._streams import AnyByteStream as AnyByteStream +from ._streams import AnyByteStreamConnectable as AnyByteStreamConnectable +from ._streams import AnyUnreliableByteReceiveStream as AnyUnreliableByteReceiveStream +from ._streams import AnyUnreliableByteSendStream as AnyUnreliableByteSendStream +from ._streams import AnyUnreliableByteStream as AnyUnreliableByteStream +from ._streams import ByteReceiveStream as ByteReceiveStream +from ._streams import ByteSendStream as ByteSendStream +from ._streams import ByteStream as ByteStream +from ._streams import ByteStreamConnectable as ByteStreamConnectable +from ._streams import Listener as Listener +from ._streams import ObjectReceiveStream as ObjectReceiveStream +from ._streams import ObjectSendStream as ObjectSendStream +from ._streams import ObjectStream as ObjectStream +from ._streams import ObjectStreamConnectable as ObjectStreamConnectable +from ._streams import UnreliableObjectReceiveStream as UnreliableObjectReceiveStream +from ._streams import UnreliableObjectSendStream as UnreliableObjectSendStream +from ._streams import UnreliableObjectStream as UnreliableObjectStream +from ._subprocesses import Process as Process +from ._tasks import TaskGroup as TaskGroup +from ._tasks import TaskStatus as TaskStatus +from ._testing import TestRunner as TestRunner + +# Re-exported here, for backwards compatibility +# isort: off +from .._core._synchronization import ( + CapacityLimiter as CapacityLimiter, + Condition as Condition, + Event as Event, + Lock as Lock, + Semaphore as Semaphore, +) +from .._core._tasks import CancelScope as CancelScope +from ..from_thread import BlockingPortal as BlockingPortal + +# Re-export imports so they look like they live directly in this package +for __value in list(locals().values()): + if getattr(__value, "__module__", "").startswith("anyio.abc."): + __value.__module__ = __name__ + +del __value diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..daa7f5f Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_eventloop.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_eventloop.cpython-311.pyc new file mode 100644 index 0000000..47dceed Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_eventloop.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_resources.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_resources.cpython-311.pyc new file mode 100644 index 0000000..5de8ab6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_resources.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_sockets.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_sockets.cpython-311.pyc new file mode 100644 index 0000000..069e32c Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_sockets.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_streams.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_streams.cpython-311.pyc new file mode 100644 index 0000000..9b909b3 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_streams.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-311.pyc new file mode 100644 index 0000000..0ad2bc8 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_subprocesses.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_tasks.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_tasks.cpython-311.pyc new file mode 100644 index 0000000..c9ab7e5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_tasks.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_testing.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_testing.cpython-311.pyc new file mode 100644 index 0000000..6ddb581 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/abc/__pycache__/_testing.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_eventloop.py b/venv/lib/python3.11/site-packages/anyio/abc/_eventloop.py new file mode 100644 index 0000000..cad3fa7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/abc/_eventloop.py @@ -0,0 +1,410 @@ +from __future__ import annotations + +import math +import sys +from abc import ABCMeta, abstractmethod +from collections.abc import AsyncIterator, Awaitable, Callable, Coroutine, Sequence +from contextlib import AbstractContextManager +from os import PathLike +from signal import Signals +from socket import AddressFamily, SocketKind, socket +from typing import ( + IO, + TYPE_CHECKING, + Any, + TypeAlias, + TypeVar, + overload, +) + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if TYPE_CHECKING: + from _typeshed import FileDescriptorLike + + from .._core._synchronization import CapacityLimiter, Event, Lock, Semaphore + from .._core._tasks import CancelScope + from .._core._testing import TaskInfo + from ._sockets import ( + ConnectedUDPSocket, + ConnectedUNIXDatagramSocket, + IPSockAddrType, + SocketListener, + SocketStream, + UDPSocket, + UNIXDatagramSocket, + UNIXSocketStream, + ) + from ._subprocesses import Process + from ._tasks import TaskGroup + from ._testing import TestRunner + +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +PosArgsT = TypeVarTuple("PosArgsT") +StrOrBytesPath: TypeAlias = str | bytes | PathLike[str] | PathLike[bytes] + + +class AsyncBackend(metaclass=ABCMeta): + @classmethod + @abstractmethod + def run( + cls, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + options: dict[str, Any], + ) -> T_Retval: + """ + Run the given coroutine function in an asynchronous event loop. + + The current thread must not be already running an event loop. + + :param func: a coroutine function + :param args: positional arguments to ``func`` + :param kwargs: positional arguments to ``func`` + :param options: keyword arguments to call the backend ``run()`` implementation + with + :return: the return value of the coroutine function + """ + + @classmethod + @abstractmethod + def current_token(cls) -> object: + """ + Return an object that allows other threads to run code inside the event loop. + + :return: a token object, specific to the event loop running in the current + thread + """ + + @classmethod + @abstractmethod + def current_time(cls) -> float: + """ + Return the current value of the event loop's internal clock. + + :return: the clock value (seconds) + """ + + @classmethod + @abstractmethod + def cancelled_exception_class(cls) -> type[BaseException]: + """Return the exception class that is raised in a task if it's cancelled.""" + + @classmethod + @abstractmethod + async def checkpoint(cls) -> None: + """ + Check if the task has been cancelled, and allow rescheduling of other tasks. + + This is effectively the same as running :meth:`checkpoint_if_cancelled` and then + :meth:`cancel_shielded_checkpoint`. + """ + + @classmethod + async def checkpoint_if_cancelled(cls) -> None: + """ + Check if the current task group has been cancelled. + + This will check if the task has been cancelled, but will not allow other tasks + to be scheduled if not. + + """ + if cls.current_effective_deadline() == -math.inf: + await cls.checkpoint() + + @classmethod + async def cancel_shielded_checkpoint(cls) -> None: + """ + Allow the rescheduling of other tasks. + + This will give other tasks the opportunity to run, but without checking if the + current task group has been cancelled, unlike with :meth:`checkpoint`. + + """ + with cls.create_cancel_scope(shield=True): + await cls.sleep(0) + + @classmethod + @abstractmethod + async def sleep(cls, delay: float) -> None: + """ + Pause the current task for the specified duration. + + :param delay: the duration, in seconds + """ + + @classmethod + @abstractmethod + def create_cancel_scope( + cls, *, deadline: float = math.inf, shield: bool = False + ) -> CancelScope: + pass + + @classmethod + @abstractmethod + def current_effective_deadline(cls) -> float: + """ + Return the nearest deadline among all the cancel scopes effective for the + current task. + + :return: + - a clock value from the event loop's internal clock + - ``inf`` if there is no deadline in effect + - ``-inf`` if the current scope has been cancelled + :rtype: float + """ + + @classmethod + @abstractmethod + def create_task_group(cls) -> TaskGroup: + pass + + @classmethod + @abstractmethod + def create_event(cls) -> Event: + pass + + @classmethod + @abstractmethod + def create_lock(cls, *, fast_acquire: bool) -> Lock: + pass + + @classmethod + @abstractmethod + def create_semaphore( + cls, + initial_value: int, + *, + max_value: int | None = None, + fast_acquire: bool = False, + ) -> Semaphore: + pass + + @classmethod + @abstractmethod + def create_capacity_limiter(cls, total_tokens: float) -> CapacityLimiter: + pass + + @classmethod + @abstractmethod + async def run_sync_in_worker_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + abandon_on_cancel: bool = False, + limiter: CapacityLimiter | None = None, + ) -> T_Retval: + pass + + @classmethod + @abstractmethod + def check_cancelled(cls) -> None: + pass + + @classmethod + @abstractmethod + def run_async_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_co: + pass + + @classmethod + @abstractmethod + def run_sync_from_thread( + cls, + func: Callable[[Unpack[PosArgsT]], T_Retval], + args: tuple[Unpack[PosArgsT]], + token: object, + ) -> T_Retval: + pass + + @classmethod + @abstractmethod + async def open_process( + cls, + command: StrOrBytesPath | Sequence[StrOrBytesPath], + *, + stdin: int | IO[Any] | None, + stdout: int | IO[Any] | None, + stderr: int | IO[Any] | None, + **kwargs: Any, + ) -> Process: + pass + + @classmethod + @abstractmethod + def setup_process_pool_exit_at_shutdown(cls, workers: set[Process]) -> None: + pass + + @classmethod + @abstractmethod + async def connect_tcp( + cls, host: str, port: int, local_address: IPSockAddrType | None = None + ) -> SocketStream: + pass + + @classmethod + @abstractmethod + async def connect_unix(cls, path: str | bytes) -> UNIXSocketStream: + pass + + @classmethod + @abstractmethod + def create_tcp_listener(cls, sock: socket) -> SocketListener: + pass + + @classmethod + @abstractmethod + def create_unix_listener(cls, sock: socket) -> SocketListener: + pass + + @classmethod + @abstractmethod + async def create_udp_socket( + cls, + family: AddressFamily, + local_address: IPSockAddrType | None, + remote_address: IPSockAddrType | None, + reuse_port: bool, + ) -> UDPSocket | ConnectedUDPSocket: + pass + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket, remote_path: None + ) -> UNIXDatagramSocket: ... + + @classmethod + @overload + async def create_unix_datagram_socket( + cls, raw_socket: socket, remote_path: str | bytes + ) -> ConnectedUNIXDatagramSocket: ... + + @classmethod + @abstractmethod + async def create_unix_datagram_socket( + cls, raw_socket: socket, remote_path: str | bytes | None + ) -> UNIXDatagramSocket | ConnectedUNIXDatagramSocket: + pass + + @classmethod + @abstractmethod + async def getaddrinfo( + cls, + host: bytes | str | None, + port: str | int | None, + *, + family: int | AddressFamily = 0, + type: int | SocketKind = 0, + proto: int = 0, + flags: int = 0, + ) -> Sequence[ + tuple[ + AddressFamily, + SocketKind, + int, + str, + tuple[str, int] | tuple[str, int, int, int] | tuple[int, bytes], + ] + ]: + pass + + @classmethod + @abstractmethod + async def getnameinfo( + cls, sockaddr: IPSockAddrType, flags: int = 0 + ) -> tuple[str, str]: + pass + + @classmethod + @abstractmethod + async def wait_readable(cls, obj: FileDescriptorLike) -> None: + pass + + @classmethod + @abstractmethod + async def wait_writable(cls, obj: FileDescriptorLike) -> None: + pass + + @classmethod + @abstractmethod + def notify_closing(cls, obj: FileDescriptorLike) -> None: + pass + + @classmethod + @abstractmethod + async def wrap_listener_socket(cls, sock: socket) -> SocketListener: + pass + + @classmethod + @abstractmethod + async def wrap_stream_socket(cls, sock: socket) -> SocketStream: + pass + + @classmethod + @abstractmethod + async def wrap_unix_stream_socket(cls, sock: socket) -> UNIXSocketStream: + pass + + @classmethod + @abstractmethod + async def wrap_udp_socket(cls, sock: socket) -> UDPSocket: + pass + + @classmethod + @abstractmethod + async def wrap_connected_udp_socket(cls, sock: socket) -> ConnectedUDPSocket: + pass + + @classmethod + @abstractmethod + async def wrap_unix_datagram_socket(cls, sock: socket) -> UNIXDatagramSocket: + pass + + @classmethod + @abstractmethod + async def wrap_connected_unix_datagram_socket( + cls, sock: socket + ) -> ConnectedUNIXDatagramSocket: + pass + + @classmethod + @abstractmethod + def current_default_thread_limiter(cls) -> CapacityLimiter: + pass + + @classmethod + @abstractmethod + def open_signal_receiver( + cls, *signals: Signals + ) -> AbstractContextManager[AsyncIterator[Signals]]: + pass + + @classmethod + @abstractmethod + def get_current_task(cls) -> TaskInfo: + pass + + @classmethod + @abstractmethod + def get_running_tasks(cls) -> Sequence[TaskInfo]: + pass + + @classmethod + @abstractmethod + async def wait_all_tasks_blocked(cls) -> None: + pass + + @classmethod + @abstractmethod + def create_test_runner(cls, options: dict[str, Any]) -> TestRunner: + pass diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_resources.py b/venv/lib/python3.11/site-packages/anyio/abc/_resources.py new file mode 100644 index 0000000..10df115 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/abc/_resources.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from types import TracebackType +from typing import TypeVar + +T = TypeVar("T") + + +class AsyncResource(metaclass=ABCMeta): + """ + Abstract base class for all closeable asynchronous resources. + + Works as an asynchronous context manager which returns the instance itself on enter, + and calls :meth:`aclose` on exit. + """ + + __slots__ = () + + async def __aenter__(self: T) -> T: + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + await self.aclose() + + @abstractmethod + async def aclose(self) -> None: + """Close the resource.""" diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_sockets.py b/venv/lib/python3.11/site-packages/anyio/abc/_sockets.py new file mode 100644 index 0000000..feb26bd --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/abc/_sockets.py @@ -0,0 +1,399 @@ +from __future__ import annotations + +import errno +import socket +from abc import abstractmethod +from collections.abc import Callable, Collection, Mapping +from contextlib import AsyncExitStack +from io import IOBase +from ipaddress import IPv4Address, IPv6Address +from socket import AddressFamily +from typing import Any, TypeAlias, TypeVar + +from .._core._eventloop import get_async_backend +from .._core._typedattr import ( + TypedAttributeProvider, + TypedAttributeSet, + typed_attribute, +) +from ._streams import ByteStream, Listener, UnreliableObjectStream +from ._tasks import TaskGroup + +IPAddressType: TypeAlias = str | IPv4Address | IPv6Address +IPSockAddrType: TypeAlias = tuple[str, int] +SockAddrType: TypeAlias = IPSockAddrType | str +UDPPacketType: TypeAlias = tuple[bytes, IPSockAddrType] +UNIXDatagramPacketType: TypeAlias = tuple[bytes, str] +T_Retval = TypeVar("T_Retval") + + +def _validate_socket( + sock_or_fd: socket.socket | int, + sock_type: socket.SocketKind, + addr_family: socket.AddressFamily = socket.AF_UNSPEC, + *, + require_connected: bool = False, + require_bound: bool = False, +) -> socket.socket: + if isinstance(sock_or_fd, int): + try: + sock = socket.socket(fileno=sock_or_fd) + except OSError as exc: + if exc.errno == errno.ENOTSOCK: + raise ValueError( + "the file descriptor does not refer to a socket" + ) from exc + elif require_connected: + raise ValueError("the socket must be connected") from exc + elif require_bound: + raise ValueError("the socket must be bound to a local address") from exc + else: + raise + elif isinstance(sock_or_fd, socket.socket): + sock = sock_or_fd + else: + raise TypeError( + f"expected an int or socket, got {type(sock_or_fd).__qualname__} instead" + ) + + try: + if require_connected: + try: + sock.getpeername() + except OSError as exc: + raise ValueError("the socket must be connected") from exc + + if require_bound: + try: + if sock.family in (socket.AF_INET, socket.AF_INET6): + bound_addr = sock.getsockname()[1] + else: + bound_addr = sock.getsockname() + except OSError: + bound_addr = None + + if not bound_addr: + raise ValueError("the socket must be bound to a local address") + + if addr_family != socket.AF_UNSPEC and sock.family != addr_family: + raise ValueError( + f"address family mismatch: expected {addr_family.name}, got " + f"{sock.family.name}" + ) + + if sock.type != sock_type: + raise ValueError( + f"socket type mismatch: expected {sock_type.name}, got {sock.type.name}" + ) + except BaseException: + # Avoid ResourceWarning from the locally constructed socket object + if isinstance(sock_or_fd, int): + sock.detach() + + raise + + sock.setblocking(False) + return sock + + +class SocketAttribute(TypedAttributeSet): + """ + .. attribute:: family + :type: socket.AddressFamily + + the address family of the underlying socket + + .. attribute:: local_address + :type: tuple[str, int] | str + + the local address the underlying socket is connected to + + .. attribute:: local_port + :type: int + + for IP based sockets, the local port the underlying socket is bound to + + .. attribute:: raw_socket + :type: socket.socket + + the underlying stdlib socket object + + .. attribute:: remote_address + :type: tuple[str, int] | str + + the remote address the underlying socket is connected to + + .. attribute:: remote_port + :type: int + + for IP based sockets, the remote port the underlying socket is connected to + """ + + family: AddressFamily = typed_attribute() + local_address: SockAddrType = typed_attribute() + local_port: int = typed_attribute() + raw_socket: socket.socket = typed_attribute() + remote_address: SockAddrType = typed_attribute() + remote_port: int = typed_attribute() + + +class _SocketProvider(TypedAttributeProvider): + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + from .._core._sockets import convert_ipv6_sockaddr as convert + + attributes: dict[Any, Callable[[], Any]] = { + SocketAttribute.family: lambda: self._raw_socket.family, + SocketAttribute.local_address: lambda: convert( + self._raw_socket.getsockname() + ), + SocketAttribute.raw_socket: lambda: self._raw_socket, + } + try: + peername: tuple[str, int] | None = convert(self._raw_socket.getpeername()) + except OSError: + peername = None + + # Provide the remote address for connected sockets + if peername is not None: + attributes[SocketAttribute.remote_address] = lambda: peername + + # Provide local and remote ports for IP based sockets + if self._raw_socket.family in (AddressFamily.AF_INET, AddressFamily.AF_INET6): + attributes[SocketAttribute.local_port] = lambda: ( + self._raw_socket.getsockname()[1] + ) + if peername is not None: + remote_port = peername[1] + attributes[SocketAttribute.remote_port] = lambda: remote_port + + return attributes + + @property + @abstractmethod + def _raw_socket(self) -> socket.socket: + pass + + +class SocketStream(ByteStream, _SocketProvider): + """ + Transports bytes over a socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> SocketStream: + """ + Wrap an existing socket object or file descriptor as a socket stream. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a socket stream + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_connected=True) + return await get_async_backend().wrap_stream_socket(sock) + + +class UNIXSocketStream(SocketStream): + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> UNIXSocketStream: + """ + Wrap an existing socket object or file descriptor as a UNIX socket stream. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a UNIX socket stream + + """ + sock = _validate_socket( + sock_or_fd, socket.SOCK_STREAM, socket.AF_UNIX, require_connected=True + ) + return await get_async_backend().wrap_unix_stream_socket(sock) + + @abstractmethod + async def send_fds(self, message: bytes, fds: Collection[int | IOBase]) -> None: + """ + Send file descriptors along with a message to the peer. + + :param message: a non-empty bytestring + :param fds: a collection of files (either numeric file descriptors or open file + or socket objects) + """ + + @abstractmethod + async def receive_fds(self, msglen: int, maxfds: int) -> tuple[bytes, list[int]]: + """ + Receive file descriptors along with a message from the peer. + + :param msglen: length of the message to expect from the peer + :param maxfds: maximum number of file descriptors to expect from the peer + :return: a tuple of (message, file descriptors) + """ + + +class SocketListener(Listener[SocketStream], _SocketProvider): + """ + Listens to incoming socket connections. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket( + cls, + sock_or_fd: socket.socket | int, + ) -> SocketListener: + """ + Wrap an existing socket object or file descriptor as a socket listener. + + The newly created listener takes ownership of the socket being passed in. + + :param sock_or_fd: a socket object or file descriptor + :return: a socket listener + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_STREAM, require_bound=True) + return await get_async_backend().wrap_listener_socket(sock) + + @abstractmethod + async def accept(self) -> SocketStream: + """Accept an incoming connection.""" + + async def serve( + self, + handler: Callable[[SocketStream], Any], + task_group: TaskGroup | None = None, + ) -> None: + from .. import create_task_group + + async with AsyncExitStack() as stack: + if task_group is None: + task_group = await stack.enter_async_context(create_task_group()) + + while True: + stream = await self.accept() + task_group.start_soon(handler, stream) + + +class UDPSocket(UnreliableObjectStream[UDPPacketType], _SocketProvider): + """ + Represents an unconnected UDP socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> UDPSocket: + """ + Wrap an existing socket object or file descriptor as a UDP socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must be bound to a local address. + + :param sock_or_fd: a socket object or file descriptor + :return: a UDP socket + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, require_bound=True) + return await get_async_backend().wrap_udp_socket(sock) + + async def sendto(self, data: bytes, host: str, port: int) -> None: + """ + Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, (host, port))). + + """ + return await self.send((data, (host, port))) + + +class ConnectedUDPSocket(UnreliableObjectStream[bytes], _SocketProvider): + """ + Represents an connected UDP socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket(cls, sock_or_fd: socket.socket | int) -> ConnectedUDPSocket: + """ + Wrap an existing socket object or file descriptor as a connected UDP socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a connected UDP socket + + """ + sock = _validate_socket( + sock_or_fd, + socket.SOCK_DGRAM, + require_connected=True, + ) + return await get_async_backend().wrap_connected_udp_socket(sock) + + +class UNIXDatagramSocket( + UnreliableObjectStream[UNIXDatagramPacketType], _SocketProvider +): + """ + Represents an unconnected Unix datagram socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket( + cls, + sock_or_fd: socket.socket | int, + ) -> UNIXDatagramSocket: + """ + Wrap an existing socket object or file descriptor as a UNIX datagram + socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + + :param sock_or_fd: a socket object or file descriptor + :return: a UNIX datagram socket + + """ + sock = _validate_socket(sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX) + return await get_async_backend().wrap_unix_datagram_socket(sock) + + async def sendto(self, data: bytes, path: str) -> None: + """Alias for :meth:`~.UnreliableObjectSendStream.send` ((data, path)).""" + return await self.send((data, path)) + + +class ConnectedUNIXDatagramSocket(UnreliableObjectStream[bytes], _SocketProvider): + """ + Represents a connected Unix datagram socket. + + Supports all relevant extra attributes from :class:`~SocketAttribute`. + """ + + @classmethod + async def from_socket( + cls, + sock_or_fd: socket.socket | int, + ) -> ConnectedUNIXDatagramSocket: + """ + Wrap an existing socket object or file descriptor as a connected UNIX datagram + socket. + + The newly created socket wrapper takes ownership of the socket being passed in. + The existing socket must already be connected. + + :param sock_or_fd: a socket object or file descriptor + :return: a connected UNIX datagram socket + + """ + sock = _validate_socket( + sock_or_fd, socket.SOCK_DGRAM, socket.AF_UNIX, require_connected=True + ) + return await get_async_backend().wrap_connected_unix_datagram_socket(sock) diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_streams.py b/venv/lib/python3.11/site-packages/anyio/abc/_streams.py new file mode 100644 index 0000000..186e3f5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/abc/_streams.py @@ -0,0 +1,233 @@ +from __future__ import annotations + +from abc import ABCMeta, abstractmethod +from collections.abc import Callable +from typing import Any, Generic, TypeAlias, TypeVar + +from .._core._exceptions import EndOfStream +from .._core._typedattr import TypedAttributeProvider +from ._resources import AsyncResource +from ._tasks import TaskGroup + +T_Item = TypeVar("T_Item") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) + + +class UnreliableObjectReceiveStream( + Generic[T_co], AsyncResource, TypedAttributeProvider +): + """ + An interface for receiving objects. + + This interface makes no guarantees that the received messages arrive in the order in + which they were sent, or that no messages are missed. + + Asynchronously iterating over objects of this type will yield objects matching the + given type parameter. + """ + + def __aiter__(self) -> UnreliableObjectReceiveStream[T_co]: + return self + + async def __anext__(self) -> T_co: + try: + return await self.receive() + except EndOfStream: + raise StopAsyncIteration from None + + @abstractmethod + async def receive(self) -> T_co: + """ + Receive the next item. + + :raises ~anyio.ClosedResourceError: if the receive stream has been explicitly + closed + :raises ~anyio.EndOfStream: if this stream has been closed from the other end + :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable + due to external causes + """ + + +class UnreliableObjectSendStream( + Generic[T_contra], AsyncResource, TypedAttributeProvider +): + """ + An interface for sending objects. + + This interface makes no guarantees that the messages sent will reach the + recipient(s) in the same order in which they were sent, or at all. + """ + + @abstractmethod + async def send(self, item: T_contra) -> None: + """ + Send an item to the peer(s). + + :param item: the item to send + :raises ~anyio.ClosedResourceError: if the send stream has been explicitly + closed + :raises ~anyio.BrokenResourceError: if this stream has been rendered unusable + due to external causes + """ + + +class UnreliableObjectStream( + UnreliableObjectReceiveStream[T_Item], UnreliableObjectSendStream[T_Item] +): + """ + A bidirectional message stream which does not guarantee the order or reliability of + message delivery. + """ + + +class ObjectReceiveStream(UnreliableObjectReceiveStream[T_co]): + """ + A receive message stream which guarantees that messages are received in the same + order in which they were sent, and that no messages are missed. + """ + + +class ObjectSendStream(UnreliableObjectSendStream[T_contra]): + """ + A send message stream which guarantees that messages are delivered in the same order + in which they were sent, without missing any messages in the middle. + """ + + +class ObjectStream( + ObjectReceiveStream[T_Item], + ObjectSendStream[T_Item], + UnreliableObjectStream[T_Item], +): + """ + A bidirectional message stream which guarantees the order and reliability of message + delivery. + """ + + @abstractmethod + async def send_eof(self) -> None: + """ + Send an end-of-file indication to the peer. + + You should not try to send any further data to this stream after calling this + method. This method is idempotent (does nothing on successive calls). + """ + + +class ByteReceiveStream(AsyncResource, TypedAttributeProvider): + """ + An interface for receiving bytes from a single peer. + + Iterating this byte stream will yield a byte string of arbitrary length, but no more + than 65536 bytes. + """ + + def __aiter__(self) -> ByteReceiveStream: + return self + + async def __anext__(self) -> bytes: + try: + return await self.receive() + except EndOfStream: + raise StopAsyncIteration from None + + @abstractmethod + async def receive(self, max_bytes: int = 65536) -> bytes: + """ + Receive at most ``max_bytes`` bytes from the peer. + + .. note:: Implementers of this interface should not return an empty + :class:`bytes` object, and users should ignore them. + + :param max_bytes: maximum number of bytes to receive + :return: the received bytes + :raises ~anyio.EndOfStream: if this stream has been closed from the other end + """ + + +class ByteSendStream(AsyncResource, TypedAttributeProvider): + """An interface for sending bytes to a single peer.""" + + @abstractmethod + async def send(self, item: bytes) -> None: + """ + Send the given bytes to the peer. + + :param item: the bytes to send + """ + + +class ByteStream(ByteReceiveStream, ByteSendStream): + """A bidirectional byte stream.""" + + @abstractmethod + async def send_eof(self) -> None: + """ + Send an end-of-file indication to the peer. + + You should not try to send any further data to this stream after calling this + method. This method is idempotent (does nothing on successive calls). + """ + + +#: Type alias for all unreliable bytes-oriented receive streams. +AnyUnreliableByteReceiveStream: TypeAlias = ( + UnreliableObjectReceiveStream[bytes] | ByteReceiveStream +) +#: Type alias for all unreliable bytes-oriented send streams. +AnyUnreliableByteSendStream: TypeAlias = ( + UnreliableObjectSendStream[bytes] | ByteSendStream +) +#: Type alias for all unreliable bytes-oriented streams. +AnyUnreliableByteStream: TypeAlias = UnreliableObjectStream[bytes] | ByteStream +#: Type alias for all bytes-oriented receive streams. +AnyByteReceiveStream: TypeAlias = ObjectReceiveStream[bytes] | ByteReceiveStream +#: Type alias for all bytes-oriented send streams. +AnyByteSendStream: TypeAlias = ObjectSendStream[bytes] | ByteSendStream +#: Type alias for all bytes-oriented streams. +AnyByteStream: TypeAlias = ObjectStream[bytes] | ByteStream + + +class Listener(Generic[T_co], AsyncResource, TypedAttributeProvider): + """An interface for objects that let you accept incoming connections.""" + + @abstractmethod + async def serve( + self, handler: Callable[[T_co], Any], task_group: TaskGroup | None = None + ) -> None: + """ + Accept incoming connections as they come in and start tasks to handle them. + + :param handler: a callable that will be used to handle each accepted connection + :param task_group: the task group that will be used to start tasks for handling + each accepted connection (if omitted, an ad-hoc task group will be created) + """ + + +class ObjectStreamConnectable(Generic[T_co], metaclass=ABCMeta): + @abstractmethod + async def connect(self) -> ObjectStream[T_co]: + """ + Connect to the remote endpoint. + + :return: an object stream connected to the remote end + :raises ConnectionFailed: if the connection fails + """ + + +class ByteStreamConnectable(metaclass=ABCMeta): + @abstractmethod + async def connect(self) -> ByteStream: + """ + Connect to the remote endpoint. + + :return: a bytestream connected to the remote end + :raises ConnectionFailed: if the connection fails + """ + + +#: Type alias for all connectables returning bytestreams or bytes-oriented object streams +AnyByteStreamConnectable: TypeAlias = ( + ObjectStreamConnectable[bytes] | ByteStreamConnectable +) diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_subprocesses.py b/venv/lib/python3.11/site-packages/anyio/abc/_subprocesses.py new file mode 100644 index 0000000..ce0564c --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/abc/_subprocesses.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +from abc import abstractmethod +from signal import Signals + +from ._resources import AsyncResource +from ._streams import ByteReceiveStream, ByteSendStream + + +class Process(AsyncResource): + """An asynchronous version of :class:`subprocess.Popen`.""" + + @abstractmethod + async def wait(self) -> int: + """ + Wait until the process exits. + + :return: the exit code of the process + """ + + @abstractmethod + def terminate(self) -> None: + """ + Terminates the process, gracefully if possible. + + On Windows, this calls ``TerminateProcess()``. + On POSIX systems, this sends ``SIGTERM`` to the process. + + .. seealso:: :meth:`subprocess.Popen.terminate` + """ + + @abstractmethod + def kill(self) -> None: + """ + Kills the process. + + On Windows, this calls ``TerminateProcess()``. + On POSIX systems, this sends ``SIGKILL`` to the process. + + .. seealso:: :meth:`subprocess.Popen.kill` + """ + + @abstractmethod + def send_signal(self, signal: Signals) -> None: + """ + Send a signal to the subprocess. + + .. seealso:: :meth:`subprocess.Popen.send_signal` + + :param signal: the signal number (e.g. :data:`signal.SIGHUP`) + """ + + @property + @abstractmethod + def pid(self) -> int: + """The process ID of the process.""" + + @property + @abstractmethod + def returncode(self) -> int | None: + """ + The return code of the process. If the process has not yet terminated, this will + be ``None``. + """ + + @property + @abstractmethod + def stdin(self) -> ByteSendStream | None: + """The stream for the standard input of the process.""" + + @property + @abstractmethod + def stdout(self) -> ByteReceiveStream | None: + """The stream for the standard output of the process.""" + + @property + @abstractmethod + def stderr(self) -> ByteReceiveStream | None: + """The stream for the standard error output of the process.""" diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_tasks.py b/venv/lib/python3.11/site-packages/anyio/abc/_tasks.py new file mode 100644 index 0000000..44ee3a7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/abc/_tasks.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import sys +from abc import ABCMeta, abstractmethod +from collections.abc import Callable, Coroutine +from contextvars import Context +from types import TracebackType +from typing import TYPE_CHECKING, Any, Literal, Protocol, final, overload + +if sys.version_info >= (3, 13): + from typing import TypeVar +else: + from typing_extensions import TypeVar + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if TYPE_CHECKING: + from .._core._tasks import CancelScope, TaskHandle + +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True, default=None) +PosArgsT = TypeVarTuple("PosArgsT") + + +def get_callable_name(func: Callable, override: object = None) -> str: + if override is not None: + return str(override) + + module = getattr(func, "__module__", None) + qualname = getattr(func, "__qualname__", None) + return ".".join([x for x in (module, qualname) if x]) + + +def call_for_coroutine( + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + args: tuple[Unpack[PosArgsT]], + **kwargs: Any, +) -> Coroutine[Any, Any, T_co]: + """ + Call the given function with the given positional and keyword arguments. + + :return: the resulting coroutine + :raises TypeError: if the return value was not a coroutine object + + """ + coro = func(*args, **kwargs) + if not isinstance(coro, Coroutine): + prefix = f"{func.__module__}." if hasattr(func, "__module__") else "" + raise TypeError( + f"Expected {prefix}{func.__qualname__}() to return a coroutine, but " + f"the return value ({coro!r}) is not a coroutine object" + ) + + return coro + + +class TaskStatus(Protocol[T_contra]): + @overload + def started(self: TaskStatus[None]) -> None: ... + + @overload + def started(self, value: T_contra) -> None: ... + + def started(self, value: T_contra | None = None) -> None: + """ + Signal that the task has started. + + :param value: object passed back to the starter of the task + """ + + +class TaskGroup(metaclass=ABCMeta): + """ + Groups several asynchronous tasks together. + + :ivar cancel_scope: the cancel scope inherited by all child tasks + :vartype cancel_scope: CancelScope + + .. note:: On asyncio, support for eager task factories is considered to be + **experimental**. In particular, they don't follow the usual semantics of new + tasks being scheduled on the next iteration of the event loop, and may thus + cause unexpected behavior in code that wasn't written with such semantics in + mind. + """ + + cancel_scope: CancelScope + + def cancel(self, reason: str | None = None) -> None: + """ + Cancel this task group's cancel scope immediately. + + This is a shortcut for calling ``.cancel_scope.cancel()`` on the task group. + + :param reason: a message describing the reason for the cancellation + + .. versionadded:: 4.14.0 + + """ + self.cancel_scope.cancel(reason) + + @abstractmethod + def create_task( + self, + coro: Coroutine[Any, Any, T_co], + *, + name: object = None, + context: Context | None = None, + ) -> TaskHandle[T_co]: + """ + Create a new task from a coroutine object and schedule it to run. + + :param coro: a coroutine object + :param name: optional name to give the task + :param context: optional context to run the task in + :return: a task handle + + .. versionadded:: 4.14.0 + """ + + @final + def start_soon( + self, + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + name: object = None, + ) -> TaskHandle[T_co]: + """ + Start a new task in this task group. + + :param func: a coroutine function + :param args: positional arguments to call the function with + :param name: name of the task, for the purposes of introspection and debugging + :return: a task handle + + .. versionadded:: 3.0 + .. versionchanged:: 4.14.0 + This method now returns a task handle. + + """ + final_name = get_callable_name(func, name) + return self.create_task(call_for_coroutine(func, args), name=final_name) + + @overload + async def start( + self, + func: Callable[..., Coroutine[Any, Any, T_co]], + *args: object, + name: object = None, + return_handle: Literal[False] = ..., + ) -> Any: ... + + @overload + async def start( + self, + func: Callable[..., Coroutine[Any, Any, T_co]], + *args: object, + name: object = None, + return_handle: Literal[True], + ) -> TaskHandle[T_co, Any]: ... + + @abstractmethod + async def start( + self, + func: Callable[..., Coroutine[Any, Any, T_co]], + *args: object, + name: object = None, + return_handle: Literal[False] | Literal[True] = False, + ) -> Any: + """ + Start a new task and wait until it signals for readiness. + + The target callable must accept a keyword argument ``task_status`` (of type + :class:`TaskStatus`). Awaiting on this method will return whatever was passed to + ``task_status.started()`` (``None`` by default). + + .. note:: The :class:`TaskStatus` class is generic, and the type argument should + indicate the type of the value that will be passed to + ``task_status.started()``. + + :param func: a coroutine function that accepts the ``task_status`` keyword + argument + :param args: positional arguments to call the function with + :param name: an optional name for the task, for introspection and debugging + :param return_handle: if ``True``, return a :class:`TaskHandle` which also + contains the start value in ``start_value`` + :return: the value passed to ``task_status.started()`` + :raises RuntimeError: if the task finishes without calling + ``task_status.started()`` + + .. seealso:: :ref:`start_initialize` + + .. versionadded:: 3.0 + """ + + @abstractmethod + async def __aenter__(self) -> TaskGroup: + """Enter the task group context and allow starting new tasks.""" + + @abstractmethod + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + """Exit the task group context waiting for all tasks to finish.""" diff --git a/venv/lib/python3.11/site-packages/anyio/abc/_testing.py b/venv/lib/python3.11/site-packages/anyio/abc/_testing.py new file mode 100644 index 0000000..2a93fb7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/abc/_testing.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import types +from abc import ABCMeta, abstractmethod +from collections.abc import AsyncGenerator, Callable, Coroutine, Iterable +from typing import Any, TypeVar + +_T = TypeVar("_T") + + +class TestRunner(metaclass=ABCMeta): + """ + Encapsulates a running event loop. Every call made through this object will use the + same event loop. + """ + + def __enter__(self) -> TestRunner: + return self + + @abstractmethod + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: types.TracebackType | None, + ) -> bool | None: ... + + @abstractmethod + def run_asyncgen_fixture( + self, + fixture_func: Callable[..., AsyncGenerator[_T, Any]], + kwargs: dict[str, Any], + ) -> Iterable[_T]: + """ + Run an async generator fixture. + + :param fixture_func: the fixture function + :param kwargs: keyword arguments to call the fixture function with + :return: an iterator yielding the value yielded from the async generator + """ + + @abstractmethod + def run_fixture( + self, + fixture_func: Callable[..., Coroutine[Any, Any, _T]], + kwargs: dict[str, Any], + ) -> _T: + """ + Run an async fixture. + + :param fixture_func: the fixture function + :param kwargs: keyword arguments to call the fixture function with + :return: the return value of the fixture function + """ + + @abstractmethod + def run_test( + self, test_func: Callable[..., Coroutine[Any, Any, Any]], kwargs: dict[str, Any] + ) -> None: + """ + Run an async test function. + + :param test_func: the test function + :param kwargs: keyword arguments to call the test function with + """ + + @abstractmethod + def is_running(self) -> bool: + """ + Check if the test runner is running. + + :return: ``True`` if the coroutine is currently being run, ``False`` otherwise. + """ diff --git a/venv/lib/python3.11/site-packages/anyio/from_thread.py b/venv/lib/python3.11/site-packages/anyio/from_thread.py new file mode 100644 index 0000000..8c7914c --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/from_thread.py @@ -0,0 +1,582 @@ +from __future__ import annotations + +__all__ = ( + "BlockingPortal", + "BlockingPortalProvider", + "check_cancelled", + "run", + "run_sync", + "start_blocking_portal", +) + +import sys +from collections.abc import Awaitable, Callable, Coroutine, Generator +from concurrent.futures import Future +from contextlib import ( + AbstractAsyncContextManager, + AbstractContextManager, + contextmanager, +) +from dataclasses import dataclass, field +from functools import partial +from inspect import isawaitable +from threading import Lock, Thread, current_thread, get_ident +from types import TracebackType +from typing import ( + Any, + Generic, + TypeVar, + cast, + overload, +) + +from ._core._eventloop import ( + get_cancelled_exc_class, + threadlocals, +) +from ._core._eventloop import run as run_eventloop +from ._core._exceptions import NoEventLoopError +from ._core._synchronization import Event +from ._core._tasks import CancelScope, create_task_group +from .abc._tasks import TaskStatus +from .lowlevel import EventLoopToken, current_token + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +T_Retval = TypeVar("T_Retval") +T_co = TypeVar("T_co", covariant=True) +PosArgsT = TypeVarTuple("PosArgsT") + + +def _token_or_error(token: EventLoopToken | None) -> EventLoopToken: + if token is not None: + return token + + try: + return threadlocals.current_token + except AttributeError: + raise NoEventLoopError( + "Not running inside an AnyIO worker thread, and no event loop token was " + "provided" + ) from None + + +def run( + func: Callable[[Unpack[PosArgsT]], Coroutine[Any, Any, T_co]], + *args: Unpack[PosArgsT], + token: EventLoopToken | None = None, +) -> T_co: + """ + Call a coroutine function from a worker thread. + + :param func: a coroutine function + :param args: positional arguments for the callable + :param token: an event loop token to use to get back to the event loop thread + (required if calling this function from outside an AnyIO worker thread) + :return: the return value of the coroutine function + :raises MissingTokenError: if no token was provided and called from outside an + AnyIO worker thread + :raises RunFinishedError: if the event loop tied to ``token`` is no longer running + + .. versionchanged:: 4.11.0 + Added the ``token`` parameter. + + """ + explicit_token = token is not None + token = _token_or_error(token) + return token.backend_class.run_async_from_thread( + func, args, token=token.native_token if explicit_token else None + ) + + +def run_sync( + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + token: EventLoopToken | None = None, +) -> T_Retval: + """ + Call a function in the event loop thread from a worker thread. + + :param func: a callable + :param args: positional arguments for the callable + :param token: an event loop token to use to get back to the event loop thread + (required if calling this function from outside an AnyIO worker thread) + :return: the return value of the callable + :raises MissingTokenError: if no token was provided and called from outside an + AnyIO worker thread + :raises RunFinishedError: if the event loop tied to ``token`` is no longer running + + .. versionchanged:: 4.11.0 + Added the ``token`` parameter. + + """ + explicit_token = token is not None + token = _token_or_error(token) + return token.backend_class.run_sync_from_thread( + func, args, token=token.native_token if explicit_token else None + ) + + +class _BlockingAsyncContextManager(Generic[T_co], AbstractContextManager): + _enter_future: Future[T_co] + _exit_future: Future[bool | None] + _exit_event: Event + _exit_exc_info: tuple[ + type[BaseException] | None, BaseException | None, TracebackType | None + ] = (None, None, None) + + def __init__( + self, async_cm: AbstractAsyncContextManager[T_co], portal: BlockingPortal + ): + self._async_cm = async_cm + self._portal = portal + + async def run_async_cm(self) -> bool | None: + try: + self._exit_event = Event() + value = await self._async_cm.__aenter__() + except BaseException as exc: + self._enter_future.set_exception(exc) + raise + else: + self._enter_future.set_result(value) + + try: + # Wait for the sync context manager to exit. + # This next statement can raise `get_cancelled_exc_class()` if + # something went wrong in a task group in this async context + # manager. + await self._exit_event.wait() + finally: + # In case of cancellation, it could be that we end up here before + # `_BlockingAsyncContextManager.__exit__` is called, and an + # `_exit_exc_info` has been set. + result = await self._async_cm.__aexit__(*self._exit_exc_info) + + return result + + def __enter__(self) -> T_co: + self._enter_future = Future() + self._exit_future = self._portal.start_task_soon(self.run_async_cm) + return self._enter_future.result() + + def __exit__( + self, + __exc_type: type[BaseException] | None, + __exc_value: BaseException | None, + __traceback: TracebackType | None, + ) -> bool | None: + self._exit_exc_info = __exc_type, __exc_value, __traceback + self._portal.call(self._exit_event.set) + return self._exit_future.result() + + +class _BlockingPortalTaskStatus(TaskStatus): + def __init__(self, future: Future): + self._future = future + + def started(self, value: object = None) -> None: + self._future.set_result(value) + + +class BlockingPortal: + """ + An object that lets external threads run code in an asynchronous event loop. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + """ + + def __init__(self) -> None: + self._token = current_token() + self._event_loop_thread_id: int | None = get_ident() + self._stop_event = Event() + self._task_group = create_task_group() + + async def __aenter__(self) -> BlockingPortal: + await self._task_group.__aenter__() + return self + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> bool: + await self.stop() + return await self._task_group.__aexit__(exc_type, exc_val, exc_tb) + + def _check_running(self) -> None: + if self._event_loop_thread_id is None: + raise RuntimeError("This portal is not running") + if self._event_loop_thread_id == get_ident(): + raise RuntimeError( + "This method cannot be called from the event loop thread" + ) + + async def sleep_until_stopped(self) -> None: + """Sleep until :meth:`stop` is called.""" + await self._stop_event.wait() + + async def stop(self, cancel_remaining: bool = False) -> None: + """ + Signal the portal to shut down. + + This marks the portal as no longer accepting new calls and exits from + :meth:`sleep_until_stopped`. + + :param cancel_remaining: ``True`` to cancel all the remaining tasks, ``False`` + to let them finish before returning + + """ + self._event_loop_thread_id = None + self._stop_event.set() + if cancel_remaining: + self._task_group.cancel_scope.cancel("the blocking portal is shutting down") + + async def _call_func( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + future: Future[T_Retval], + ) -> None: + event_loop_thread_id = self._event_loop_thread_id + + def callback(f: Future[T_Retval]) -> None: + if f.cancelled(): + if event_loop_thread_id == get_ident(): + scope.cancel("the future was cancelled") + elif event_loop_thread_id is not None: + run_sync( + scope.cancel, "the future was cancelled", token=self._token + ) + + try: + retval_or_awaitable = func(*args, **kwargs) + if isawaitable(retval_or_awaitable): + with CancelScope() as scope: + future.add_done_callback(callback) + retval = await retval_or_awaitable + else: + retval = retval_or_awaitable + except get_cancelled_exc_class(): + future.cancel() + future.set_running_or_notify_cancel() + except BaseException as exc: + if not future.cancelled(): + future.set_exception(exc) + + # Let base exceptions fall through + if not isinstance(exc, Exception): + raise + else: + if not future.cancelled(): + future.set_result(retval) + finally: + scope = None # type: ignore[assignment] + + def _spawn_task_from_thread( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + args: tuple[Unpack[PosArgsT]], + kwargs: dict[str, Any], + name: object, + future: Future[T_Retval], + ) -> None: + """ + Spawn a new task using the given callable. + + :param func: a callable + :param args: positional arguments to be passed to the callable + :param kwargs: keyword arguments to be passed to the callable + :param name: name of the task (will be coerced to a string if not ``None``) + :param future: a future that will resolve to the return value of the callable, + or the exception raised during its execution + + """ + run_sync( + partial(self._task_group.start_soon, name=name), + self._call_func, + func, + args, + kwargs, + future, + token=self._token, + ) + + @overload + def call( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + *args: Unpack[PosArgsT], + ) -> T_Retval: ... + + @overload + def call( + self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] + ) -> T_Retval: ... + + def call( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + *args: Unpack[PosArgsT], + ) -> T_Retval: + """ + Call the given function in the event loop thread. + + If the callable returns a coroutine object, it is awaited on. + + :param func: any callable + :raises RuntimeError: if the portal is not running or if this method is called + from within the event loop thread + + """ + return cast(T_Retval, self.start_task_soon(func, *args).result()) + + @overload + def start_task_soon( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval]], + *args: Unpack[PosArgsT], + name: object = None, + ) -> Future[T_Retval]: ... + + @overload + def start_task_soon( + self, + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + name: object = None, + ) -> Future[T_Retval]: ... + + def start_task_soon( + self, + func: Callable[[Unpack[PosArgsT]], Awaitable[T_Retval] | T_Retval], + *args: Unpack[PosArgsT], + name: object = None, + ) -> Future[T_Retval]: + """ + Start a task in the portal's task group. + + The task will be run inside a cancel scope which can be cancelled by cancelling + the returned future. + + :param func: the target function + :param args: positional arguments passed to ``func`` + :param name: name of the task (will be coerced to a string if not ``None``) + :return: a future that resolves with the return value of the callable if the + task completes successfully, or with the exception raised in the task + :raises RuntimeError: if the portal is not running or if this method is called + from within the event loop thread + :rtype: concurrent.futures.Future[T_Retval] + + .. versionadded:: 3.0 + + """ + self._check_running() + f: Future[T_Retval] = Future() + self._spawn_task_from_thread(func, args, {}, name, f) + return f + + def start_task( + self, + func: Callable[..., Awaitable[T_Retval]], + *args: object, + name: object = None, + ) -> tuple[Future[T_Retval], Any]: + """ + Start a task in the portal's task group and wait until it signals for readiness. + + This method works the same way as :meth:`.abc.TaskGroup.start`. + + :param func: the target function + :param args: positional arguments passed to ``func`` + :param name: name of the task (will be coerced to a string if not ``None``) + :return: a tuple of (future, task_status_value) where the ``task_status_value`` + is the value passed to ``task_status.started()`` from within the target + function + :rtype: tuple[concurrent.futures.Future[T_Retval], Any] + + .. versionadded:: 3.0 + + """ + + def task_done(future: Future[T_Retval]) -> None: + if not task_status_future.done(): + if future.cancelled(): + task_status_future.cancel() + elif future.exception(): + task_status_future.set_exception(future.exception()) + else: + exc = RuntimeError( + "Task exited without calling task_status.started()" + ) + task_status_future.set_exception(exc) + + self._check_running() + task_status_future: Future = Future() + task_status = _BlockingPortalTaskStatus(task_status_future) + f: Future = Future() + f.add_done_callback(task_done) + self._spawn_task_from_thread(func, args, {"task_status": task_status}, name, f) + return f, task_status_future.result() + + def wrap_async_context_manager( + self, cm: AbstractAsyncContextManager[T_co] + ) -> AbstractContextManager[T_co]: + """ + Wrap an async context manager as a synchronous context manager via this portal. + + Spawns a task that will call both ``__aenter__()`` and ``__aexit__()``, stopping + in the middle until the synchronous context manager exits. + + :param cm: an asynchronous context manager + :return: a synchronous context manager + + .. versionadded:: 2.1 + + """ + return _BlockingAsyncContextManager(cm, self) + + +@dataclass +class BlockingPortalProvider: + """ + A manager for a blocking portal. Used as a context manager. The first thread to + enter this context manager causes a blocking portal to be started with the specific + parameters, and the last thread to exit causes the portal to be shut down. Thus, + there will be exactly one blocking portal running in this context as long as at + least one thread has entered this context manager. + + The parameters are the same as for :func:`~anyio.run`. + + :param backend: name of the backend + :param backend_options: backend options + + .. versionadded:: 4.4 + """ + + backend: str = "asyncio" + backend_options: dict[str, Any] | None = None + _lock: Lock = field(init=False, default_factory=Lock) + _leases: int = field(init=False, default=0) + _portal: BlockingPortal = field(init=False) + _portal_cm: AbstractContextManager[BlockingPortal] | None = field( + init=False, default=None + ) + + def __enter__(self) -> BlockingPortal: + with self._lock: + if self._portal_cm is None: + self._portal_cm = start_blocking_portal( + self.backend, self.backend_options + ) + self._portal = self._portal_cm.__enter__() + + self._leases += 1 + return self._portal + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + portal_cm: AbstractContextManager[BlockingPortal] | None = None + with self._lock: + assert self._portal_cm + assert self._leases > 0 + self._leases -= 1 + if not self._leases: + portal_cm = self._portal_cm + self._portal_cm = None + del self._portal + + if portal_cm: + portal_cm.__exit__(None, None, None) + + +@contextmanager +def start_blocking_portal( + backend: str = "asyncio", + backend_options: dict[str, Any] | None = None, + *, + name: str | None = None, +) -> Generator[BlockingPortal, Any, None]: + """ + Start a new event loop in a new thread and run a blocking portal in its main task. + + The parameters are the same as for :func:`~anyio.run`. + + :param backend: name of the backend + :param backend_options: backend options + :param name: name of the thread + :return: a context manager that yields a blocking portal + + .. versionchanged:: 3.0 + Usage as a context manager is now required. + + """ + + async def run_portal() -> None: + async with BlockingPortal() as portal_: + if name is None: + current_thread().name = f"{backend}-portal-{id(portal_):x}" + + future.set_result(portal_) + await portal_.sleep_until_stopped() + + def run_blocking_portal() -> None: + if future.set_running_or_notify_cancel(): + try: + run_eventloop( + run_portal, backend=backend, backend_options=backend_options + ) + except BaseException as exc: + if not future.done(): + future.set_exception(exc) + + future: Future[BlockingPortal] = Future() + thread = Thread(target=run_blocking_portal, daemon=True, name=name) + thread.start() + try: + cancel_remaining_tasks = False + portal = future.result() + try: + yield portal + except BaseException: + cancel_remaining_tasks = True + raise + finally: + try: + portal.call(portal.stop, cancel_remaining_tasks) + except RuntimeError: + pass + finally: + thread.join() + + +def check_cancelled() -> None: + """ + Check if the cancel scope of the host task's running the current worker thread has + been cancelled. + + If the host task's current cancel scope has indeed been cancelled, the + backend-specific cancellation exception will be raised. + + :raises RuntimeError: if the current thread was not spawned by + :func:`.to_thread.run_sync` + + """ + try: + token: EventLoopToken = threadlocals.current_token + except AttributeError: + raise NoEventLoopError( + "This function can only be called inside an AnyIO worker thread" + ) from None + + token.backend_class.check_cancelled() diff --git a/venv/lib/python3.11/site-packages/anyio/functools.py b/venv/lib/python3.11/site-packages/anyio/functools.py new file mode 100644 index 0000000..b0bdfb4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/functools.py @@ -0,0 +1,400 @@ +from __future__ import annotations + +__all__ = ( + "AsyncCacheInfo", + "AsyncCacheParameters", + "AsyncLRUCacheWrapper", + "cache", + "lru_cache", + "reduce", +) + +import functools +from collections import OrderedDict +from collections.abc import ( + AsyncIterable, + Awaitable, + Callable, + Coroutine, + Hashable, + Iterable, +) +from functools import update_wrapper +from inspect import iscoroutinefunction +from typing import ( + Any, + Generic, + NamedTuple, + ParamSpec, + TypedDict, + TypeVar, + cast, + final, + overload, +) +from weakref import WeakKeyDictionary + +from ._core._eventloop import current_time +from ._core._synchronization import Lock +from .lowlevel import RunVar, checkpoint + +T = TypeVar("T") +S = TypeVar("S") +P = ParamSpec("P") +lru_cache_items: RunVar[ + WeakKeyDictionary[ + AsyncLRUCacheWrapper[Any, Any], + OrderedDict[ + Hashable, + tuple[_InitialMissingType, Lock, float | None] + | tuple[Any, None, float | None], + ], + ] +] = RunVar("lru_cache_items") + + +class _InitialMissingType: + pass + + +initial_missing: _InitialMissingType = _InitialMissingType() + + +class AsyncCacheInfo(NamedTuple): + hits: int + misses: int + maxsize: int | None + currsize: int + ttl: int | None + + +class AsyncCacheParameters(TypedDict): + maxsize: int | None + typed: bool + always_checkpoint: bool + ttl: int | None + + +class _LRUMethodWrapper(Generic[T]): + def __init__(self, wrapper: AsyncLRUCacheWrapper[..., T], instance: object): + self.__wrapper = wrapper + self.__instance = instance + + def cache_info(self) -> AsyncCacheInfo: + return self.__wrapper.cache_info() + + def cache_parameters(self) -> AsyncCacheParameters: + return self.__wrapper.cache_parameters() + + def cache_clear(self) -> None: + self.__wrapper.cache_clear() + + async def __call__(self, *args: Any, **kwargs: Any) -> T: + if self.__instance is None: + return await self.__wrapper(*args, **kwargs) + + return await self.__wrapper(self.__instance, *args, **kwargs) + + +@final +class AsyncLRUCacheWrapper(Generic[P, T]): + def __init__( + self, + func: Callable[P, Awaitable[T]], + maxsize: int | None, + typed: bool, + always_checkpoint: bool, + ttl: int | None, + ): + self.__wrapped__ = func + self._hits: int = 0 + self._misses: int = 0 + self._maxsize = max(maxsize, 0) if maxsize is not None else None + self._currsize: int = 0 + self._typed = typed + self._always_checkpoint = always_checkpoint + self._ttl = ttl + update_wrapper(self, func) + + def cache_info(self) -> AsyncCacheInfo: + return AsyncCacheInfo( + self._hits, self._misses, self._maxsize, self._currsize, self._ttl + ) + + def cache_parameters(self) -> AsyncCacheParameters: + return { + "maxsize": self._maxsize, + "typed": self._typed, + "always_checkpoint": self._always_checkpoint, + "ttl": self._ttl, + } + + def cache_clear(self) -> None: + if cache := lru_cache_items.get(None): + cache.pop(self, None) + self._hits = self._misses = self._currsize = 0 + + async def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T: + # Easy case first: if maxsize == 0, no caching is done + if self._maxsize == 0: + value = await self.__wrapped__(*args, **kwargs) + self._misses += 1 + return value + + # The key is constructed as a flat tuple to avoid memory overhead + key: tuple[Any, ...] = args + if kwargs: + # initial_missing is used as a separator + key += (initial_missing,) + sum(kwargs.items(), ()) + + if self._typed: + key += tuple(type(arg) for arg in args) + if kwargs: + key += (initial_missing,) + tuple(type(val) for val in kwargs.values()) + + try: + cache = lru_cache_items.get() + except LookupError: + cache = WeakKeyDictionary() + lru_cache_items.set(cache) + + try: + cache_entry = cache[self] + except KeyError: + cache_entry = cache[self] = OrderedDict() + + cached_value: T | _InitialMissingType + try: + cached_value, lock, expires_at = cache_entry[key] + except KeyError: + # We're the first task to call this function + cached_value, lock, expires_at = ( + initial_missing, + Lock(fast_acquire=not self._always_checkpoint), + None, + ) + cache_entry[key] = cached_value, lock, expires_at + + if lock is None: + if expires_at is not None and current_time() >= expires_at: + self._currsize -= 1 + cached_value, lock, expires_at = ( + initial_missing, + Lock(fast_acquire=not self._always_checkpoint), + None, + ) + cache_entry[key] = cached_value, lock, expires_at + else: + # The value was already cached + self._hits += 1 + cache_entry.move_to_end(key) + if self._always_checkpoint: + await checkpoint() + + return cast(T, cached_value) + + async with lock: + # Check if another task filled the cache while we acquired the lock + if (cached_value := cache_entry[key][0]) is initial_missing: + self._misses += 1 + if self._maxsize is not None and self._currsize >= self._maxsize: + cache_entry.popitem(last=False) + else: + self._currsize += 1 + + value = await self.__wrapped__(*args, **kwargs) + expires_at = ( + current_time() + self._ttl if self._ttl is not None else None + ) + cache_entry[key] = value, None, expires_at + else: + # Another task filled the cache while we were waiting for the lock + self._hits += 1 + cache_entry.move_to_end(key) + value = cast(T, cached_value) + + return value + + def __get__( + self, instance: object, owner: type | None = None + ) -> _LRUMethodWrapper[T]: + wrapper = _LRUMethodWrapper(self, instance) + update_wrapper(wrapper, self.__wrapped__) + return wrapper + + +class _LRUCacheWrapper: + def __init__( + self, maxsize: int | None, typed: bool, always_checkpoint: bool, ttl: int | None + ): + self._maxsize = maxsize + self._typed = typed + self._always_checkpoint = always_checkpoint + self._ttl = ttl + + @overload + def __call__( # type: ignore[overload-overlap] + self, func: Callable[P, Coroutine[Any, Any, T]], / + ) -> AsyncLRUCacheWrapper[P, T]: ... + + @overload + def __call__( + self, func: Callable[..., T], / + ) -> functools._lru_cache_wrapper[T]: ... + + def __call__( + self, f: Callable[P, Coroutine[Any, Any, T]] | Callable[..., T], / + ) -> AsyncLRUCacheWrapper[P, T] | functools._lru_cache_wrapper[T]: + if iscoroutinefunction(f): + return AsyncLRUCacheWrapper( + f, self._maxsize, self._typed, self._always_checkpoint, self._ttl + ) + + return functools.lru_cache(maxsize=self._maxsize, typed=self._typed)(f) # type: ignore[arg-type] + + +@overload +def cache( # type: ignore[overload-overlap] + func: Callable[P, Coroutine[Any, Any, T]], / +) -> AsyncLRUCacheWrapper[P, T]: ... + + +@overload +def cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ... + + +def cache(func: Callable[..., Any] | Callable[P, Coroutine[Any, Any, Any]], /) -> Any: + """ + A convenient shortcut for :func:`lru_cache` with ``maxsize=None``. + + This is the asynchronous equivalent to :func:`functools.cache`. + + """ + return lru_cache(maxsize=None)(func) + + +@overload +def lru_cache( + *, + maxsize: int | None = ..., + typed: bool = ..., + always_checkpoint: bool = ..., + ttl: int | None = ..., +) -> _LRUCacheWrapper: ... + + +@overload +def lru_cache( # type: ignore[overload-overlap] + func: Callable[P, Coroutine[Any, Any, T]], / +) -> AsyncLRUCacheWrapper[P, T]: ... + + +@overload +def lru_cache(func: Callable[..., T], /) -> functools._lru_cache_wrapper[T]: ... + + +def lru_cache( + func: Callable[..., Coroutine[Any, Any, Any]] | Callable[..., Any] | None = None, + /, + *, + maxsize: int | None = 128, + typed: bool = False, + always_checkpoint: bool = False, + ttl: int | None = None, +) -> Any: + """ + An asynchronous version of :func:`functools.lru_cache`. + + If a synchronous function is passed, the standard library + :func:`functools.lru_cache` is applied instead. + + :param always_checkpoint: if ``True``, every call to the cached function will be + guaranteed to yield control to the event loop at least once + :param ttl: time in seconds after which to invalidate cache entries + + .. note:: Caches and locks are managed on a per-event loop basis. + + """ + if func is None: + return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl) + + if not callable(func): + raise TypeError("the first argument must be callable") + + return _LRUCacheWrapper(maxsize, typed, always_checkpoint, ttl)(func) + + +@overload +async def reduce( + function: Callable[[T, S], Awaitable[T]], + iterable: Iterable[S] | AsyncIterable[S], + /, + initial: T, +) -> T: ... + + +@overload +async def reduce( + function: Callable[[T, T], Awaitable[T]], + iterable: Iterable[T] | AsyncIterable[T], + /, +) -> T: ... + + +async def reduce( # type: ignore[misc] + function: Callable[[T, T], Awaitable[T]] | Callable[[T, S], Awaitable[T]], + iterable: Iterable[T] | Iterable[S] | AsyncIterable[T] | AsyncIterable[S], + /, + initial: T | _InitialMissingType = initial_missing, +) -> T: + """ + Asynchronous version of :func:`functools.reduce`. + + :param function: a coroutine function that takes two arguments: the accumulated + value and the next element from the iterable + :param iterable: an iterable or async iterable + :param initial: the initial value (if missing, the first element of the iterable is + used as the initial value) + + """ + element: Any + function_called = False + if isinstance(iterable, AsyncIterable): + async_it = iterable.__aiter__() + if initial is initial_missing: + try: + value = cast(T, await async_it.__anext__()) + except StopAsyncIteration: + raise TypeError( + "reduce() of empty sequence with no initial value" + ) from None + else: + value = cast(T, initial) + + async for element in async_it: + value = await function(value, element) + function_called = True + elif isinstance(iterable, Iterable): + it = iter(iterable) + if initial is initial_missing: + try: + value = cast(T, next(it)) + except StopIteration: + raise TypeError( + "reduce() of empty sequence with no initial value" + ) from None + else: + value = cast(T, initial) + + for element in it: + value = await function(value, element) + function_called = True + else: + raise TypeError("reduce() argument 2 must be an iterable or async iterable") + + # Make sure there is at least one checkpoint, even if an empty iterable and an + # initial value were given + if not function_called: + await checkpoint() + + return value diff --git a/venv/lib/python3.11/site-packages/anyio/itertools.py b/venv/lib/python3.11/site-packages/anyio/itertools.py new file mode 100644 index 0000000..7e5248e --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/itertools.py @@ -0,0 +1,626 @@ +from __future__ import annotations + +__all__ = ( + "accumulate", + "batched", + "Chain", + "combinations", + "combinations_with_replacement", + "compress", + "count", + "cycle", + "dropwhile", + "filterfalse", + "groupby", + "islice", + "pairwise", + "permutations", + "product", + "repeat", + "starmap", + "tee", + "takewhile", + "zip_longest", +) + +import itertools +import operator +import sys +from collections.abc import ( + AsyncGenerator, + AsyncIterable, + AsyncIterator, + Awaitable, + Callable, + Iterable, + Iterator, +) +from dataclasses import dataclass, field +from typing import Any, Generic, TypeVar, cast, overload + +from ._core._synchronization import Lock +from ._core._tasks import CancelScope +from .lowlevel import cancel_shielded_checkpoint, checkpoint, checkpoint_if_cancelled + +T = TypeVar("T") +R = TypeVar("R") +_tee_end = object() + + +@dataclass(eq=False) +class _IterableAsyncIterator(AsyncIterator[T]): + iterator: Iterator[T] + + async def __anext__(self) -> T: + await checkpoint_if_cancelled() + try: + result = next(self.iterator) + except StopIteration: + await cancel_shielded_checkpoint() + raise StopAsyncIteration from None + + await cancel_shielded_checkpoint() + return result + + +def _iterate(iterable: Iterable[T] | AsyncIterable[T]) -> AsyncIterator[T]: + if isinstance(iterable, AsyncIterator): + return iterable + + if isinstance(iterable, AsyncIterable): + return iterable.__aiter__() + + return _IterableAsyncIterator(iter(iterable)) + + +@dataclass(eq=False) +class _TeeLink(Generic[T]): + value: object | None = None + next: _TeeLink[T] | None = None + filled: bool = False + + +@dataclass(eq=False) +class _TeeState(Generic[T]): + iterator: AsyncIterator[T] + lock: Lock = field(default_factory=Lock) + + async def fill(self, link: _TeeLink[T]) -> bool: + if link.filled: + return False + + async with self.lock: + if link.filled: + return True + + link.value = await anext(self.iterator, _tee_end) + if link.value is not _tee_end: + link.next = _TeeLink() + + link.filled = True + return True + + +class _TeeAsyncIterator(AsyncIterator[T]): + _state: _TeeState[T] + _link: _TeeLink[T] + _element_yielded: bool + + def __init__( + self, iterable: Iterable[T] | AsyncIterable[T] | _TeeAsyncIterator[T] + ) -> None: + if isinstance(iterable, _TeeAsyncIterator): + self._state = iterable._state + self._link = iterable._link + else: + self._state = _TeeState(_iterate(iterable)) + self._link = _TeeLink() + + self._element_yielded = False + + async def __anext__(self) -> T: + had_yieldpoint = await self._state.fill(self._link) + if self._link.value is _tee_end: + if not self._element_yielded: + await checkpoint() + + raise StopAsyncIteration + + if not had_yieldpoint: + await checkpoint_if_cancelled() + + self._element_yielded = True + value = cast(T, self._link.value) + next_link = self._link.next + assert next_link is not None + self._link = next_link + if not had_yieldpoint: + await cancel_shielded_checkpoint() + + return value + + +async def _operator_add(x: T, y: T) -> T: + return operator.add(x, y) + + +async def accumulate( + iterable: Iterable[T] | AsyncIterable[T], + function: Callable[[T, T], Awaitable[T]] = _operator_add, + *, + initial: T | None = None, +) -> AsyncGenerator[T, None]: + iterator = _iterate(iterable) + if initial is None: + try: + total = await anext(iterator) + except StopAsyncIteration: + await checkpoint() + return + else: + await checkpoint_if_cancelled() + total = initial + await cancel_shielded_checkpoint() + + yield total + + async for element in iterator: + total = await function(total, element) + yield total + + +async def batched( + iterable: Iterable[T] | AsyncIterable[T], n: int, *, strict: bool = False +) -> AsyncGenerator[tuple[T, ...], None]: + if n < 1: + raise ValueError("n must be at least one") + + iterator = _iterate(iterable) + + while True: + batch: list[T] = [] + for _ in range(n): + try: + batch.append(await anext(iterator)) + except StopAsyncIteration: + if not batch: + await checkpoint() + return + if strict: + raise ValueError("batched(): incomplete batch") from None + + yield tuple(batch) + return + + yield tuple(batch) + + +class Chain: + def __call__( + self, *iterables: Iterable[T] | AsyncIterable[T] + ) -> AsyncGenerator[T, None]: + return self.from_iterable(iterables) + + async def from_iterable( + self, + iterables: ( + Iterable[Iterable[T] | AsyncIterable[T]] + | AsyncIterable[Iterable[T] | AsyncIterable[T]] + ), + ) -> AsyncGenerator[T, None]: + element_yielded = False + outer_iter = _iterate(iterables) + + try: + async for iterable in outer_iter: + async for element in _iterate(iterable): + element_yielded = True + yield element + finally: + aclose = getattr(outer_iter, "aclose", None) + if aclose is not None: + with CancelScope(shield=True): + await aclose() + + if not element_yielded: + await checkpoint() + + +chain: Chain = Chain() + + +async def combinations( + iterable: Iterable[T] | AsyncIterable[T], r: int +) -> AsyncGenerator[tuple[T, ...], None]: + pool: list[T] = [element async for element in _iterate(iterable)] + async for combination in _iterate(itertools.combinations(pool, r)): + yield combination + + +async def combinations_with_replacement( + iterable: Iterable[T] | AsyncIterable[T], r: int +) -> AsyncGenerator[tuple[T, ...], None]: + pool: list[T] = [element async for element in _iterate(iterable)] + async for combination in _iterate(itertools.combinations_with_replacement(pool, r)): + yield combination + + +async def compress( + data: Iterable[T] | AsyncIterable[T], + selectors: Iterable[object] | AsyncIterable[object], +) -> AsyncGenerator[T, None]: + data_iterator = _iterate(data) + selector_iterator = _iterate(selectors) + element_yielded = False + + while True: + try: + datum = await anext(data_iterator) + selector = await anext(selector_iterator) + except StopAsyncIteration: + if not element_yielded: + await checkpoint() + + return + + if selector: + element_yielded = True + yield datum + + +async def count(start: int = 0, step: int = 1) -> AsyncGenerator[int, None]: + n = start + while True: + await checkpoint_if_cancelled() + value = n + n += step + await cancel_shielded_checkpoint() + yield value + + +async def cycle( + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + saved: list[T] = [] + async for element in _iterate(iterable): + saved.append(element) + yield element + + if not saved: + await checkpoint() + return + + while True: + for element in saved: + await checkpoint() + yield element + + +async def dropwhile( + predicate: Callable[[T], Awaitable[object]], + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + element_yielded = False + dropping = True + + async for element in _iterate(iterable): + if dropping and await predicate(element): + continue + + dropping = False + element_yielded = True + yield element + + if not element_yielded: + await checkpoint() + + +async def filterfalse( + predicate: Callable[[T], Awaitable[object]], + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + element_yielded = False + + async for element in _iterate(iterable): + if not await predicate(element): + element_yielded = True + yield element + + if not element_yielded: + await checkpoint() + + +@overload +def groupby( + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[tuple[T, list[T]], None]: ... + + +@overload +def groupby( + iterable: Iterable[T] | AsyncIterable[T], + key: Callable[[T], Awaitable[R]], +) -> AsyncGenerator[tuple[R, list[T]], None]: ... + + +async def groupby( + iterable: Iterable[T] | AsyncIterable[T], + key: Callable[[T], Awaitable[object]] | None = None, +) -> AsyncGenerator[tuple[object, list[T]], None]: + iterator = _iterate(iterable) + try: + element = await anext(iterator) + except StopAsyncIteration: + await checkpoint() + return + + group_key = element if key is None else await key(element) + values = [element] + + async for element in iterator: + next_key = element if key is None else await key(element) + if next_key != group_key: + completed_group = group_key, values + group_key = next_key + values = [element] + yield completed_group + else: + values.append(element) + + yield group_key, values + + +@overload +def islice( + iterable: Iterable[T] | AsyncIterable[T], + stop: int | None, + /, +) -> AsyncGenerator[T, None]: ... + + +@overload +def islice( + iterable: Iterable[T] | AsyncIterable[T], + start: int | None, + stop: int | None, + step: int | None = 1, + /, +) -> AsyncGenerator[T, None]: ... + + +async def islice( + iterable: Iterable[T] | AsyncIterable[T], + *args: int | None, +) -> AsyncGenerator[T, None]: + if not args: + raise TypeError("islice expected at least 2 arguments, got 1") + if len(args) > 3: + raise TypeError(f"islice expected at most 4 arguments, got {len(args) + 1}") + + slice_args = slice(*args) + + start_message = ( + "Indices for islice() must be None or an integer: 0 <= x <= sys.maxsize." + ) + stop_message = ( + "Stop argument for islice() must be None or an integer: 0 <= x <= sys.maxsize." + ) + step_message = "Step for islice() must be a positive integer or None." + + def normalize_index(value: object, message: str) -> int: + try: + index = operator.index(cast(Any, value)) + except TypeError: + raise ValueError(message) from None + + if index < 0 or index > sys.maxsize: + raise ValueError(message) + + return index + + start = ( + 0 + if slice_args.start is None + else normalize_index(slice_args.start, start_message) + ) + stop = ( + None + if slice_args.stop is None + else normalize_index(slice_args.stop, stop_message) + ) + step = ( + 1 if slice_args.step is None else normalize_index(slice_args.step, step_message) + ) + + if step <= 0: + raise ValueError(step_message) + + if stop == 0 or start == stop: + await checkpoint() + return + + iterator = _iterate(iterable) + index = 0 + element_yielded = False + + while stop is None or index < stop: + try: + element = await anext(iterator) + except StopAsyncIteration: + if not element_yielded: + await checkpoint() + + return + + if index >= start and (index - start) % step == 0: + index += 1 + element_yielded = True + yield element + else: + index += 1 + + if not element_yielded: + await checkpoint() + + +async def pairwise( + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[tuple[T, T], None]: + iterator = _iterate(iterable) + try: + previous = await anext(iterator) + except StopAsyncIteration: + await checkpoint() + return + + element_yielded = False + async for element in iterator: + element_yielded = True + pair = (previous, element) + previous = element + yield pair + + if not element_yielded: + await checkpoint() + + +async def permutations( + iterable: Iterable[T] | AsyncIterable[T], r: int | None = None +) -> AsyncGenerator[tuple[T, ...], None]: + pool: list[T] = [element async for element in _iterate(iterable)] + n = len(pool) + if r is None: + r = n + elif not isinstance(r, int): + raise TypeError("Expected int as r") + elif r < 0: + raise ValueError("r must be non-negative") + + async for permutation in _iterate(itertools.permutations(pool, r)): + yield permutation + + +async def product( + *iterables: Iterable[T] | AsyncIterable[T], repeat: int = 1 +) -> AsyncGenerator[tuple[T, ...], None]: + repeat = operator.index(repeat) + if repeat < 0: + raise ValueError("repeat argument cannot be negative") + + pools: list[tuple[T, ...]] = [] + for iterable in iterables: + pool: list[T] = [element async for element in _iterate(iterable)] + pools.append(tuple(pool)) + + async for value in _iterate(itertools.product(*pools, repeat=repeat)): + yield value + + +async def repeat(element: T, times: int | None = None) -> AsyncGenerator[T, None]: + if times is None: + while True: + await checkpoint() + yield element + + remaining = operator.index(cast(Any, times)) + if remaining <= 0: + await checkpoint() + return + + while remaining > 0: + await checkpoint_if_cancelled() + remaining -= 1 + await cancel_shielded_checkpoint() + yield element + + +async def starmap( + function: Callable[..., Awaitable[R]], + iterable: ( + Iterable[Iterable[object] | AsyncIterable[object]] + | AsyncIterable[Iterable[object] | AsyncIterable[object]] + ), +) -> AsyncGenerator[R, None]: + result_yielded = False + + async for args_iterable in _iterate(iterable): + args = [element async for element in _iterate(args_iterable)] + result_yielded = True + yield await function(*args) + + if not result_yielded: + await checkpoint() + + +def tee( + iterable: Iterable[T] | AsyncIterable[T], n: int = 2 +) -> tuple[AsyncIterator[T], ...]: + n = operator.index(cast(Any, n)) + if n < 0: + raise ValueError("n must be >= 0") + if n == 0: + return () + + iterator = _TeeAsyncIterator(iterable) + iterators: list[AsyncIterator[T]] = [iterator] + iterators.extend(_TeeAsyncIterator(iterator) for _ in range(n - 1)) + return tuple(iterators) + + +async def takewhile( + predicate: Callable[[T], Awaitable[object]], + iterable: Iterable[T] | AsyncIterable[T], +) -> AsyncGenerator[T, None]: + element_yielded = False + + async for element in _iterate(iterable): + if not await predicate(element): + if not element_yielded: + await checkpoint() + + return + + element_yielded = True + yield element + + if not element_yielded: + await checkpoint() + + +async def zip_longest( + *iterables: Iterable[object] | AsyncIterable[object], + fillvalue: object = None, +) -> AsyncGenerator[tuple[object, ...], None]: + iterators = [_iterate(iterable) for iterable in iterables] + num_active = len(iterators) + if not num_active: + await checkpoint() + return + + active = [True] * num_active + tuple_yielded = False + + while True: + values: list[object] = [] + for index, iterator in enumerate(iterators): + if not active[index]: + values.append(fillvalue) + continue + + try: + value = await anext(iterator) + except StopAsyncIteration: + active[index] = False + num_active -= 1 + if not num_active: + if not tuple_yielded: + await checkpoint() + + return + + value = fillvalue + + values.append(value) + + tuple_yielded = True + yield tuple(values) diff --git a/venv/lib/python3.11/site-packages/anyio/lowlevel.py b/venv/lib/python3.11/site-packages/anyio/lowlevel.py new file mode 100644 index 0000000..d045791 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/lowlevel.py @@ -0,0 +1,226 @@ +from __future__ import annotations + +__all__ = ( + "EventLoopToken", + "RunvarToken", + "RunVar", + "checkpoint", + "checkpoint_if_cancelled", + "cancel_shielded_checkpoint", + "current_token", +) + +import enum +from dataclasses import dataclass +from types import TracebackType +from typing import Any, Generic, Literal, TypeVar, final, overload +from weakref import WeakKeyDictionary + +from ._core._eventloop import get_async_backend +from .abc import AsyncBackend + +T = TypeVar("T") +D = TypeVar("D") + + +async def checkpoint() -> None: + """ + Check for cancellation and allow the scheduler to switch to another task. + + Equivalent to (but more efficient than):: + + await checkpoint_if_cancelled() + await cancel_shielded_checkpoint() + + .. versionadded:: 3.0 + + """ + await get_async_backend().checkpoint() + + +async def checkpoint_if_cancelled() -> None: + """ + Enter a checkpoint if the enclosing cancel scope has been cancelled. + + This does not allow the scheduler to switch to a different task. + + .. versionadded:: 3.0 + + """ + await get_async_backend().checkpoint_if_cancelled() + + +async def cancel_shielded_checkpoint() -> None: + """ + Allow the scheduler to switch to another task but without checking for cancellation. + + Equivalent to (but potentially more efficient than):: + + with CancelScope(shield=True): + await checkpoint() + + .. versionadded:: 3.0 + + """ + await get_async_backend().cancel_shielded_checkpoint() + + +@final +@dataclass(frozen=True, repr=False) +class EventLoopToken: + """ + An opaque object that holds a reference to an event loop. + + .. versionadded:: 4.11.0 + """ + + backend_class: type[AsyncBackend] + native_token: object + + +def current_token() -> EventLoopToken: + """ + Return a token object that can be used to call code in the current event loop from + another thread. + + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + .. versionadded:: 4.11.0 + + """ + backend_class = get_async_backend() + raw_token = backend_class.current_token() + return EventLoopToken(backend_class, raw_token) + + +_run_vars: WeakKeyDictionary[object, dict[RunVar[Any], Any]] = WeakKeyDictionary() + + +class _NoValueSet(enum.Enum): + NO_VALUE_SET = enum.auto() + + +class RunvarToken(Generic[T]): + """ + A token that can be used to restore a :class:`RunVar` to its previous value. + + Returned by :meth:`RunVar.set`. Can be used as a context manager to automatically + reset the variable on exit, or passed directly to :meth:`RunVar.reset`. + """ + + __slots__ = "_var", "_value", "_redeemed" + + def __init__(self, var: RunVar[T], value: T | Literal[_NoValueSet.NO_VALUE_SET]): + self._var = var + self._value: T | Literal[_NoValueSet.NO_VALUE_SET] = value + self._redeemed = False + + def __enter__(self) -> RunvarToken[T]: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self._var.reset(self) + + +class RunVar(Generic[T]): + """ + Like a :class:`~contextvars.ContextVar`, except scoped to the running event loop. + + Can be used as a context manager, Just like :class:`~contextvars.ContextVar`, that + will reset the variable to its previous value when the context block is exited. + """ + + __slots__ = "_name", "_default" + + NO_VALUE_SET: Literal[_NoValueSet.NO_VALUE_SET] = _NoValueSet.NO_VALUE_SET + + def __init__( + self, name: str, default: T | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET + ): + self._name = name + self._default = default + + @property + def _current_vars(self) -> dict[RunVar[T], T]: + native_token = current_token().native_token + try: + return _run_vars[native_token] + except KeyError: + run_vars = _run_vars[native_token] = {} + return run_vars + + @overload + def get(self, default: D) -> T | D: ... + + @overload + def get(self) -> T: ... + + def get( + self, default: D | Literal[_NoValueSet.NO_VALUE_SET] = NO_VALUE_SET + ) -> T | D: + """ + Return the current value of this run variable. + + :param default: a fallback value to return if no value has been set + :return: the current value, the provided default, or the variable's own default + :raises LookupError: if no value is set and no default is available + + """ + try: + return self._current_vars[self] + except KeyError: + if default is not RunVar.NO_VALUE_SET: + return default + elif self._default is not RunVar.NO_VALUE_SET: + return self._default + + raise LookupError( + f'Run variable "{self._name}" has no value and no default set' + ) + + def set(self, value: T) -> RunvarToken[T]: + """ + Set the value of this run variable for the current event loop. + + :param value: the new value + :return: a token that can be used to restore the previous value + + """ + current_vars = self._current_vars + token = RunvarToken(self, current_vars.get(self, RunVar.NO_VALUE_SET)) + current_vars[self] = value + return token + + def reset(self, token: RunvarToken[T]) -> None: + """ + Restore this run variable to the value it held before the matching :meth:`set`. + + :param token: the token returned by :meth:`set` + :raises ValueError: if the token belongs to a different :class:`RunVar` or the token + has already been used + + """ + if token._var is not self: + raise ValueError("This token does not belong to this RunVar") + + if token._redeemed: + raise ValueError("This token has already been used") + + if token._value is _NoValueSet.NO_VALUE_SET: + try: + del self._current_vars[self] + except KeyError: + pass + else: + self._current_vars[self] = token._value + + token._redeemed = True + + def __repr__(self) -> str: + return f"" diff --git a/venv/lib/python3.11/site-packages/anyio/py.typed b/venv/lib/python3.11/site-packages/anyio/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/anyio/pytest_plugin.py b/venv/lib/python3.11/site-packages/anyio/pytest_plugin.py new file mode 100644 index 0000000..5c66759 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/pytest_plugin.py @@ -0,0 +1,375 @@ +from __future__ import annotations + +import dataclasses +import socket +import sys +from collections.abc import Callable, Generator, Iterator +from contextlib import ExitStack, contextmanager +from inspect import isasyncgenfunction, iscoroutinefunction, ismethod +from typing import Any, cast + +import pytest +from _pytest.fixtures import FuncFixtureInfo, SubRequest +from _pytest.outcomes import Exit +from _pytest.python import CallSpec2 +from _pytest.scope import Scope + +from . import get_available_backends +from ._core._eventloop import ( + current_async_library, + get_async_backend, + reset_current_async_library, + set_current_async_library, +) +from ._core._exceptions import iterate_exceptions +from .abc import TestRunner + +if sys.version_info < (3, 11): + from exceptiongroup import ExceptionGroup + +_current_runner: TestRunner | None = None +_runner_stack: ExitStack | None = None +_runner_leases = 0 + + +def extract_backend_and_options(backend: object) -> tuple[str, dict[str, Any]]: + if isinstance(backend, str): + return backend, {} + elif isinstance(backend, tuple) and len(backend) == 2: + if isinstance(backend[0], str) and isinstance(backend[1], dict): + return cast(tuple[str, dict[str, Any]], backend) + + raise TypeError("anyio_backend must be either a string or tuple of (string, dict)") + + +@contextmanager +def get_runner( + backend_name: str, backend_options: dict[str, Any] +) -> Iterator[TestRunner]: + global _current_runner, _runner_leases, _runner_stack + if _current_runner is None: + asynclib = get_async_backend(backend_name) + _runner_stack = ExitStack() + if current_async_library() is None: + # Since we're in control of the event loop, we can cache the name of the + # async library + token = set_current_async_library(backend_name) + _runner_stack.callback(reset_current_async_library, token) + + backend_options = backend_options or {} + _current_runner = _runner_stack.enter_context( + asynclib.create_test_runner(backend_options) + ) + + _runner_leases += 1 + try: + yield _current_runner + finally: + _runner_leases -= 1 + if not _runner_leases: + assert _runner_stack is not None + _runner_stack.close() + _runner_stack = _current_runner = None + + +def pytest_addoption(parser: pytest.Parser) -> None: + parser.addini( + "anyio_mode", + default="strict", + help='AnyIO plugin mode (either "strict" or "auto")', + ) + + +def pytest_configure(config: pytest.Config) -> None: + config.addinivalue_line( + "markers", + "anyio: mark the (coroutine function) test to be run asynchronously via anyio.", + ) + if ( + config.getini("anyio_mode") == "auto" + and config.pluginmanager.has_plugin("asyncio") + and config.getini("asyncio_mode") == "auto" + ): + config.issue_config_time_warning( + pytest.PytestConfigWarning( + "AnyIO auto mode has been enabled together with pytest-asyncio auto " + "mode. This may cause unexpected behavior." + ), + 1, + ) + + +@pytest.hookimpl(hookwrapper=True) +def pytest_fixture_setup(fixturedef: Any, request: Any) -> Generator[Any]: + def wrapper(anyio_backend: Any, request: SubRequest, **kwargs: Any) -> Any: + # Rebind any fixture methods to the request instance + if ( + request.instance + and ismethod(func) + and type(func.__self__) is type(request.instance) + ): + local_func = func.__func__.__get__(request.instance) + else: + local_func = func + + backend_name, backend_options = extract_backend_and_options(anyio_backend) + if has_backend_arg: + kwargs["anyio_backend"] = anyio_backend + + if has_request_arg: + kwargs["request"] = request + + with get_runner(backend_name, backend_options) as runner: + # re-entrant call into the test runner detected. this happens when an async fixture + # is dynamically requested via request.getfixturevalue() from inside a running async + # test or fixture. on asyncio this raises RuntimeError: This event loop is already + # running, on trio the runner deadlocks - the host loop blocks waiting for the + # coroutine to return, but the coroutine is waiting for the host loop. raising here + # prevents the hang and gives a consistent error across backends. + if runner.is_running(): + raise RuntimeError( + "Cannot schedule a coroutine in the test runner while another is already running; " + "likely caused by request.getfixturevalue() on an async fixture." + ) + + if isasyncgenfunction(local_func): + yield from runner.run_asyncgen_fixture(local_func, kwargs) + else: + yield runner.run_fixture(local_func, kwargs) + + # Only apply this to coroutine functions and async generator functions in requests + # that involve the anyio_backend fixture + func = fixturedef.func + if isasyncgenfunction(func) or iscoroutinefunction(func): + if "anyio_backend" in request.fixturenames: + fixturedef.func = wrapper + original_argname = fixturedef.argnames + + if not (has_backend_arg := "anyio_backend" in fixturedef.argnames): + fixturedef.argnames += ("anyio_backend",) + + if not (has_request_arg := "request" in fixturedef.argnames): + fixturedef.argnames += ("request",) + + try: + return (yield) + finally: + fixturedef.func = func + fixturedef.argnames = original_argname + + return (yield) + + +@pytest.hookimpl(tryfirst=True) +def pytest_pycollect_makeitem( + collector: pytest.Module | pytest.Class, name: str, obj: object +) -> None: + if collector.istestfunction(obj, name): + inner_func = obj.hypothesis.inner_test if hasattr(obj, "hypothesis") else obj + if iscoroutinefunction(inner_func): + anyio_auto_mode = collector.config.getini("anyio_mode") == "auto" + marker = collector.get_closest_marker("anyio") + own_markers = getattr(obj, "pytestmark", ()) + if ( + anyio_auto_mode + or marker + or any(marker.name == "anyio" for marker in own_markers) + ): + pytest.mark.usefixtures("anyio_backend")(obj) + + +def pytest_collection_finish(session: pytest.Session) -> None: + for i, item in reversed(list(enumerate(session.items))): + if ( + isinstance(item, pytest.Function) + and iscoroutinefunction(item.function) + and item.get_closest_marker("anyio") is not None + and "anyio_backend" not in item.fixturenames + ): + new_items = [] + try: + cs_fields = {f.name for f in dataclasses.fields(CallSpec2)} + except TypeError: + cs_fields = set() + + for param_index, backend in enumerate(get_available_backends()): + if "_arg2scope" in cs_fields: # pytest >= 8 + callspec = CallSpec2( + params={"anyio_backend": backend}, + indices={"anyio_backend": param_index}, + _arg2scope={"anyio_backend": Scope.Module}, + _idlist=[backend], + marks=[], + ) + else: # pytest 7.x + callspec = CallSpec2( # type: ignore[call-arg] + funcargs={}, + params={"anyio_backend": backend}, + indices={"anyio_backend": param_index}, + arg2scope={"anyio_backend": Scope.Module}, + idlist=[backend], + marks=[], + ) + + fi = item._fixtureinfo + new_names_closure = list(fi.names_closure) + if "anyio_backend" not in new_names_closure: + new_names_closure.append("anyio_backend") + + new_fixtureinfo = FuncFixtureInfo( + argnames=fi.argnames, + initialnames=fi.initialnames, + names_closure=new_names_closure, + name2fixturedefs=fi.name2fixturedefs, + ) + new_item = pytest.Function.from_parent( + item.parent, + name=f"{item.originalname}[{backend}]", + callspec=callspec, + callobj=item.obj, + fixtureinfo=new_fixtureinfo, + keywords=item.keywords, + originalname=item.originalname, + ) + new_items.append(new_item) + + session.items[i : i + 1] = new_items + + +@pytest.hookimpl(tryfirst=True) +def pytest_pyfunc_call(pyfuncitem: Any) -> bool | None: + def run_with_hypothesis(**kwargs: Any) -> None: + with get_runner(backend_name, backend_options) as runner: + runner.run_test(original_func, kwargs) + + backend = pyfuncitem.funcargs.get("anyio_backend") + if backend: + backend_name, backend_options = extract_backend_and_options(backend) + + if hasattr(pyfuncitem.obj, "hypothesis"): + # Wrap the inner test function unless it's already wrapped + original_func = pyfuncitem.obj.hypothesis.inner_test + if original_func.__qualname__ != run_with_hypothesis.__qualname__: + if iscoroutinefunction(original_func): + pyfuncitem.obj.hypothesis.inner_test = run_with_hypothesis + + return None + + if iscoroutinefunction(pyfuncitem.obj): + funcargs = pyfuncitem.funcargs + testargs = {arg: funcargs[arg] for arg in pyfuncitem._fixtureinfo.argnames} + with get_runner(backend_name, backend_options) as runner: + try: + runner.run_test(pyfuncitem.obj, testargs) + except ExceptionGroup as excgrp: + for exc in iterate_exceptions(excgrp): + if isinstance(exc, (Exit, KeyboardInterrupt, SystemExit)): + raise exc from excgrp + + raise + + return True + + return None + + +@pytest.fixture(scope="module", params=get_available_backends()) +def anyio_backend(request: Any) -> Any: + return request.param + + +@pytest.fixture +def anyio_backend_name(anyio_backend: Any) -> str: + if isinstance(anyio_backend, str): + return anyio_backend + else: + return anyio_backend[0] + + +@pytest.fixture +def anyio_backend_options(anyio_backend: Any) -> dict[str, Any]: + if isinstance(anyio_backend, str): + return {} + else: + return anyio_backend[1] + + +class FreePortFactory: + """ + Manages port generation based on specified socket kind, ensuring no duplicate + ports are generated. + + This class provides functionality for generating available free ports on the + system. It is initialized with a specific socket kind and can generate ports + for given address families while avoiding reuse of previously generated ports. + + Users should not instantiate this class directly, but use the + ``free_tcp_port_factory`` and ``free_udp_port_factory`` fixtures instead. For simple + uses cases, ``free_tcp_port`` and ``free_udp_port`` can be used instead. + """ + + def __init__(self, kind: socket.SocketKind) -> None: + self._kind = kind + self._generated = set[int]() + + @property + def kind(self) -> socket.SocketKind: + """ + The type of socket connection (e.g., :data:`~socket.SOCK_STREAM` or + :data:`~socket.SOCK_DGRAM`) used to bind for checking port availability + + """ + return self._kind + + def __call__(self, family: socket.AddressFamily | None = None) -> int: + """ + Return an unbound port for the given address family. + + :param family: if omitted, both IPv4 and IPv6 addresses will be tried + :return: a port number + + """ + if family is not None: + families = [family] + else: + families = [socket.AF_INET] + if socket.has_ipv6: + families.append(socket.AF_INET6) + + while True: + port = 0 + with ExitStack() as stack: + for family in families: + sock = stack.enter_context(socket.socket(family, self._kind)) + addr = "::1" if family == socket.AF_INET6 else "127.0.0.1" + try: + sock.bind((addr, port)) + except OSError: + break + + if not port: + port = sock.getsockname()[1] + else: + if port not in self._generated: + self._generated.add(port) + return port + + +@pytest.fixture(scope="session") +def free_tcp_port_factory() -> FreePortFactory: + return FreePortFactory(socket.SOCK_STREAM) + + +@pytest.fixture(scope="session") +def free_udp_port_factory() -> FreePortFactory: + return FreePortFactory(socket.SOCK_DGRAM) + + +@pytest.fixture +def free_tcp_port(free_tcp_port_factory: Callable[[], int]) -> int: + return free_tcp_port_factory() + + +@pytest.fixture +def free_udp_port(free_udp_port_factory: Callable[[], int]) -> int: + return free_udp_port_factory() diff --git a/venv/lib/python3.11/site-packages/anyio/streams/__init__.py b/venv/lib/python3.11/site-packages/anyio/streams/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..d68a2fd Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/buffered.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/buffered.cpython-311.pyc new file mode 100644 index 0000000..1e2fed9 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/buffered.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/file.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/file.cpython-311.pyc new file mode 100644 index 0000000..51b29ad Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/file.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/memory.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/memory.cpython-311.pyc new file mode 100644 index 0000000..86b4bba Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/memory.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/stapled.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/stapled.cpython-311.pyc new file mode 100644 index 0000000..b6cb319 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/stapled.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/text.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/text.cpython-311.pyc new file mode 100644 index 0000000..58287c3 Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/text.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/tls.cpython-311.pyc b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/tls.cpython-311.pyc new file mode 100644 index 0000000..f2be5fa Binary files /dev/null and b/venv/lib/python3.11/site-packages/anyio/streams/__pycache__/tls.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/anyio/streams/buffered.py b/venv/lib/python3.11/site-packages/anyio/streams/buffered.py new file mode 100644 index 0000000..acf312b --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/streams/buffered.py @@ -0,0 +1,198 @@ +from __future__ import annotations + +__all__ = ( + "BufferedByteReceiveStream", + "BufferedByteStream", + "BufferedConnectable", +) + +import sys +from collections.abc import Callable, Iterable, Mapping +from dataclasses import dataclass, field +from typing import Any, SupportsIndex + +from .. import ClosedResourceError, DelimiterNotFound, EndOfStream, IncompleteRead +from ..abc import ( + AnyByteReceiveStream, + AnyByteStream, + AnyByteStreamConnectable, + ByteReceiveStream, + ByteStream, + ByteStreamConnectable, +) + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + +@dataclass(eq=False) +class BufferedByteReceiveStream(ByteReceiveStream): + """ + Wraps any bytes-based receive stream and uses a buffer to provide sophisticated + receiving capabilities in the form of a byte stream. + """ + + receive_stream: AnyByteReceiveStream + _buffer: bytearray = field(init=False, default_factory=bytearray) + _closed: bool = field(init=False, default=False) + + async def aclose(self) -> None: + await self.receive_stream.aclose() + self._closed = True + + @property + def buffer(self) -> bytes: + """The bytes currently in the buffer.""" + return bytes(self._buffer) + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return self.receive_stream.extra_attributes + + def feed_data(self, data: Iterable[SupportsIndex], /) -> None: + """ + Append data directly into the buffer. + + Any data in the buffer will be consumed by receive operations before receiving + anything from the wrapped stream. + + :param data: the data to append to the buffer (can be bytes or anything else + that supports ``__index__()``) + + """ + self._buffer.extend(data) + + async def receive(self, max_bytes: int = 65536) -> bytes: + if self._closed: + raise ClosedResourceError + + if self._buffer: + chunk = bytes(self._buffer[:max_bytes]) + del self._buffer[:max_bytes] + return chunk + elif isinstance(self.receive_stream, ByteReceiveStream): + return await self.receive_stream.receive(max_bytes) + else: + # With a bytes-oriented object stream, we need to handle any surplus bytes + # we get from the receive() call + chunk = await self.receive_stream.receive() + if len(chunk) > max_bytes: + # Save the surplus bytes in the buffer + self._buffer.extend(chunk[max_bytes:]) + return chunk[:max_bytes] + else: + return chunk + + async def receive_exactly(self, nbytes: int) -> bytes: + """ + Read exactly the given amount of bytes from the stream. + + :param nbytes: the number of bytes to read + :return: the bytes read + :raises ~anyio.IncompleteRead: if the stream was closed before the requested + amount of bytes could be read from the stream + + """ + while True: + remaining = nbytes - len(self._buffer) + if remaining <= 0: + retval = self._buffer[:nbytes] + del self._buffer[:nbytes] + return bytes(retval) + + try: + if isinstance(self.receive_stream, ByteReceiveStream): + chunk = await self.receive_stream.receive(remaining) + else: + chunk = await self.receive_stream.receive() + except EndOfStream as exc: + raise IncompleteRead from exc + + self._buffer.extend(chunk) + + async def receive_until(self, delimiter: bytes, max_bytes: int) -> bytes: + """ + Read from the stream until the delimiter is found or max_bytes have been read. + + :param delimiter: the marker to look for in the stream + :param max_bytes: maximum number of bytes that will be read before raising + :exc:`~anyio.DelimiterNotFound` + :return: the bytes read (not including the delimiter) + :raises ~anyio.IncompleteRead: if the stream was closed before the delimiter + was found + :raises ~anyio.DelimiterNotFound: if the delimiter is not found within the + bytes read up to the maximum allowed + + """ + delimiter_size = len(delimiter) + offset = 0 + while True: + # Check if the delimiter can be found in the current buffer + index = self._buffer.find(delimiter, offset) + if index >= 0: + found = self._buffer[:index] + del self._buffer[: index + len(delimiter) :] + return bytes(found) + + # Check if the buffer is already at or over the limit + if len(self._buffer) >= max_bytes: + raise DelimiterNotFound(max_bytes) + + # Read more data into the buffer from the socket + try: + data = await self.receive_stream.receive() + except EndOfStream as exc: + raise IncompleteRead from exc + + # Move the offset forward and add the new data to the buffer + offset = max(len(self._buffer) - delimiter_size + 1, 0) + self._buffer.extend(data) + + +class BufferedByteStream(BufferedByteReceiveStream, ByteStream): + """ + A full-duplex variant of :class:`BufferedByteReceiveStream`. All writes are passed + through to the wrapped stream as-is. + """ + + def __init__(self, stream: AnyByteStream): + """ + :param stream: the stream to be wrapped + + """ + super().__init__(stream) + self._stream = stream + + @override + async def send_eof(self) -> None: + await self._stream.send_eof() + + @override + async def send(self, item: bytes) -> None: + await self._stream.send(item) + + +class BufferedConnectable(ByteStreamConnectable): + """ + Wraps a byte stream connectable to produce :class:`BufferedByteStream` connections. + + Use this when you want the streams returned by :meth:`connect` to have the buffered + receive API (e.g. :meth:`~BufferedByteReceiveStream.receive_exactly` and + :meth:`~BufferedByteReceiveStream.receive_until`). + + :param connectable: the byte stream connectable to wrap + """ + + def __init__(self, connectable: AnyByteStreamConnectable): + """ + :param connectable: the connectable to wrap + + """ + self.connectable = connectable + + @override + async def connect(self) -> BufferedByteStream: + stream = await self.connectable.connect() + return BufferedByteStream(stream) diff --git a/venv/lib/python3.11/site-packages/anyio/streams/file.py b/venv/lib/python3.11/site-packages/anyio/streams/file.py new file mode 100644 index 0000000..79c3d50 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/streams/file.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +__all__ = ( + "FileReadStream", + "FileStreamAttribute", + "FileWriteStream", +) + +from collections.abc import Callable, Mapping +from io import SEEK_SET, UnsupportedOperation +from os import PathLike +from pathlib import Path +from typing import IO, Any + +from .. import ( + BrokenResourceError, + ClosedResourceError, + EndOfStream, + TypedAttributeSet, + to_thread, + typed_attribute, +) +from ..abc import ByteReceiveStream, ByteSendStream + + +class FileStreamAttribute(TypedAttributeSet): + #: the open file descriptor + file: IO[bytes] = typed_attribute() + #: the path of the file on the file system, if available (file must be a real file) + path: Path = typed_attribute() + #: the file number, if available (file must be a real file or a TTY) + fileno: int = typed_attribute() + + +class _BaseFileStream: + def __init__(self, file: IO[bytes]): + self._file = file + + async def aclose(self) -> None: + await to_thread.run_sync(self._file.close) + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + attributes: dict[Any, Callable[[], Any]] = { + FileStreamAttribute.file: lambda: self._file, + } + + if hasattr(self._file, "name"): + attributes[FileStreamAttribute.path] = lambda: Path(self._file.name) + + try: + self._file.fileno() + except UnsupportedOperation: + pass + else: + attributes[FileStreamAttribute.fileno] = lambda: self._file.fileno() + + return attributes + + +class FileReadStream(_BaseFileStream, ByteReceiveStream): + """ + A byte stream that reads from a file in the file system. + + :param file: a file that has been opened for reading in binary mode + + .. versionadded:: 3.0 + """ + + @classmethod + async def from_path(cls, path: str | PathLike[str]) -> FileReadStream: + """ + Create a file read stream by opening the given file. + + :param path: path of the file to read from + + """ + file = await to_thread.run_sync(Path(path).open, "rb") + return cls(file) + + async def receive(self, max_bytes: int = 65536) -> bytes: + try: + data = await to_thread.run_sync(self._file.read, max_bytes) + except ValueError: + raise ClosedResourceError from None + except OSError as exc: + raise BrokenResourceError from exc + + if data: + return data + else: + raise EndOfStream + + async def seek(self, position: int, whence: int = SEEK_SET) -> int: + """ + Seek the file to the given position. + + .. seealso:: :meth:`io.IOBase.seek` + + .. note:: Not all file descriptors are seekable. + + :param position: position to seek the file to + :param whence: controls how ``position`` is interpreted + :return: the new absolute position + :raises OSError: if the file is not seekable + + """ + return await to_thread.run_sync(self._file.seek, position, whence) + + async def tell(self) -> int: + """ + Return the current stream position. + + .. note:: Not all file descriptors are seekable. + + :return: the current absolute position + :raises OSError: if the file is not seekable + + """ + return await to_thread.run_sync(self._file.tell) + + +class FileWriteStream(_BaseFileStream, ByteSendStream): + """ + A byte stream that writes to a file in the file system. + + :param file: a file that has been opened for writing in binary mode + + .. versionadded:: 3.0 + """ + + @classmethod + async def from_path( + cls, path: str | PathLike[str], append: bool = False + ) -> FileWriteStream: + """ + Create a file write stream by opening the given file for writing. + + :param path: path of the file to write to + :param append: if ``True``, open the file for appending; if ``False``, any + existing file at the given path will be truncated + + """ + mode = "ab" if append else "wb" + file = await to_thread.run_sync(Path(path).open, mode) + return cls(file) + + async def send(self, item: bytes) -> None: + try: + await to_thread.run_sync(self._file.write, item) + except ValueError: + raise ClosedResourceError from None + except OSError as exc: + raise BrokenResourceError from exc diff --git a/venv/lib/python3.11/site-packages/anyio/streams/memory.py b/venv/lib/python3.11/site-packages/anyio/streams/memory.py new file mode 100644 index 0000000..a3fa0c3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/streams/memory.py @@ -0,0 +1,325 @@ +from __future__ import annotations + +__all__ = ( + "MemoryObjectReceiveStream", + "MemoryObjectSendStream", + "MemoryObjectStreamStatistics", +) + +import warnings +from collections import OrderedDict, deque +from dataclasses import dataclass, field +from types import TracebackType +from typing import Generic, NamedTuple, TypeVar + +from .. import ( + BrokenResourceError, + ClosedResourceError, + EndOfStream, + WouldBlock, +) +from .._core._testing import TaskInfo, get_current_task +from ..abc import Event, ObjectReceiveStream, ObjectSendStream +from ..lowlevel import checkpoint + +T_Item = TypeVar("T_Item") +T_co = TypeVar("T_co", covariant=True) +T_contra = TypeVar("T_contra", contravariant=True) + + +class MemoryObjectStreamStatistics(NamedTuple): + current_buffer_used: int #: number of items stored in the buffer + #: maximum number of items that can be stored on this stream (or :data:`math.inf`) + max_buffer_size: float + open_send_streams: int #: number of unclosed clones of the send stream + open_receive_streams: int #: number of unclosed clones of the receive stream + #: number of tasks blocked on :meth:`MemoryObjectSendStream.send` + tasks_waiting_send: int + #: number of tasks blocked on :meth:`MemoryObjectReceiveStream.receive` + tasks_waiting_receive: int + + +@dataclass(eq=False) +class _MemoryObjectItemReceiver(Generic[T_Item]): + task_info: TaskInfo = field(init=False, default_factory=get_current_task) + item: T_Item = field(init=False) + + def __repr__(self) -> str: + # When item is not defined, we get following error with default __repr__: + # AttributeError: 'MemoryObjectItemReceiver' object has no attribute 'item' + item = getattr(self, "item", None) + return f"{self.__class__.__name__}(task_info={self.task_info}, item={item!r})" + + +@dataclass(eq=False) +class _MemoryObjectStreamState(Generic[T_Item]): + max_buffer_size: float = field() + buffer: deque[T_Item] = field(init=False, default_factory=deque) + open_send_channels: int = field(init=False, default=0) + open_receive_channels: int = field(init=False, default=0) + waiting_receivers: OrderedDict[Event, _MemoryObjectItemReceiver[T_Item]] = field( + init=False, default_factory=OrderedDict + ) + waiting_senders: OrderedDict[Event, T_Item] = field( + init=False, default_factory=OrderedDict + ) + + def statistics(self) -> MemoryObjectStreamStatistics: + return MemoryObjectStreamStatistics( + len(self.buffer), + self.max_buffer_size, + self.open_send_channels, + self.open_receive_channels, + len(self.waiting_senders), + len(self.waiting_receivers), + ) + + +@dataclass(eq=False) +class MemoryObjectReceiveStream(Generic[T_co], ObjectReceiveStream[T_co]): + _state: _MemoryObjectStreamState[T_co] + _closed: bool = field(init=False, default=False) + + def __post_init__(self) -> None: + self._state.open_receive_channels += 1 + + def receive_nowait(self) -> T_co: + """ + Receive the next item if it can be done without waiting. + + :return: the received item + :raises ~anyio.ClosedResourceError: if this send stream has been closed + :raises ~anyio.EndOfStream: if the buffer is empty and this stream has been + closed from the sending end + :raises ~anyio.WouldBlock: if there are no items in the buffer and no tasks + waiting to send + + """ + if self._closed: + raise ClosedResourceError + + if self._state.waiting_senders: + # Get the item from the next sender + send_event, item = self._state.waiting_senders.popitem(last=False) + self._state.buffer.append(item) + send_event.set() + + if self._state.buffer: + return self._state.buffer.popleft() + elif not self._state.open_send_channels: + raise EndOfStream + + raise WouldBlock + + async def receive(self) -> T_co: + await checkpoint() + try: + return self.receive_nowait() + except WouldBlock: + # Add ourselves in the queue + receive_event = Event() + receiver = _MemoryObjectItemReceiver[T_co]() + self._state.waiting_receivers[receive_event] = receiver + + try: + await receive_event.wait() + finally: + self._state.waiting_receivers.pop(receive_event, None) + + try: + return receiver.item + except AttributeError: + raise EndOfStream from None + + def clone(self) -> MemoryObjectReceiveStream[T_co]: + """ + Create a clone of this receive stream. + + Each clone can be closed separately. Only when all clones have been closed will + the receiving end of the memory stream be considered closed by the sending ends. + + :return: the cloned stream + + """ + if self._closed: + raise ClosedResourceError + + return MemoryObjectReceiveStream(_state=self._state) + + def close(self) -> None: + """ + Close the stream. + + This works the exact same way as :meth:`aclose`, but is provided as a special + case for the benefit of synchronous callbacks. + + """ + if not self._closed: + self._closed = True + self._state.open_receive_channels -= 1 + if self._state.open_receive_channels == 0: + send_events = list(self._state.waiting_senders.keys()) + for event in send_events: + event.set() + + async def aclose(self) -> None: + self.close() + + def statistics(self) -> MemoryObjectStreamStatistics: + """ + Return statistics about the current state of this stream. + + .. versionadded:: 3.0 + """ + return self._state.statistics() + + def __enter__(self) -> MemoryObjectReceiveStream[T_co]: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def __del__(self) -> None: + if not self._closed: + warnings.warn( + f"Unclosed <{self.__class__.__name__} at {id(self):x}>", + ResourceWarning, + stacklevel=1, + source=self, + ) + + +@dataclass(eq=False) +class MemoryObjectSendStream(Generic[T_contra], ObjectSendStream[T_contra]): + _state: _MemoryObjectStreamState[T_contra] + _closed: bool = field(init=False, default=False) + + def __post_init__(self) -> None: + self._state.open_send_channels += 1 + + def send_nowait(self, item: T_contra) -> None: + """ + Send an item immediately if it can be done without waiting. + + :param item: the item to send + :raises ~anyio.ClosedResourceError: if this send stream has been closed + :raises ~anyio.BrokenResourceError: if the stream has been closed from the + receiving end + :raises ~anyio.WouldBlock: if the buffer is full and there are no tasks waiting + to receive + + """ + if self._closed: + raise ClosedResourceError + if not self._state.open_receive_channels: + raise BrokenResourceError + + while self._state.waiting_receivers: + receive_event, receiver = self._state.waiting_receivers.popitem(last=False) + if not receiver.task_info.has_pending_cancellation(): + receiver.item = item + receive_event.set() + return + + if len(self._state.buffer) < self._state.max_buffer_size: + self._state.buffer.append(item) + else: + raise WouldBlock + + async def send(self, item: T_contra) -> None: + """ + Send an item to the stream. + + If the buffer is full, this method blocks until there is again room in the + buffer or the item can be sent directly to a receiver. + + :param item: the item to send + :raises ~anyio.ClosedResourceError: if this send stream has been closed + :raises ~anyio.BrokenResourceError: if the stream has been closed from the + receiving end + + """ + await checkpoint() + try: + self.send_nowait(item) + except WouldBlock: + # Wait until there's someone on the receiving end + send_event = Event() + self._state.waiting_senders[send_event] = item + try: + await send_event.wait() + except BaseException: + self._state.waiting_senders.pop(send_event, None) + raise + + if send_event in self._state.waiting_senders: + del self._state.waiting_senders[send_event] + raise BrokenResourceError from None + + def clone(self) -> MemoryObjectSendStream[T_contra]: + """ + Create a clone of this send stream. + + Each clone can be closed separately. Only when all clones have been closed will + the sending end of the memory stream be considered closed by the receiving ends. + + :return: the cloned stream + + """ + if self._closed: + raise ClosedResourceError + + return MemoryObjectSendStream(_state=self._state) + + def close(self) -> None: + """ + Close the stream. + + This works the exact same way as :meth:`aclose`, but is provided as a special + case for the benefit of synchronous callbacks. + + """ + if not self._closed: + self._closed = True + self._state.open_send_channels -= 1 + if self._state.open_send_channels == 0: + receive_events = list(self._state.waiting_receivers.keys()) + self._state.waiting_receivers.clear() + for event in receive_events: + event.set() + + async def aclose(self) -> None: + self.close() + + def statistics(self) -> MemoryObjectStreamStatistics: + """ + Return statistics about the current state of this stream. + + .. versionadded:: 3.0 + """ + return self._state.statistics() + + def __enter__(self) -> MemoryObjectSendStream[T_contra]: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + self.close() + + def __del__(self) -> None: + if not self._closed: + warnings.warn( + f"Unclosed <{self.__class__.__name__} at {id(self):x}>", + ResourceWarning, + stacklevel=1, + source=self, + ) diff --git a/venv/lib/python3.11/site-packages/anyio/streams/stapled.py b/venv/lib/python3.11/site-packages/anyio/streams/stapled.py new file mode 100644 index 0000000..9248b68 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/streams/stapled.py @@ -0,0 +1,147 @@ +from __future__ import annotations + +__all__ = ( + "MultiListener", + "StapledByteStream", + "StapledObjectStream", +) + +from collections.abc import Callable, Mapping, Sequence +from dataclasses import dataclass +from typing import Any, Generic, TypeVar + +from ..abc import ( + ByteReceiveStream, + ByteSendStream, + ByteStream, + Listener, + ObjectReceiveStream, + ObjectSendStream, + ObjectStream, + TaskGroup, +) + +T_Item = TypeVar("T_Item") +T_Stream = TypeVar("T_Stream") + + +@dataclass(eq=False) +class StapledByteStream(ByteStream): + """ + Combines two byte streams into a single, bidirectional byte stream. + + Extra attributes will be provided from both streams, with the receive stream + providing the values in case of a conflict. + + :param ByteSendStream send_stream: the sending byte stream + :param ByteReceiveStream receive_stream: the receiving byte stream + """ + + send_stream: ByteSendStream + receive_stream: ByteReceiveStream + + async def receive(self, max_bytes: int = 65536) -> bytes: + return await self.receive_stream.receive(max_bytes) + + async def send(self, item: bytes) -> None: + await self.send_stream.send(item) + + async def send_eof(self) -> None: + await self.send_stream.aclose() + + async def aclose(self) -> None: + await self.send_stream.aclose() + await self.receive_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self.send_stream.extra_attributes, + **self.receive_stream.extra_attributes, + } + + +@dataclass(eq=False) +class StapledObjectStream(Generic[T_Item], ObjectStream[T_Item]): + """ + Combines two object streams into a single, bidirectional object stream. + + Extra attributes will be provided from both streams, with the receive stream + providing the values in case of a conflict. + + :param ObjectSendStream send_stream: the sending object stream + :param ObjectReceiveStream receive_stream: the receiving object stream + """ + + send_stream: ObjectSendStream[T_Item] + receive_stream: ObjectReceiveStream[T_Item] + + async def receive(self) -> T_Item: + return await self.receive_stream.receive() + + async def send(self, item: T_Item) -> None: + await self.send_stream.send(item) + + async def send_eof(self) -> None: + await self.send_stream.aclose() + + async def aclose(self) -> None: + await self.send_stream.aclose() + await self.receive_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self.send_stream.extra_attributes, + **self.receive_stream.extra_attributes, + } + + +@dataclass(eq=False) +class MultiListener(Generic[T_Stream], Listener[T_Stream]): + """ + Combines multiple listeners into one, serving connections from all of them at once. + + Any MultiListeners in the given collection of listeners will have their listeners + moved into this one. + + Extra attributes are provided from each listener, with each successive listener + overriding any conflicting attributes from the previous one. + + :param listeners: listeners to serve + :type listeners: Sequence[Listener[T_Stream]] + """ + + listeners: Sequence[Listener[T_Stream]] + + def __post_init__(self) -> None: + listeners: list[Listener[T_Stream]] = [] + for listener in self.listeners: + if isinstance(listener, MultiListener): + listeners.extend(listener.listeners) + del listener.listeners[:] # type: ignore[attr-defined] + else: + listeners.append(listener) + + self.listeners = listeners + + async def serve( + self, handler: Callable[[T_Stream], Any], task_group: TaskGroup | None = None + ) -> None: + from .. import create_task_group + + async with create_task_group() as tg: + for listener in self.listeners: + tg.start_soon(listener.serve, handler, task_group) + + async def aclose(self) -> None: + for listener in self.listeners: + await listener.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + attributes: dict = {} + for listener in self.listeners: + attributes.update(listener.extra_attributes) + + return attributes diff --git a/venv/lib/python3.11/site-packages/anyio/streams/text.py b/venv/lib/python3.11/site-packages/anyio/streams/text.py new file mode 100644 index 0000000..296cd25 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/streams/text.py @@ -0,0 +1,176 @@ +from __future__ import annotations + +__all__ = ( + "TextConnectable", + "TextReceiveStream", + "TextSendStream", + "TextStream", +) + +import codecs +import sys +from collections.abc import Callable, Mapping +from dataclasses import InitVar, dataclass, field +from typing import Any + +from ..abc import ( + AnyByteReceiveStream, + AnyByteSendStream, + AnyByteStream, + AnyByteStreamConnectable, + ObjectReceiveStream, + ObjectSendStream, + ObjectStream, + ObjectStreamConnectable, +) + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + + +@dataclass(eq=False) +class TextReceiveStream(ObjectReceiveStream[str]): + """ + Stream wrapper that decodes bytes to strings using the given encoding. + + Decoding is done using :class:`~codecs.IncrementalDecoder` which returns any + completely received unicode characters as soon as they come in. + + :param transport_stream: any bytes-based receive stream + :param encoding: character encoding to use for decoding bytes to strings (defaults + to ``utf-8``) + :param errors: handling scheme for decoding errors (defaults to ``strict``; see the + `codecs module documentation`_ for a comprehensive list of options) + + .. _codecs module documentation: + https://docs.python.org/3/library/codecs.html#codec-objects + """ + + transport_stream: AnyByteReceiveStream + encoding: InitVar[str] = "utf-8" + errors: InitVar[str] = "strict" + _decoder: codecs.IncrementalDecoder = field(init=False) + + def __post_init__(self, encoding: str, errors: str) -> None: + decoder_class = codecs.getincrementaldecoder(encoding) + self._decoder = decoder_class(errors=errors) + + async def receive(self) -> str: + while True: + chunk = await self.transport_stream.receive() + decoded = self._decoder.decode(chunk) + if decoded: + return decoded + + async def aclose(self) -> None: + await self.transport_stream.aclose() + self._decoder.reset() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return self.transport_stream.extra_attributes + + +@dataclass(eq=False) +class TextSendStream(ObjectSendStream[str]): + """ + Sends strings to the wrapped stream as bytes using the given encoding. + + :param AnyByteSendStream transport_stream: any bytes-based send stream + :param str encoding: character encoding to use for encoding strings to bytes + (defaults to ``utf-8``) + :param str errors: handling scheme for encoding errors (defaults to ``strict``; see + the `codecs module documentation`_ for a comprehensive list of options) + + .. _codecs module documentation: + https://docs.python.org/3/library/codecs.html#codec-objects + """ + + transport_stream: AnyByteSendStream + encoding: InitVar[str] = "utf-8" + errors: str = "strict" + _encoder: Callable[..., tuple[bytes, int]] = field(init=False) + + def __post_init__(self, encoding: str) -> None: + self._encoder = codecs.getencoder(encoding) + + async def send(self, item: str) -> None: + encoded = self._encoder(item, self.errors)[0] + await self.transport_stream.send(encoded) + + async def aclose(self) -> None: + await self.transport_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return self.transport_stream.extra_attributes + + +@dataclass(eq=False) +class TextStream(ObjectStream[str]): + """ + A bidirectional stream that decodes bytes to strings on receive and encodes strings + to bytes on send. + + Extra attributes will be provided from both streams, with the receive stream + providing the values in case of a conflict. + + :param AnyByteStream transport_stream: any bytes-based stream + :param str encoding: character encoding to use for encoding/decoding strings to/from + bytes (defaults to ``utf-8``) + :param str errors: handling scheme for encoding errors (defaults to ``strict``; see + the `codecs module documentation`_ for a comprehensive list of options) + + .. _codecs module documentation: + https://docs.python.org/3/library/codecs.html#codec-objects + """ + + transport_stream: AnyByteStream + encoding: InitVar[str] = "utf-8" + errors: InitVar[str] = "strict" + _receive_stream: TextReceiveStream = field(init=False) + _send_stream: TextSendStream = field(init=False) + + def __post_init__(self, encoding: str, errors: str) -> None: + self._receive_stream = TextReceiveStream( + self.transport_stream, encoding=encoding, errors=errors + ) + self._send_stream = TextSendStream( + self.transport_stream, encoding=encoding, errors=errors + ) + + async def receive(self) -> str: + return await self._receive_stream.receive() + + async def send(self, item: str) -> None: + await self._send_stream.send(item) + + async def send_eof(self) -> None: + await self.transport_stream.send_eof() + + async def aclose(self) -> None: + await self._send_stream.aclose() + await self._receive_stream.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self._send_stream.extra_attributes, + **self._receive_stream.extra_attributes, + } + + +class TextConnectable(ObjectStreamConnectable[str]): + def __init__(self, connectable: AnyByteStreamConnectable): + """ + :param connectable: the bytestream endpoint to wrap + + """ + self.connectable = connectable + + @override + async def connect(self) -> TextStream: + stream = await self.connectable.connect() + return TextStream(stream) diff --git a/venv/lib/python3.11/site-packages/anyio/streams/tls.py b/venv/lib/python3.11/site-packages/anyio/streams/tls.py new file mode 100644 index 0000000..e2a7ca5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/streams/tls.py @@ -0,0 +1,421 @@ +from __future__ import annotations + +__all__ = ( + "TLSAttribute", + "TLSConnectable", + "TLSListener", + "TLSStream", +) + +import logging +import re +import ssl +import sys +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from functools import wraps +from ssl import SSLContext +from typing import Any, TypeAlias, TypeVar + +from .. import ( + BrokenResourceError, + EndOfStream, + aclose_forcefully, + get_cancelled_exc_class, + to_thread, +) +from .._core._typedattr import TypedAttributeSet, typed_attribute +from ..abc import ( + AnyByteStream, + AnyByteStreamConnectable, + ByteStream, + ByteStreamConnectable, + Listener, + TaskGroup, +) + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if sys.version_info >= (3, 12): + from typing import override +else: + from typing_extensions import override + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") +_PCTRTT: TypeAlias = tuple[tuple[str, str], ...] +_PCTRTTT: TypeAlias = tuple[_PCTRTT, ...] + + +class TLSAttribute(TypedAttributeSet): + """Contains Transport Layer Security related attributes.""" + + #: the selected ALPN protocol + alpn_protocol: str | None = typed_attribute() + #: the channel binding for type ``tls-unique`` + channel_binding_tls_unique: bytes = typed_attribute() + #: the selected cipher + cipher: tuple[str, str, int] = typed_attribute() + #: the peer certificate in dictionary form (see :meth:`ssl.SSLSocket.getpeercert` + # for more information) + peer_certificate: None | (dict[str, str | _PCTRTTT | _PCTRTT]) = typed_attribute() + #: the peer certificate in binary form + peer_certificate_binary: bytes | None = typed_attribute() + #: ``True`` if this is the server side of the connection + server_side: bool = typed_attribute() + #: ciphers shared by the client during the TLS handshake (``None`` if this is the + #: client side) + shared_ciphers: list[tuple[str, str, int]] | None = typed_attribute() + #: the :class:`~ssl.SSLObject` used for encryption + ssl_object: ssl.SSLObject = typed_attribute() + #: ``True`` if this stream does (and expects) a closing TLS handshake when the + #: stream is being closed + standard_compatible: bool = typed_attribute() + #: the TLS protocol version (e.g. ``TLSv1.2``) + tls_version: str = typed_attribute() + + +@dataclass(eq=False) +class TLSStream(ByteStream): + """ + A stream wrapper that encrypts all sent data and decrypts received data. + + This class has no public initializer; use :meth:`wrap` instead. + All extra attributes from :class:`~TLSAttribute` are supported. + + :var AnyByteStream transport_stream: the wrapped stream + + """ + + transport_stream: AnyByteStream + standard_compatible: bool + _ssl_object: ssl.SSLObject + _read_bio: ssl.MemoryBIO + _write_bio: ssl.MemoryBIO + + @classmethod + async def wrap( + cls, + transport_stream: AnyByteStream, + *, + server_side: bool | None = None, + hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + standard_compatible: bool = True, + ) -> TLSStream: + """ + Wrap an existing stream with Transport Layer Security. + + This performs a TLS handshake with the peer. + + :param transport_stream: a bytes-transporting stream to wrap + :param server_side: ``True`` if this is the server side of the connection, + ``False`` if this is the client side (if omitted, will be set to ``False`` + if ``hostname`` has been provided, ``False`` otherwise). Used only to create + a default context when an explicit context has not been provided. + :param hostname: host name of the peer (if host name checking is desired) + :param ssl_context: the SSLContext object to use (if not provided, a secure + default will be created) + :param standard_compatible: if ``False``, skip the closing handshake when + closing the connection, and don't raise an exception if the peer does the + same + :raises ~ssl.SSLError: if the TLS handshake fails + + """ + if server_side is None: + server_side = not hostname + + if not ssl_context: + purpose = ( + ssl.Purpose.CLIENT_AUTH if server_side else ssl.Purpose.SERVER_AUTH + ) + ssl_context = ssl.create_default_context(purpose) + + # Re-enable detection of unexpected EOFs if it was disabled by Python + if hasattr(ssl, "OP_IGNORE_UNEXPECTED_EOF"): + ssl_context.options &= ~ssl.OP_IGNORE_UNEXPECTED_EOF + + bio_in = ssl.MemoryBIO() + bio_out = ssl.MemoryBIO() + + # External SSLContext implementations may do blocking I/O in wrap_bio(), + # but the standard library implementation won't + if type(ssl_context) is ssl.SSLContext: + ssl_object = ssl_context.wrap_bio( + bio_in, bio_out, server_side=server_side, server_hostname=hostname + ) + else: + ssl_object = await to_thread.run_sync( + ssl_context.wrap_bio, + bio_in, + bio_out, + server_side, + hostname, + None, + ) + + wrapper = cls( + transport_stream=transport_stream, + standard_compatible=standard_compatible, + _ssl_object=ssl_object, + _read_bio=bio_in, + _write_bio=bio_out, + ) + await wrapper._call_sslobject_method(ssl_object.do_handshake) + return wrapper + + async def _call_sslobject_method( + self, func: Callable[[Unpack[PosArgsT]], T_Retval], *args: Unpack[PosArgsT] + ) -> T_Retval: + while True: + try: + result = func(*args) + except ssl.SSLWantReadError: + try: + # Flush any pending writes first + if self._write_bio.pending: + await self.transport_stream.send(self._write_bio.read()) + + data = await self.transport_stream.receive() + except EndOfStream: + self._read_bio.write_eof() + except OSError as exc: + self._read_bio.write_eof() + self._write_bio.write_eof() + raise BrokenResourceError from exc + else: + self._read_bio.write(data) + except ssl.SSLWantWriteError: + await self.transport_stream.send(self._write_bio.read()) + except ssl.SSLSyscallError as exc: + self._read_bio.write_eof() + self._write_bio.write_eof() + raise BrokenResourceError from exc + except ssl.SSLError as exc: + self._read_bio.write_eof() + self._write_bio.write_eof() + if isinstance(exc, ssl.SSLEOFError) or ( + exc.strerror and "UNEXPECTED_EOF_WHILE_READING" in exc.strerror + ): + if self.standard_compatible: + raise BrokenResourceError from exc + else: + raise EndOfStream from None + + raise + else: + # Flush any pending writes first + if self._write_bio.pending: + await self.transport_stream.send(self._write_bio.read()) + + return result + + async def unwrap(self) -> tuple[AnyByteStream, bytes]: + """ + Does the TLS closing handshake. + + :return: a tuple of (wrapped byte stream, bytes left in the read buffer) + + """ + await self._call_sslobject_method(self._ssl_object.unwrap) + self._read_bio.write_eof() + self._write_bio.write_eof() + return self.transport_stream, self._read_bio.read() + + async def aclose(self) -> None: + if self.standard_compatible: + try: + await self.unwrap() + except BaseException: + await aclose_forcefully(self.transport_stream) + raise + + await self.transport_stream.aclose() + + async def receive(self, max_bytes: int = 65536) -> bytes: + data = await self._call_sslobject_method(self._ssl_object.read, max_bytes) + if not data: + raise EndOfStream + + return data + + async def send(self, item: bytes) -> None: + await self._call_sslobject_method(self._ssl_object.write, item) + + async def send_eof(self) -> None: + tls_version = self.extra(TLSAttribute.tls_version) + match = re.match(r"TLSv(\d+)(?:\.(\d+))?", tls_version) + if match: + major, minor = int(match.group(1)), int(match.group(2) or 0) + if (major, minor) < (1, 3): + raise NotImplementedError( + f"send_eof() requires at least TLSv1.3; current " + f"session uses {tls_version}" + ) + + raise NotImplementedError( + "send_eof() has not yet been implemented for TLS streams" + ) + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + **self.transport_stream.extra_attributes, + TLSAttribute.alpn_protocol: self._ssl_object.selected_alpn_protocol, + TLSAttribute.channel_binding_tls_unique: ( + self._ssl_object.get_channel_binding + ), + TLSAttribute.cipher: self._ssl_object.cipher, + TLSAttribute.peer_certificate: lambda: self._ssl_object.getpeercert(False), + TLSAttribute.peer_certificate_binary: lambda: self._ssl_object.getpeercert( + True + ), + TLSAttribute.server_side: lambda: self._ssl_object.server_side, + TLSAttribute.shared_ciphers: lambda: ( + self._ssl_object.shared_ciphers() + if self._ssl_object.server_side + else None + ), + TLSAttribute.standard_compatible: lambda: self.standard_compatible, + TLSAttribute.ssl_object: lambda: self._ssl_object, + TLSAttribute.tls_version: self._ssl_object.version, + } + + +@dataclass(eq=False) +class TLSListener(Listener[TLSStream]): + """ + A convenience listener that wraps another listener and auto-negotiates a TLS session + on every accepted connection. + + If the TLS handshake times out or raises an exception, + :meth:`handle_handshake_error` is called to do whatever post-mortem processing is + deemed necessary. + + Supports only the :attr:`~TLSAttribute.standard_compatible` extra attribute. + + :param Listener listener: the listener to wrap + :param ssl_context: the SSL context object + :param standard_compatible: a flag passed through to :meth:`TLSStream.wrap` + :param handshake_timeout: time limit for the TLS handshake + (passed to :func:`~anyio.fail_after`) + """ + + listener: Listener[Any] + ssl_context: ssl.SSLContext + standard_compatible: bool = True + handshake_timeout: float = 30 + + @staticmethod + async def handle_handshake_error(exc: BaseException, stream: AnyByteStream) -> None: + """ + Handle an exception raised during the TLS handshake. + + This method does 3 things: + + #. Forcefully closes the original stream + #. Logs the exception (unless it was a cancellation exception) using the + ``anyio.streams.tls`` logger + #. Reraises the exception if it was a base exception or a cancellation exception + + :param exc: the exception + :param stream: the original stream + + """ + await aclose_forcefully(stream) + + # Log all except cancellation exceptions + if not isinstance(exc, get_cancelled_exc_class()): + # CPython (as of 3.11.5) returns incorrect `sys.exc_info()` here when using + # any asyncio implementation, so we explicitly pass the exception to log + # (https://github.com/python/cpython/issues/108668). Trio does not have this + # issue because it works around the CPython bug. + logging.getLogger(__name__).exception( + "Error during TLS handshake", exc_info=exc + ) + + # Only reraise base exceptions and cancellation exceptions + if not isinstance(exc, Exception) or isinstance(exc, get_cancelled_exc_class()): + raise + + async def serve( + self, + handler: Callable[[TLSStream], Any], + task_group: TaskGroup | None = None, + ) -> None: + @wraps(handler) + async def handler_wrapper(stream: AnyByteStream) -> None: + from .. import fail_after + + try: + with fail_after(self.handshake_timeout): + wrapped_stream = await TLSStream.wrap( + stream, + ssl_context=self.ssl_context, + standard_compatible=self.standard_compatible, + ) + except BaseException as exc: + await self.handle_handshake_error(exc, stream) + else: + await handler(wrapped_stream) + + await self.listener.serve(handler_wrapper, task_group) + + async def aclose(self) -> None: + await self.listener.aclose() + + @property + def extra_attributes(self) -> Mapping[Any, Callable[[], Any]]: + return { + TLSAttribute.standard_compatible: lambda: self.standard_compatible, + } + + +class TLSConnectable(ByteStreamConnectable): + """ + Wraps another connectable and does TLS negotiation after a successful connection. + + :param connectable: the connectable to wrap + :param hostname: host name of the server (if host name checking is desired) + :param ssl_context: the SSLContext object to use (if not provided, a secure default + will be created) + :param standard_compatible: if ``False``, skip the closing handshake when closing + the connection, and don't raise an exception if the server does the same + """ + + def __init__( + self, + connectable: AnyByteStreamConnectable, + *, + hostname: str | None = None, + ssl_context: ssl.SSLContext | None = None, + standard_compatible: bool = True, + ) -> None: + self.connectable = connectable + self.ssl_context: SSLContext = ssl_context or ssl.create_default_context( + ssl.Purpose.SERVER_AUTH + ) + if not isinstance(self.ssl_context, ssl.SSLContext): + raise TypeError( + "ssl_context must be an instance of ssl.SSLContext, not " + f"{type(self.ssl_context).__name__}" + ) + self.hostname = hostname + self.standard_compatible = standard_compatible + + @override + async def connect(self) -> TLSStream: + stream = await self.connectable.connect() + try: + return await TLSStream.wrap( + stream, + hostname=self.hostname, + ssl_context=self.ssl_context, + standard_compatible=self.standard_compatible, + ) + except BaseException: + await aclose_forcefully(stream) + raise diff --git a/venv/lib/python3.11/site-packages/anyio/to_interpreter.py b/venv/lib/python3.11/site-packages/anyio/to_interpreter.py new file mode 100644 index 0000000..694dbe7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/to_interpreter.py @@ -0,0 +1,246 @@ +from __future__ import annotations + +__all__ = ( + "run_sync", + "current_default_interpreter_limiter", +) + +import atexit +import os +import sys +from collections import deque +from collections.abc import Callable +from typing import Any, Final, TypeVar + +from . import current_time, to_thread +from ._core._exceptions import BrokenWorkerInterpreter +from ._core._synchronization import CapacityLimiter +from .lowlevel import RunVar + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +if sys.version_info >= (3, 14): + from concurrent.interpreters import ExecutionFailed, create + + def _interp_call( + func: Callable[..., Any], args: tuple[Any, ...] + ) -> tuple[Any, bool]: + try: + retval = func(*args) + except BaseException as exc: + return exc, True + else: + return retval, False + + class _Worker: + last_used: float = 0 + + def __init__(self) -> None: + self._interpreter = create() + + def destroy(self) -> None: + self._interpreter.close() + + def call( + self, + func: Callable[..., T_Retval], + args: tuple[Any, ...], + ) -> T_Retval: + try: + res, is_exception = self._interpreter.call(_interp_call, func, args) + except ExecutionFailed as exc: + raise BrokenWorkerInterpreter(exc.excinfo) from exc + + if is_exception: + raise res + + return res +elif sys.version_info >= (3, 13): + import _interpqueues + import _interpreters + + UNBOUND: Final = 2 # I have no clue how this works, but it was used in the stdlib + FMT_UNPICKLED: Final = 0 + FMT_PICKLED: Final = 1 + QUEUE_PICKLE_ARGS: Final = (FMT_PICKLED, UNBOUND) + QUEUE_UNPICKLE_ARGS: Final = (FMT_UNPICKLED, UNBOUND) + + _run_func = compile( + """ +import _interpqueues +from _interpreters import NotShareableError +from pickle import loads, dumps, HIGHEST_PROTOCOL + +QUEUE_PICKLE_ARGS = (1, 2) +QUEUE_UNPICKLE_ARGS = (0, 2) + +item = _interpqueues.get(queue_id)[0] +try: + func, args = loads(item) + retval = func(*args) +except BaseException as exc: + is_exception = True + retval = exc +else: + is_exception = False + +try: + _interpqueues.put(queue_id, (retval, is_exception), *QUEUE_UNPICKLE_ARGS) +except NotShareableError: + retval = dumps(retval, HIGHEST_PROTOCOL) + _interpqueues.put(queue_id, (retval, is_exception), *QUEUE_PICKLE_ARGS) + """, + "", + "exec", + ) + + class _Worker: + last_used: float = 0 + + def __init__(self) -> None: + self._interpreter_id = _interpreters.create() + self._queue_id = _interpqueues.create(1, *QUEUE_UNPICKLE_ARGS) + _interpreters.set___main___attrs( + self._interpreter_id, {"queue_id": self._queue_id} + ) + + def destroy(self) -> None: + _interpqueues.destroy(self._queue_id) + _interpreters.destroy(self._interpreter_id) + + def call( + self, + func: Callable[..., T_Retval], + args: tuple[Any, ...], + ) -> T_Retval: + import pickle + + item = pickle.dumps((func, args), pickle.HIGHEST_PROTOCOL) + _interpqueues.put(self._queue_id, item, *QUEUE_PICKLE_ARGS) + exc_info = _interpreters.exec(self._interpreter_id, _run_func) + if exc_info: + raise BrokenWorkerInterpreter(exc_info) + + res = _interpqueues.get(self._queue_id) + (res, is_exception), fmt = res[:2] + if fmt == FMT_PICKLED: + res = pickle.loads(res) + + if is_exception: + raise res + + return res +else: + + class _Worker: + last_used: float = 0 + + def __init__(self) -> None: + raise RuntimeError("subinterpreters require at least Python 3.13") + + def call( + self, + func: Callable[..., T_Retval], + args: tuple[Any, ...], + ) -> T_Retval: + raise NotImplementedError + + def destroy(self) -> None: + pass + + +DEFAULT_CPU_COUNT: Final = 8 # this is just an arbitrarily selected value +MAX_WORKER_IDLE_TIME = ( + 30 # seconds a subinterpreter can be idle before becoming eligible for pruning +) + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + +_idle_workers = RunVar[deque[_Worker]]("_available_workers") +_default_interpreter_limiter = RunVar[CapacityLimiter]("_default_interpreter_limiter") + + +def _stop_workers(workers: deque[_Worker]) -> None: + for worker in workers: + worker.destroy() + + workers.clear() + + +async def run_sync( + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + limiter: CapacityLimiter | None = None, +) -> T_Retval: + """ + Call the given function with the given arguments in a subinterpreter. + + .. warning:: On Python 3.13, the :mod:`concurrent.interpreters` module was not yet + available, so the code path for that Python version relies on an undocumented, + private API. As such, it is recommended to not rely on this function for anything + mission-critical on Python 3.13. + + :param func: a callable + :param args: the positional arguments for the callable + :param limiter: capacity limiter to use to limit the total number of subinterpreters + running (if omitted, the default limiter is used) + :return: the result of the call + :raises BrokenWorkerInterpreter: if there's an internal error in a subinterpreter + + """ + if limiter is None: + limiter = current_default_interpreter_limiter() + + try: + idle_workers = _idle_workers.get() + except LookupError: + idle_workers = deque() + _idle_workers.set(idle_workers) + atexit.register(_stop_workers, idle_workers) + + async with limiter: + try: + worker = idle_workers.pop() + except IndexError: + worker = _Worker() + + try: + return await to_thread.run_sync( + worker.call, + func, + args, + limiter=limiter, + ) + finally: + # Prune workers that have been idle for too long + now = current_time() + while idle_workers: + if now - idle_workers[0].last_used <= MAX_WORKER_IDLE_TIME: + break + + await to_thread.run_sync(idle_workers.popleft().destroy, limiter=limiter) + + worker.last_used = current_time() + idle_workers.append(worker) + + +def current_default_interpreter_limiter() -> CapacityLimiter: + """ + Return the capacity limiter used by default to limit the number of concurrently + running subinterpreters. + + Defaults to the number of CPU cores. + + :return: a capacity limiter object + + """ + try: + return _default_interpreter_limiter.get() + except LookupError: + limiter = CapacityLimiter(os.cpu_count() or DEFAULT_CPU_COUNT) + _default_interpreter_limiter.set(limiter) + return limiter diff --git a/venv/lib/python3.11/site-packages/anyio/to_process.py b/venv/lib/python3.11/site-packages/anyio/to_process.py new file mode 100644 index 0000000..fd65b18 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/to_process.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +__all__ = ( + "current_default_process_limiter", + "process_worker", + "run_sync", +) + +import os +import pickle +import runpy +import subprocess +import sys +from collections import deque +from collections.abc import Callable +from types import ModuleType +from typing import TypeVar, cast + +from ._core._eventloop import current_time, get_async_backend, get_cancelled_exc_class +from ._core._exceptions import BrokenWorkerProcess +from ._core._subprocesses import open_process +from ._core._synchronization import CapacityLimiter +from ._core._tasks import CancelScope, fail_after +from .abc import ByteReceiveStream, ByteSendStream, Process +from .lowlevel import RunVar, checkpoint_if_cancelled +from .streams.buffered import BufferedByteReceiveStream + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +WORKER_MAX_IDLE_TIME = 300 # 5 minutes + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + +_process_pool_workers: RunVar[set[Process]] = RunVar("_process_pool_workers") +_process_pool_idle_workers: RunVar[deque[tuple[Process, float]]] = RunVar( + "_process_pool_idle_workers" +) +_default_process_limiter: RunVar[CapacityLimiter] = RunVar("_default_process_limiter") + + +async def run_sync( # type: ignore[return] + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + cancellable: bool = False, + limiter: CapacityLimiter | None = None, +) -> T_Retval: + """ + Call the given function with the given arguments in a worker process. + + If the ``cancellable`` option is enabled and the task waiting for its completion is + cancelled, the worker process running it will be abruptly terminated using SIGKILL + (or ``terminateProcess()`` on Windows). + + :param func: a callable + :param args: positional arguments for the callable + :param cancellable: ``True`` to allow cancellation of the operation while it's + running + :param limiter: capacity limiter to use to limit the total amount of processes + running (if omitted, the default limiter is used) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + :return: an awaitable that yields the return value of the function. + + """ + + async def send_raw_command(pickled_cmd: bytes) -> object: + try: + await stdin.send(pickled_cmd) + response = await buffered.receive_until(b"\n", 50) + status, length = response.split(b" ") + if status not in (b"RETURN", b"EXCEPTION"): + raise RuntimeError( + f"Worker process returned unexpected response: {response!r}" + ) + + pickled_response = await buffered.receive_exactly(int(length)) + except BaseException as exc: + workers.discard(process) + try: + process.kill() + with CancelScope(shield=True): + await process.aclose() + except ProcessLookupError: + pass + + if isinstance(exc, get_cancelled_exc_class()): + raise + else: + raise BrokenWorkerProcess from exc + + retval = pickle.loads(pickled_response) + if status == b"EXCEPTION": + assert isinstance(retval, BaseException) + raise retval + else: + return retval + + # First pickle the request before trying to reserve a worker process + await checkpoint_if_cancelled() + request = pickle.dumps(("run", func, args), protocol=pickle.HIGHEST_PROTOCOL) + + # If this is the first run in this event loop thread, set up the necessary variables + try: + workers = _process_pool_workers.get() + idle_workers = _process_pool_idle_workers.get() + except LookupError: + workers = set() + idle_workers = deque() + _process_pool_workers.set(workers) + _process_pool_idle_workers.set(idle_workers) + get_async_backend().setup_process_pool_exit_at_shutdown(workers) + + async with limiter or current_default_process_limiter(): + # Pop processes from the pool (starting from the most recently used) until we + # find one that hasn't exited yet + process: Process + while idle_workers: + process, idle_since = idle_workers.pop() + if process.returncode is None: + stdin = cast(ByteSendStream, process.stdin) + buffered = BufferedByteReceiveStream( + cast(ByteReceiveStream, process.stdout) + ) + + # Prune any other workers that have been idle for WORKER_MAX_IDLE_TIME + # seconds or longer + now = current_time() + killed_processes: list[Process] = [] + while idle_workers: + if now - idle_workers[0][1] < WORKER_MAX_IDLE_TIME: + break + + process_to_kill, idle_since = idle_workers.popleft() + process_to_kill.kill() + workers.remove(process_to_kill) + killed_processes.append(process_to_kill) + + with CancelScope(shield=True): + for killed_process in killed_processes: + await killed_process.aclose() + + break + + workers.remove(process) + else: + command = [sys.executable, "-u", "-m", __name__] + process = await open_process( + command, stdin=subprocess.PIPE, stdout=subprocess.PIPE + ) + try: + stdin = cast(ByteSendStream, process.stdin) + buffered = BufferedByteReceiveStream( + cast(ByteReceiveStream, process.stdout) + ) + with fail_after(20): + message = await buffered.receive(6) + + if message != b"READY\n": + raise BrokenWorkerProcess( + f"Worker process returned unexpected response: {message!r}" + ) + + main_module_path = getattr(sys.modules["__main__"], "__file__", None) + pickled = pickle.dumps( + ("init", sys.path, main_module_path), + protocol=pickle.HIGHEST_PROTOCOL, + ) + await send_raw_command(pickled) + except (BrokenWorkerProcess, get_cancelled_exc_class()): + raise + except BaseException as exc: + process.kill() + raise BrokenWorkerProcess( + "Error during worker process initialization" + ) from exc + + workers.add(process) + + with CancelScope(shield=not cancellable): + try: + return cast(T_Retval, await send_raw_command(request)) + finally: + if process in workers: + idle_workers.append((process, current_time())) + + +def current_default_process_limiter() -> CapacityLimiter: + """ + Return the capacity limiter that is used by default to limit the number of worker + processes. + + :return: a capacity limiter object + + """ + try: + return _default_process_limiter.get() + except LookupError: + limiter = CapacityLimiter(os.cpu_count() or 2) + _default_process_limiter.set(limiter) + return limiter + + +def process_worker() -> None: + # Redirect standard streams to os.devnull so that user code won't interfere with the + # parent-worker communication + stdin = sys.stdin + stdout = sys.stdout + sys.stdin = open(os.devnull) + sys.stdout = open(os.devnull, "w") + + stdout.buffer.write(b"READY\n") + while True: + retval = exception = None + try: + command, *args = pickle.load(stdin.buffer) + except EOFError: + return + except BaseException as exc: + exception = exc + else: + if command == "run": + func, args = args + try: + retval = func(*args) + except BaseException as exc: + exception = exc + elif command == "init": + main_module_path: str | None + sys.path, main_module_path = args + del sys.modules["__main__"] + if main_module_path and os.path.isfile(main_module_path): + # Load the parent's main module but as __mp_main__ instead of + # __main__ (like multiprocessing does) to avoid infinite recursion + try: + main = ModuleType("__mp_main__") + main_content = runpy.run_path( + main_module_path, run_name="__mp_main__" + ) + main.__dict__.update(main_content) + sys.modules["__main__"] = sys.modules["__mp_main__"] = main + except BaseException as exc: + exception = exc + try: + if exception is not None: + status = b"EXCEPTION" + pickled = pickle.dumps(exception, pickle.HIGHEST_PROTOCOL) + else: + status = b"RETURN" + pickled = pickle.dumps(retval, pickle.HIGHEST_PROTOCOL) + except BaseException as exc: + exception = exc + status = b"EXCEPTION" + pickled = pickle.dumps(exc, pickle.HIGHEST_PROTOCOL) + + stdout.buffer.write(b"%s %d\n" % (status, len(pickled))) + stdout.buffer.write(pickled) + + # Respect SIGTERM + if isinstance(exception, SystemExit): + raise exception + + +if __name__ == "__main__": + process_worker() diff --git a/venv/lib/python3.11/site-packages/anyio/to_thread.py b/venv/lib/python3.11/site-packages/anyio/to_thread.py new file mode 100644 index 0000000..83c79d1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/anyio/to_thread.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +__all__ = ( + "run_sync", + "current_default_thread_limiter", +) + +import sys +from collections.abc import Callable +from typing import TypeVar +from warnings import warn + +from ._core._eventloop import get_async_backend +from .abc import CapacityLimiter + +if sys.version_info >= (3, 11): + from typing import TypeVarTuple, Unpack +else: + from typing_extensions import TypeVarTuple, Unpack + +T_Retval = TypeVar("T_Retval") +PosArgsT = TypeVarTuple("PosArgsT") + + +async def run_sync( + func: Callable[[Unpack[PosArgsT]], T_Retval], + *args: Unpack[PosArgsT], + abandon_on_cancel: bool = False, + cancellable: bool | None = None, + limiter: CapacityLimiter | None = None, +) -> T_Retval: + """ + Call the given function with the given arguments in a worker thread. + + If the ``abandon_on_cancel`` option is enabled and the task waiting for its + completion is cancelled, the thread will still run its course but its + return value (or any raised exception) will be ignored. + + :param func: a callable + :param args: positional arguments for the callable + :param abandon_on_cancel: ``True`` to abandon the thread (leaving it to run + unchecked on own) if the host task is cancelled, ``False`` to ignore + cancellations in the host task until the operation has completed in the worker + thread + :param cancellable: deprecated alias of ``abandon_on_cancel``; will override + ``abandon_on_cancel`` if both parameters are passed + :param limiter: capacity limiter to use to limit the total amount of threads running + (if omitted, the default limiter is used) + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + :return: an awaitable that yields the return value of the function. + + """ + if cancellable is not None: + abandon_on_cancel = cancellable + warn( + "The `cancellable=` keyword argument to `anyio.to_thread.run_sync` is " + "deprecated since AnyIO 4.1.0; use `abandon_on_cancel=` instead", + DeprecationWarning, + stacklevel=2, + ) + + return await get_async_backend().run_sync_in_worker_thread( + func, args, abandon_on_cancel=abandon_on_cancel, limiter=limiter + ) + + +def current_default_thread_limiter() -> CapacityLimiter: + """ + Return the capacity limiter that is used by default to limit the number of + concurrent threads. + + :return: a capacity limiter object + :raises NoEventLoopError: if no supported asynchronous event loop is running in the + current thread + + """ + return get_async_backend().current_default_thread_limiter() diff --git a/venv/lib/python3.11/site-packages/attr/__init__.py b/venv/lib/python3.11/site-packages/attr/__init__.py new file mode 100644 index 0000000..5c6e065 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/__init__.py @@ -0,0 +1,104 @@ +# SPDX-License-Identifier: MIT + +""" +Classes Without Boilerplate +""" + +from functools import partial +from typing import Callable, Literal, Protocol + +from . import converters, exceptions, filters, setters, validators +from ._cmp import cmp_using +from ._config import get_run_validators, set_run_validators +from ._funcs import asdict, assoc, astuple, has, resolve_types +from ._make import ( + NOTHING, + Attribute, + Converter, + Factory, + _Nothing, + attrib, + attrs, + evolve, + fields, + fields_dict, + make_class, + validate, +) +from ._next_gen import define, field, frozen, mutable +from ._version_info import VersionInfo + + +s = attributes = attrs +ib = attr = attrib +dataclass = partial(attrs, auto_attribs=True) # happy Easter ;) + + +class AttrsInstance(Protocol): + pass + + +NothingType = Literal[_Nothing.NOTHING] + +__all__ = [ + "NOTHING", + "Attribute", + "AttrsInstance", + "Converter", + "Factory", + "NothingType", + "asdict", + "assoc", + "astuple", + "attr", + "attrib", + "attributes", + "attrs", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "field", + "fields", + "fields_dict", + "filters", + "frozen", + "get_run_validators", + "has", + "ib", + "make_class", + "mutable", + "resolve_types", + "s", + "set_run_validators", + "setters", + "validate", + "validators", +] + + +def _make_getattr(mod_name: str) -> Callable: + """ + Create a metadata proxy for packaging information that uses *mod_name* in + its warnings and errors. + """ + + def __getattr__(name: str) -> str: + if name not in ("__version__", "__version_info__"): + msg = f"module {mod_name} has no attribute {name}" + raise AttributeError(msg) + + from importlib.metadata import metadata + + meta = metadata("attrs") + + if name == "__version_info__": + return VersionInfo._from_version_string(meta["version"]) + + return meta["version"] + + return __getattr__ + + +__getattr__ = _make_getattr(__name__) diff --git a/venv/lib/python3.11/site-packages/attr/__init__.pyi b/venv/lib/python3.11/site-packages/attr/__init__.pyi new file mode 100644 index 0000000..758decf --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/__init__.pyi @@ -0,0 +1,389 @@ +import enum +import sys + +from typing import ( + Any, + Callable, + Generic, + Literal, + Mapping, + Protocol, + Sequence, + TypeVar, + overload, +) + +# `import X as X` is required to make these public +from . import converters as converters +from . import exceptions as exceptions +from . import filters as filters +from . import setters as setters +from . import validators as validators +from ._cmp import cmp_using as cmp_using +from ._typing_compat import AttrsInstance_ +from ._version_info import VersionInfo +from attrs import ( + define as define, + field as field, + mutable as mutable, + frozen as frozen, + _EqOrderType, + _ValidatorType, + _ConverterType, + _ReprArgType, + _OnSetAttrType, + _OnSetAttrArgType, + _FieldTransformer, + _ValidatorArgType, +) + +if sys.version_info >= (3, 10): + from typing import TypeGuard, TypeAlias +else: + from typing_extensions import TypeGuard, TypeAlias + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from typing_extensions import dataclass_transform + +__version__: str +__version_info__: VersionInfo +__title__: str +__description__: str +__url__: str +__uri__: str +__author__: str +__email__: str +__license__: str +__copyright__: str + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_FilterType = Callable[["Attribute[_T]", _T], bool] + +# We subclass this here to keep the protocol's qualified name clean. +class AttrsInstance(AttrsInstance_, Protocol): + pass + +_A = TypeVar("_A", bound=type[AttrsInstance]) + +class _Nothing(enum.Enum): + NOTHING = enum.auto() + +NOTHING = _Nothing.NOTHING +NothingType: TypeAlias = Literal[_Nothing.NOTHING] + +# NOTE: Factory lies about its return type to make this possible: +# `x: List[int] # = Factory(list)` +# Work around mypy issue #4554 in the common case by using an overload. + +@overload +def Factory(factory: Callable[[], _T]) -> _T: ... +@overload +def Factory( + factory: Callable[[Any], _T], + takes_self: Literal[True], +) -> _T: ... +@overload +def Factory( + factory: Callable[[], _T], + takes_self: Literal[False], +) -> _T: ... + +In = TypeVar("In") +Out = TypeVar("Out") + +class Converter(Generic[In, Out]): + @overload + def __init__(self, converter: Callable[[In], Out]) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, AttrsInstance, Attribute], Out], + *, + takes_self: Literal[True], + takes_field: Literal[True], + ) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, Attribute], Out], + *, + takes_field: Literal[True], + ) -> None: ... + @overload + def __init__( + self, + converter: Callable[[In, AttrsInstance], Out], + *, + takes_self: Literal[True], + ) -> None: ... + +class Attribute(Generic[_T]): + name: str + default: _T | None + validator: _ValidatorType[_T] | None + repr: _ReprArgType + cmp: _EqOrderType + eq: _EqOrderType + order: _EqOrderType + hash: bool | None + init: bool + converter: Converter | None + metadata: dict[Any, Any] + type: type[_T] | None + kw_only: bool + on_setattr: _OnSetAttrType + alias: str | None + + def evolve(self, **changes: Any) -> "Attribute[Any]": ... + +# NOTE: We had several choices for the annotation to use for type arg: +# 1) Type[_T] +# - Pros: Handles simple cases correctly +# - Cons: Might produce less informative errors in the case of conflicting +# TypeVars e.g. `attr.ib(default='bad', type=int)` +# 2) Callable[..., _T] +# - Pros: Better error messages than #1 for conflicting TypeVars +# - Cons: Terrible error messages for validator checks. +# e.g. attr.ib(type=int, validator=validate_str) +# -> error: Cannot infer function type argument +# 3) type (and do all of the work in the mypy plugin) +# - Pros: Simple here, and we could customize the plugin with our own errors. +# - Cons: Would need to write mypy plugin code to handle all the cases. +# We chose option #1. + +# `attr` lies about its return type to make the following possible: +# attr() -> Any +# attr(8) -> int +# attr(validator=) -> Whatever the callable expects. +# This makes this type of assignments possible: +# x: int = attr(8) +# +# This form catches explicit None or no default but with no other arguments +# returns Any. +@overload +def attrib( + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def attrib( + default: None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def attrib( + default: _T, + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: type[_T] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def attrib( + default: _T | None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + type: object = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., +) -> Any: ... +@overload +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) +def attrs( + maybe_cls: _C, + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., + unsafe_hash: bool | None = ..., +) -> _C: ... +@overload +@dataclass_transform(order_default=True, field_specifiers=(attrib, field)) +def attrs( + maybe_cls: None = ..., + these: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + auto_detect: bool = ..., + collect_by_mro: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., + unsafe_hash: bool | None = ..., +) -> Callable[[_C], _C]: ... +def fields(cls: type[AttrsInstance] | AttrsInstance) -> Any: ... +def fields_dict(cls: type[AttrsInstance]) -> dict[str, Attribute[Any]]: ... +def validate(inst: AttrsInstance) -> None: ... +def resolve_types( + cls: _A, + globalns: dict[str, Any] | None = ..., + localns: dict[str, Any] | None = ..., + attribs: list[Attribute[Any]] | None = ..., + include_extras: bool = ..., +) -> _A: ... + +# TODO: add support for returning a proper attrs class from the mypy plugin +# we use Any instead of _CountingAttr so that e.g. `make_class('Foo', +# [attr.ib()])` is valid +def make_class( + name: str, + attrs: list[str] | tuple[str, ...] | dict[str, Any], + bases: tuple[type, ...] = ..., + class_body: dict[str, Any] | None = ..., + repr_ns: str | None = ..., + repr: bool = ..., + cmp: _EqOrderType | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + collect_by_mro: bool = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., +) -> type: ... + +# _funcs -- + +# TODO: add support for returning TypedDict from the mypy plugin +# FIXME: asdict/astuple do not honor their factory args. Waiting on one of +# these: +# https://github.com/python/mypy/issues/4236 +# https://github.com/python/typing/issues/253 +# XXX: remember to fix attrs.asdict/astuple too! +def asdict( + inst: AttrsInstance, + recurse: bool = ..., + filter: _FilterType[Any] | None = ..., + dict_factory: type[Mapping[Any, Any]] = ..., + retain_collection_types: bool = ..., + value_serializer: Callable[[type, Attribute[Any], Any], Any] | None = ..., + tuple_keys: bool | None = ..., +) -> dict[str, Any]: ... + +# TODO: add support for returning NamedTuple from the mypy plugin +def astuple( + inst: AttrsInstance, + recurse: bool = ..., + filter: _FilterType[Any] | None = ..., + tuple_factory: type[Sequence[Any]] = ..., + retain_collection_types: bool = ..., +) -> tuple[Any, ...]: ... +def has(cls: type) -> TypeGuard[type[AttrsInstance]]: ... +def assoc(inst: _T, **changes: Any) -> _T: ... +def evolve(inst: _T, **changes: Any) -> _T: ... + +# _config -- + +def set_run_validators(run: bool) -> None: ... +def get_run_validators() -> bool: ... + +# aliases -- + +s = attributes = attrs +ib = attr = attrib +dataclass = attrs # Technically, partial(attrs, auto_attribs=True) ;) diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..dbdf17d Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/_cmp.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/_cmp.cpython-311.pyc new file mode 100644 index 0000000..54da112 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/_cmp.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/_compat.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/_compat.cpython-311.pyc new file mode 100644 index 0000000..1e5975f Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/_compat.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/_config.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/_config.cpython-311.pyc new file mode 100644 index 0000000..0f8c4ad Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/_config.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/_funcs.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/_funcs.cpython-311.pyc new file mode 100644 index 0000000..62774b2 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/_funcs.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/_make.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/_make.cpython-311.pyc new file mode 100644 index 0000000..157db0f Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/_make.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/_next_gen.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/_next_gen.cpython-311.pyc new file mode 100644 index 0000000..3aa3a7c Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/_next_gen.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/_version_info.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/_version_info.cpython-311.pyc new file mode 100644 index 0000000..c56cb6f Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/_version_info.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/converters.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/converters.cpython-311.pyc new file mode 100644 index 0000000..06de7e1 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/converters.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/exceptions.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000..d734c63 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/exceptions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/filters.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/filters.cpython-311.pyc new file mode 100644 index 0000000..6920da6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/filters.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/setters.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/setters.cpython-311.pyc new file mode 100644 index 0000000..44cfa0f Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/setters.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/__pycache__/validators.cpython-311.pyc b/venv/lib/python3.11/site-packages/attr/__pycache__/validators.cpython-311.pyc new file mode 100644 index 0000000..7280f55 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attr/__pycache__/validators.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attr/_cmp.py b/venv/lib/python3.11/site-packages/attr/_cmp.py new file mode 100644 index 0000000..09bab49 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_cmp.py @@ -0,0 +1,160 @@ +# SPDX-License-Identifier: MIT + + +import functools +import types + +from ._make import __ne__ + + +_operation_names = {"eq": "==", "lt": "<", "le": "<=", "gt": ">", "ge": ">="} + + +def cmp_using( + eq=None, + lt=None, + le=None, + gt=None, + ge=None, + require_same_type=True, + class_name="Comparable", +): + """ + Create a class that can be passed into `attrs.field`'s ``eq``, ``order``, + and ``cmp`` arguments to customize field comparison. + + The resulting class will have a full set of ordering methods if at least + one of ``{lt, le, gt, ge}`` and ``eq`` are provided. + + Args: + eq (typing.Callable | None): + Callable used to evaluate equality of two objects. + + lt (typing.Callable | None): + Callable used to evaluate whether one object is less than another + object. + + le (typing.Callable | None): + Callable used to evaluate whether one object is less than or equal + to another object. + + gt (typing.Callable | None): + Callable used to evaluate whether one object is greater than + another object. + + ge (typing.Callable | None): + Callable used to evaluate whether one object is greater than or + equal to another object. + + require_same_type (bool): + When `True`, equality and ordering methods will return + `NotImplemented` if objects are not of the same type. + + class_name (str | None): Name of class. Defaults to "Comparable". + + See `comparison` for more details. + + .. versionadded:: 21.1.0 + """ + + body = { + "__slots__": ["value"], + "__init__": _make_init(), + "_requirements": [], + "_is_comparable_to": _is_comparable_to, + } + + # Add operations. + num_order_functions = 0 + has_eq_function = False + + if eq is not None: + has_eq_function = True + body["__eq__"] = _make_operator("eq", eq) + body["__ne__"] = __ne__ + + if lt is not None: + num_order_functions += 1 + body["__lt__"] = _make_operator("lt", lt) + + if le is not None: + num_order_functions += 1 + body["__le__"] = _make_operator("le", le) + + if gt is not None: + num_order_functions += 1 + body["__gt__"] = _make_operator("gt", gt) + + if ge is not None: + num_order_functions += 1 + body["__ge__"] = _make_operator("ge", ge) + + type_ = types.new_class( + class_name, (object,), {}, lambda ns: ns.update(body) + ) + + # Add same type requirement. + if require_same_type: + type_._requirements.append(_check_same_type) + + # Add total ordering if at least one operation was defined. + if 0 < num_order_functions < 4: + if not has_eq_function: + # functools.total_ordering requires __eq__ to be defined, + # so raise early error here to keep a nice stack. + msg = "eq must be define is order to complete ordering from lt, le, gt, ge." + raise ValueError(msg) + type_ = functools.total_ordering(type_) + + return type_ + + +def _make_init(): + """ + Create __init__ method. + """ + + def __init__(self, value): + """ + Initialize object with *value*. + """ + self.value = value + + return __init__ + + +def _make_operator(name, func): + """ + Create operator method. + """ + + def method(self, other): + if not self._is_comparable_to(other): + return NotImplemented + + result = func(self.value, other.value) + if result is NotImplemented: + return NotImplemented + + return result + + method.__name__ = f"__{name}__" + method.__doc__ = ( + f"Return a {_operation_names[name]} b. Computed by attrs." + ) + + return method + + +def _is_comparable_to(self, other): + """ + Check whether `other` is comparable to `self`. + """ + return all(func(self, other) for func in self._requirements) + + +def _check_same_type(self, other): + """ + Return True if *self* and *other* are of the same type, False otherwise. + """ + return other.value.__class__ is self.value.__class__ diff --git a/venv/lib/python3.11/site-packages/attr/_cmp.pyi b/venv/lib/python3.11/site-packages/attr/_cmp.pyi new file mode 100644 index 0000000..cc7893b --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_cmp.pyi @@ -0,0 +1,13 @@ +from typing import Any, Callable + +_CompareWithType = Callable[[Any, Any], bool] + +def cmp_using( + eq: _CompareWithType | None = ..., + lt: _CompareWithType | None = ..., + le: _CompareWithType | None = ..., + gt: _CompareWithType | None = ..., + ge: _CompareWithType | None = ..., + require_same_type: bool = ..., + class_name: str = ..., +) -> type: ... diff --git a/venv/lib/python3.11/site-packages/attr/_compat.py b/venv/lib/python3.11/site-packages/attr/_compat.py new file mode 100644 index 0000000..bc68ed9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_compat.py @@ -0,0 +1,99 @@ +# SPDX-License-Identifier: MIT + +import inspect +import platform +import sys +import threading + +from collections.abc import Mapping, Sequence # noqa: F401 +from typing import _GenericAlias + + +PYPY = platform.python_implementation() == "PyPy" +PY_3_10_PLUS = sys.version_info[:2] >= (3, 10) +PY_3_11_PLUS = sys.version_info[:2] >= (3, 11) +PY_3_12_PLUS = sys.version_info[:2] >= (3, 12) +PY_3_13_PLUS = sys.version_info[:2] >= (3, 13) +PY_3_14_PLUS = sys.version_info[:2] >= (3, 14) + + +if PY_3_14_PLUS: + import annotationlib + + # We request forward-ref annotations to not break in the presence of + # forward references. + + def _get_annotations(cls): + return annotationlib.get_annotations( + cls, format=annotationlib.Format.FORWARDREF + ) + +else: + + def _get_annotations(cls): + """ + Get annotations for *cls*. + """ + return cls.__dict__.get("__annotations__", {}) + + +class _AnnotationExtractor: + """ + Extract type annotations from a callable, returning None whenever there + is none. + """ + + __slots__ = ["sig"] + + def __init__(self, callable): + try: + self.sig = inspect.signature(callable) + except (ValueError, TypeError): # inspect failed + self.sig = None + + def get_first_param_type(self): + """ + Return the type annotation of the first argument if it's not empty. + """ + if not self.sig: + return None + + params = list(self.sig.parameters.values()) + if params and params[0].annotation is not inspect.Parameter.empty: + return params[0].annotation + + return None + + def get_return_type(self): + """ + Return the return type if it's not empty. + """ + if ( + self.sig + and self.sig.return_annotation is not inspect.Signature.empty + ): + return self.sig.return_annotation + + return None + + +# Thread-local global to track attrs instances which are already being repr'd. +# This is needed because there is no other (thread-safe) way to pass info +# about the instances that are already being repr'd through the call stack +# in order to ensure we don't perform infinite recursion. +# +# For instance, if an instance contains a dict which contains that instance, +# we need to know that we're already repr'ing the outside instance from within +# the dict's repr() call. +# +# This lives here rather than in _make.py so that the functions in _make.py +# don't have a direct reference to the thread-local in their globals dict. +# If they have such a reference, it breaks cloudpickle. +repr_context = threading.local() + + +def get_generic_base(cl): + """If this is a generic class (A[str]), return the generic base for it.""" + if cl.__class__ is _GenericAlias: + return cl.__origin__ + return None diff --git a/venv/lib/python3.11/site-packages/attr/_config.py b/venv/lib/python3.11/site-packages/attr/_config.py new file mode 100644 index 0000000..4b25772 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_config.py @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: MIT + +__all__ = ["get_run_validators", "set_run_validators"] + +_run_validators = True + + +def set_run_validators(run): + """ + Set whether or not validators are run. By default, they are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.set_disabled()` + instead. + """ + if not isinstance(run, bool): + msg = "'run' must be bool." + raise TypeError(msg) + global _run_validators + _run_validators = run + + +def get_run_validators(): + """ + Return whether or not validators are run. + + .. deprecated:: 21.3.0 It will not be removed, but it also will not be + moved to new ``attrs`` namespace. Use `attrs.validators.get_disabled()` + instead. + """ + return _run_validators diff --git a/venv/lib/python3.11/site-packages/attr/_funcs.py b/venv/lib/python3.11/site-packages/attr/_funcs.py new file mode 100644 index 0000000..1adb500 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_funcs.py @@ -0,0 +1,497 @@ +# SPDX-License-Identifier: MIT + + +import copy + +from ._compat import get_generic_base +from ._make import _OBJ_SETATTR, NOTHING, fields +from .exceptions import AttrsAttributeNotFoundError + + +_ATOMIC_TYPES = frozenset( + { + type(None), + bool, + int, + float, + str, + complex, + bytes, + type(...), + type, + range, + property, + } +) + + +def asdict( + inst, + recurse=True, + filter=None, + dict_factory=dict, + retain_collection_types=False, + value_serializer=None, +): + """ + Return the *attrs* attribute values of *inst* as a dict. + + Optionally recurse into other *attrs*-decorated classes. + + Args: + inst: Instance of an *attrs*-decorated class. + + recurse (bool): Recurse into classes that are also *attrs*-decorated. + + filter (~typing.Callable): + A callable whose return code determines whether an attribute or + element is included (`True`) or dropped (`False`). Is called with + the `attrs.Attribute` as the first argument and the value as the + second argument. + + dict_factory (~typing.Callable): + A callable to produce dictionaries from. For example, to produce + ordered dictionaries instead of normal Python dictionaries, pass in + ``collections.OrderedDict``. + + retain_collection_types (bool): + Do not convert to `list` when encountering an attribute whose type + is `tuple` or `set`. Only meaningful if *recurse* is `True`. + + value_serializer (typing.Callable | None): + A hook that is called for every attribute or dict key/value. It + receives the current instance, field and value and must return the + (updated) value. The hook is run *after* the optional *filter* has + been applied. + + Returns: + Return type of *dict_factory*. + + Raises: + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 16.0.0 *dict_factory* + .. versionadded:: 16.1.0 *retain_collection_types* + .. versionadded:: 20.3.0 *value_serializer* + .. versionadded:: 21.3.0 + If a dict has a collection for a key, it is serialized as a tuple. + """ + attrs = fields(inst.__class__) + rv = dict_factory() + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + + if value_serializer is not None: + v = value_serializer(inst, a, v) + + if recurse is True: + value_type = type(v) + if value_type in _ATOMIC_TYPES: + rv[a.name] = v + elif has(value_type): + rv[a.name] = asdict( + v, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif issubclass(value_type, (tuple, list, set, frozenset)): + cf = value_type if retain_collection_types is True else list + items = [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in v + ] + try: + rv[a.name] = cf(items) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv[a.name] = cf(*items) + elif issubclass(value_type, dict): + df = dict_factory + rv[a.name] = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in v.items() + ) + else: + rv[a.name] = v + else: + rv[a.name] = v + return rv + + +def _asdict_anything( + val, + is_key, + filter, + dict_factory, + retain_collection_types, + value_serializer, +): + """ + ``asdict`` only works on attrs instances, this works on anything. + """ + val_type = type(val) + if val_type in _ATOMIC_TYPES: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + elif getattr(val_type, "__attrs_attrs__", None) is not None: + # Attrs class. + rv = asdict( + val, + recurse=True, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + elif issubclass(val_type, (tuple, list, set, frozenset)): + if retain_collection_types is True: + cf = val.__class__ + elif is_key: + cf = tuple + else: + cf = list + + rv = cf( + [ + _asdict_anything( + i, + is_key=False, + filter=filter, + dict_factory=dict_factory, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ) + for i in val + ] + ) + elif issubclass(val_type, dict): + df = dict_factory + rv = df( + ( + _asdict_anything( + kk, + is_key=True, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + _asdict_anything( + vv, + is_key=False, + filter=filter, + dict_factory=df, + retain_collection_types=retain_collection_types, + value_serializer=value_serializer, + ), + ) + for kk, vv in val.items() + ) + else: + rv = val + if value_serializer is not None: + rv = value_serializer(None, None, rv) + + return rv + + +def astuple( + inst, + recurse=True, + filter=None, + tuple_factory=tuple, + retain_collection_types=False, +): + """ + Return the *attrs* attribute values of *inst* as a tuple. + + Optionally recurse into other *attrs*-decorated classes. + + Args: + inst: Instance of an *attrs*-decorated class. + + recurse (bool): + Recurse into classes that are also *attrs*-decorated. + + filter (~typing.Callable): + A callable whose return code determines whether an attribute or + element is included (`True`) or dropped (`False`). Is called with + the `attrs.Attribute` as the first argument and the value as the + second argument. + + tuple_factory (~typing.Callable): + A callable to produce tuples from. For example, to produce lists + instead of tuples. + + retain_collection_types (bool): + Do not convert to `list` or `dict` when encountering an attribute + which type is `tuple`, `dict` or `set`. Only meaningful if + *recurse* is `True`. + + Returns: + Return type of *tuple_factory* + + Raises: + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 16.2.0 + """ + attrs = fields(inst.__class__) + rv = [] + retain = retain_collection_types # Very long. :/ + for a in attrs: + v = getattr(inst, a.name) + if filter is not None and not filter(a, v): + continue + value_type = type(v) + if recurse is True: + if value_type in _ATOMIC_TYPES: + rv.append(v) + elif has(value_type): + rv.append( + astuple( + v, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + ) + elif issubclass(value_type, (tuple, list, set, frozenset)): + cf = v.__class__ if retain is True else list + items = [ + ( + astuple( + j, + recurse=True, + filter=filter, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(j.__class__) + else j + ) + for j in v + ] + try: + rv.append(cf(items)) + except TypeError: + if not issubclass(cf, tuple): + raise + # Workaround for TypeError: cf.__new__() missing 1 required + # positional argument (which appears, for a namedturle) + rv.append(cf(*items)) + elif issubclass(value_type, dict): + df = value_type if retain is True else dict + rv.append( + df( + ( + ( + astuple( + kk, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(kk.__class__) + else kk + ), + ( + astuple( + vv, + tuple_factory=tuple_factory, + retain_collection_types=retain, + ) + if has(vv.__class__) + else vv + ), + ) + for kk, vv in v.items() + ) + ) + else: + rv.append(v) + else: + rv.append(v) + + return rv if tuple_factory is list else tuple_factory(rv) + + +def has(cls): + """ + Check whether *cls* is a class with *attrs* attributes. + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + Returns: + bool: + """ + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is not None: + return True + + # No attrs, maybe it's a specialized generic (A[str])? + generic_base = get_generic_base(cls) + if generic_base is not None: + generic_attrs = getattr(generic_base, "__attrs_attrs__", None) + if generic_attrs is not None: + # Stick it on here for speed next time. + cls.__attrs_attrs__ = generic_attrs + return generic_attrs is not None + return False + + +def assoc(inst, **changes): + """ + Copy *inst* and apply *changes*. + + This is different from `evolve` that applies the changes to the arguments + that create the new instance. + + `evolve`'s behavior is preferable, but there are `edge cases`_ where it + doesn't work. Therefore `assoc` is deprecated, but will not be removed. + + .. _`edge cases`: https://github.com/python-attrs/attrs/issues/251 + + Args: + inst: Instance of a class with *attrs* attributes. + + changes: Keyword changes in the new copy. + + Returns: + A copy of inst with *changes* incorporated. + + Raises: + attrs.exceptions.AttrsAttributeNotFoundError: + If *attr_name* couldn't be found on *cls*. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. deprecated:: 17.1.0 + Use `attrs.evolve` instead if you can. This function will not be + removed du to the slightly different approach compared to + `attrs.evolve`, though. + """ + new = copy.copy(inst) + attrs = fields(inst.__class__) + for k, v in changes.items(): + a = getattr(attrs, k, NOTHING) + if a is NOTHING: + msg = f"{k} is not an attrs attribute on {new.__class__}." + raise AttrsAttributeNotFoundError(msg) + _OBJ_SETATTR(new, k, v) + return new + + +def resolve_types( + cls, globalns=None, localns=None, attribs=None, include_extras=True +): + """ + Resolve any strings and forward annotations in type annotations. + + This is only required if you need concrete types in :class:`Attribute`'s + *type* field. In other words, you don't need to resolve your types if you + only use them for static type checking. + + With no arguments, names will be looked up in the module in which the class + was created. If this is not what you want, for example, if the name only + exists inside a method, you may pass *globalns* or *localns* to specify + other dictionaries in which to look up these names. See the docs of + `typing.get_type_hints` for more details. + + Args: + cls (type): Class to resolve. + + globalns (dict | None): Dictionary containing global variables. + + localns (dict | None): Dictionary containing local variables. + + attribs (list | None): + List of attribs for the given class. This is necessary when calling + from inside a ``field_transformer`` since *cls* is not an *attrs* + class yet. + + include_extras (bool): + Resolve more accurately, if possible. Pass ``include_extras`` to + ``typing.get_hints``, if supported by the typing module. On + supported Python versions (3.9+), this resolves the types more + accurately. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class and you didn't pass any attribs. + + NameError: If types cannot be resolved because of missing variables. + + Returns: + *cls* so you can use this function also as a class decorator. Please + note that you have to apply it **after** `attrs.define`. That means the + decorator has to come in the line **before** `attrs.define`. + + .. versionadded:: 20.1.0 + .. versionadded:: 21.1.0 *attribs* + .. versionadded:: 23.1.0 *include_extras* + """ + # Since calling get_type_hints is expensive we cache whether we've + # done it already. + if getattr(cls, "__attrs_types_resolved__", None) != cls: + import typing + + kwargs = { + "globalns": globalns, + "localns": localns, + "include_extras": include_extras, + } + + hints = typing.get_type_hints(cls, **kwargs) + for field in fields(cls) if attribs is None else attribs: + if field.name in hints: + # Since fields have been frozen we must work around it. + _OBJ_SETATTR(field, "type", hints[field.name]) + # We store the class we resolved so that subclasses know they haven't + # been resolved. + cls.__attrs_types_resolved__ = cls + + # Return the class so you can use it as a decorator too. + return cls diff --git a/venv/lib/python3.11/site-packages/attr/_make.py b/venv/lib/python3.11/site-packages/attr/_make.py new file mode 100644 index 0000000..4b32d6a --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_make.py @@ -0,0 +1,3406 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + +import abc +import contextlib +import copy +import enum +import inspect +import itertools +import linecache +import sys +import types +import unicodedata +import weakref + +from collections.abc import Callable, Mapping +from functools import cached_property +from typing import Any, NamedTuple, TypeVar + +# We need to import _compat itself in addition to the _compat members to avoid +# having the thread-local in the globals here. +from . import _compat, _config, setters +from ._compat import ( + PY_3_10_PLUS, + PY_3_11_PLUS, + PY_3_13_PLUS, + _AnnotationExtractor, + _get_annotations, + get_generic_base, +) +from .exceptions import ( + DefaultAlreadySetError, + FrozenInstanceError, + NotAnAttrsClassError, + UnannotatedAttributeError, +) + + +# This is used at least twice, so cache it here. +_OBJ_SETATTR = object.__setattr__ +_INIT_FACTORY_PAT = "__attr_factory_%s" +_CLASSVAR_PREFIXES = ( + "typing.ClassVar", + "t.ClassVar", + "ClassVar", + "typing_extensions.ClassVar", +) +# we don't use a double-underscore prefix because that triggers +# name mangling when trying to create a slot for the field +# (when slots=True) +_HASH_CACHE_FIELD = "_attrs_cached_hash" + +_EMPTY_METADATA_SINGLETON = types.MappingProxyType({}) + +# Unique object for unequivocal getattr() defaults. +_SENTINEL = object() + +_DEFAULT_ON_SETATTR = setters.pipe(setters.convert, setters.validate) + + +class _Nothing(enum.Enum): + """ + Sentinel to indicate the lack of a value when `None` is ambiguous. + + If extending attrs, you can use ``typing.Literal[NOTHING]`` to show + that a value may be ``NOTHING``. + + .. versionchanged:: 21.1.0 ``bool(NOTHING)`` is now False. + .. versionchanged:: 22.2.0 ``NOTHING`` is now an ``enum.Enum`` variant. + """ + + NOTHING = enum.auto() + + def __repr__(self): + return "NOTHING" + + def __bool__(self): + return False + + +NOTHING = _Nothing.NOTHING +""" +Sentinel to indicate the lack of a value when `None` is ambiguous. + +When using in 3rd party code, use `attrs.NothingType` for type annotations. +""" + + +class _CacheHashWrapper(int): + """ + An integer subclass that pickles / copies as None + + This is used for non-slots classes with ``cache_hash=True``, to avoid + serializing a potentially (even likely) invalid hash value. Since `None` + is the default value for uncalculated hashes, whenever this is copied, + the copy's value for the hash should automatically reset. + + See GH #613 for more details. + """ + + def __reduce__(self, _none_constructor=type(None), _args=()): # noqa: B008 + return _none_constructor, _args + + +def attrib( + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=None, + eq=None, + order=None, + on_setattr=None, + alias=None, +): + """ + Create a new field / attribute on a class. + + Identical to `attrs.field`, except it's not keyword-only. + + Consider using `attrs.field` in new code (``attr.ib`` will *never* go away, + though). + + .. warning:: + + Does **nothing** unless the class is also decorated with + `attr.s` (or similar)! + + + .. versionadded:: 15.2.0 *convert* + .. versionadded:: 16.3.0 *metadata* + .. versionchanged:: 17.1.0 *validator* can be a ``list`` now. + .. versionchanged:: 17.1.0 + *hash* is `None` and therefore mirrors *eq* by default. + .. versionadded:: 17.3.0 *type* + .. deprecated:: 17.4.0 *convert* + .. versionadded:: 17.4.0 + *converter* as a replacement for the deprecated *convert* to achieve + consistency with other noun-based arguments. + .. versionadded:: 18.1.0 + ``factory=f`` is syntactic sugar for ``default=attr.Factory(f)``. + .. versionadded:: 18.2.0 *kw_only* + .. versionchanged:: 19.2.0 *convert* keyword argument removed. + .. versionchanged:: 19.2.0 *repr* also accepts a custom callable. + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.3.0 *kw_only* backported to Python 2 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 22.2.0 *alias* + .. versionchanged:: 25.4.0 + *kw_only* can now be None, and its default is also changed from False to + None. + """ + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq, order, True + ) + + if hash is not None and hash is not True and hash is not False: + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + if factory is not None: + if default is not NOTHING: + msg = ( + "The `default` and `factory` arguments are mutually exclusive." + ) + raise ValueError(msg) + if not callable(factory): + msg = "The `factory` argument must be a callable." + raise ValueError(msg) + default = Factory(factory) + + if metadata is None: + metadata = {} + + # Apply syntactic sugar by auto-wrapping. + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + if validator and isinstance(validator, (list, tuple)): + validator = and_(*validator) + + if converter and isinstance(converter, (list, tuple)): + converter = pipe(*converter) + + return _CountingAttr( + default=default, + validator=validator, + repr=repr, + cmp=None, + hash=hash, + init=init, + converter=converter, + metadata=metadata, + type=type, + kw_only=kw_only, + eq=eq, + eq_key=eq_key, + order=order, + order_key=order_key, + on_setattr=on_setattr, + alias=alias, + ) + + +def _compile_and_eval( + script: str, + globs: dict[str, Any] | None, + locs: Mapping[str, object] | None = None, + filename: str = "", +) -> None: + """ + Evaluate the script with the given global (globs) and local (locs) + variables. + """ + bytecode = compile(script, filename, "exec") + eval(bytecode, globs, locs) + + +def _linecache_and_compile( + script: str, + filename: str, + globs: dict[str, Any] | None, + locals: Mapping[str, object] | None = None, +) -> dict[str, Any]: + """ + Cache the script with _linecache_, compile it and return the _locals_. + """ + + locs = {} if locals is None else locals + + # In order of debuggers like PDB being able to step through the code, + # we add a fake linecache entry. + count = 1 + base_filename = filename + while True: + linecache_tuple = ( + len(script), + None, + script.splitlines(True), + filename, + ) + old_val = linecache.cache.setdefault(filename, linecache_tuple) + if old_val == linecache_tuple: + break + + filename = f"{base_filename[:-1]}-{count}>" + count += 1 + + _compile_and_eval(script, globs, locs, filename) + + return locs + + +def _make_attr_tuple_class(cls_name: str, attr_names: list[str]) -> type: + """ + Create a tuple subclass to hold `Attribute`s for an `attrs` class. + + The subclass is a bare tuple with properties for names. + + class MyClassAttributes(tuple): + __slots__ = () + x = property(itemgetter(0)) + """ + attr_class_name = f"{cls_name}Attributes" + body = {} + for i, attr_name in enumerate(attr_names): + + def getter(self, i=i): + return self[i] + + body[attr_name] = property(getter) + return type(attr_class_name, (tuple,), body) + + +# Tuple class for extracted attributes from a class definition. +# `base_attrs` is a subset of `attrs`. +class _Attributes(NamedTuple): + attrs: type + base_attrs: list[Attribute] + base_attrs_map: dict[str, type] + + +def _is_class_var(annot): + """ + Check whether *annot* is a typing.ClassVar. + + The string comparison hack is used to avoid evaluating all string + annotations which would put attrs-based classes at a performance + disadvantage compared to plain old classes. + """ + annot = str(annot) + + # Annotation can be quoted. + if annot.startswith(("'", '"')) and annot.endswith(("'", '"')): + annot = annot[1:-1] + + return annot.startswith(_CLASSVAR_PREFIXES) + + +def _has_own_attribute(cls, attrib_name): + """ + Check whether *cls* defines *attrib_name* (and doesn't just inherit it). + """ + return attrib_name in cls.__dict__ + + +def _collect_base_attrs( + cls, taken_attr_names +) -> tuple[list[Attribute], dict[str, type]]: + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in reversed(cls.__mro__[1:-1]): + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.inherited or a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) # noqa: PLW2901 + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + # For each name, only keep the freshest definition i.e. the furthest at the + # back. base_attr_map is fine because it gets overwritten with every new + # instance. + filtered = [] + seen = set() + for a in reversed(base_attrs): + if a.name in seen: + continue + filtered.insert(0, a) + seen.add(a.name) + + return filtered, base_attr_map + + +def _collect_base_attrs_broken(cls, taken_attr_names): + """ + Collect attr.ibs from base classes of *cls*, except *taken_attr_names*. + + N.B. *taken_attr_names* will be mutated. + + Adhere to the old incorrect behavior. + + Notably it collects from the front and considers inherited attributes which + leads to the buggy behavior reported in #428. + """ + base_attrs = [] + base_attr_map = {} # A dictionary of base attrs to their classes. + + # Traverse the MRO and collect attributes. + for base_cls in cls.__mro__[1:-1]: + for a in getattr(base_cls, "__attrs_attrs__", []): + if a.name in taken_attr_names: + continue + + a = a.evolve(inherited=True) # noqa: PLW2901 + taken_attr_names.add(a.name) + base_attrs.append(a) + base_attr_map[a.name] = base_cls + + return base_attrs, base_attr_map + + +def _transform_attrs( + cls, + these, + auto_attribs, + kw_only, + collect_by_mro, + field_transformer, +) -> _Attributes: + """ + Transform all `_CountingAttr`s on a class into `Attribute`s. + + If *these* is passed, use that and don't look for them on the class. + + If *collect_by_mro* is True, collect them in the correct MRO order, + otherwise use the old -- incorrect -- order. See #428. + + Return an `_Attributes`. + """ + cd = cls.__dict__ + anns = _get_annotations(cls) + + if these is not None: + ca_list = list(these.items()) + elif auto_attribs is True: + ca_names = { + name + for name, attr in cd.items() + if attr.__class__ is _CountingAttr + } + ca_list = [] + annot_names = set() + for attr_name, type in anns.items(): + if _is_class_var(type): + continue + annot_names.add(attr_name) + a = cd.get(attr_name, NOTHING) + + if a.__class__ is not _CountingAttr: + a = attrib(a) + ca_list.append((attr_name, a)) + + unannotated = ca_names - annot_names + if unannotated: + raise UnannotatedAttributeError( + "The following `attr.ib`s lack a type annotation: " + + ", ".join( + sorted(unannotated, key=lambda n: cd.get(n).counter) + ) + + "." + ) + else: + ca_list = sorted( + ( + (name, attr) + for name, attr in cd.items() + if attr.__class__ is _CountingAttr + ), + key=lambda e: e[1].counter, + ) + + fca = Attribute.from_counting_attr + no = ClassProps.KeywordOnly.NO + own_attrs = [ + fca( + attr_name, + ca, + kw_only is not no, + anns.get(attr_name), + ) + for attr_name, ca in ca_list + ] + + if collect_by_mro: + base_attrs, base_attr_map = _collect_base_attrs( + cls, {a.name for a in own_attrs} + ) + else: + base_attrs, base_attr_map = _collect_base_attrs_broken( + cls, {a.name for a in own_attrs} + ) + + if kw_only is ClassProps.KeywordOnly.FORCE: + own_attrs = [a.evolve(kw_only=True) for a in own_attrs] + base_attrs = [a.evolve(kw_only=True) for a in base_attrs] + + attrs = base_attrs + own_attrs + + # Resolve default field alias before executing field_transformer, so that + # the transformer receives fully populated Attribute objects with usable + # alias values. + for a in attrs: + if not a.alias: + # Evolve is very slow, so we hold our nose and do it dirty. + _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) + _OBJ_SETATTR.__get__(a)("alias_is_default", True) + + if field_transformer is not None: + attrs = tuple(field_transformer(cls, attrs)) + + # Check attr order after executing the field_transformer. + # Mandatory vs non-mandatory attr order only matters when they are part of + # the __init__ signature and when they aren't kw_only (which are moved to + # the end and can be mandatory or non-mandatory in any order, as they will + # be specified as keyword args anyway). Check the order of those attrs: + had_default = False + for a in (a for a in attrs if a.init is not False and a.kw_only is False): + if had_default is True and a.default is NOTHING: + msg = f"No mandatory attributes allowed after an attribute with a default value or factory. Attribute in question: {a!r}" + raise ValueError(msg) + + if had_default is False and a.default is not NOTHING: + had_default = True + + # Resolve default field alias for any new attributes that the + # field_transformer may have added without setting an alias. + for a in attrs: + if not a.alias: + _OBJ_SETATTR.__get__(a)("alias", _default_init_alias_for(a.name)) + _OBJ_SETATTR.__get__(a)("alias_is_default", True) + + # Create AttrsClass *after* applying the field_transformer since it may + # add or remove attributes! + attr_names = [a.name for a in attrs] + AttrsClass = _make_attr_tuple_class(cls.__name__, attr_names) + + return _Attributes(AttrsClass(attrs), base_attrs, base_attr_map) + + +def _make_cached_property_getattr(cached_properties, original_getattr, cls): + lines = [ + # Wrapped to get `__class__` into closure cell for super() + # (It will be replaced with the newly constructed class after construction). + "def wrapper(_cls):", + " __class__ = _cls", + " def __getattr__(self, item, cached_properties=cached_properties, original_getattr=original_getattr, _cached_setattr_get=_cached_setattr_get):", + " func = cached_properties.get(item)", + " if func is not None:", + " result = func(self)", + " _setter = _cached_setattr_get(self)", + " _setter(item, result)", + " return result", + ] + if original_getattr is not None: + lines.append( + " return original_getattr(self, item)", + ) + else: + lines.extend( + [ + " try:", + " return super().__getattribute__(item)", + " except AttributeError:", + " if not hasattr(super(), '__getattr__'):", + " raise", + " return super().__getattr__(item)", + " original_error = f\"'{self.__class__.__name__}' object has no attribute '{item}'\"", + " raise AttributeError(original_error)", + ] + ) + + lines.extend( + [ + " return __getattr__", + "__getattr__ = wrapper(_cls)", + ] + ) + + unique_filename = _generate_unique_filename(cls, "getattr") + + glob = { + "cached_properties": cached_properties, + "_cached_setattr_get": _OBJ_SETATTR.__get__, + "original_getattr": original_getattr, + } + + return _linecache_and_compile( + "\n".join(lines), unique_filename, glob, locals={"_cls": cls} + )["__getattr__"] + + +def _frozen_setattrs(self, name, value): + """ + Attached to frozen classes as __setattr__. + """ + if isinstance(self, BaseException) and name in ( + "__cause__", + "__context__", + "__traceback__", + "__suppress_context__", + "__notes__", + ): + BaseException.__setattr__(self, name, value) + return + + raise FrozenInstanceError + + +def _frozen_delattrs(self, name): + """ + Attached to frozen classes as __delattr__. + """ + if isinstance(self, BaseException) and name == "__notes__": + BaseException.__delattr__(self, name) + return + + raise FrozenInstanceError + + +def evolve(*args, **changes): + """ + Create a new instance, based on the first positional argument with + *changes* applied. + + .. tip:: + + On Python 3.13 and later, you can also use `copy.replace` instead. + + Args: + + inst: + Instance of a class with *attrs* attributes. *inst* must be passed + as a positional argument. + + changes: + Keyword changes in the new copy. + + Returns: + A copy of inst with *changes* incorporated. + + Raises: + TypeError: + If *attr_name* couldn't be found in the class ``__init__``. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + .. versionadded:: 17.1.0 + .. deprecated:: 23.1.0 + It is now deprecated to pass the instance using the keyword argument + *inst*. It will raise a warning until at least April 2024, after which + it will become an error. Always pass the instance as a positional + argument. + .. versionchanged:: 24.1.0 + *inst* can't be passed as a keyword argument anymore. + """ + try: + (inst,) = args + except ValueError: + msg = ( + f"evolve() takes 1 positional argument, but {len(args)} were given" + ) + raise TypeError(msg) from None + + cls = inst.__class__ + attrs = fields(cls) + for a in attrs: + if not a.init: + continue + attr_name = a.name # To deal with private attributes. + init_name = a.alias + if init_name not in changes: + changes[init_name] = getattr(inst, attr_name) + + return cls(**changes) + + +class _ClassBuilder: + """ + Iteratively build *one* class. + """ + + __slots__ = ( + "_add_method_dunders", + "_attr_names", + "_attrs", + "_base_attr_map", + "_base_names", + "_cache_hash", + "_cls", + "_cls_dict", + "_delete_attribs", + "_frozen", + "_has_custom_setattr", + "_has_post_init", + "_has_pre_init", + "_is_exc", + "_on_setattr", + "_pre_init_has_args", + "_repr_added", + "_script_snippets", + "_slots", + "_weakref_slot", + "_wrote_own_setattr", + ) + + def __init__( + self, + cls: type, + these, + auto_attribs: bool, + props: ClassProps, + has_custom_setattr: bool, + ): + attrs, base_attrs, base_map = _transform_attrs( + cls, + these, + auto_attribs, + props.kw_only, + props.collected_fields_by_mro, + props.field_transformer, + ) + + self._cls = cls + self._cls_dict = dict(cls.__dict__) if props.is_slotted else {} + self._attrs = attrs + self._base_names = {a.name for a in base_attrs} + self._base_attr_map = base_map + self._attr_names = tuple(a.name for a in attrs) + self._slots = props.is_slotted + self._frozen = props.is_frozen + self._weakref_slot = props.has_weakref_slot + self._cache_hash = ( + props.hashability is ClassProps.Hashability.HASHABLE_CACHED + ) + self._has_pre_init = bool(getattr(cls, "__attrs_pre_init__", False)) + self._pre_init_has_args = False + if self._has_pre_init: + # Check if the pre init method has more arguments than just `self` + # We want to pass arguments if pre init expects arguments + pre_init_func = cls.__attrs_pre_init__ + pre_init_signature = inspect.signature(pre_init_func) + self._pre_init_has_args = len(pre_init_signature.parameters) > 1 + self._has_post_init = bool(getattr(cls, "__attrs_post_init__", False)) + self._delete_attribs = not bool(these) + self._is_exc = props.is_exception + self._on_setattr = props.on_setattr_hook + + self._has_custom_setattr = has_custom_setattr + self._wrote_own_setattr = False + + self._cls_dict["__attrs_attrs__"] = self._attrs + self._cls_dict["__attrs_props__"] = props + + if props.is_frozen: + self._cls_dict["__setattr__"] = _frozen_setattrs + self._cls_dict["__delattr__"] = _frozen_delattrs + + self._wrote_own_setattr = True + elif self._on_setattr in ( + _DEFAULT_ON_SETATTR, + setters.validate, + setters.convert, + ): + has_validator = has_converter = False + for a in attrs: + if a.validator is not None: + has_validator = True + if a.converter is not None: + has_converter = True + + if has_validator and has_converter: + break + if ( + ( + self._on_setattr == _DEFAULT_ON_SETATTR + and not (has_validator or has_converter) + ) + or (self._on_setattr == setters.validate and not has_validator) + or (self._on_setattr == setters.convert and not has_converter) + ): + # If class-level on_setattr is set to convert + validate, but + # there's no field to convert or validate, pretend like there's + # no on_setattr. + self._on_setattr = None + + if props.added_pickling: + ( + self._cls_dict["__getstate__"], + self._cls_dict["__setstate__"], + ) = self._make_getstate_setstate() + + # tuples of script, globs, hook + self._script_snippets: list[ + tuple[str, dict, Callable[[dict, dict], Any]] + ] = [] + self._repr_added = False + + # We want to only do this check once; in 99.9% of cases these + # exist. + if not hasattr(self._cls, "__module__") or not hasattr( + self._cls, "__qualname__" + ): + self._add_method_dunders = self._add_method_dunders_safe + else: + self._add_method_dunders = self._add_method_dunders_unsafe + + def __repr__(self): + return f"<_ClassBuilder(cls={self._cls.__name__})>" + + def _eval_snippets(self) -> None: + """ + Evaluate any registered snippets in one go. + """ + script = "\n".join([snippet[0] for snippet in self._script_snippets]) + globs = {} + for _, snippet_globs, _ in self._script_snippets: + globs.update(snippet_globs) + + locs = _linecache_and_compile( + script, + _generate_unique_filename(self._cls, "methods"), + globs, + ) + + for _, _, hook in self._script_snippets: + hook(self._cls_dict, locs) + + def build_class(self): + """ + Finalize class based on the accumulated configuration. + + Builder cannot be used after calling this method. + """ + self._eval_snippets() + if self._slots is True: + cls = self._create_slots_class() + self._cls.__attrs_base_of_slotted__ = weakref.ref(cls) + else: + cls = self._patch_original_class() + if PY_3_10_PLUS: + cls = abc.update_abstractmethods(cls) + + # The method gets only called if it's not inherited from a base class. + # _has_own_attribute does NOT work properly for classmethods. + if ( + getattr(cls, "__attrs_init_subclass__", None) + and "__attrs_init_subclass__" not in cls.__dict__ + ): + cls.__attrs_init_subclass__() + + return cls + + def _patch_original_class(self): + """ + Apply accumulated methods and return the class. + """ + cls = self._cls + base_names = self._base_names + + # Clean class of attribute definitions (`attr.ib()`s). + if self._delete_attribs: + for name in self._attr_names: + if ( + name not in base_names + and getattr(cls, name, _SENTINEL) is not _SENTINEL + ): + # An AttributeError can happen if a base class defines a + # class variable and we want to set an attribute with the + # same name by using only a type annotation. + with contextlib.suppress(AttributeError): + delattr(cls, name) + + # Attach our dunder methods. + for name, value in self._cls_dict.items(): + setattr(cls, name, value) + + # If we've inherited an attrs __setattr__ and don't write our own, + # reset it to object's. + if not self._wrote_own_setattr and getattr( + cls, "__attrs_own_setattr__", False + ): + cls.__attrs_own_setattr__ = False + + if not self._has_custom_setattr: + cls.__setattr__ = _OBJ_SETATTR + + return cls + + def _create_slots_class(self): + """ + Build and return a new class with a `__slots__` attribute. + """ + cd = { + k: v + for k, v in self._cls_dict.items() + if k not in (*tuple(self._attr_names), "__dict__", "__weakref__") + } + + # 3.14.0rc2+ + if hasattr(sys, "_clear_type_descriptors"): + sys._clear_type_descriptors(self._cls) + + # If our class doesn't have its own implementation of __setattr__ + # (either from the user or by us), check the bases, if one of them has + # an attrs-made __setattr__, that needs to be reset. We don't walk the + # MRO because we only care about our immediate base classes. + # XXX: This can be confused by subclassing a slotted attrs class with + # XXX: a non-attrs class and subclass the resulting class with an attrs + # XXX: class. See `test_slotted_confused` for details. For now that's + # XXX: OK with us. + if not self._wrote_own_setattr: + cd["__attrs_own_setattr__"] = False + + if not self._has_custom_setattr: + for base_cls in self._cls.__bases__: + if base_cls.__dict__.get("__attrs_own_setattr__", False): + cd["__setattr__"] = _OBJ_SETATTR + break + + # Traverse the MRO to collect existing slots + # and check for an existing __weakref__. + existing_slots = {} + weakref_inherited = False + for base_cls in self._cls.__mro__[1:-1]: + if base_cls.__dict__.get("__weakref__", None) is not None: + weakref_inherited = True + existing_slots.update( + { + name: getattr(base_cls, name) + for name in getattr(base_cls, "__slots__", []) + } + ) + + base_names = set(self._base_names) + + names = self._attr_names + if ( + self._weakref_slot + and "__weakref__" not in getattr(self._cls, "__slots__", ()) + and "__weakref__" not in names + and not weakref_inherited + ): + names += ("__weakref__",) + + cached_properties = { + name: cached_prop.func + for name, cached_prop in cd.items() + if isinstance(cached_prop, cached_property) + } + + # Collect methods with a `__class__` reference that are shadowed in the new class. + # To know to update them. + additional_closure_functions_to_update = [] + if cached_properties: + class_annotations = _get_annotations(self._cls) + for name, func in cached_properties.items(): + # Add cached properties to names for slotting. + names += (name,) + # Clear out function from class to avoid clashing. + del cd[name] + additional_closure_functions_to_update.append(func) + annotation = inspect.signature(func).return_annotation + if annotation is not inspect.Parameter.empty: + class_annotations[name] = annotation + + original_getattr = cd.get("__getattr__") + if original_getattr is not None: + additional_closure_functions_to_update.append(original_getattr) + + cd["__getattr__"] = _make_cached_property_getattr( + cached_properties, original_getattr, self._cls + ) + + # We only add the names of attributes that aren't inherited. + # Setting __slots__ to inherited attributes wastes memory. + slot_names = [name for name in names if name not in base_names] + + # There are slots for attributes from current class + # that are defined in parent classes. + # As their descriptors may be overridden by a child class, + # we collect them here and update the class dict + reused_slots = { + slot: slot_descriptor + for slot, slot_descriptor in existing_slots.items() + if slot in slot_names + } + slot_names = [name for name in slot_names if name not in reused_slots] + cd.update(reused_slots) + if self._cache_hash: + slot_names.append(_HASH_CACHE_FIELD) + + cd["__slots__"] = tuple(slot_names) + + cd["__qualname__"] = self._cls.__qualname__ + + # Create new class based on old class and our methods. + cls = type(self._cls)(self._cls.__name__, self._cls.__bases__, cd) + + # The following is a fix for + # . + # If a method mentions `__class__` or uses the no-arg super(), the + # compiler will bake a reference to the class in the method itself + # as `method.__closure__`. Since we replace the class with a + # clone, we rewrite these references so it keeps working. + for item in itertools.chain( + cls.__dict__.values(), additional_closure_functions_to_update + ): + if isinstance(item, (classmethod, staticmethod)): + # Class- and staticmethods hide their functions inside. + # These might need to be rewritten as well. + closure_cells = getattr(item.__func__, "__closure__", None) + elif isinstance(item, property): + # Workaround for property `super()` shortcut (PY3-only). + # There is no universal way for other descriptors. + closure_cells = getattr(item.fget, "__closure__", None) + else: + closure_cells = getattr(item, "__closure__", None) + + if not closure_cells: # Catch None or the empty list. + continue + for cell in closure_cells: + try: + match = cell.cell_contents is self._cls + except ValueError: # noqa: PERF203 + # ValueError: Cell is empty + pass + else: + if match: + cell.cell_contents = cls + return cls + + def add_repr(self, ns): + script, globs = _make_repr_script(self._attrs, ns) + + def _attach_repr(cls_dict, globs): + cls_dict["__repr__"] = self._add_method_dunders(globs["__repr__"]) + + self._script_snippets.append((script, globs, _attach_repr)) + self._repr_added = True + return self + + def add_str(self): + if not self._repr_added: + msg = "__str__ can only be generated if a __repr__ exists." + raise ValueError(msg) + + def __str__(self): + return self.__repr__() + + self._cls_dict["__str__"] = self._add_method_dunders(__str__) + return self + + def _make_getstate_setstate(self): + """ + Create custom __setstate__ and __getstate__ methods. + """ + # __weakref__ is not writable. + state_attr_names = tuple( + an for an in self._attr_names if an != "__weakref__" + ) + + def slots_getstate(self): + """ + Automatically created by attrs. + """ + return {name: getattr(self, name) for name in state_attr_names} + + hash_caching_enabled = self._cache_hash + + def slots_setstate(self, state): + """ + Automatically created by attrs. + """ + __bound_setattr = _OBJ_SETATTR.__get__(self) + if isinstance(state, tuple): + # Backward compatibility with attrs instances pickled with + # attrs versions before v22.2.0 which stored tuples. + for name, value in zip(state_attr_names, state): + __bound_setattr(name, value) + else: + for name in state_attr_names: + if name in state: + __bound_setattr(name, state[name]) + + # The hash code cache is not included when the object is + # serialized, but it still needs to be initialized to None to + # indicate that the first call to __hash__ should be a cache + # miss. + if hash_caching_enabled: + __bound_setattr(_HASH_CACHE_FIELD, None) + + return slots_getstate, slots_setstate + + def make_unhashable(self): + self._cls_dict["__hash__"] = None + return self + + def add_hash(self): + script, globs = _make_hash_script( + self._cls, + self._attrs, + frozen=self._frozen, + cache_hash=self._cache_hash, + ) + + def attach_hash(cls_dict: dict, locs: dict) -> None: + cls_dict["__hash__"] = self._add_method_dunders(locs["__hash__"]) + + self._script_snippets.append((script, globs, attach_hash)) + + return self + + def add_init(self): + script, globs, annotations = _make_init_script( + self._cls, + self._attrs, + self._has_pre_init, + self._pre_init_has_args, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=False, + ) + + def _attach_init(cls_dict, globs): + init = globs["__init__"] + init.__annotations__ = annotations + cls_dict["__init__"] = self._add_method_dunders(init) + + self._script_snippets.append((script, globs, _attach_init)) + + return self + + def add_replace(self): + self._cls_dict["__replace__"] = self._add_method_dunders(evolve) + return self + + def add_match_args(self): + self._cls_dict["__match_args__"] = tuple( + field.name + for field in self._attrs + if field.init and not field.kw_only + ) + + def add_attrs_init(self): + script, globs, annotations = _make_init_script( + self._cls, + self._attrs, + self._has_pre_init, + self._pre_init_has_args, + self._has_post_init, + self._frozen, + self._slots, + self._cache_hash, + self._base_attr_map, + self._is_exc, + self._on_setattr, + attrs_init=True, + ) + + def _attach_attrs_init(cls_dict, globs): + init = globs["__attrs_init__"] + init.__annotations__ = annotations + cls_dict["__attrs_init__"] = self._add_method_dunders(init) + + self._script_snippets.append((script, globs, _attach_attrs_init)) + + return self + + def add_eq(self): + cd = self._cls_dict + + script, globs = _make_eq_script(self._attrs) + + def _attach_eq(cls_dict, globs): + cls_dict["__eq__"] = self._add_method_dunders(globs["__eq__"]) + + self._script_snippets.append((script, globs, _attach_eq)) + + cd["__ne__"] = __ne__ + + return self + + def add_order(self): + cd = self._cls_dict + + cd["__lt__"], cd["__le__"], cd["__gt__"], cd["__ge__"] = ( + self._add_method_dunders(meth) + for meth in _make_order(self._cls, self._attrs) + ) + + return self + + def add_setattr(self): + sa_attrs = {} + for a in self._attrs: + on_setattr = a.on_setattr or self._on_setattr + if on_setattr and on_setattr is not setters.NO_OP: + sa_attrs[a.name] = a, on_setattr + + if not sa_attrs: + return self + + if self._has_custom_setattr: + # We need to write a __setattr__ but there already is one! + msg = "Can't combine custom __setattr__ with on_setattr hooks." + raise ValueError(msg) + + # docstring comes from _add_method_dunders + def __setattr__(self, name, val): + try: + a, hook = sa_attrs[name] + except KeyError: + nval = val + else: + nval = hook(self, a, val) + + _OBJ_SETATTR(self, name, nval) + + self._cls_dict["__attrs_own_setattr__"] = True + self._cls_dict["__setattr__"] = self._add_method_dunders(__setattr__) + self._wrote_own_setattr = True + + return self + + def _add_method_dunders_unsafe(self, method: Callable) -> Callable: + """ + Add __module__ and __qualname__ to a *method*. + """ + method.__module__ = self._cls.__module__ + + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" + + method.__doc__ = ( + f"Method generated by attrs for class {self._cls.__qualname__}." + ) + + return method + + def _add_method_dunders_safe(self, method: Callable) -> Callable: + """ + Add __module__ and __qualname__ to a *method* if possible. + """ + with contextlib.suppress(AttributeError): + method.__module__ = self._cls.__module__ + + with contextlib.suppress(AttributeError): + method.__qualname__ = f"{self._cls.__qualname__}.{method.__name__}" + + with contextlib.suppress(AttributeError): + method.__doc__ = f"Method generated by attrs for class {self._cls.__qualname__}." + + return method + + +def _determine_attrs_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + return cmp, cmp + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq = default_eq + + if order is None: + order = eq + + if eq is False and order is True: + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) + + return eq, order + + +def _determine_attrib_eq_order(cmp, eq, order, default_eq): + """ + Validate the combination of *cmp*, *eq*, and *order*. Derive the effective + values of eq and order. If *eq* is None, set it to *default_eq*. + """ + if cmp is not None and any((eq is not None, order is not None)): + msg = "Don't mix `cmp` with `eq' and `order`." + raise ValueError(msg) + + def decide_callable_or_boolean(value): + """ + Decide whether a key function is used. + """ + if callable(value): + value, key = True, value + else: + key = None + return value, key + + # cmp takes precedence due to bw-compatibility. + if cmp is not None: + cmp, cmp_key = decide_callable_or_boolean(cmp) + return cmp, cmp_key, cmp, cmp_key + + # If left None, equality is set to the specified default and ordering + # mirrors equality. + if eq is None: + eq, eq_key = default_eq, None + else: + eq, eq_key = decide_callable_or_boolean(eq) + + if order is None: + order, order_key = eq, eq_key + else: + order, order_key = decide_callable_or_boolean(order) + + if eq is False and order is True: + msg = "`order` can only be True if `eq` is True too." + raise ValueError(msg) + + return eq, eq_key, order, order_key + + +def _determine_whether_to_implement( + cls, flag, auto_detect, dunders, default=True +): + """ + Check whether we should implement a set of methods for *cls*. + + *flag* is the argument passed into @attr.s like 'init', *auto_detect* the + same as passed into @attr.s and *dunders* is a tuple of attribute names + whose presence signal that the user has implemented it themselves. + + Return *default* if no reason for either for or against is found. + """ + if flag is True or flag is False: + return flag + + if flag is None and auto_detect is False: + return default + + # Logically, flag is None and auto_detect is True here. + for dunder in dunders: + if _has_own_attribute(cls, dunder): + return False + + return default + + +def attrs( + maybe_cls=None, + these=None, + repr_ns=None, + repr=None, + cmp=None, + hash=None, + init=None, + slots=False, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=False, + kw_only=False, + cache_hash=False, + auto_exc=False, + eq=None, + order=None, + auto_detect=False, + collect_by_mro=False, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, + unsafe_hash=None, + force_kw_only=True, +): + r""" + A class decorator that adds :term:`dunder methods` according to the + specified attributes using `attr.ib` or the *these* argument. + + Consider using `attrs.define` / `attrs.frozen` in new code (``attr.s`` will + *never* go away, though). + + Args: + repr_ns (str): + When using nested classes, there was no way in Python 2 to + automatically detect that. This argument allows to set a custom + name for a more meaningful ``repr`` output. This argument is + pointless in Python 3 and is therefore deprecated. + + .. caution:: + Refer to `attrs.define` for the rest of the parameters, but note that they + can have different defaults. + + Notably, leaving *on_setattr* as `None` will **not** add any hooks. + + .. versionadded:: 16.0.0 *slots* + .. versionadded:: 16.1.0 *frozen* + .. versionadded:: 16.3.0 *str* + .. versionadded:: 16.3.0 Support for ``__attrs_post_init__``. + .. versionchanged:: 17.1.0 + *hash* supports `None` as value which is also the default now. + .. versionadded:: 17.3.0 *auto_attribs* + .. versionchanged:: 18.1.0 + If *these* is passed, no attributes are deleted from the class body. + .. versionchanged:: 18.1.0 If *these* is ordered, the order is retained. + .. versionadded:: 18.2.0 *weakref_slot* + .. deprecated:: 18.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now raise a + `DeprecationWarning` if the classes compared are subclasses of + each other. ``__eq`` and ``__ne__`` never tried to compared subclasses + to each other. + .. versionchanged:: 19.2.0 + ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` now do not consider + subclasses comparable anymore. + .. versionadded:: 18.2.0 *kw_only* + .. versionadded:: 18.2.0 *cache_hash* + .. versionadded:: 19.1.0 *auto_exc* + .. deprecated:: 19.2.0 *cmp* Removal on or after 2021-06-01. + .. versionadded:: 19.2.0 *eq* and *order* + .. versionadded:: 20.1.0 *auto_detect* + .. versionadded:: 20.1.0 *collect_by_mro* + .. versionadded:: 20.1.0 *getstate_setstate* + .. versionadded:: 20.1.0 *on_setattr* + .. versionadded:: 20.3.0 *field_transformer* + .. versionchanged:: 21.1.0 + ``init=False`` injects ``__attrs_init__`` + .. versionchanged:: 21.1.0 Support for ``__attrs_pre_init__`` + .. versionchanged:: 21.1.0 *cmp* undeprecated + .. versionadded:: 21.3.0 *match_args* + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). + .. deprecated:: 24.1.0 *repr_ns* + .. versionchanged:: 24.1.0 + Instances are not compared as tuples of attributes anymore, but using a + big ``and`` condition. This is faster and has more correct behavior for + uncomparable values like `math.nan`. + .. versionadded:: 24.1.0 + If a class has an *inherited* classmethod called + ``__attrs_init_subclass__``, it is executed after the class is created. + .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. + .. versionchanged:: 25.4.0 + *kw_only* now only applies to attributes defined in the current class, + and respects attribute-level ``kw_only=False`` settings. + .. versionadded:: 25.4.0 *force_kw_only* + """ + if repr_ns is not None: + import warnings + + warnings.warn( + DeprecationWarning( + "The `repr_ns` argument is deprecated and will be removed in or after August 2025." + ), + stacklevel=2, + ) + + eq_, order_ = _determine_attrs_eq_order(cmp, eq, order, None) + + # unsafe_hash takes precedence due to PEP 681. + if unsafe_hash is not None: + hash = unsafe_hash + + if isinstance(on_setattr, (list, tuple)): + on_setattr = setters.pipe(*on_setattr) + + def wrap(cls): + nonlocal hash + is_frozen = frozen or _has_frozen_base_class(cls) + is_exc = auto_exc is True and issubclass(cls, BaseException) + has_own_setattr = auto_detect and _has_own_attribute( + cls, "__setattr__" + ) + + if has_own_setattr and is_frozen: + msg = "Can't freeze a class with a custom __setattr__." + raise ValueError(msg) + + eq = not is_exc and _determine_whether_to_implement( + cls, eq_, auto_detect, ("__eq__", "__ne__") + ) + + Hashability = ClassProps.Hashability + + if is_exc: + hashability = Hashability.LEAVE_ALONE + elif hash is True: + hashability = ( + Hashability.HASHABLE_CACHED + if cache_hash + else Hashability.HASHABLE + ) + elif hash is False: + hashability = Hashability.LEAVE_ALONE + elif hash is None: + if auto_detect is True and _has_own_attribute(cls, "__hash__"): + hashability = Hashability.LEAVE_ALONE + elif eq is True and is_frozen is True: + hashability = ( + Hashability.HASHABLE_CACHED + if cache_hash + else Hashability.HASHABLE + ) + elif eq is False: + hashability = Hashability.LEAVE_ALONE + else: + hashability = Hashability.UNHASHABLE + else: + msg = "Invalid value for hash. Must be True, False, or None." + raise TypeError(msg) + + KeywordOnly = ClassProps.KeywordOnly + if kw_only: + kwo = KeywordOnly.FORCE if force_kw_only else KeywordOnly.YES + else: + kwo = KeywordOnly.NO + + props = ClassProps( + is_exception=is_exc, + is_frozen=is_frozen, + is_slotted=slots, + collected_fields_by_mro=collect_by_mro, + added_init=_determine_whether_to_implement( + cls, init, auto_detect, ("__init__",) + ), + added_repr=_determine_whether_to_implement( + cls, repr, auto_detect, ("__repr__",) + ), + added_eq=eq, + added_ordering=not is_exc + and _determine_whether_to_implement( + cls, + order_, + auto_detect, + ("__lt__", "__le__", "__gt__", "__ge__"), + ), + hashability=hashability, + added_match_args=match_args, + kw_only=kwo, + has_weakref_slot=weakref_slot, + added_str=str, + added_pickling=_determine_whether_to_implement( + cls, + getstate_setstate, + auto_detect, + ("__getstate__", "__setstate__"), + default=slots, + ), + on_setattr_hook=on_setattr, + field_transformer=field_transformer, + ) + + if not props.is_hashable and cache_hash: + msg = "Invalid value for cache_hash. To use hash caching, hashing must be either explicitly or implicitly enabled." + raise TypeError(msg) + + builder = _ClassBuilder( + cls, + these, + auto_attribs=auto_attribs, + props=props, + has_custom_setattr=has_own_setattr, + ) + + if props.added_repr: + builder.add_repr(repr_ns) + + if props.added_str: + builder.add_str() + + if props.added_eq: + builder.add_eq() + if props.added_ordering: + builder.add_order() + + if not frozen: + builder.add_setattr() + + if props.is_hashable: + builder.add_hash() + elif props.hashability is Hashability.UNHASHABLE: + builder.make_unhashable() + + if props.added_init: + builder.add_init() + else: + builder.add_attrs_init() + if cache_hash: + msg = "Invalid value for cache_hash. To use hash caching, init must be True." + raise TypeError(msg) + + if PY_3_13_PLUS and not _has_own_attribute(cls, "__replace__"): + builder.add_replace() + + if ( + PY_3_10_PLUS + and match_args + and not _has_own_attribute(cls, "__match_args__") + ): + builder.add_match_args() + + return builder.build_class() + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but `None` if used as `@attrs()`. + if maybe_cls is None: + return wrap + + return wrap(maybe_cls) + + +_attrs = attrs +""" +Internal alias so we can use it in functions that take an argument called +*attrs*. +""" + + +def _has_frozen_base_class(cls): + """ + Check whether *cls* has a frozen ancestor by looking at its + __setattr__. + """ + return cls.__setattr__ is _frozen_setattrs + + +def _generate_unique_filename(cls: type, func_name: str) -> str: + """ + Create a "filename" suitable for a function being generated. + """ + return ( + f"" + ) + + +def _make_hash_script( + cls: type, attrs: list[Attribute], frozen: bool, cache_hash: bool +) -> tuple[str, dict]: + attrs = tuple( + a for a in attrs if a.hash is True or (a.hash is None and a.eq is True) + ) + + tab = " " + + type_hash = hash(_generate_unique_filename(cls, "hash")) + # If eq is custom generated, we need to include the functions in globs + globs = {} + + hash_def = "def __hash__(self" + hash_func = "hash((" + closing_braces = "))" + if not cache_hash: + hash_def += "):" + else: + hash_def += ", *" + + hash_def += ", _cache_wrapper=__import__('attr._make')._make._CacheHashWrapper):" + hash_func = "_cache_wrapper(" + hash_func + closing_braces += ")" + + method_lines = [hash_def] + + def append_hash_computation_lines(prefix, indent): + """ + Generate the code for actually computing the hash code. + Below this will either be returned directly or used to compute + a value which is then cached, depending on the value of cache_hash + """ + + method_lines.extend( + [ + indent + prefix + hash_func, + indent + f" {type_hash},", + ] + ) + + for a in attrs: + if a.eq_key: + cmp_name = f"_{a.name}_key" + globs[cmp_name] = a.eq_key + method_lines.append( + indent + f" {cmp_name}(self.{a.name})," + ) + else: + method_lines.append(indent + f" self.{a.name},") + + method_lines.append(indent + " " + closing_braces) + + if cache_hash: + method_lines.append(tab + f"if self.{_HASH_CACHE_FIELD} is None:") + if frozen: + append_hash_computation_lines( + f"object.__setattr__(self, '{_HASH_CACHE_FIELD}', ", tab * 2 + ) + method_lines.append(tab * 2 + ")") # close __setattr__ + else: + append_hash_computation_lines( + f"self.{_HASH_CACHE_FIELD} = ", tab * 2 + ) + method_lines.append(tab + f"return self.{_HASH_CACHE_FIELD}") + else: + append_hash_computation_lines("return ", tab) + + script = "\n".join(method_lines) + return script, globs + + +def _add_hash(cls: type, attrs: list[Attribute]): + """ + Add a hash method to *cls*. + """ + script, globs = _make_hash_script( + cls, attrs, frozen=False, cache_hash=False + ) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__hash__") + ) + cls.__hash__ = globs["__hash__"] + return cls + + +def __ne__(self, other): + """ + Check equality and either forward a NotImplemented or + return the result negated. + """ + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + + return not result + + +def _make_eq_script(attrs: list) -> tuple[str, dict]: + """ + Create __eq__ method for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.eq] + + lines = [ + "def __eq__(self, other):", + " if other.__class__ is not self.__class__:", + " return NotImplemented", + ] + + globs = {} + if attrs: + lines.append(" return (") + for a in attrs: + if a.eq_key: + cmp_name = f"_{a.name}_key" + # Add the key function to the global namespace + # of the evaluated function. + globs[cmp_name] = a.eq_key + lines.append( + f" {cmp_name}(self.{a.name}) == {cmp_name}(other.{a.name})" + ) + else: + lines.append(f" self.{a.name} == other.{a.name}") + if a is not attrs[-1]: + lines[-1] = f"{lines[-1]} and" + lines.append(" )") + else: + lines.append(" return True") + + script = "\n".join(lines) + + return script, globs + + +def _make_order(cls, attrs): + """ + Create ordering methods for *cls* with *attrs*. + """ + attrs = [a for a in attrs if a.order] + + def attrs_to_tuple(obj): + """ + Save us some typing. + """ + return tuple( + key(value) if key else value + for value, key in ( + (getattr(obj, a.name), a.order_key) for a in attrs + ) + ) + + def __lt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) < attrs_to_tuple(other) + + return NotImplemented + + def __le__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) <= attrs_to_tuple(other) + + return NotImplemented + + def __gt__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) > attrs_to_tuple(other) + + return NotImplemented + + def __ge__(self, other): + """ + Automatically created by attrs. + """ + if other.__class__ is self.__class__: + return attrs_to_tuple(self) >= attrs_to_tuple(other) + + return NotImplemented + + return __lt__, __le__, __gt__, __ge__ + + +def _add_eq(cls, attrs=None): + """ + Add equality methods to *cls* with *attrs*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + script, globs = _make_eq_script(attrs) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__eq__") + ) + cls.__eq__ = globs["__eq__"] + cls.__ne__ = __ne__ + + return cls + + +def _make_repr_script(attrs, ns) -> tuple[str, dict]: + """ + Create the source and globs for a __repr__ and return it. + """ + # Figure out which attributes to include, and which function to use to + # format them. The a.repr value can be either bool or a custom + # callable. + attr_names_with_reprs = tuple( + (a.name, (repr if a.repr is True else a.repr), a.init) + for a in attrs + if a.repr is not False + ) + globs = { + name + "_repr": r for name, r, _ in attr_names_with_reprs if r != repr + } + globs["_compat"] = _compat + globs["AttributeError"] = AttributeError + globs["NOTHING"] = NOTHING + attribute_fragments = [] + for name, r, i in attr_names_with_reprs: + accessor = ( + "self." + name if i else 'getattr(self, "' + name + '", NOTHING)' + ) + fragment = ( + "%s={%s!r}" % (name, accessor) + if r == repr + else "%s={%s_repr(%s)}" % (name, name, accessor) + ) + attribute_fragments.append(fragment) + repr_fragment = ", ".join(attribute_fragments) + + if ns is None: + cls_name_fragment = '{self.__class__.__qualname__.rsplit(">.", 1)[-1]}' + else: + cls_name_fragment = ns + ".{self.__class__.__name__}" + + lines = [ + "def __repr__(self):", + " try:", + " already_repring = _compat.repr_context.already_repring", + " except AttributeError:", + " already_repring = {id(self),}", + " _compat.repr_context.already_repring = already_repring", + " else:", + " if id(self) in already_repring:", + " return '...'", + " else:", + " already_repring.add(id(self))", + " try:", + f" return f'{cls_name_fragment}({repr_fragment})'", + " finally:", + " already_repring.remove(id(self))", + ] + + return "\n".join(lines), globs + + +def _add_repr(cls, ns=None, attrs=None): + """ + Add a repr method to *cls*. + """ + if attrs is None: + attrs = cls.__attrs_attrs__ + + script, globs = _make_repr_script(attrs, ns) + _compile_and_eval( + script, globs, filename=_generate_unique_filename(cls, "__repr__") + ) + cls.__repr__ = globs["__repr__"] + return cls + + +def fields(cls): + """ + Return the tuple of *attrs* attributes for a class or instance. + + The tuple also allows accessing the fields by their names (see below for + examples). + + Args: + cls (type): Class or instance to introspect. + + Raises: + TypeError: If *cls* is neither a class nor an *attrs* instance. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + Returns: + tuple (with name accessors) of `attrs.Attribute` + + .. versionchanged:: 16.2.0 Returned tuple allows accessing the fields + by name. + .. versionchanged:: 23.1.0 Add support for generic classes. + .. versionchanged:: 26.1.0 Add support for instances. + """ + generic_base = get_generic_base(cls) + + if generic_base is None and not isinstance(cls, type): + type_ = type(cls) + if getattr(type_, "__attrs_attrs__", None) is None: + msg = "Passed object must be a class or attrs instance." + raise TypeError(msg) + + return fields(type_) + + attrs = getattr(cls, "__attrs_attrs__", None) + + if attrs is None: + if generic_base is not None: + attrs = getattr(generic_base, "__attrs_attrs__", None) + if attrs is not None: + # Even though this is global state, stick it on here to speed + # it up. We rely on `cls` being cached for this to be + # efficient. + cls.__attrs_attrs__ = attrs + return attrs + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + + return attrs + + +def fields_dict(cls): + """ + Return an ordered dictionary of *attrs* attributes for a class, whose keys + are the attribute names. + + Args: + cls (type): Class to introspect. + + Raises: + TypeError: If *cls* is not a class. + + attrs.exceptions.NotAnAttrsClassError: + If *cls* is not an *attrs* class. + + Returns: + dict[str, attrs.Attribute]: Dict of attribute name to definition + + .. versionadded:: 18.1.0 + """ + if not isinstance(cls, type): + msg = "Passed object must be a class." + raise TypeError(msg) + attrs = getattr(cls, "__attrs_attrs__", None) + if attrs is None: + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) + return {a.name: a for a in attrs} + + +def validate(inst): + """ + Validate all attributes on *inst* that have a validator. + + Leaves all exceptions through. + + Args: + inst: Instance of a class with *attrs* attributes. + """ + if _config._run_validators is False: + return + + for a in fields(inst.__class__): + v = a.validator + if v is not None: + v(inst, a, getattr(inst, a.name)) + + +def _is_slot_attr(a_name, base_attr_map): + """ + Check if the attribute name comes from a slot class. + """ + cls = base_attr_map.get(a_name) + return cls and "__slots__" in cls.__dict__ + + +def _make_init_script( + cls, + attrs, + pre_init, + pre_init_has_args, + post_init, + frozen, + slots, + cache_hash, + base_attr_map, + is_exc, + cls_on_setattr, + attrs_init, +) -> tuple[str, dict, dict]: + has_cls_on_setattr = ( + cls_on_setattr is not None and cls_on_setattr is not setters.NO_OP + ) + + if frozen and has_cls_on_setattr: + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) + + needs_cached_setattr = cache_hash or frozen + filtered_attrs = [] + attr_dict = {} + for a in attrs: + if not a.init and a.default is NOTHING: + continue + + filtered_attrs.append(a) + attr_dict[a.name] = a + + if a.on_setattr is not None: + if frozen is True and a.on_setattr is not setters.NO_OP: + msg = "Frozen classes can't use on_setattr." + raise ValueError(msg) + + needs_cached_setattr = True + elif has_cls_on_setattr and a.on_setattr is not setters.NO_OP: + needs_cached_setattr = True + + script, globs, annotations = _attrs_to_init_script( + filtered_attrs, + frozen, + slots, + pre_init, + pre_init_has_args, + post_init, + cache_hash, + base_attr_map, + is_exc, + needs_cached_setattr, + has_cls_on_setattr, + "__attrs_init__" if attrs_init else "__init__", + ) + if cls.__module__ in sys.modules: + # This makes typing.get_type_hints(CLS.__init__) resolve string types. + globs.update(sys.modules[cls.__module__].__dict__) + + globs.update({"NOTHING": NOTHING, "attr_dict": attr_dict}) + + if needs_cached_setattr: + # Save the lookup overhead in __init__ if we need to circumvent + # setattr hooks. + globs["_cached_setattr_get"] = _OBJ_SETATTR.__get__ + + return script, globs, annotations + + +def _setattr(attr_name: str, value_var: str, has_on_setattr: bool) -> str: + """ + Use the cached object.setattr to set *attr_name* to *value_var*. + """ + return f"_setattr('{attr_name}', {value_var})" + + +def _setattr_with_converter( + attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter +) -> str: + """ + Use the cached object.setattr to set *attr_name* to *value_var*, but run + its converter first. + """ + return f"_setattr('{attr_name}', {converter._fmt_converter_call(attr_name, value_var)})" + + +def _assign(attr_name: str, value: str, has_on_setattr: bool) -> str: + """ + Unless *attr_name* has an on_setattr hook, use normal assignment. Otherwise + relegate to _setattr. + """ + if has_on_setattr: + return _setattr(attr_name, value, True) + + return f"self.{attr_name} = {value}" + + +def _assign_with_converter( + attr_name: str, value_var: str, has_on_setattr: bool, converter: Converter +) -> str: + """ + Unless *attr_name* has an on_setattr hook, use normal assignment after + conversion. Otherwise relegate to _setattr_with_converter. + """ + if has_on_setattr: + return _setattr_with_converter(attr_name, value_var, True, converter) + + return f"self.{attr_name} = {converter._fmt_converter_call(attr_name, value_var)}" + + +def _determine_setters( + frozen: bool, slots: bool, base_attr_map: dict[str, type] +): + """ + Determine the correct setter functions based on whether a class is frozen + and/or slotted. + """ + if frozen is True: + if slots is True: + return (), _setattr, _setattr_with_converter + + # Dict frozen classes assign directly to __dict__. + # But only if the attribute doesn't come from an ancestor slot + # class. + # Note _inst_dict will be used again below if cache_hash is True + + def fmt_setter( + attr_name: str, value_var: str, has_on_setattr: bool + ) -> str: + if _is_slot_attr(attr_name, base_attr_map): + return _setattr(attr_name, value_var, has_on_setattr) + + return f"_inst_dict['{attr_name}'] = {value_var}" + + def fmt_setter_with_converter( + attr_name: str, + value_var: str, + has_on_setattr: bool, + converter: Converter, + ) -> str: + if has_on_setattr or _is_slot_attr(attr_name, base_attr_map): + return _setattr_with_converter( + attr_name, value_var, has_on_setattr, converter + ) + + return f"_inst_dict['{attr_name}'] = {converter._fmt_converter_call(attr_name, value_var)}" + + return ( + ("_inst_dict = self.__dict__",), + fmt_setter, + fmt_setter_with_converter, + ) + + # Not frozen -- we can just assign directly. + return (), _assign, _assign_with_converter + + +def _attrs_to_init_script( + attrs: list[Attribute], + is_frozen: bool, + is_slotted: bool, + call_pre_init: bool, + pre_init_has_args: bool, + call_post_init: bool, + does_cache_hash: bool, + base_attr_map: dict[str, type], + is_exc: bool, + needs_cached_setattr: bool, + has_cls_on_setattr: bool, + method_name: str, +) -> tuple[str, dict, dict]: + """ + Return a script of an initializer for *attrs*, a dict of globals, and + annotations for the initializer. + + The globals are required by the generated script. + """ + lines = ["self.__attrs_pre_init__()"] if call_pre_init else [] + + if needs_cached_setattr: + lines.append( + # Circumvent the __setattr__ descriptor to save one lookup per + # assignment. Note _setattr will be used again below if + # does_cache_hash is True. + "_setattr = _cached_setattr_get(self)" + ) + + extra_lines, fmt_setter, fmt_setter_with_converter = _determine_setters( + is_frozen, is_slotted, base_attr_map + ) + lines.extend(extra_lines) + + args = [] # Parameters in the definition of __init__ + pre_init_args = [] # Parameters in the call to __attrs_pre_init__ + kw_only_args = [] # Used for both 'args' and 'pre_init_args' above + attrs_to_validate = [] + + # This is a dictionary of names to validator and converter callables. + # Injecting this into __init__ globals lets us avoid lookups. + names_for_globals = {} + annotations = {"return": None} + + for a in attrs: + if a.validator: + attrs_to_validate.append(a) + + attr_name = a.name + has_on_setattr = a.on_setattr is not None or ( + a.on_setattr is not setters.NO_OP and has_cls_on_setattr + ) + # a.alias is set to maybe-mangled attr_name in _ClassBuilder if not + # explicitly provided + arg_name = a.alias + + has_factory = isinstance(a.default, Factory) + maybe_self = "self" if has_factory and a.default.takes_self else "" + + if a.converter is not None and not isinstance(a.converter, Converter): + converter = Converter(a.converter) + else: + converter = a.converter + + if a.init is False: + if has_factory: + init_factory_name = _INIT_FACTORY_PAT % (a.name,) + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + init_factory_name + f"({maybe_self})", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + fmt_setter( + attr_name, + init_factory_name + f"({maybe_self})", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + elif converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + fmt_setter( + attr_name, + f"attr_dict['{attr_name}'].default", + has_on_setattr, + ) + ) + elif a.default is not NOTHING and not has_factory: + arg = f"{arg_name}=attr_dict['{attr_name}'].default" + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + pre_init_args.append(arg_name) + + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + elif has_factory: + arg = f"{arg_name}=NOTHING" + if a.kw_only: + kw_only_args.append(arg) + else: + args.append(arg) + pre_init_args.append(arg_name) + lines.append(f"if {arg_name} is not NOTHING:") + + init_factory_name = _INIT_FACTORY_PAT % (a.name,) + if converter is not None: + lines.append( + " " + + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter_with_converter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + converter, + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append( + " " + fmt_setter(attr_name, arg_name, has_on_setattr) + ) + lines.append("else:") + lines.append( + " " + + fmt_setter( + attr_name, + init_factory_name + "(" + maybe_self + ")", + has_on_setattr, + ) + ) + names_for_globals[init_factory_name] = a.default.factory + else: + if a.kw_only: + kw_only_args.append(arg_name) + else: + args.append(arg_name) + pre_init_args.append(arg_name) + + if converter is not None: + lines.append( + fmt_setter_with_converter( + attr_name, arg_name, has_on_setattr, converter + ) + ) + names_for_globals[converter._get_global_name(a.name)] = ( + converter.converter + ) + else: + lines.append(fmt_setter(attr_name, arg_name, has_on_setattr)) + + if a.init is True: + if a.type is not None and converter is None: + annotations[arg_name] = a.type + elif converter is not None and converter._first_param_type: + # Use the type from the converter if present. + annotations[arg_name] = converter._first_param_type + + if attrs_to_validate: # we can skip this if there are no validators. + names_for_globals["_config"] = _config + lines.append("if _config._run_validators is True:") + for a in attrs_to_validate: + val_name = "__attr_validator_" + a.name + attr_name = "__attr_" + a.name + lines.append(f" {val_name}(self, {attr_name}, self.{a.name})") + names_for_globals[val_name] = a.validator + names_for_globals[attr_name] = a + + if call_post_init: + lines.append("self.__attrs_post_init__()") + + # Because this is set only after __attrs_post_init__ is called, a crash + # will result if post-init tries to access the hash code. This seemed + # preferable to setting this beforehand, in which case alteration to field + # values during post-init combined with post-init accessing the hash code + # would result in silent bugs. + if does_cache_hash: + if is_frozen: + if is_slotted: + init_hash_cache = f"_setattr('{_HASH_CACHE_FIELD}', None)" + else: + init_hash_cache = f"_inst_dict['{_HASH_CACHE_FIELD}'] = None" + else: + init_hash_cache = f"self.{_HASH_CACHE_FIELD} = None" + lines.append(init_hash_cache) + + # For exceptions we rely on BaseException.__init__ for proper + # initialization. + if is_exc: + vals = ",".join(f"self.{a.name}" for a in attrs if a.init) + + lines.append(f"BaseException.__init__(self, {vals})") + + args = ", ".join(args) + pre_init_args = ", ".join(pre_init_args) + if kw_only_args: + # leading comma & kw_only args + args += f"{', ' if args else ''}*, {', '.join(kw_only_args)}" + pre_init_kw_only_args = ", ".join( + [ + f"{kw_arg_name}={kw_arg_name}" + # We need to remove the defaults from the kw_only_args. + for kw_arg_name in (kwa.split("=")[0] for kwa in kw_only_args) + ] + ) + pre_init_args += ", " if pre_init_args else "" + pre_init_args += pre_init_kw_only_args + + if call_pre_init and pre_init_has_args: + # If pre init method has arguments, pass the values given to __init__. + lines[0] = f"self.__attrs_pre_init__({pre_init_args})" + + # Python <3.12 doesn't allow backslashes in f-strings. + NL = "\n " + return ( + f"""def {method_name}(self, {args}): + {NL.join(lines) if lines else "pass"} +""", + names_for_globals, + annotations, + ) + + +def _default_init_alias_for(name: str) -> str: + """ + The default __init__ parameter name for a field. + + This performs private-name adjustment via leading-unscore stripping, + and is the default value of Attribute.alias if not provided. + """ + + return name.lstrip("_") + + +class Attribute: + """ + *Read-only* representation of an attribute. + + .. warning:: + + You should never instantiate this class yourself. + + The class has *all* arguments of `attr.ib` (except for ``factory`` which is + only syntactic sugar for ``default=Factory(...)`` plus the following: + + - ``name`` (`str`): The name of the attribute. + - ``alias`` (`str`): The __init__ parameter name of the attribute, after + any explicit overrides and default private-attribute-name handling. + - ``alias_is_default`` (`bool`): Whether the ``alias`` was automatically + generated (``True``) or explicitly provided by the user (``False``). + - ``inherited`` (`bool`): Whether or not that attribute has been inherited + from a base class. + - ``eq_key`` and ``order_key`` (`typing.Callable` or `None`): The + callables that are used for comparing and ordering objects by this + attribute, respectively. These are set by passing a callable to + `attr.ib`'s ``eq``, ``order``, or ``cmp`` arguments. See also + :ref:`comparison customization `. + + Instances of this class are frequently used for introspection purposes + like: + + - `fields` returns a tuple of them. + - Validators get them passed as the first argument. + - The :ref:`field transformer ` hook receives a list of + them. + - The ``alias`` property exposes the __init__ parameter name of the field, + with any overrides and default private-attribute handling applied. + + + .. versionadded:: 20.1.0 *inherited* + .. versionadded:: 20.1.0 *on_setattr* + .. versionchanged:: 20.2.0 *inherited* is not taken into account for + equality checks and hashing anymore. + .. versionadded:: 21.1.0 *eq_key* and *order_key* + .. versionadded:: 22.2.0 *alias* + .. versionadded:: 26.1.0 *alias_is_default* + + For the full version history of the fields, see `attr.ib`. + """ + + # These slots must NOT be reordered because we use them later for + # instantiation. + __slots__ = ( # noqa: RUF023 + "name", + "default", + "validator", + "repr", + "eq", + "eq_key", + "order", + "order_key", + "hash", + "init", + "metadata", + "type", + "converter", + "kw_only", + "inherited", + "on_setattr", + "alias", + "alias_is_default", + ) + + def __init__( + self, + name, + default, + validator, + repr, + cmp, # XXX: unused, remove along with other cmp code. + hash, + init, + inherited, + metadata=None, + type=None, + converter=None, + kw_only=False, + eq=None, + eq_key=None, + order=None, + order_key=None, + on_setattr=None, + alias=None, + alias_is_default=None, + ): + eq, eq_key, order, order_key = _determine_attrib_eq_order( + cmp, eq_key or eq, order_key or order, True + ) + + # Cache this descriptor here to speed things up later. + bound_setattr = _OBJ_SETATTR.__get__(self) + + # Despite the big red warning, people *do* instantiate `Attribute` + # themselves. + bound_setattr("name", name) + bound_setattr("default", default) + bound_setattr("validator", validator) + bound_setattr("repr", repr) + bound_setattr("eq", eq) + bound_setattr("eq_key", eq_key) + bound_setattr("order", order) + bound_setattr("order_key", order_key) + bound_setattr("hash", hash) + bound_setattr("init", init) + bound_setattr("converter", converter) + bound_setattr( + "metadata", + ( + types.MappingProxyType(dict(metadata)) # Shallow copy + if metadata + else _EMPTY_METADATA_SINGLETON + ), + ) + bound_setattr("type", type) + bound_setattr("kw_only", kw_only) + bound_setattr("inherited", inherited) + bound_setattr("on_setattr", on_setattr) + bound_setattr("alias", alias) + bound_setattr( + "alias_is_default", + alias is None if alias_is_default is None else alias_is_default, + ) + + def __setattr__(self, name, value): + raise FrozenInstanceError + + @classmethod + def from_counting_attr( + cls, name: str, ca: _CountingAttr, kw_only: bool, type=None + ): + # The 'kw_only' argument is the class-level setting, and is used if the + # attribute itself does not explicitly set 'kw_only'. + # type holds the annotated value. deal with conflicts: + if type is None: + type = ca.type + elif ca.type is not None: + msg = f"Type annotation and type argument cannot both be present for '{name}'." + raise ValueError(msg) + return cls( + name, + ca._default, + ca._validator, + ca.repr, + None, + ca.hash, + ca.init, + False, + ca.metadata, + type, + ca.converter, + kw_only if ca.kw_only is None else ca.kw_only, + ca.eq, + ca.eq_key, + ca.order, + ca.order_key, + ca.on_setattr, + ca.alias, + ca.alias is None, + ) + + # Don't use attrs.evolve since fields(Attribute) doesn't work + def evolve(self, **changes): + """ + Copy *self* and apply *changes*. + + This works similarly to `attrs.evolve` but that function does not work + with :class:`attrs.Attribute`. + + It is mainly meant to be used for `transform-fields`. + + .. versionadded:: 20.3.0 + """ + new = copy.copy(self) + + new._setattrs(changes.items()) + + if "alias" in changes and "alias_is_default" not in changes: + # Explicit alias provided -- no longer the default. + _OBJ_SETATTR.__get__(new)("alias_is_default", False) + elif ( + "name" in changes + and "alias" not in changes + # Don't auto-generate alias if the user picked picked the old one. + and self.alias_is_default + ): + # Name changed, alias was auto-generated -- update it. + _OBJ_SETATTR.__get__(new)( + "alias", _default_init_alias_for(new.name) + ) + + return new + + # Don't use _add_pickle since fields(Attribute) doesn't work + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple( + getattr(self, name) if name != "metadata" else dict(self.metadata) + for name in self.__slots__ + ) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + if len(state) < len(self.__slots__): + # Pre-26.1.0 pickle without alias_is_default -- infer it + # heuristically. + state_dict = dict(zip(self.__slots__, state)) + alias_is_default = state_dict.get( + "alias" + ) is None or state_dict.get("alias") == _default_init_alias_for( + state_dict["name"] + ) + state = (*state, alias_is_default) + + self._setattrs(zip(self.__slots__, state)) + + def _setattrs(self, name_values_pairs): + bound_setattr = _OBJ_SETATTR.__get__(self) + for name, value in name_values_pairs: + if name != "metadata": + bound_setattr(name, value) + else: + bound_setattr( + name, + ( + types.MappingProxyType(dict(value)) + if value + else _EMPTY_METADATA_SINGLETON + ), + ) + + +_a = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=(name != "alias_is_default"), + cmp=None, + eq=True, + order=False, + hash=(name != "metadata"), + init=True, + inherited=False, + alias=_default_init_alias_for(name), + ) + for name in Attribute.__slots__ +] + +Attribute = _add_hash( + _add_eq( + _add_repr(Attribute, attrs=_a), + attrs=[a for a in _a if a.name != "inherited"], + ), + attrs=[a for a in _a if a.hash and a.name != "inherited"], +) + + +class _CountingAttr: + """ + Intermediate representation of attributes that uses a counter to preserve + the order in which the attributes have been defined. + + *Internal* data structure of the attrs library. Running into is most + likely the result of a bug like a forgotten `@attr.s` decorator. + """ + + __slots__ = ( + "_default", + "_validator", + "alias", + "converter", + "counter", + "eq", + "eq_key", + "hash", + "init", + "kw_only", + "metadata", + "on_setattr", + "order", + "order_key", + "repr", + "type", + ) + __attrs_attrs__ = ( + *tuple( + Attribute( + name=name, + alias=_default_init_alias_for(name), + default=NOTHING, + validator=None, + repr=True, + cmp=None, + hash=True, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ) + for name in ( + "counter", + "_default", + "repr", + "eq", + "order", + "hash", + "init", + "on_setattr", + "alias", + ) + ), + Attribute( + name="metadata", + alias="metadata", + default=None, + validator=None, + repr=True, + cmp=None, + hash=False, + init=True, + kw_only=False, + eq=True, + eq_key=None, + order=False, + order_key=None, + inherited=False, + on_setattr=None, + ), + ) + cls_counter = 0 + + def __init__( + self, + default, + validator, + repr, + cmp, + hash, + init, + converter, + metadata, + type, + kw_only, + eq, + eq_key, + order, + order_key, + on_setattr, + alias, + ): + _CountingAttr.cls_counter += 1 + self.counter = _CountingAttr.cls_counter + self._default = default + self._validator = validator + self.converter = converter + self.repr = repr + self.eq = eq + self.eq_key = eq_key + self.order = order + self.order_key = order_key + self.hash = hash + self.init = init + self.metadata = metadata + self.type = type + self.kw_only = kw_only + self.on_setattr = on_setattr + self.alias = alias + + def validator(self, meth): + """ + Decorator that adds *meth* to the list of validators. + + Returns *meth* unchanged. + + .. versionadded:: 17.1.0 + """ + if self._validator is None: + self._validator = meth + else: + self._validator = and_(self._validator, meth) + return meth + + def default(self, meth): + """ + Decorator that allows to set the default for an attribute. + + Returns *meth* unchanged. + + Raises: + DefaultAlreadySetError: If default has been set before. + + .. versionadded:: 17.1.0 + """ + if self._default is not NOTHING: + raise DefaultAlreadySetError + + self._default = Factory(meth, takes_self=True) + + return meth + + +_CountingAttr = _add_eq(_add_repr(_CountingAttr)) + + +class ClassProps: + """ + Effective class properties as derived from parameters to `attr.s()` or + `define()` decorators. + + This is the same data structure that *attrs* uses internally to decide how + to construct the final class. + + Warning: + + This feature is currently **experimental** and is not covered by our + strict backwards-compatibility guarantees. + + + Attributes: + is_exception (bool): + Whether the class is treated as an exception class. + + is_slotted (bool): + Whether the class is `slotted `. + + has_weakref_slot (bool): + Whether the class has a slot for weak references. + + is_frozen (bool): + Whether the class is frozen. + + kw_only (KeywordOnly): + Whether / how the class enforces keyword-only arguments on the + ``__init__`` method. + + collected_fields_by_mro (bool): + Whether the class fields were collected by method resolution order. + That is, correctly but unlike `dataclasses`. + + added_init (bool): + Whether the class has an *attrs*-generated ``__init__`` method. + + added_repr (bool): + Whether the class has an *attrs*-generated ``__repr__`` method. + + added_eq (bool): + Whether the class has *attrs*-generated equality methods. + + added_ordering (bool): + Whether the class has *attrs*-generated ordering methods. + + hashability (Hashability): How `hashable ` the class is. + + added_match_args (bool): + Whether the class supports positional `match ` over its + fields. + + added_str (bool): + Whether the class has an *attrs*-generated ``__str__`` method. + + added_pickling (bool): + Whether the class has *attrs*-generated ``__getstate__`` and + ``__setstate__`` methods for `pickle`. + + on_setattr_hook (Callable[[Any, Attribute[Any], Any], Any] | None): + The class's ``__setattr__`` hook. + + field_transformer (Callable[[Attribute[Any]], Attribute[Any]] | None): + The class's `field transformers `. + + .. versionadded:: 25.4.0 + """ + + class Hashability(enum.Enum): + """ + The hashability of a class. + + .. versionadded:: 25.4.0 + """ + + HASHABLE = "hashable" + """Write a ``__hash__``.""" + HASHABLE_CACHED = "hashable_cache" + """Write a ``__hash__`` and cache the hash.""" + UNHASHABLE = "unhashable" + """Set ``__hash__`` to ``None``.""" + LEAVE_ALONE = "leave_alone" + """Don't touch ``__hash__``.""" + + class KeywordOnly(enum.Enum): + """ + How attributes should be treated regarding keyword-only parameters. + + .. versionadded:: 25.4.0 + """ + + NO = "no" + """Attributes are not keyword-only.""" + YES = "yes" + """Attributes in current class without kw_only=False are keyword-only.""" + FORCE = "force" + """All attributes are keyword-only.""" + + __slots__ = ( # noqa: RUF023 -- order matters for __init__ + "is_exception", + "is_slotted", + "has_weakref_slot", + "is_frozen", + "kw_only", + "collected_fields_by_mro", + "added_init", + "added_repr", + "added_eq", + "added_ordering", + "hashability", + "added_match_args", + "added_str", + "added_pickling", + "on_setattr_hook", + "field_transformer", + ) + + def __init__( + self, + is_exception, + is_slotted, + has_weakref_slot, + is_frozen, + kw_only, + collected_fields_by_mro, + added_init, + added_repr, + added_eq, + added_ordering, + hashability, + added_match_args, + added_str, + added_pickling, + on_setattr_hook, + field_transformer, + ): + self.is_exception = is_exception + self.is_slotted = is_slotted + self.has_weakref_slot = has_weakref_slot + self.is_frozen = is_frozen + self.kw_only = kw_only + self.collected_fields_by_mro = collected_fields_by_mro + self.added_init = added_init + self.added_repr = added_repr + self.added_eq = added_eq + self.added_ordering = added_ordering + self.hashability = hashability + self.added_match_args = added_match_args + self.added_str = added_str + self.added_pickling = added_pickling + self.on_setattr_hook = on_setattr_hook + self.field_transformer = field_transformer + + @property + def is_hashable(self): + return ( + self.hashability is ClassProps.Hashability.HASHABLE + or self.hashability is ClassProps.Hashability.HASHABLE_CACHED + ) + + +_cas = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + alias=_default_init_alias_for(name), + ) + for name in ClassProps.__slots__ +] + +ClassProps = _add_eq(_add_repr(ClassProps, attrs=_cas), attrs=_cas) + + +class Factory: + """ + Stores a factory callable. + + If passed as the default value to `attrs.field`, the factory is used to + generate a new value. + + Args: + factory (typing.Callable): + A callable that takes either none or exactly one mandatory + positional argument depending on *takes_self*. + + takes_self (bool): + Pass the partially initialized instance that is being initialized + as a positional argument. + + .. versionadded:: 17.1.0 *takes_self* + """ + + __slots__ = ("factory", "takes_self") + + def __init__(self, factory, takes_self=False): + self.factory = factory + self.takes_self = takes_self + + def __getstate__(self): + """ + Play nice with pickle. + """ + return tuple(getattr(self, name) for name in self.__slots__) + + def __setstate__(self, state): + """ + Play nice with pickle. + """ + for name, value in zip(self.__slots__, state): + setattr(self, name, value) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in Factory.__slots__ +] + +Factory = _add_hash(_add_eq(_add_repr(Factory, attrs=_f), attrs=_f), attrs=_f) + + +class Converter: + """ + Stores a converter callable. + + Allows for the wrapped converter to take additional arguments. The + arguments are passed in the order they are documented. + + Args: + converter (Callable): A callable that converts the passed value. + + takes_self (bool): + Pass the partially initialized instance that is being initialized + as a positional argument. (default: `False`) + + takes_field (bool): + Pass the field definition (an :class:`Attribute`) into the + converter as a positional argument. (default: `False`) + + .. versionadded:: 24.1.0 + """ + + __slots__ = ( + "__call__", + "_first_param_type", + "_global_name", + "converter", + "takes_field", + "takes_self", + ) + + def __init__(self, converter, *, takes_self=False, takes_field=False): + self.converter = converter + self.takes_self = takes_self + self.takes_field = takes_field + + ex = _AnnotationExtractor(converter) + self._first_param_type = ex.get_first_param_type() + + if not (self.takes_self or self.takes_field): + self.__call__ = lambda value, _, __: self.converter(value) + elif self.takes_self and not self.takes_field: + self.__call__ = lambda value, instance, __: self.converter( + value, instance + ) + elif not self.takes_self and self.takes_field: + self.__call__ = lambda value, __, field: self.converter( + value, field + ) + else: + self.__call__ = self.converter + + rt = ex.get_return_type() + if rt is not None: + self.__call__.__annotations__["return"] = rt + + @staticmethod + def _get_global_name(attr_name: str) -> str: + """ + Return the name that a converter for an attribute name *attr_name* + would have. + """ + return f"__attr_converter_{attr_name}" + + def _fmt_converter_call(self, attr_name: str, value_var: str) -> str: + """ + Return a string that calls the converter for an attribute name + *attr_name* and the value in variable named *value_var* according to + `self.takes_self` and `self.takes_field`. + """ + if not (self.takes_self or self.takes_field): + return f"{self._get_global_name(attr_name)}({value_var})" + + if self.takes_self and self.takes_field: + return f"{self._get_global_name(attr_name)}({value_var}, self, attr_dict['{attr_name}'])" + + if self.takes_self: + return f"{self._get_global_name(attr_name)}({value_var}, self)" + + return f"{self._get_global_name(attr_name)}({value_var}, attr_dict['{attr_name}'])" + + def __getstate__(self): + """ + Return a dict containing only converter and takes_self -- the rest gets + computed when loading. + """ + return { + "converter": self.converter, + "takes_self": self.takes_self, + "takes_field": self.takes_field, + } + + def __setstate__(self, state): + """ + Load instance from state. + """ + self.__init__(**state) + + +_f = [ + Attribute( + name=name, + default=NOTHING, + validator=None, + repr=True, + cmp=None, + eq=True, + order=False, + hash=True, + init=True, + inherited=False, + ) + for name in ("converter", "takes_self", "takes_field") +] + +Converter = _add_hash( + _add_eq(_add_repr(Converter, attrs=_f), attrs=_f), attrs=_f +) + + +def make_class( + name, attrs, bases=(object,), class_body=None, **attributes_arguments +): + r""" + A quick way to create a new class called *name* with *attrs*. + + .. note:: + + ``make_class()`` is a thin wrapper around `attr.s`, not `attrs.define` + which means that it doesn't come with some of the improved defaults. + + For example, if you want the same ``on_setattr`` behavior as in + `attrs.define`, you have to pass the hooks yourself: ``make_class(..., + on_setattr=setters.pipe(setters.convert, setters.validate)`` + + .. warning:: + + It is *your* duty to ensure that the class name and the attribute names + are valid identifiers. ``make_class()`` will *not* validate them for + you. + + Args: + name (str): The name for the new class. + + attrs (list | dict): + A list of names or a dictionary of mappings of names to `attr.ib`\ + s / `attrs.field`\ s. + + The order is deduced from the order of the names or attributes + inside *attrs*. Otherwise the order of the definition of the + attributes is used. + + bases (tuple[type, ...]): Classes that the new class will subclass. + + class_body (dict): + An optional dictionary of class attributes for the new class. + + attributes_arguments: Passed unmodified to `attr.s`. + + Returns: + type: A new class with *attrs*. + + .. versionadded:: 17.1.0 *bases* + .. versionchanged:: 18.1.0 If *attrs* is ordered, the order is retained. + .. versionchanged:: 23.2.0 *class_body* + .. versionchanged:: 25.2.0 Class names can now be unicode. + """ + # Class identifiers are converted into the normal form NFKC while parsing + name = unicodedata.normalize("NFKC", name) + + if isinstance(attrs, dict): + cls_dict = attrs + elif isinstance(attrs, (list, tuple)): + cls_dict = {a: attrib() for a in attrs} + else: + msg = "attrs argument must be a dict or a list." + raise TypeError(msg) + + pre_init = cls_dict.pop("__attrs_pre_init__", None) + post_init = cls_dict.pop("__attrs_post_init__", None) + user_init = cls_dict.pop("__init__", None) + + body = {} + if class_body is not None: + body.update(class_body) + if pre_init is not None: + body["__attrs_pre_init__"] = pre_init + if post_init is not None: + body["__attrs_post_init__"] = post_init + if user_init is not None: + body["__init__"] = user_init + + type_ = types.new_class(name, bases, {}, lambda ns: ns.update(body)) + + # For pickling to work, the __module__ variable needs to be set to the + # frame where the class is created. Bypass this step in environments where + # sys._getframe is not defined (Jython for example) or sys._getframe is not + # defined for arguments greater than 0 (IronPython). + with contextlib.suppress(AttributeError, ValueError): + type_.__module__ = sys._getframe(1).f_globals.get( + "__name__", "__main__" + ) + + # We do it here for proper warnings with meaningful stacklevel. + cmp = attributes_arguments.pop("cmp", None) + ( + attributes_arguments["eq"], + attributes_arguments["order"], + ) = _determine_attrs_eq_order( + cmp, + attributes_arguments.get("eq"), + attributes_arguments.get("order"), + True, + ) + + cls = _attrs(these=cls_dict, **attributes_arguments)(type_) + # Only add type annotations now or "_attrs()" will complain: + cls.__annotations__ = { + k: v.type for k, v in cls_dict.items() if v.type is not None + } + return cls + + +# These are required by within this module so we define them here and merely +# import into .validators / .converters. + + +@attrs(slots=True, unsafe_hash=True) +class _AndValidator: + """ + Compose many validators to a single one. + """ + + _validators = attrib() + + def __call__(self, inst, attr, value): + for v in self._validators: + v(inst, attr, value) + + +def and_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators. + + Args: + validators (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of validators. + + .. versionadded:: 17.1.0 + """ + vals = [] + for validator in validators: + vals.extend( + validator._validators + if isinstance(validator, _AndValidator) + else [validator] + ) + + return _AndValidator(tuple(vals)) + + +def pipe(*converters): + """ + A converter that composes multiple converters into one. + + When called on a value, it runs all wrapped converters, returning the + *last* value. + + Type annotations will be inferred from the wrapped converters', if they + have any. + + converters (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of converters. + + .. versionadded:: 20.1.0 + """ + + return_instance = any(isinstance(c, Converter) for c in converters) + + if return_instance: + + def pipe_converter(val, inst, field): + for c in converters: + val = ( + c(val, inst, field) if isinstance(c, Converter) else c(val) + ) + + return val + + else: + + def pipe_converter(val): + for c in converters: + val = c(val) + + return val + + if not converters: + # If the converter list is empty, pipe_converter is the identity. + A = TypeVar("A") + pipe_converter.__annotations__.update({"val": A, "return": A}) + else: + # Get parameter type from first converter. + t = _AnnotationExtractor(converters[0]).get_first_param_type() + if t: + pipe_converter.__annotations__["val"] = t + + last = converters[-1] + if not PY_3_11_PLUS and isinstance(last, Converter): + last = last.__call__ + + # Get return type from last converter. + rt = _AnnotationExtractor(last).get_return_type() + if rt: + pipe_converter.__annotations__["return"] = rt + + if return_instance: + return Converter(pipe_converter, takes_self=True, takes_field=True) + return pipe_converter diff --git a/venv/lib/python3.11/site-packages/attr/_next_gen.py b/venv/lib/python3.11/site-packages/attr/_next_gen.py new file mode 100644 index 0000000..4ccd0da --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_next_gen.py @@ -0,0 +1,674 @@ +# SPDX-License-Identifier: MIT + +""" +These are keyword-only APIs that call `attr.s` and `attr.ib` with different +default values. +""" + +from functools import partial + +from . import setters +from ._funcs import asdict as _asdict +from ._funcs import astuple as _astuple +from ._make import ( + _DEFAULT_ON_SETATTR, + NOTHING, + _frozen_setattrs, + attrib, + attrs, +) +from .exceptions import NotAnAttrsClassError, UnannotatedAttributeError + + +def define( + maybe_cls=None, + *, + these=None, + repr=None, + unsafe_hash=None, + hash=None, + init=None, + slots=True, + frozen=False, + weakref_slot=True, + str=False, + auto_attribs=None, + kw_only=False, + cache_hash=False, + auto_exc=True, + eq=None, + order=False, + auto_detect=True, + getstate_setstate=None, + on_setattr=None, + field_transformer=None, + match_args=True, + force_kw_only=False, +): + r""" + A class decorator that adds :term:`dunder methods` according to + :term:`fields ` specified using :doc:`type annotations `, + `field()` calls, or the *these* argument. + + Since *attrs* patches or replaces an existing class, you cannot use + `object.__init_subclass__` with *attrs* classes, because it runs too early. + As a replacement, you can define ``__attrs_init_subclass__`` on your class. + It will be called by *attrs* classes that subclass it after they're + created. See also :ref:`init-subclass`. + + Args: + slots (bool): + Create a :term:`slotted class ` that's more + memory-efficient. Slotted classes are generally superior to the + default dict classes, but have some gotchas you should know about, + so we encourage you to read the :term:`glossary entry `. + + auto_detect (bool): + Instead of setting the *init*, *repr*, *eq*, and *hash* arguments + explicitly, assume they are set to True **unless any** of the + involved methods for one of the arguments is implemented in the + *current* class (meaning, it is *not* inherited from some base + class). + + So, for example by implementing ``__eq__`` on a class yourself, + *attrs* will deduce ``eq=False`` and will create *neither* + ``__eq__`` *nor* ``__ne__`` (but Python classes come with a + sensible ``__ne__`` by default, so it *should* be enough to only + implement ``__eq__`` in most cases). + + Passing :data:`True` or :data:`False` to *init*, *repr*, *eq*, or *hash* + overrides whatever *auto_detect* would determine. + + auto_exc (bool): + If the class subclasses `BaseException` (which implicitly includes + any subclass of any exception), the following happens to behave + like a well-behaved Python exception class: + + - the values for *eq*, *order*, and *hash* are ignored and the + instances compare and hash by the instance's ids [#]_ , + - all attributes that are either passed into ``__init__`` or have a + default value are additionally available as a tuple in the + ``args`` attribute, + - the value of *str* is ignored leaving ``__str__`` to base + classes. + + .. [#] + Note that *attrs* will *not* remove existing implementations of + ``__hash__`` or the equality methods. It just won't add own + ones. + + on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]): + A callable that is run whenever the user attempts to set an + attribute (either by assignment like ``i.x = 42`` or by using + `setattr` like ``setattr(i, "x", 42)``). It receives the same + arguments as validators: the instance, the attribute that is being + modified, and the new value. + + If no exception is raised, the attribute is set to the return value + of the callable. + + If a list of callables is passed, they're automatically wrapped in + an `attrs.setters.pipe`. + + If left None, the default behavior is to run converters and + validators whenever an attribute is set. + + init (bool): + Create a ``__init__`` method that initializes the *attrs* + attributes. Leading underscores are stripped for the argument name, + unless an alias is set on the attribute. + + .. seealso:: + `init` shows advanced ways to customize the generated + ``__init__`` method, including executing code before and after. + + repr(bool): + Create a ``__repr__`` method with a human readable representation + of *attrs* attributes. + + str (bool): + Create a ``__str__`` method that is identical to ``__repr__``. This + is usually not necessary except for `Exception`\ s. + + eq (bool | None): + If True or None (default), add ``__eq__`` and ``__ne__`` methods + that check two instances for equality. + + .. seealso:: + `comparison` describes how to customize the comparison behavior + going as far comparing NumPy arrays. + + order (bool | None): + If True, add ``__lt__``, ``__le__``, ``__gt__``, and ``__ge__`` + methods that behave like *eq* above and allow instances to be + ordered. + + They compare the instances as if they were tuples of their *attrs* + attributes if and only if the types of both classes are + *identical*. + + If `None` mirror value of *eq*. + + .. seealso:: `comparison` + + unsafe_hash (bool | None): + If None (default), the ``__hash__`` method is generated according + how *eq* and *frozen* are set. + + 1. If *both* are True, *attrs* will generate a ``__hash__`` for + you. + 2. If *eq* is True and *frozen* is False, ``__hash__`` will be set + to None, marking it unhashable (which it is). + 3. If *eq* is False, ``__hash__`` will be left untouched meaning + the ``__hash__`` method of the base class will be used. If the + base class is `object`, this means it will fall back to id-based + hashing. + + Although not recommended, you can decide for yourself and force + *attrs* to create one (for example, if the class is immutable even + though you didn't freeze it programmatically) by passing True or + not. Both of these cases are rather special and should be used + carefully. + + .. seealso:: + + - Our documentation on `hashing`, + - Python's documentation on `object.__hash__`, + - and the `GitHub issue that led to the default \ behavior + `_ for more + details. + + hash (bool | None): + Deprecated alias for *unsafe_hash*. *unsafe_hash* takes precedence. + + cache_hash (bool): + Ensure that the object's hash code is computed only once and stored + on the object. If this is set to True, hashing must be either + explicitly or implicitly enabled for this class. If the hash code + is cached, avoid any reassignments of fields involved in hash code + computation or mutations of the objects those fields point to after + object creation. If such changes occur, the behavior of the + object's hash code is undefined. + + frozen (bool): + Make instances immutable after initialization. If someone attempts + to modify a frozen instance, `attrs.exceptions.FrozenInstanceError` + is raised. + + .. note:: + + 1. This is achieved by installing a custom ``__setattr__`` + method on your class, so you can't implement your own. + + 2. True immutability is impossible in Python. + + 3. This *does* have a minor a runtime performance `impact + ` when initializing new instances. In other + words: ``__init__`` is slightly slower with ``frozen=True``. + + 4. If a class is frozen, you cannot modify ``self`` in + ``__attrs_post_init__`` or a self-written ``__init__``. You + can circumvent that limitation by using + ``object.__setattr__(self, "attribute_name", value)``. + + 5. Subclasses of a frozen class are frozen too. + + kw_only (bool): + Make attributes keyword-only in the generated ``__init__`` (if + *init* is False, this parameter is ignored). Attributes that + explicitly set ``kw_only=False`` are not affected; base class + attributes are also not affected. + + Also see *force_kw_only*. + + weakref_slot (bool): + Make instances weak-referenceable. This has no effect unless + *slots* is True. + + field_transformer (~typing.Callable | None): + A function that is called with the original class object and all + fields right before *attrs* finalizes the class. You can use this, + for example, to automatically add converters or validators to + fields based on their types. + + .. seealso:: `transform-fields` + + match_args (bool): + If True (default), set ``__match_args__`` on the class to support + :pep:`634` (*Structural Pattern Matching*). It is a tuple of all + non-keyword-only ``__init__`` parameter names on Python 3.10 and + later. Ignored on older Python versions. + + collect_by_mro (bool): + If True, *attrs* collects attributes from base classes correctly + according to the `method resolution order + `_. If False, *attrs* + will mimic the (wrong) behavior of `dataclasses` and :pep:`681`. + + See also `issue #428 + `_. + + force_kw_only (bool): + A back-compat flag for restoring pre-25.4.0 behavior. If True and + ``kw_only=True``, all attributes are made keyword-only, including + base class attributes, and those set to ``kw_only=False`` at the + attribute level. Defaults to False. + + See also `issue #980 + `_. + + getstate_setstate (bool | None): + .. note:: + + This is usually only interesting for slotted classes and you + should probably just set *auto_detect* to True. + + If True, ``__getstate__`` and ``__setstate__`` are generated and + attached to the class. This is necessary for slotted classes to be + pickleable. If left None, it's True by default for slotted classes + and False for dict classes. + + If *auto_detect* is True, and *getstate_setstate* is left None, and + **either** ``__getstate__`` or ``__setstate__`` is detected + directly on the class (meaning: not inherited), it is set to False + (this is usually what you want). + + auto_attribs (bool | None): + If True, look at type annotations to determine which attributes to + use, like `dataclasses`. If False, it will only look for explicit + :func:`field` class attributes, like classic *attrs*. + + If left None, it will guess: + + 1. If any attributes are annotated and no unannotated + `attrs.field`\ s are found, it assumes *auto_attribs=True*. + 2. Otherwise it assumes *auto_attribs=False* and tries to collect + `attrs.field`\ s. + + If *attrs* decides to look at type annotations, **all** fields + **must** be annotated. If *attrs* encounters a field that is set to + a :func:`field` / `attr.ib` but lacks a type annotation, an + `attrs.exceptions.UnannotatedAttributeError` is raised. Use + ``field_name: typing.Any = field(...)`` if you don't want to set a + type. + + .. warning:: + + For features that use the attribute name to create decorators + (for example, :ref:`validators `), you still *must* + assign :func:`field` / `attr.ib` to them. Otherwise Python will + either not find the name or try to use the default value to + call, for example, ``validator`` on it. + + Attributes annotated as `typing.ClassVar`, and attributes that are + neither annotated nor set to an `field()` are **ignored**. + + these (dict[str, object]): + A dictionary of name to the (private) return value of `field()` + mappings. This is useful to avoid the definition of your attributes + within the class body because you can't (for example, if you want + to add ``__repr__`` methods to Django models) or don't want to. + + If *these* is not `None`, *attrs* will *not* search the class body + for attributes and will *not* remove any attributes from it. + + The order is deduced from the order of the attributes inside + *these*. + + Arguably, this is a rather obscure feature. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.3.0 Converters are also run ``on_setattr``. + .. versionadded:: 22.2.0 + *unsafe_hash* as an alias for *hash* (for :pep:`681` compliance). + .. versionchanged:: 24.1.0 + Instances are not compared as tuples of attributes anymore, but using a + big ``and`` condition. This is faster and has more correct behavior for + uncomparable values like `math.nan`. + .. versionadded:: 24.1.0 + If a class has an *inherited* classmethod called + ``__attrs_init_subclass__``, it is executed after the class is created. + .. deprecated:: 24.1.0 *hash* is deprecated in favor of *unsafe_hash*. + .. versionadded:: 24.3.0 + Unless already present, a ``__replace__`` method is automatically + created for `copy.replace` (Python 3.13+ only). + .. versionchanged:: 25.4.0 + *kw_only* now only applies to attributes defined in the current class, + and respects attribute-level ``kw_only=False`` settings. + .. versionadded:: 25.4.0 + Added *force_kw_only* to go back to the previous *kw_only* behavior. + + .. note:: + + The main differences to the classic `attr.s` are: + + - Automatically detect whether or not *auto_attribs* should be `True` + (c.f. *auto_attribs* parameter). + - Converters and validators run when attributes are set by default -- + if *frozen* is `False`. + - *slots=True* + + Usually, this has only upsides and few visible effects in everyday + programming. But it *can* lead to some surprising behaviors, so + please make sure to read :term:`slotted classes`. + + - *auto_exc=True* + - *auto_detect=True* + - *order=False* + - *force_kw_only=False* + - Some options that were only relevant on Python 2 or were kept around + for backwards-compatibility have been removed. + + """ + + def do_it(cls, auto_attribs): + return attrs( + maybe_cls=cls, + these=these, + repr=repr, + hash=hash, + unsafe_hash=unsafe_hash, + init=init, + slots=slots, + frozen=frozen, + weakref_slot=weakref_slot, + str=str, + auto_attribs=auto_attribs, + kw_only=kw_only, + cache_hash=cache_hash, + auto_exc=auto_exc, + eq=eq, + order=order, + auto_detect=auto_detect, + collect_by_mro=True, + getstate_setstate=getstate_setstate, + on_setattr=on_setattr, + field_transformer=field_transformer, + match_args=match_args, + force_kw_only=force_kw_only, + ) + + def wrap(cls): + """ + Making this a wrapper ensures this code runs during class creation. + + We also ensure that frozen-ness of classes is inherited. + """ + nonlocal frozen, on_setattr + + had_on_setattr = on_setattr not in (None, setters.NO_OP) + + # By default, mutable classes convert & validate on setattr. + if frozen is False and on_setattr is None: + on_setattr = _DEFAULT_ON_SETATTR + + # However, if we subclass a frozen class, we inherit the immutability + # and disable on_setattr. + for base_cls in cls.__bases__: + if base_cls.__setattr__ is _frozen_setattrs: + if had_on_setattr: + msg = "Frozen classes can't use on_setattr (frozen-ness was inherited)." + raise ValueError(msg) + + on_setattr = setters.NO_OP + break + + if auto_attribs is not None: + return do_it(cls, auto_attribs) + + try: + return do_it(cls, True) + except UnannotatedAttributeError: + return do_it(cls, False) + + # maybe_cls's type depends on the usage of the decorator. It's a class + # if it's used as `@attrs` but `None` if used as `@attrs()`. + if maybe_cls is None: + return wrap + + return wrap(maybe_cls) + + +mutable = define +frozen = partial(define, frozen=True, on_setattr=None) + + +def field( + *, + default=NOTHING, + validator=None, + repr=True, + hash=None, + init=True, + metadata=None, + type=None, + converter=None, + factory=None, + kw_only=None, + eq=None, + order=None, + on_setattr=None, + alias=None, +): + """ + Create a new :term:`field` / :term:`attribute` on a class. + + .. warning:: + + Does **nothing** unless the class is also decorated with + `attrs.define` (or similar)! + + Args: + default: + A value that is used if an *attrs*-generated ``__init__`` is used + and no value is passed while instantiating or the attribute is + excluded using ``init=False``. + + If the value is an instance of `attrs.Factory`, its callable will + be used to construct a new value (useful for mutable data types + like lists or dicts). + + If a default is not set (or set manually to `attrs.NOTHING`), a + value *must* be supplied when instantiating; otherwise a + `TypeError` will be raised. + + .. seealso:: `defaults` + + factory (~typing.Callable): + Syntactic sugar for ``default=attr.Factory(factory)``. + + validator (~typing.Callable | list[~typing.Callable]): + Callable that is called by *attrs*-generated ``__init__`` methods + after the instance has been initialized. They receive the + initialized instance, the :func:`~attrs.Attribute`, and the passed + value. + + The return value is *not* inspected so the validator has to throw + an exception itself. + + If a `list` is passed, its items are treated as validators and must + all pass. + + Validators can be globally disabled and re-enabled using + `attrs.validators.get_disabled` / `attrs.validators.set_disabled`. + + The validator can also be set using decorator notation as shown + below. + + .. seealso:: :ref:`validators` + + repr (bool | ~typing.Callable): + Include this attribute in the generated ``__repr__`` method. If + True, include the attribute; if False, omit it. By default, the + built-in ``repr()`` function is used. To override how the attribute + value is formatted, pass a ``callable`` that takes a single value + and returns a string. Note that the resulting string is used as-is, + which means it will be used directly *instead* of calling + ``repr()`` (the default). + + eq (bool | ~typing.Callable): + If True (default), include this attribute in the generated + ``__eq__`` and ``__ne__`` methods that check two instances for + equality. To override how the attribute value is compared, pass a + callable that takes a single value and returns the value to be + compared. + + .. seealso:: `comparison` + + order (bool | ~typing.Callable): + If True (default), include this attributes in the generated + ``__lt__``, ``__le__``, ``__gt__`` and ``__ge__`` methods. To + override how the attribute value is ordered, pass a callable that + takes a single value and returns the value to be ordered. + + .. seealso:: `comparison` + + hash (bool | None): + Include this attribute in the generated ``__hash__`` method. If + None (default), mirror *eq*'s value. This is the correct behavior + according the Python spec. Setting this value to anything else + than None is *discouraged*. + + .. seealso:: `hashing` + + init (bool): + Include this attribute in the generated ``__init__`` method. + + It is possible to set this to False and set a default value. In + that case this attributed is unconditionally initialized with the + specified default value or factory. + + .. seealso:: `init` + + converter (typing.Callable | Converter): + A callable that is called by *attrs*-generated ``__init__`` methods + to convert attribute's value to the desired format. + + If a vanilla callable is passed, it is given the passed-in value as + the only positional argument. It is possible to receive additional + arguments by wrapping the callable in a `Converter`. + + Either way, the returned value will be used as the new value of the + attribute. The value is converted before being passed to the + validator, if any. + + .. seealso:: :ref:`converters` + + metadata (dict | None): + An arbitrary mapping, to be used by third-party code. + + .. seealso:: `extending-metadata`. + + type (type): + The type of the attribute. Nowadays, the preferred method to + specify the type is using a variable annotation (see :pep:`526`). + This argument is provided for backwards-compatibility and for usage + with `make_class`. Regardless of the approach used, the type will + be stored on ``Attribute.type``. + + Please note that *attrs* doesn't do anything with this metadata by + itself. You can use it as part of your own code or for `static type + checking `. + + kw_only (bool | None): + Make this attribute keyword-only in the generated ``__init__`` (if + *init* is False, this parameter is ignored). If None (default), + mirror the setting from `attrs.define`. + + on_setattr (~typing.Callable | list[~typing.Callable] | None | ~typing.Literal[attrs.setters.NO_OP]): + Allows to overwrite the *on_setattr* setting from `attr.s`. If left + None, the *on_setattr* value from `attr.s` is used. Set to + `attrs.setters.NO_OP` to run **no** `setattr` hooks for this + attribute -- regardless of the setting in `define()`. + + alias (str | None): + Override this attribute's parameter name in the generated + ``__init__`` method. If left None, default to ``name`` stripped + of leading underscores. See `private-attributes`. + + .. versionadded:: 20.1.0 + .. versionchanged:: 21.1.0 + *eq*, *order*, and *cmp* also accept a custom callable + .. versionadded:: 22.2.0 *alias* + .. versionadded:: 23.1.0 + The *type* parameter has been re-added; mostly for `attrs.make_class`. + Please note that type checkers ignore this metadata. + .. versionchanged:: 25.4.0 + *kw_only* can now be None, and its default is also changed from False to + None. + + .. seealso:: + + `attr.ib` + """ + return attrib( + default=default, + validator=validator, + repr=repr, + hash=hash, + init=init, + metadata=metadata, + type=type, + converter=converter, + factory=factory, + kw_only=kw_only, + eq=eq, + order=order, + on_setattr=on_setattr, + alias=alias, + ) + + +def asdict(inst, *, recurse=True, filter=None, value_serializer=None): + """ + Same as `attr.asdict`, except that collections types are always retained + and dict is always used as *dict_factory*. + + .. versionadded:: 21.3.0 + """ + return _asdict( + inst=inst, + recurse=recurse, + filter=filter, + value_serializer=value_serializer, + retain_collection_types=True, + ) + + +def astuple(inst, *, recurse=True, filter=None): + """ + Same as `attr.astuple`, except that collections types are always retained + and `tuple` is always used as the *tuple_factory*. + + .. versionadded:: 21.3.0 + """ + return _astuple( + inst=inst, recurse=recurse, filter=filter, retain_collection_types=True + ) + + +def inspect(cls): + """ + Inspect the class and return its effective build parameters. + + Warning: + This feature is currently **experimental** and is not covered by our + strict backwards-compatibility guarantees. + + Args: + cls: The *attrs*-decorated class to inspect. + + Returns: + The effective build parameters of the class. + + Raises: + NotAnAttrsClassError: If the class is not an *attrs*-decorated class. + + .. versionadded:: 25.4.0 + """ + try: + return cls.__dict__["__attrs_props__"] + except KeyError: + msg = f"{cls!r} is not an attrs-decorated class." + raise NotAnAttrsClassError(msg) from None diff --git a/venv/lib/python3.11/site-packages/attr/_typing_compat.pyi b/venv/lib/python3.11/site-packages/attr/_typing_compat.pyi new file mode 100644 index 0000000..ca7b71e --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_typing_compat.pyi @@ -0,0 +1,15 @@ +from typing import Any, ClassVar, Protocol + +# MYPY is a special constant in mypy which works the same way as `TYPE_CHECKING`. +MYPY = False + +if MYPY: + # A protocol to be able to statically accept an attrs class. + class AttrsInstance_(Protocol): + __attrs_attrs__: ClassVar[Any] + +else: + # For type checkers without plug-in support use an empty protocol that + # will (hopefully) be combined into a union. + class AttrsInstance_(Protocol): + pass diff --git a/venv/lib/python3.11/site-packages/attr/_version_info.py b/venv/lib/python3.11/site-packages/attr/_version_info.py new file mode 100644 index 0000000..27f1888 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_version_info.py @@ -0,0 +1,89 @@ +# SPDX-License-Identifier: MIT + + +from functools import total_ordering + +from ._funcs import astuple +from ._make import attrib, attrs + + +@total_ordering +@attrs(eq=False, order=False, slots=True, frozen=True) +class VersionInfo: + """ + A version object that can be compared to tuple of length 1--4: + + >>> attr.VersionInfo(19, 1, 0, "final") <= (19, 2) + True + >>> attr.VersionInfo(19, 1, 0, "final") < (19, 1, 1) + True + >>> vi = attr.VersionInfo(19, 2, 0, "final") + >>> vi < (19, 1, 1) + False + >>> vi < (19,) + False + >>> vi == (19, 2,) + True + >>> vi == (19, 2, 1) + False + + .. versionadded:: 19.2 + """ + + year = attrib(type=int) + minor = attrib(type=int) + micro = attrib(type=int) + releaselevel = attrib(type=str) + + @classmethod + def _from_version_string(cls, s): + """ + Parse *s* and return a _VersionInfo. + """ + v = s.split(".") + if len(v) == 3: + v.append("final") + + return cls( + year=int(v[0]), minor=int(v[1]), micro=int(v[2]), releaselevel=v[3] + ) + + def _ensure_tuple(self, other): + """ + Ensure *other* is a tuple of a valid length. + + Returns a possibly transformed *other* and ourselves as a tuple of + the same length as *other*. + """ + + if self.__class__ is other.__class__: + other = astuple(other) + + if not isinstance(other, tuple): + raise NotImplementedError + + if not (1 <= len(other) <= 4): + raise NotImplementedError + + return astuple(self)[: len(other)], other + + def __eq__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + return us == them + + def __lt__(self, other): + try: + us, them = self._ensure_tuple(other) + except NotImplementedError: + return NotImplemented + + # Since alphabetically "dev0" < "final" < "post1" < "post2", we don't + # have to do anything special with releaselevel for now. + return us < them + + def __hash__(self): + return hash((self.year, self.minor, self.micro, self.releaselevel)) diff --git a/venv/lib/python3.11/site-packages/attr/_version_info.pyi b/venv/lib/python3.11/site-packages/attr/_version_info.pyi new file mode 100644 index 0000000..45ced08 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/_version_info.pyi @@ -0,0 +1,9 @@ +class VersionInfo: + @property + def year(self) -> int: ... + @property + def minor(self) -> int: ... + @property + def micro(self) -> int: ... + @property + def releaselevel(self) -> str: ... diff --git a/venv/lib/python3.11/site-packages/attr/converters.py b/venv/lib/python3.11/site-packages/attr/converters.py new file mode 100644 index 0000000..0a79dee --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/converters.py @@ -0,0 +1,162 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful converters. +""" + +import typing + +from ._compat import _AnnotationExtractor +from ._make import NOTHING, Converter, Factory, pipe + + +__all__ = [ + "default_if_none", + "optional", + "pipe", + "to_bool", +] + + +def optional(converter): + """ + A converter that allows an attribute to be optional. An optional attribute + is one which can be set to `None`. + + Type annotations will be inferred from the wrapped converter's, if it has + any. + + Args: + converter (typing.Callable): + the converter that is used for non-`None` values. + + .. versionadded:: 17.1.0 + """ + + if isinstance(converter, Converter): + + def optional_converter(val, inst, field): + if val is None: + return None + return converter(val, inst, field) + + else: + + def optional_converter(val): + if val is None: + return None + return converter(val) + + xtr = _AnnotationExtractor(converter) + + t = xtr.get_first_param_type() + if t: + optional_converter.__annotations__["val"] = typing.Optional[t] + + rt = xtr.get_return_type() + if rt: + optional_converter.__annotations__["return"] = typing.Optional[rt] + + if isinstance(converter, Converter): + return Converter(optional_converter, takes_self=True, takes_field=True) + + return optional_converter + + +def default_if_none(default=NOTHING, factory=None): + """ + A converter that allows to replace `None` values by *default* or the result + of *factory*. + + Args: + default: + Value to be used if `None` is passed. Passing an instance of + `attrs.Factory` is supported, however the ``takes_self`` option is + *not*. + + factory (typing.Callable): + A callable that takes no parameters whose result is used if `None` + is passed. + + Raises: + TypeError: If **neither** *default* or *factory* is passed. + + TypeError: If **both** *default* and *factory* are passed. + + ValueError: + If an instance of `attrs.Factory` is passed with + ``takes_self=True``. + + .. versionadded:: 18.2.0 + """ + if default is NOTHING and factory is None: + msg = "Must pass either `default` or `factory`." + raise TypeError(msg) + + if default is not NOTHING and factory is not None: + msg = "Must pass either `default` or `factory` but not both." + raise TypeError(msg) + + if factory is not None: + default = Factory(factory) + + if isinstance(default, Factory): + if default.takes_self: + msg = "`takes_self` is not supported by default_if_none." + raise ValueError(msg) + + def default_if_none_converter(val): + if val is not None: + return val + + return default.factory() + + else: + + def default_if_none_converter(val): + if val is not None: + return val + + return default + + return default_if_none_converter + + +def to_bool(val): + """ + Convert "boolean" strings (for example, from environment variables) to real + booleans. + + Values mapping to `True`: + + - ``True`` + - ``"true"`` / ``"t"`` + - ``"yes"`` / ``"y"`` + - ``"on"`` + - ``"1"`` + - ``1`` + + Values mapping to `False`: + + - ``False`` + - ``"false"`` / ``"f"`` + - ``"no"`` / ``"n"`` + - ``"off"`` + - ``"0"`` + - ``0`` + + Raises: + ValueError: For any other value. + + .. versionadded:: 21.3.0 + """ + if isinstance(val, str): + val = val.lower() + + if val in (True, "true", "t", "yes", "y", "on", "1", 1): + return True + if val in (False, "false", "f", "no", "n", "off", "0", 0): + return False + + msg = f"Cannot convert value to bool: {val!r}" + raise ValueError(msg) diff --git a/venv/lib/python3.11/site-packages/attr/converters.pyi b/venv/lib/python3.11/site-packages/attr/converters.pyi new file mode 100644 index 0000000..12bd0c4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/converters.pyi @@ -0,0 +1,19 @@ +from typing import Callable, Any, overload + +from attrs import _ConverterType, _CallableConverterType + +@overload +def pipe(*validators: _CallableConverterType) -> _CallableConverterType: ... +@overload +def pipe(*validators: _ConverterType) -> _ConverterType: ... +@overload +def optional(converter: _CallableConverterType) -> _CallableConverterType: ... +@overload +def optional(converter: _ConverterType) -> _ConverterType: ... +@overload +def default_if_none(default: Any) -> _CallableConverterType: ... +@overload +def default_if_none( + *, factory: Callable[[], Any] +) -> _CallableConverterType: ... +def to_bool(val: str | int | bool) -> bool: ... diff --git a/venv/lib/python3.11/site-packages/attr/exceptions.py b/venv/lib/python3.11/site-packages/attr/exceptions.py new file mode 100644 index 0000000..a207df4 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/exceptions.py @@ -0,0 +1,95 @@ +# SPDX-License-Identifier: MIT + +from __future__ import annotations + + +class FrozenError(AttributeError): + """ + A frozen/immutable instance or attribute have been attempted to be + modified. + + It mirrors the behavior of ``namedtuples`` by using the same error message + and subclassing `AttributeError`. + + .. versionadded:: 20.1.0 + """ + + def __init__(self): + msg = "can't set attribute" + super().__init__(msg) + self.msg = msg + + +class FrozenInstanceError(FrozenError): + """ + A frozen instance has been attempted to be modified. + + .. versionadded:: 16.1.0 + """ + + +class FrozenAttributeError(FrozenError): + """ + A frozen attribute has been attempted to be modified. + + .. versionadded:: 20.1.0 + """ + + +class AttrsAttributeNotFoundError(ValueError): + """ + An *attrs* function couldn't find an attribute that the user asked for. + + .. versionadded:: 16.2.0 + """ + + +class NotAnAttrsClassError(ValueError): + """ + A non-*attrs* class has been passed into an *attrs* function. + + .. versionadded:: 16.2.0 + """ + + +class DefaultAlreadySetError(RuntimeError): + """ + A default has been set when defining the field and is attempted to be reset + using the decorator. + + .. versionadded:: 17.1.0 + """ + + +class UnannotatedAttributeError(RuntimeError): + """ + A class with ``auto_attribs=True`` has a field without a type annotation. + + .. versionadded:: 17.3.0 + """ + + +class PythonTooOldError(RuntimeError): + """ + It was attempted to use an *attrs* feature that requires a newer Python + version. + + .. versionadded:: 18.2.0 + """ + + +class NotCallableError(TypeError): + """ + A field requiring a callable has been set with a value that is not + callable. + + .. versionadded:: 19.2.0 + """ + + def __init__(self, msg, value): + super(TypeError, self).__init__(msg, value) + self.msg = msg + self.value = value + + def __str__(self): + return str(self.msg) diff --git a/venv/lib/python3.11/site-packages/attr/exceptions.pyi b/venv/lib/python3.11/site-packages/attr/exceptions.pyi new file mode 100644 index 0000000..f268011 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/exceptions.pyi @@ -0,0 +1,17 @@ +from typing import Any + +class FrozenError(AttributeError): + msg: str = ... + +class FrozenInstanceError(FrozenError): ... +class FrozenAttributeError(FrozenError): ... +class AttrsAttributeNotFoundError(ValueError): ... +class NotAnAttrsClassError(ValueError): ... +class DefaultAlreadySetError(RuntimeError): ... +class UnannotatedAttributeError(RuntimeError): ... +class PythonTooOldError(RuntimeError): ... + +class NotCallableError(TypeError): + msg: str = ... + value: Any = ... + def __init__(self, msg: str, value: Any) -> None: ... diff --git a/venv/lib/python3.11/site-packages/attr/filters.py b/venv/lib/python3.11/site-packages/attr/filters.py new file mode 100644 index 0000000..689b170 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/filters.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful filters for `attrs.asdict` and `attrs.astuple`. +""" + +from ._make import Attribute + + +def _split_what(what): + """ + Returns a tuple of `frozenset`s of classes and attributes. + """ + return ( + frozenset(cls for cls in what if isinstance(cls, type)), + frozenset(cls for cls in what if isinstance(cls, str)), + frozenset(cls for cls in what if isinstance(cls, Attribute)), + ) + + +def include(*what): + """ + Create a filter that only allows *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to include. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.1.0 Accept strings with field names. + """ + cls, names, attrs = _split_what(what) + + def include_(attribute, value): + return ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) + + return include_ + + +def exclude(*what): + """ + Create a filter that does **not** allow *what*. + + Args: + what (list[type, str, attrs.Attribute]): + What to exclude. Can be a type, a name, or an attribute. + + Returns: + Callable: + A callable that can be passed to `attrs.asdict`'s and + `attrs.astuple`'s *filter* argument. + + .. versionchanged:: 23.3.0 Accept field name string as input argument + """ + cls, names, attrs = _split_what(what) + + def exclude_(attribute, value): + return not ( + value.__class__ in cls + or attribute.name in names + or attribute in attrs + ) + + return exclude_ diff --git a/venv/lib/python3.11/site-packages/attr/filters.pyi b/venv/lib/python3.11/site-packages/attr/filters.pyi new file mode 100644 index 0000000..974abdc --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/filters.pyi @@ -0,0 +1,6 @@ +from typing import Any + +from . import Attribute, _FilterType + +def include(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... +def exclude(*what: type | str | Attribute[Any]) -> _FilterType[Any]: ... diff --git a/venv/lib/python3.11/site-packages/attr/py.typed b/venv/lib/python3.11/site-packages/attr/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/attr/setters.py b/venv/lib/python3.11/site-packages/attr/setters.py new file mode 100644 index 0000000..78b0839 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/setters.py @@ -0,0 +1,79 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly used hooks for on_setattr. +""" + +from . import _config +from .exceptions import FrozenAttributeError + + +def pipe(*setters): + """ + Run all *setters* and return the return value of the last one. + + .. versionadded:: 20.1.0 + """ + + def wrapped_pipe(instance, attrib, new_value): + rv = new_value + + for setter in setters: + rv = setter(instance, attrib, rv) + + return rv + + return wrapped_pipe + + +def frozen(_, __, ___): + """ + Prevent an attribute to be modified. + + .. versionadded:: 20.1.0 + """ + raise FrozenAttributeError + + +def validate(instance, attrib, new_value): + """ + Run *attrib*'s validator on *new_value* if it has one. + + .. versionadded:: 20.1.0 + """ + if _config._run_validators is False: + return new_value + + v = attrib.validator + if not v: + return new_value + + v(instance, attrib, new_value) + + return new_value + + +def convert(instance, attrib, new_value): + """ + Run *attrib*'s converter -- if it has one -- on *new_value* and return the + result. + + .. versionadded:: 20.1.0 + """ + c = attrib.converter + if c: + # This can be removed once we drop 3.8 and use attrs.Converter instead. + from ._make import Converter + + if not isinstance(c, Converter): + return c(new_value) + + return c(new_value, instance, attrib) + + return new_value + + +# Sentinel for disabling class-wide *on_setattr* hooks for certain attributes. +# Sphinx's autodata stopped working, so the docstring is inlined in the API +# docs. +NO_OP = object() diff --git a/venv/lib/python3.11/site-packages/attr/setters.pyi b/venv/lib/python3.11/site-packages/attr/setters.pyi new file mode 100644 index 0000000..73abf36 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/setters.pyi @@ -0,0 +1,20 @@ +from typing import Any, NewType, NoReturn, TypeVar + +from . import Attribute +from attrs import _OnSetAttrType + +_T = TypeVar("_T") + +def frozen( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> NoReturn: ... +def pipe(*setters: _OnSetAttrType) -> _OnSetAttrType: ... +def validate(instance: Any, attribute: Attribute[_T], new_value: _T) -> _T: ... + +# convert is allowed to return Any, because they can be chained using pipe. +def convert( + instance: Any, attribute: Attribute[Any], new_value: Any +) -> Any: ... + +_NoOpType = NewType("_NoOpType", object) +NO_OP: _NoOpType diff --git a/venv/lib/python3.11/site-packages/attr/validators.py b/venv/lib/python3.11/site-packages/attr/validators.py new file mode 100644 index 0000000..0b1a294 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/validators.py @@ -0,0 +1,750 @@ +# SPDX-License-Identifier: MIT + +""" +Commonly useful validators. +""" + +import operator +import re + +from contextlib import contextmanager +from re import Pattern + +from ._config import get_run_validators, set_run_validators +from ._make import _AndValidator, and_, attrib, attrs +from .converters import default_if_none +from .exceptions import NotCallableError + + +__all__ = [ + "and_", + "deep_iterable", + "deep_mapping", + "disabled", + "ge", + "get_disabled", + "gt", + "in_", + "instance_of", + "is_callable", + "le", + "lt", + "matches_re", + "max_len", + "min_len", + "not_", + "optional", + "or_", + "set_disabled", +] + + +def set_disabled(disabled): + """ + Globally disable or enable running validators. + + By default, they are run. + + Args: + disabled (bool): If `True`, disable running all validators. + + .. warning:: + + This function is not thread-safe! + + .. versionadded:: 21.3.0 + """ + set_run_validators(not disabled) + + +def get_disabled(): + """ + Return a bool indicating whether validators are currently disabled or not. + + Returns: + bool:`True` if validators are currently disabled. + + .. versionadded:: 21.3.0 + """ + return not get_run_validators() + + +@contextmanager +def disabled(): + """ + Context manager that disables running validators within its context. + + .. warning:: + + This context manager is not thread-safe! + + .. versionadded:: 21.3.0 + .. versionchanged:: 26.1.0 The contextmanager is nestable. + """ + prev = get_run_validators() + set_run_validators(False) + try: + yield + finally: + set_run_validators(prev) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _InstanceOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not isinstance(value, self.type): + msg = f"'{attr.name}' must be {self.type!r} (got {value!r} that is a {value.__class__!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"" + + +def instance_of(type): + """ + A validator that raises a `TypeError` if the initializer is called with a + wrong type for this particular attribute (checks are performed using + `isinstance` therefore it's also valid to pass a tuple of types). + + Args: + type (type | tuple[type]): The type to check for. + + Raises: + TypeError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected type, and the value it got. + """ + return _InstanceOfValidator(type) + + +@attrs(repr=False, frozen=True, slots=True) +class _MatchesReValidator: + pattern = attrib() + match_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.match_func(value): + msg = f"'{attr.name}' must match regex {self.pattern.pattern!r} ({value!r} doesn't)" + raise ValueError( + msg, + attr, + self.pattern, + value, + ) + + def __repr__(self): + return f"" + + +def matches_re(regex, flags=0, func=None): + r""" + A validator that raises `ValueError` if the initializer is called with a + string that doesn't match *regex*. + + Args: + regex (str, re.Pattern): + A regex string or precompiled pattern to match against + + flags (int): + Flags that will be passed to the underlying re function (default 0) + + func (typing.Callable): + Which underlying `re` function to call. Valid options are + `re.fullmatch`, `re.search`, and `re.match`; the default `None` + means `re.fullmatch`. For performance reasons, the pattern is + always precompiled using `re.compile`. + + .. versionadded:: 19.2.0 + .. versionchanged:: 21.3.0 *regex* can be a pre-compiled pattern. + """ + valid_funcs = (re.fullmatch, None, re.search, re.match) + if func not in valid_funcs: + msg = "'func' must be one of {}.".format( + ", ".join( + sorted((e and e.__name__) or "None" for e in set(valid_funcs)) + ) + ) + raise ValueError(msg) + + if isinstance(regex, Pattern): + if flags: + msg = "'flags' can only be used with a string pattern; pass flags to re.compile() instead" + raise TypeError(msg) + pattern = regex + else: + pattern = re.compile(regex, flags) + + if func is re.match: + match_func = pattern.match + elif func is re.search: + match_func = pattern.search + else: + match_func = pattern.fullmatch + + return _MatchesReValidator(pattern, match_func) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _OptionalValidator: + validator = attrib() + + def __call__(self, inst, attr, value): + if value is None: + return + + self.validator(inst, attr, value) + + def __repr__(self): + return f"" + + +def optional(validator): + """ + A validator that makes an attribute optional. An optional attribute is one + which can be set to `None` in addition to satisfying the requirements of + the sub-validator. + + Args: + validator + (typing.Callable | tuple[typing.Callable] | list[typing.Callable]): + A validator (or validators) that is used for non-`None` values. + + .. versionadded:: 15.1.0 + .. versionchanged:: 17.1.0 *validator* can be a list of validators. + .. versionchanged:: 23.1.0 *validator* can also be a tuple of validators. + """ + if isinstance(validator, (list, tuple)): + return _OptionalValidator(_AndValidator(validator)) + + return _OptionalValidator(validator) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _InValidator: + options = attrib() + _original_options = attrib(hash=False) + + def __call__(self, inst, attr, value): + try: + in_options = value in self.options + except TypeError: # e.g. `1 in "abc"` + in_options = False + + if not in_options: + msg = f"'{attr.name}' must be in {self._original_options!r} (got {value!r})" + raise ValueError( + msg, + attr, + self._original_options, + value, + ) + + def __repr__(self): + return f"" + + +def in_(options): + """ + A validator that raises a `ValueError` if the initializer is called with a + value that does not belong in the *options* provided. + + The check is performed using ``value in options``, so *options* has to + support that operation. + + To keep the validator hashable, dicts, lists, and sets are transparently + transformed into a `tuple`. + + Args: + options: Allowed options. + + Raises: + ValueError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected options, and the value it got. + + .. versionadded:: 17.1.0 + .. versionchanged:: 22.1.0 + The ValueError was incomplete until now and only contained the human + readable error message. Now it contains all the information that has + been promised since 17.1.0. + .. versionchanged:: 24.1.0 + *options* that are a list, dict, or a set are now transformed into a + tuple to keep the validator hashable. + """ + repr_options = options + if isinstance(options, (list, dict, set)): + options = tuple(options) + + return _InValidator(options, repr_options) + + +@attrs(repr=False, slots=False, unsafe_hash=True) +class _IsCallableValidator: + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not callable(value): + message = ( + "'{name}' must be callable " + "(got {value!r} that is a {actual!r})." + ) + raise NotCallableError( + msg=message.format( + name=attr.name, value=value, actual=value.__class__ + ), + value=value, + ) + + def __repr__(self): + return "" + + +def is_callable(): + """ + A validator that raises a `attrs.exceptions.NotCallableError` if the + initializer is called with a value for this particular attribute that is + not callable. + + .. versionadded:: 19.1.0 + + Raises: + attrs.exceptions.NotCallableError: + With a human readable error message containing the attribute + (`attrs.Attribute`) name, and the value it got. + """ + return _IsCallableValidator() + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _DeepIterable: + member_validator = attrib(validator=is_callable()) + iterable_validator = attrib( + default=None, validator=optional(is_callable()) + ) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.iterable_validator is not None: + self.iterable_validator(inst, attr, value) + + for member in value: + self.member_validator(inst, attr, member) + + def __repr__(self): + iterable_identifier = ( + "" + if self.iterable_validator is None + else f" {self.iterable_validator!r}" + ) + return ( + f"" + ) + + +def deep_iterable(member_validator, iterable_validator=None): + """ + A validator that performs deep validation of an iterable. + + Args: + member_validator: Validator(s) to apply to iterable members. + + iterable_validator: + Validator(s) to apply to iterable itself (optional). + + Raises + TypeError: if any sub-validators fail + + .. versionadded:: 19.1.0 + + .. versionchanged:: 25.4.0 + *member_validator* and *iterable_validator* can now be a list or tuple + of validators. + """ + if isinstance(member_validator, (list, tuple)): + member_validator = and_(*member_validator) + if isinstance(iterable_validator, (list, tuple)): + iterable_validator = and_(*iterable_validator) + return _DeepIterable(member_validator, iterable_validator) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _DeepMapping: + key_validator = attrib(validator=optional(is_callable())) + value_validator = attrib(validator=optional(is_callable())) + mapping_validator = attrib(validator=optional(is_callable())) + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if self.mapping_validator is not None: + self.mapping_validator(inst, attr, value) + + for key in value: + if self.key_validator is not None: + self.key_validator(inst, attr, key) + if self.value_validator is not None: + self.value_validator(inst, attr, value[key]) + + def __repr__(self): + return f"" + + +def deep_mapping( + key_validator=None, value_validator=None, mapping_validator=None +): + """ + A validator that performs deep validation of a dictionary. + + All validators are optional, but at least one of *key_validator* or + *value_validator* must be provided. + + Args: + key_validator: Validator(s) to apply to dictionary keys. + + value_validator: Validator(s) to apply to dictionary values. + + mapping_validator: + Validator(s) to apply to top-level mapping attribute. + + .. versionadded:: 19.1.0 + + .. versionchanged:: 25.4.0 + *key_validator* and *value_validator* are now optional, but at least one + of them must be provided. + + .. versionchanged:: 25.4.0 + *key_validator*, *value_validator*, and *mapping_validator* can now be a + list or tuple of validators. + + Raises: + TypeError: If any sub-validator fails on validation. + + ValueError: + If neither *key_validator* nor *value_validator* is provided on + instantiation. + """ + if key_validator is None and value_validator is None: + msg = ( + "At least one of key_validator or value_validator must be provided" + ) + raise ValueError(msg) + + if isinstance(key_validator, (list, tuple)): + key_validator = and_(*key_validator) + if isinstance(value_validator, (list, tuple)): + value_validator = and_(*value_validator) + if isinstance(mapping_validator, (list, tuple)): + mapping_validator = and_(*mapping_validator) + + return _DeepMapping(key_validator, value_validator, mapping_validator) + + +@attrs(repr=False, frozen=True, slots=True) +class _NumberValidator: + bound = attrib() + compare_op = attrib() + compare_func = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not self.compare_func(value, self.bound): + msg = f"'{attr.name}' must be {self.compare_op} {self.bound}: {value}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def lt(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number larger or equal to *val*. + + The validator uses `operator.lt` to compare the values. + + Args: + val: Exclusive upper bound for values. + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<", operator.lt) + + +def le(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number greater than *val*. + + The validator uses `operator.le` to compare the values. + + Args: + val: Inclusive upper bound for values. + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, "<=", operator.le) + + +def ge(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number smaller than *val*. + + The validator uses `operator.ge` to compare the values. + + Args: + val: Inclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">=", operator.ge) + + +def gt(val): + """ + A validator that raises `ValueError` if the initializer is called with a + number smaller or equal to *val*. + + The validator uses `operator.gt` to compare the values. + + Args: + val: Exclusive lower bound for values + + .. versionadded:: 21.3.0 + """ + return _NumberValidator(val, ">", operator.gt) + + +@attrs(repr=False, frozen=True, slots=True) +class _MaxLengthValidator: + max_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) > self.max_length: + msg = f"Length of '{attr.name}' must be <= {self.max_length}: {len(value)}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def max_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is longer than *length*. + + Args: + length (int): Maximum length of the string or iterable + + .. versionadded:: 21.3.0 + """ + return _MaxLengthValidator(length) + + +@attrs(repr=False, frozen=True, slots=True) +class _MinLengthValidator: + min_length = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if len(value) < self.min_length: + msg = f"Length of '{attr.name}' must be >= {self.min_length}: {len(value)}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def min_len(length): + """ + A validator that raises `ValueError` if the initializer is called + with a string or iterable that is shorter than *length*. + + Args: + length (int): Minimum length of the string or iterable + + .. versionadded:: 22.1.0 + """ + return _MinLengthValidator(length) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _SubclassOfValidator: + type = attrib() + + def __call__(self, inst, attr, value): + """ + We use a callable class to be able to change the ``__repr__``. + """ + if not issubclass(value, self.type): + msg = f"'{attr.name}' must be a subclass of {self.type!r} (got {value!r})." + raise TypeError( + msg, + attr, + self.type, + value, + ) + + def __repr__(self): + return f"" + + +def _subclass_of(type): + """ + A validator that raises a `TypeError` if the initializer is called with a + wrong type for this particular attribute (checks are performed using + `issubclass` therefore it's also valid to pass a tuple of types). + + Args: + type (type | tuple[type, ...]): The type(s) to check for. + + Raises: + TypeError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the expected type, and the value it got. + """ + return _SubclassOfValidator(type) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _NotValidator: + validator = attrib() + msg = attrib( + converter=default_if_none( + "not_ validator child '{validator!r}' " + "did not raise a captured error" + ) + ) + exc_types = attrib( + validator=deep_iterable( + member_validator=_subclass_of(Exception), + iterable_validator=instance_of(tuple), + ), + ) + + def __call__(self, inst, attr, value): + try: + self.validator(inst, attr, value) + except self.exc_types: + pass # suppress error to invert validity + else: + raise ValueError( + self.msg.format( + validator=self.validator, + exc_types=self.exc_types, + ), + attr, + self.validator, + value, + self.exc_types, + ) + + def __repr__(self): + return f"" + + +def not_(validator, *, msg=None, exc_types=(ValueError, TypeError)): + """ + A validator that wraps and logically 'inverts' the validator passed to it. + It will raise a `ValueError` if the provided validator *doesn't* raise a + `ValueError` or `TypeError` (by default), and will suppress the exception + if the provided validator *does*. + + Intended to be used with existing validators to compose logic without + needing to create inverted variants, for example, ``not_(in_(...))``. + + Args: + validator: A validator to be logically inverted. + + msg (str): + Message to raise if validator fails. Formatted with keys + ``exc_types`` and ``validator``. + + exc_types (tuple[type, ...]): + Exception type(s) to capture. Other types raised by child + validators will not be intercepted and pass through. + + Raises: + ValueError: + With a human readable error message, the attribute (of type + `attrs.Attribute`), the validator that failed to raise an + exception, the value it got, and the expected exception types. + + .. versionadded:: 22.2.0 + """ + try: + exc_types = tuple(exc_types) + except TypeError: + exc_types = (exc_types,) + return _NotValidator(validator, msg, exc_types) + + +@attrs(repr=False, slots=True, unsafe_hash=True) +class _OrValidator: + validators = attrib() + + def __call__(self, inst, attr, value): + for v in self.validators: + try: + v(inst, attr, value) + except Exception: # noqa: BLE001, PERF203, S112 + continue + else: + return + + msg = f"None of {self.validators!r} satisfied for value {value!r}" + raise ValueError(msg) + + def __repr__(self): + return f"" + + +def or_(*validators): + """ + A validator that composes multiple validators into one. + + When called on a value, it runs all wrapped validators until one of them is + satisfied. + + Args: + validators (~collections.abc.Iterable[typing.Callable]): + Arbitrary number of validators. + + Raises: + ValueError: + If no validator is satisfied. Raised with a human-readable error + message listing all the wrapped validators and the value that + failed all of them. + + .. versionadded:: 24.1.0 + """ + vals = [] + for v in validators: + vals.extend(v.validators if isinstance(v, _OrValidator) else [v]) + + return _OrValidator(tuple(vals)) diff --git a/venv/lib/python3.11/site-packages/attr/validators.pyi b/venv/lib/python3.11/site-packages/attr/validators.pyi new file mode 100644 index 0000000..18fb112 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attr/validators.pyi @@ -0,0 +1,140 @@ +from types import UnionType +from typing import ( + Any, + AnyStr, + Callable, + Container, + ContextManager, + Iterable, + Mapping, + Match, + Pattern, + TypeVar, + overload, +) + +from attrs import _ValidatorType +from attrs import _ValidatorArgType + +_T = TypeVar("_T") +_T1 = TypeVar("_T1") +_T2 = TypeVar("_T2") +_T3 = TypeVar("_T3") +_T4 = TypeVar("_T4") +_T5 = TypeVar("_T5") +_T6 = TypeVar("_T6") +_I = TypeVar("_I", bound=Iterable) +_K = TypeVar("_K") +_V = TypeVar("_V") +_M = TypeVar("_M", bound=Mapping) + +def set_disabled(run: bool) -> None: ... +def get_disabled() -> bool: ... +def disabled() -> ContextManager[None]: ... + +# To be more precise on instance_of use some overloads. +# If there are more than 3 items in the tuple then we fall back to Any +@overload +def instance_of(type: type[_T]) -> _ValidatorType[_T]: ... +@overload +def instance_of(type: tuple[type[_T]]) -> _ValidatorType[_T]: ... +@overload +def instance_of( + type: tuple[type[_T1], type[_T2]], +) -> _ValidatorType[_T1 | _T2]: ... +@overload +def instance_of( + type: tuple[type[_T1], type[_T2], type[_T3]], +) -> _ValidatorType[_T1 | _T2 | _T3]: ... +@overload +def instance_of(type: tuple[type, ...]) -> _ValidatorType[Any]: ... +@overload +def instance_of(type: UnionType) -> _ValidatorType[Any]: ... +def optional( + validator: ( + _ValidatorType[_T] + | list[_ValidatorType[_T]] + | tuple[_ValidatorType[_T], ...] + ), +) -> _ValidatorType[_T | None]: ... +def in_(options: Container[_T]) -> _ValidatorType[_T]: ... +def and_(*validators: _ValidatorType[_T]) -> _ValidatorType[_T]: ... +def matches_re( + regex: Pattern[AnyStr] | AnyStr, + flags: int = ..., + func: Callable[[AnyStr, AnyStr, int], Match[AnyStr] | None] | None = ..., +) -> _ValidatorType[AnyStr]: ... +def deep_iterable( + member_validator: _ValidatorArgType[_T], + iterable_validator: _ValidatorArgType[_I] | None = ..., +) -> _ValidatorType[_I]: ... +@overload +def deep_mapping( + key_validator: _ValidatorArgType[_K], + value_validator: _ValidatorArgType[_V] | None = ..., + mapping_validator: _ValidatorArgType[_M] | None = ..., +) -> _ValidatorType[_M]: ... +@overload +def deep_mapping( + key_validator: _ValidatorArgType[_K] | None = ..., + value_validator: _ValidatorArgType[_V] = ..., + mapping_validator: _ValidatorArgType[_M] | None = ..., +) -> _ValidatorType[_M]: ... +def is_callable() -> _ValidatorType[_T]: ... +def lt(val: _T) -> _ValidatorType[_T]: ... +def le(val: _T) -> _ValidatorType[_T]: ... +def ge(val: _T) -> _ValidatorType[_T]: ... +def gt(val: _T) -> _ValidatorType[_T]: ... +def max_len(length: int) -> _ValidatorType[_T]: ... +def min_len(length: int) -> _ValidatorType[_T]: ... +def not_( + validator: _ValidatorType[_T], + *, + msg: str | None = None, + exc_types: type[Exception] | Iterable[type[Exception]] = ..., +) -> _ValidatorType[_T]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], +) -> _ValidatorType[_T1 | _T2]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], +) -> _ValidatorType[_T1 | _T2 | _T3]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], + __v5: _ValidatorType[_T5], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5]: ... +@overload +def or_( + __v1: _ValidatorType[_T1], + __v2: _ValidatorType[_T2], + __v3: _ValidatorType[_T3], + __v4: _ValidatorType[_T4], + __v5: _ValidatorType[_T5], + __v6: _ValidatorType[_T6], +) -> _ValidatorType[_T1 | _T2 | _T3 | _T4 | _T5 | _T6]: ... +@overload +def or_( + __v1: _ValidatorType[Any], + __v2: _ValidatorType[Any], + __v3: _ValidatorType[Any], + __v4: _ValidatorType[Any], + __v5: _ValidatorType[Any], + __v6: _ValidatorType[Any], + *validators: _ValidatorType[Any], +) -> _ValidatorType[Any]: ... diff --git a/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/METADATA b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/METADATA new file mode 100644 index 0000000..5cf16a0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/METADATA @@ -0,0 +1,199 @@ +Metadata-Version: 2.4 +Name: attrs +Version: 26.1.0 +Summary: Classes Without Boilerplate +Project-URL: Documentation, https://www.attrs.org/ +Project-URL: Changelog, https://www.attrs.org/en/stable/changelog.html +Project-URL: GitHub, https://github.com/python-attrs/attrs +Project-URL: Funding, https://github.com/sponsors/hynek +Project-URL: Tidelift, https://tidelift.com/subscription/pkg/pypi-attrs?utm_source=pypi-attrs&utm_medium=pypi +Author-email: Hynek Schlawack +License-Expression: MIT +License-File: LICENSE +Keywords: attribute,boilerplate,class +Classifier: Development Status :: 5 - Production/Stable +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: Typing :: Typed +Requires-Python: >=3.9 +Description-Content-Type: text/markdown + +

    + + attrs + +

    + + +*attrs* is the Python package that will bring back the **joy** of **writing classes** by relieving you from the drudgery of implementing object protocols (aka [dunder methods](https://www.attrs.org/en/latest/glossary.html#term-dunder-methods)). +Trusted by NASA for [Mars missions since 2020](https://github.com/readme/featured/nasa-ingenuity-helicopter)! + +Its main goal is to help you to write **concise** and **correct** software without slowing down your code. + + +## Sponsors + +*attrs* would not be possible without our [amazing sponsors](https://github.com/sponsors/hynek). +Especially those generously supporting us at the *The Organization* tier and higher: + + + +

    + + + + + + + + + + + +

    + + + +

    + Please consider joining them to help make attrs’s maintenance more sustainable! +

    + + + +## Example + +*attrs* gives you a class decorator and a way to declaratively define the attributes on that class: + + + +```pycon +>>> from attrs import asdict, define, make_class, Factory + +>>> @define +... class SomeClass: +... a_number: int = 42 +... list_of_numbers: list[int] = Factory(list) +... +... def hard_math(self, another_number): +... return self.a_number + sum(self.list_of_numbers) * another_number + + +>>> sc = SomeClass(1, [1, 2, 3]) +>>> sc +SomeClass(a_number=1, list_of_numbers=[1, 2, 3]) + +>>> sc.hard_math(3) +19 +>>> sc == SomeClass(1, [1, 2, 3]) +True +>>> sc != SomeClass(2, [3, 2, 1]) +True + +>>> asdict(sc) +{'a_number': 1, 'list_of_numbers': [1, 2, 3]} + +>>> SomeClass() +SomeClass(a_number=42, list_of_numbers=[]) + +>>> C = make_class("C", ["a", "b"]) +>>> C("foo", "bar") +C(a='foo', b='bar') +``` + +After *declaring* your attributes, *attrs* gives you: + +- a concise and explicit overview of the class's attributes, +- a nice human-readable `__repr__`, +- equality-checking methods, +- an initializer, +- and much more, + +*without* writing dull boilerplate code again and again and *without* runtime performance penalties. + +--- + +This example uses *attrs*'s modern APIs that have been introduced in version 20.1.0, and the *attrs* package import name that has been added in version 21.3.0. +The classic APIs (`@attr.s`, `attr.ib`, plus their serious-business aliases) and the `attr` package import name will remain **indefinitely**. + +Check out [*On The Core API Names*](https://www.attrs.org/en/latest/names.html) for an in-depth explanation! + + +### Hate Type Annotations!? + +No problem! +Types are entirely **optional** with *attrs*. +Simply assign `attrs.field()` to the attributes instead of annotating them with types: + +```python +from attrs import define, field + +@define +class SomeClass: + a_number = field(default=42) + list_of_numbers = field(factory=list) +``` + + +## Data Classes + +On the tin, *attrs* might remind you of `dataclasses` (and indeed, `dataclasses` [are a descendant](https://hynek.me/articles/import-attrs/) of *attrs*). +In practice it does a lot more and is more flexible. +For instance, it allows you to define [special handling of NumPy arrays for equality checks](https://www.attrs.org/en/stable/comparison.html#customization), allows more ways to [plug into the initialization process](https://www.attrs.org/en/stable/init.html#hooking-yourself-into-initialization), has a replacement for `__init_subclass__`, and allows for stepping through the generated methods using a debugger. + +For more details, please refer to our [comparison page](https://www.attrs.org/en/stable/why.html#data-classes), but generally speaking, we are more likely to commit crimes against nature to make things work that one would expect to work, but that are quite complicated in practice. + + +## Project Information + +- [**Changelog**](https://www.attrs.org/en/stable/changelog.html) +- [**Documentation**](https://www.attrs.org/) +- [**PyPI**](https://pypi.org/project/attrs/) +- [**Source Code**](https://github.com/python-attrs/attrs) +- [**Contributing**](https://github.com/python-attrs/attrs/blob/main/.github/CONTRIBUTING.md) +- [**Third-party Extensions**](https://github.com/python-attrs/attrs/wiki/Extensions-to-attrs) +- **Get Help**: use the `python-attrs` tag on [Stack Overflow](https://stackoverflow.com/questions/tagged/python-attrs) + + +### *attrs* for Enterprise + +Available as part of the [Tidelift Subscription](https://tidelift.com/?utm_source=lifter&utm_medium=referral&utm_campaign=hynek). + +The maintainers of *attrs* and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. +Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use. + +## Release Information + +### Backwards-incompatible Changes + +- Field aliases are now resolved *before* calling `field_transformer`, so transformers receive fully populated `Attribute` objects with usable `alias` values instead of `None`. + The new `Attribute.alias_is_default` flag indicates whether the alias was auto-generated (`True`) or explicitly set by the user (`False`). + [#1509](https://github.com/python-attrs/attrs/issues/1509) + + +### Changes + +- Fix type annotations for `attrs.validators.optional()`, so it no longer rejects tuples with more than one validator. + [#1496](https://github.com/python-attrs/attrs/issues/1496) +- The `attrs.validators.disabled()` contextmanager can now be nested. + [#1513](https://github.com/python-attrs/attrs/issues/1513) +- Frozen classes can set `on_setattr=attrs.setters.NO_OP` in addition to `None`. + [#1515](https://github.com/python-attrs/attrs/issues/1515) +- It's now possible to pass *attrs* **instances** in addition to *attrs* **classes** to `attrs.fields()`. + [#1529](https://github.com/python-attrs/attrs/issues/1529) + + + +--- + +[Full changelog →](https://www.attrs.org/en/stable/changelog.html) diff --git a/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/RECORD b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/RECORD new file mode 100644 index 0000000..238693d --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/RECORD @@ -0,0 +1,55 @@ +attr/__init__.py,sha256=fOYIvt1eGSqQre4uCS3sJWKZ0mwAuC8UD6qba5OS9_U,2057 +attr/__init__.pyi,sha256=pVGImAUVovq2_TYl_r_HIYnGlyOaoCuEhxo-EvsnnSc,11325 +attr/__pycache__/__init__.cpython-311.pyc,, +attr/__pycache__/_cmp.cpython-311.pyc,, +attr/__pycache__/_compat.cpython-311.pyc,, +attr/__pycache__/_config.cpython-311.pyc,, +attr/__pycache__/_funcs.cpython-311.pyc,, +attr/__pycache__/_make.cpython-311.pyc,, +attr/__pycache__/_next_gen.cpython-311.pyc,, +attr/__pycache__/_version_info.cpython-311.pyc,, +attr/__pycache__/converters.cpython-311.pyc,, +attr/__pycache__/exceptions.cpython-311.pyc,, +attr/__pycache__/filters.cpython-311.pyc,, +attr/__pycache__/setters.cpython-311.pyc,, +attr/__pycache__/validators.cpython-311.pyc,, +attr/_cmp.py,sha256=3Nn1TjxllUYiX_nJoVnEkXoDk0hM1DYKj5DE7GZe4i0,4117 +attr/_cmp.pyi,sha256=U-_RU_UZOyPUEQzXE6RMYQQcjkZRY25wTH99sN0s7MM,368 +attr/_compat.py,sha256=x0g7iEUOnBVJC72zyFCgb1eKqyxS-7f2LGnNyZ_r95s,2829 +attr/_config.py,sha256=dGq3xR6fgZEF6UBt_L0T-eUHIB4i43kRmH0P28sJVw8,843 +attr/_funcs.py,sha256=Ix5IETTfz5F01F-12MF_CSFomIn2h8b67EVVz2gCtBE,16479 +attr/_make.py,sha256=H7OH2eWS5CnBzLUjNFE1WymfPrmF1r8fv2RPdt9MuYA,106129 +attr/_next_gen.py,sha256=BQtCUlzwg2gWHTYXBQvrEYBnzBUrDvO57u0Py6UCPhc,26274 +attr/_typing_compat.pyi,sha256=XDP54TUn-ZKhD62TOQebmzrwFyomhUCoGRpclb6alRA,469 +attr/_version_info.py,sha256=w4R-FYC3NK_kMkGUWJlYP4cVAlH9HRaC-um3fcjYkHM,2222 +attr/_version_info.pyi,sha256=x_M3L3WuB7r_ULXAWjx959udKQ4HLB8l-hsc1FDGNvk,209 +attr/converters.py,sha256=GlDeOzPeTFgeBBLbj9G57Ez5lAk68uhSALRYJ_exe84,3861 +attr/converters.pyi,sha256=orU2bff-VjQa2kMDyvnMQV73oJT2WRyQuw4ZR1ym1bE,643 +attr/exceptions.py,sha256=b4vMbnoQ3VpwWZhqrYi_ssXVCK8o2c4HQSS09cSUM9o,1990 +attr/exceptions.pyi,sha256=zZq8bCUnKAy9mDtBEw42ZhPhAUIHoTKedDQInJD883M,539 +attr/filters.py,sha256=ZBiKWLp3R0LfCZsq7X11pn9WX8NslS2wXM4jsnLOGc8,1795 +attr/filters.pyi,sha256=3J5BG-dTxltBk1_-RuNRUHrv2qu1v8v4aDNAQ7_mifA,208 +attr/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attr/setters.py,sha256=5-dcT63GQK35ONEzSgfXCkbB7pPkaR-qv15mm4PVSzQ,1617 +attr/setters.pyi,sha256=NnVkaFU1BB4JB8E4JuXyrzTUgvtMpj8p3wBdJY7uix4,584 +attr/validators.py,sha256=m3QRzZTANr4f2C4eVdUoFg11NgXWak8Wat4qQTGhvcs,21553 +attr/validators.pyi,sha256=gM1ZmHaBckyYWI2EirpRNzqm3B19cw5Iq6B4Kno9YCM,4087 +attrs-26.1.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +attrs-26.1.0.dist-info/METADATA,sha256=TNQOaQ8jvzfLytNO_WdY4GLfHfB8hoM_fjzpW_H6OMw,8754 +attrs-26.1.0.dist-info/RECORD,, +attrs-26.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87 +attrs-26.1.0.dist-info/licenses/LICENSE,sha256=iCEVyV38KvHutnFPjsbVy8q_Znyv-HKfQkINpj9xTp8,1109 +attrs/__init__.py,sha256=RxaAZNwYiEh-fcvHLZNpQ_DWKni73M_jxEPEftiq1Zc,1183 +attrs/__init__.pyi,sha256=2gV79g9UxJppGSM48hAZJ6h_MHb70dZoJL31ZNJeZYI,9416 +attrs/__pycache__/__init__.cpython-311.pyc,, +attrs/__pycache__/converters.cpython-311.pyc,, +attrs/__pycache__/exceptions.cpython-311.pyc,, +attrs/__pycache__/filters.cpython-311.pyc,, +attrs/__pycache__/setters.cpython-311.pyc,, +attrs/__pycache__/validators.cpython-311.pyc,, +attrs/converters.py,sha256=8kQljrVwfSTRu8INwEk8SI0eGrzmWftsT7rM0EqyohM,76 +attrs/exceptions.py,sha256=ACCCmg19-vDFaDPY9vFl199SPXCQMN_bENs4DALjzms,76 +attrs/filters.py,sha256=VOUMZug9uEU6dUuA0dF1jInUK0PL3fLgP0VBS5d-CDE,73 +attrs/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +attrs/setters.py,sha256=eL1YidYQV3T2h9_SYIZSZR1FAcHGb1TuCTy0E0Lv2SU,73 +attrs/validators.py,sha256=xcy6wD5TtTkdCG1f4XWbocPSO0faBjk5IfVJfP6SUj0,76 diff --git a/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/WHEEL b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/WHEEL new file mode 100644 index 0000000..b1b94fd --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.29.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..2bd6453 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs-26.1.0.dist-info/licenses/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Hynek Schlawack and the attrs contributors + +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.11/site-packages/attrs/__init__.py b/venv/lib/python3.11/site-packages/attrs/__init__.py new file mode 100644 index 0000000..dc1ce4b --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs/__init__.py @@ -0,0 +1,72 @@ +# SPDX-License-Identifier: MIT + +from attr import ( + NOTHING, + Attribute, + AttrsInstance, + Converter, + Factory, + NothingType, + _make_getattr, + assoc, + cmp_using, + define, + evolve, + field, + fields, + fields_dict, + frozen, + has, + make_class, + mutable, + resolve_types, + validate, +) +from attr._make import ClassProps +from attr._next_gen import asdict, astuple, inspect + +from . import converters, exceptions, filters, setters, validators + + +__all__ = [ + "NOTHING", + "Attribute", + "AttrsInstance", + "ClassProps", + "Converter", + "Factory", + "NothingType", + "__author__", + "__copyright__", + "__description__", + "__doc__", + "__email__", + "__license__", + "__title__", + "__url__", + "__version__", + "__version_info__", + "asdict", + "assoc", + "astuple", + "cmp_using", + "converters", + "define", + "evolve", + "exceptions", + "field", + "fields", + "fields_dict", + "filters", + "frozen", + "has", + "inspect", + "make_class", + "mutable", + "resolve_types", + "setters", + "validate", + "validators", +] + +__getattr__ = _make_getattr(__name__) diff --git a/venv/lib/python3.11/site-packages/attrs/__init__.pyi b/venv/lib/python3.11/site-packages/attrs/__init__.pyi new file mode 100644 index 0000000..6364bac --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs/__init__.pyi @@ -0,0 +1,314 @@ +import sys + +from typing import ( + Any, + Callable, + Mapping, + Sequence, + overload, + TypeVar, +) + +# Because we need to type our own stuff, we have to make everything from +# attr explicitly public too. +from attr import __author__ as __author__ +from attr import __copyright__ as __copyright__ +from attr import __description__ as __description__ +from attr import __email__ as __email__ +from attr import __license__ as __license__ +from attr import __title__ as __title__ +from attr import __url__ as __url__ +from attr import __version__ as __version__ +from attr import __version_info__ as __version_info__ +from attr import assoc as assoc +from attr import Attribute as Attribute +from attr import AttrsInstance as AttrsInstance +from attr import cmp_using as cmp_using +from attr import converters as converters +from attr import Converter as Converter +from attr import evolve as evolve +from attr import exceptions as exceptions +from attr import Factory as Factory +from attr import fields as fields +from attr import fields_dict as fields_dict +from attr import filters as filters +from attr import has as has +from attr import make_class as make_class +from attr import NOTHING as NOTHING +from attr import resolve_types as resolve_types +from attr import setters as setters +from attr import validate as validate +from attr import validators as validators +from attr import attrib, asdict as asdict, astuple as astuple +from attr import NothingType as NothingType + +if sys.version_info >= (3, 11): + from typing import dataclass_transform +else: + from typing_extensions import dataclass_transform + +_T = TypeVar("_T") +_C = TypeVar("_C", bound=type) + +_EqOrderType = bool | Callable[[Any], Any] +_ValidatorType = Callable[[Any, "Attribute[_T]", _T], Any] +_CallableConverterType = Callable[[Any], Any] +_ConverterType = _CallableConverterType | Converter[Any, Any] +_ReprType = Callable[[Any], str] +_ReprArgType = bool | _ReprType +_OnSetAttrType = Callable[[Any, "Attribute[Any]", Any], Any] +_OnSetAttrArgType = _OnSetAttrType | list[_OnSetAttrType] | setters._NoOpType +_FieldTransformer = Callable[ + [type, list["Attribute[Any]"]], list["Attribute[Any]"] +] +# FIXME: in reality, if multiple validators are passed they must be in a list +# or tuple, but those are invariant and so would prevent subtypes of +# _ValidatorType from working when passed in a list or tuple. +_ValidatorArgType = _ValidatorType[_T] | Sequence[_ValidatorType[_T]] + +@overload +def field( + *, + default: None = ..., + validator: None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: None = ..., + factory: None = ..., + kw_only: bool | None = ..., + eq: bool | None = ..., + order: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> Any: ... + +# This form catches an explicit None or no default and infers the type from the +# other arguments. +@overload +def field( + *, + default: None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType, ...] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> _T: ... + +# This form catches an explicit default argument. +@overload +def field( + *, + default: _T, + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType, ...] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> _T: ... + +# This form covers type=non-Type: e.g. forward references (str), Any +@overload +def field( + *, + default: _T | None = ..., + validator: _ValidatorArgType[_T] | None = ..., + repr: _ReprArgType = ..., + hash: bool | None = ..., + init: bool = ..., + metadata: Mapping[Any, Any] | None = ..., + converter: _ConverterType + | list[_ConverterType] + | tuple[_ConverterType, ...] + | None = ..., + factory: Callable[[], _T] | None = ..., + kw_only: bool | None = ..., + eq: _EqOrderType | None = ..., + order: _EqOrderType | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + alias: str | None = ..., + type: type | None = ..., +) -> Any: ... +@overload +@dataclass_transform(field_specifiers=(attrib, field)) +def define( + maybe_cls: _C, + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@dataclass_transform(field_specifiers=(attrib, field)) +def define( + maybe_cls: None = ..., + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... + +mutable = define + +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: _C, + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> _C: ... +@overload +@dataclass_transform(frozen_default=True, field_specifiers=(attrib, field)) +def frozen( + maybe_cls: None = ..., + *, + these: dict[str, Any] | None = ..., + repr: bool = ..., + unsafe_hash: bool | None = ..., + hash: bool | None = ..., + init: bool = ..., + slots: bool = ..., + frozen: bool = ..., + weakref_slot: bool = ..., + str: bool = ..., + auto_attribs: bool = ..., + kw_only: bool = ..., + cache_hash: bool = ..., + auto_exc: bool = ..., + eq: bool | None = ..., + order: bool | None = ..., + auto_detect: bool = ..., + getstate_setstate: bool | None = ..., + on_setattr: _OnSetAttrArgType | None = ..., + field_transformer: _FieldTransformer | None = ..., + match_args: bool = ..., +) -> Callable[[_C], _C]: ... + +class ClassProps: + # XXX: somehow when defining/using enums Mypy starts looking at our own + # (untyped) code and causes tons of errors. + Hashability: Any + KeywordOnly: Any + + is_exception: bool + is_slotted: bool + has_weakref_slot: bool + is_frozen: bool + # kw_only: ClassProps.KeywordOnly + kw_only: Any + collected_fields_by_mro: bool + added_init: bool + added_repr: bool + added_eq: bool + added_ordering: bool + # hashability: ClassProps.Hashability + hashability: Any + added_match_args: bool + added_str: bool + added_pickling: bool + on_setattr_hook: _OnSetAttrType | None + field_transformer: Callable[[Attribute[Any]], Attribute[Any]] | None + + def __init__( + self, + is_exception: bool, + is_slotted: bool, + has_weakref_slot: bool, + is_frozen: bool, + # kw_only: ClassProps.KeywordOnly + kw_only: Any, + collected_fields_by_mro: bool, + added_init: bool, + added_repr: bool, + added_eq: bool, + added_ordering: bool, + # hashability: ClassProps.Hashability + hashability: Any, + added_match_args: bool, + added_str: bool, + added_pickling: bool, + on_setattr_hook: _OnSetAttrType, + field_transformer: Callable[[Attribute[Any]], Attribute[Any]], + ) -> None: ... + @property + def is_hashable(self) -> bool: ... + +def inspect(cls: type) -> ClassProps: ... diff --git a/venv/lib/python3.11/site-packages/attrs/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/attrs/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..6ab2c67 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attrs/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attrs/__pycache__/converters.cpython-311.pyc b/venv/lib/python3.11/site-packages/attrs/__pycache__/converters.cpython-311.pyc new file mode 100644 index 0000000..6a381f1 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attrs/__pycache__/converters.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attrs/__pycache__/exceptions.cpython-311.pyc b/venv/lib/python3.11/site-packages/attrs/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000..c428739 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attrs/__pycache__/exceptions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attrs/__pycache__/filters.cpython-311.pyc b/venv/lib/python3.11/site-packages/attrs/__pycache__/filters.cpython-311.pyc new file mode 100644 index 0000000..65d2d53 Binary files /dev/null and b/venv/lib/python3.11/site-packages/attrs/__pycache__/filters.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attrs/__pycache__/setters.cpython-311.pyc b/venv/lib/python3.11/site-packages/attrs/__pycache__/setters.cpython-311.pyc new file mode 100644 index 0000000..8a237ce Binary files /dev/null and b/venv/lib/python3.11/site-packages/attrs/__pycache__/setters.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attrs/__pycache__/validators.cpython-311.pyc b/venv/lib/python3.11/site-packages/attrs/__pycache__/validators.cpython-311.pyc new file mode 100644 index 0000000..17b4d6a Binary files /dev/null and b/venv/lib/python3.11/site-packages/attrs/__pycache__/validators.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/attrs/converters.py b/venv/lib/python3.11/site-packages/attrs/converters.py new file mode 100644 index 0000000..7821f6c --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs/converters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.converters import * # noqa: F403 diff --git a/venv/lib/python3.11/site-packages/attrs/exceptions.py b/venv/lib/python3.11/site-packages/attrs/exceptions.py new file mode 100644 index 0000000..3323f9d --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs/exceptions.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.exceptions import * # noqa: F403 diff --git a/venv/lib/python3.11/site-packages/attrs/filters.py b/venv/lib/python3.11/site-packages/attrs/filters.py new file mode 100644 index 0000000..3080f48 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs/filters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.filters import * # noqa: F403 diff --git a/venv/lib/python3.11/site-packages/attrs/py.typed b/venv/lib/python3.11/site-packages/attrs/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/attrs/setters.py b/venv/lib/python3.11/site-packages/attrs/setters.py new file mode 100644 index 0000000..f3d73bb --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs/setters.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.setters import * # noqa: F403 diff --git a/venv/lib/python3.11/site-packages/attrs/validators.py b/venv/lib/python3.11/site-packages/attrs/validators.py new file mode 100644 index 0000000..037e124 --- /dev/null +++ b/venv/lib/python3.11/site-packages/attrs/validators.py @@ -0,0 +1,3 @@ +# SPDX-License-Identifier: MIT + +from attr.validators import * # noqa: F403 diff --git a/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/METADATA b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/METADATA new file mode 100644 index 0000000..0d4f101 --- /dev/null +++ b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/METADATA @@ -0,0 +1,78 @@ +Metadata-Version: 2.4 +Name: certifi +Version: 2026.6.17 +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/latest/ + +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.11/site-packages/certifi-2026.6.17.dist-info/RECORD b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/RECORD new file mode 100644 index 0000000..09b22d9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/RECORD @@ -0,0 +1,14 @@ +certifi-2026.6.17.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +certifi-2026.6.17.dist-info/METADATA,sha256=6hXAnt0a2el7xm2e9xvPuRCntZLjdKCkN81e47E0wN8,2474 +certifi-2026.6.17.dist-info/RECORD,, +certifi-2026.6.17.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91 +certifi-2026.6.17.dist-info/licenses/LICENSE,sha256=6TcW2mucDVpKHfYP5pWzcPBpVgPSH2-D8FPkLPwQyvc,989 +certifi-2026.6.17.dist-info/top_level.txt,sha256=KMu4vUCfsjLrkPbSNdgdekS-pVJzBAJFO__nI8NF6-U,8 +certifi/__init__.py,sha256=-W1R_y8WCaSkT1tdjuxH_zTBZY1YH6xQgdN1nbBajOE,94 +certifi/__main__.py,sha256=xBBoj905TUWBLRGANOcf7oi6e-3dMP4cEoG9OyMs11g,243 +certifi/__pycache__/__init__.cpython-311.pyc,, +certifi/__pycache__/__main__.cpython-311.pyc,, +certifi/__pycache__/core.cpython-311.pyc,, +certifi/cacert.pem,sha256=u8fpwB11UbuKFZtd7dmJuO484QWv9SK2jrGwG_hUyrA,234354 +certifi/core.py,sha256=XFXycndG5pf37ayeF8N32HUuDafsyhkVMbO4BAPWHa0,3394 +certifi/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/WHEEL b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/WHEEL new file mode 100644 index 0000000..14a883f --- /dev/null +++ b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/licenses/LICENSE new file mode 100644 index 0000000..62b076c --- /dev/null +++ b/venv/lib/python3.11/site-packages/certifi-2026.6.17.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.11/site-packages/certifi-2026.6.17.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/top_level.txt new file mode 100644 index 0000000..963eac5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/certifi-2026.6.17.dist-info/top_level.txt @@ -0,0 +1 @@ +certifi diff --git a/venv/lib/python3.11/site-packages/certifi/__init__.py b/venv/lib/python3.11/site-packages/certifi/__init__.py new file mode 100644 index 0000000..ed9a74b --- /dev/null +++ b/venv/lib/python3.11/site-packages/certifi/__init__.py @@ -0,0 +1,4 @@ +from .core import contents, where + +__all__ = ["contents", "where"] +__version__ = "2026.06.17" diff --git a/venv/lib/python3.11/site-packages/certifi/__main__.py b/venv/lib/python3.11/site-packages/certifi/__main__.py new file mode 100644 index 0000000..8945b5d --- /dev/null +++ b/venv/lib/python3.11/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.11/site-packages/certifi/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/certifi/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..b7066e0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/certifi/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/certifi/__pycache__/__main__.cpython-311.pyc b/venv/lib/python3.11/site-packages/certifi/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..5d9197a Binary files /dev/null and b/venv/lib/python3.11/site-packages/certifi/__pycache__/__main__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/certifi/__pycache__/core.cpython-311.pyc b/venv/lib/python3.11/site-packages/certifi/__pycache__/core.cpython-311.pyc new file mode 100644 index 0000000..b1c273f Binary files /dev/null and b/venv/lib/python3.11/site-packages/certifi/__pycache__/core.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/certifi/cacert.pem b/venv/lib/python3.11/site-packages/certifi/cacert.pem new file mode 100644 index 0000000..1c2dbfe --- /dev/null +++ b/venv/lib/python3.11/site-packages/certifi/cacert.pem @@ -0,0 +1,3863 @@ + +# 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=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=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=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 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=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 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----- + +# Issuer: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Subject: CN=e-Szigno TLS Root CA 2023 O=Microsec Ltd. +# Label: "e-Szigno TLS Root CA 2023" +# Serial: 71934828665710877219916191754 +# MD5 Fingerprint: 6a:e9:99:74:a5:da:5e:f1:d9:2e:f2:c8:d1:86:8b:71 +# SHA1 Fingerprint: 6f:9a:d5:d5:df:e8:2c:eb:be:37:07:ee:4f:4f:52:58:29:41:d1:fe +# SHA256 Fingerprint: b4:91:41:50:2d:00:66:3d:74:0f:2e:7e:c3:40:c5:28:00:96:26:66:12:1a:36:d0:9c:f7:dd:2b:90:38:4f:b4 +-----BEGIN CERTIFICATE----- +MIICzzCCAjGgAwIBAgINAOhvGHvWOWuYSkmYCjAKBggqhkjOPQQDBDB1MQswCQYD +VQQGEwJIVTERMA8GA1UEBwwIQnVkYXBlc3QxFjAUBgNVBAoMDU1pY3Jvc2VjIEx0 +ZC4xFzAVBgNVBGEMDlZBVEhVLTIzNTg0NDk3MSIwIAYDVQQDDBllLVN6aWdubyBU +TFMgUm9vdCBDQSAyMDIzMB4XDTIzMDcxNzE0MDAwMFoXDTM4MDcxNzE0MDAwMFow +dTELMAkGA1UEBhMCSFUxETAPBgNVBAcMCEJ1ZGFwZXN0MRYwFAYDVQQKDA1NaWNy +b3NlYyBMdGQuMRcwFQYDVQRhDA5WQVRIVS0yMzU4NDQ5NzEiMCAGA1UEAwwZZS1T +emlnbm8gVExTIFJvb3QgQ0EgMjAyMzCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE +AGgP36J8PKp0iGEKjcJMpQEiFNT3YHdCnAo4YKGMZz6zY+n6kbCLS+Y53wLCMAFS +AL/fjO1ZrTJlqwlZULUZwmgcAOAFX9pQJhzDrAQixTpN7+lXWDajwRlTEArRzT/v +SzUaQ49CE0y5LBqcvjC2xN7cS53kpDzLLtmt3999Cd8ukv+ho2MwYTAPBgNVHRMB +Af8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUWYQCYlpGePVd3I8K +ECgj3NXW+0UwHwYDVR0jBBgwFoAUWYQCYlpGePVd3I8KECgj3NXW+0UwCgYIKoZI +zj0EAwQDgYsAMIGHAkIBLdqu9S54tma4n7Zwf2Z0z+yOfP7AAXmazlIC58PRDHpt +y7Ve7hekm9sEdu4pKeiv+62sUvTXK9Z3hBC9xdIoaDQCQTV2WnXzkoYI9bIeCvZl +C9p2x1L/Cx6AcCIwwzPbGO2E14vs7dOoY4G1VnxHx1YwlGhza9IuqbnZLBwpvQy6 +uWWL +-----END CERTIFICATE----- diff --git a/venv/lib/python3.11/site-packages/certifi/core.py b/venv/lib/python3.11/site-packages/certifi/core.py new file mode 100644 index 0000000..1c9661c --- /dev/null +++ b/venv/lib/python3.11/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.11/site-packages/certifi/py.typed b/venv/lib/python3.11/site-packages/certifi/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/METADATA b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/METADATA new file mode 100644 index 0000000..6b5a360 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/METADATA @@ -0,0 +1,808 @@ +Metadata-Version: 2.4 +Name: charset-normalizer +Version: 3.4.7 +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: Programming Language :: Python :: Free Threading :: 4 - Resilient +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. +> You can also register your own set of codecs, and yes, it would work as-is. + +

    + >>>>> 👉 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`[^1] | ❌ | ✅ | ❌ | +| `Reliable` **without** distinguishable standards | ✅ | ✅ | ✅ | +| `Reliable` **with** distinguishable standards | ✅ | ✅ | ✅ | +| `License` | _Disputed_[^2]
    _restrictive_ | MIT | MPL-1.1
    _restrictive_ | +| `Native Python` | ✅ | ✅ | ❌ | +| `Detect spoken language` | ✅ | ✅ | N/A | +| `UnicodeDecodeError Safety` | ✅ | ✅ | ❌ | +| `Whl Size (min)` | 500 kB | 150 kB | ~200 kB | +| `Supported Encoding` | 99 | [99](https://charset-normalizer.readthedocs.io/en/latest/user/support.html#supported-encodings) | 40 | +| `Can register custom encoding` | ❌ | ✅ | ❌ | + +

    +Reading Normalized TextCat Reading Text +

    + +[^1]: They are clearly using specific code for a specific encoding even if covering most of used one. +[^2]: Chardet 7.0+ was relicensed from LGPL-2.1 to MIT following an AI-assisted rewrite. This relicensing is disputed on two independent grounds: **(a)** the original author [contests](https://github.com/chardet/chardet/issues/327) that the maintainer had the right to relicense, arguing the rewrite is a derivative work of the LGPL-licensed codebase since it was not a clean room implementation; **(b)** the copyright claim itself is [questionable](https://github.com/chardet/chardet/issues/334) given the code was primarily generated by an LLM, and AI-generated output may not be copyrightable under most jurisdictions. Either issue alone could undermine the MIT license. Beyond licensing, the rewrite raises questions about responsible use of AI in open source: key architectural ideas pioneered by charset-normalizer - notably decode-first validity filtering (our foundational approach since v1) and encoding pairwise similarity with the same algorithm and threshold — surfaced in chardet 7 without acknowledgment. The project also imported test files from charset-normalizer to train and benchmark against it, then claimed superior accuracy on those very files. Charset-normalizer has always been MIT-licensed, encoding-agnostic by design, and built on a verifiable human-authored history. + +## ⚡ Performance + +This package offer better performances (99th, and 95th) against Chardet. Here are some numbers. + +| Package | Accuracy | Mean per file (ms) | File per sec (est) | +|---------------------------------------------------|:--------:|:------------------:|:------------------:| +| [chardet 7.1](https://github.com/chardet/chardet) | 89 % | 3 ms | 333 file/sec | +| charset-normalizer | **97 %** | 3 ms | 333 file/sec | + +| Package | 99th percentile | 95th percentile | 50th percentile | +|---------------------------------------------------|:---------------:|:---------------:|:---------------:| +| [chardet 7.1](https://github.com/chardet/chardet) | 32 ms | 17 ms | < 1 ms | +| charset-normalizer | 16 ms | 10 ms | 1 ms | + +_updated as of March 2026 using CPython 3.12, Charset-Normalizer 3.4.6, and Chardet 7.1.0_ + +~Chardet's performance on larger file (1MB+) are very poor. Expect huge difference on large payload.~ No longer the case since Chardet 7.0+ + +> 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. +> Chardet claims on his documentation to have a greater accuracy than us based on the dataset they trained Chardet on(...) +> Well, it's normal, the opposite would have been worrying. Whereas charset-normalizer don't train on anything, our solution +> is based on a completely different algorithm, still heuristic through, it does not need weights across every encoding tables. + +## ✨ 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 + +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.7](https://github.com/Ousret/charset_normalizer/compare/3.4.6...3.4.7) (2026-04-02) + +### Changed +- Pre-built optimized version using mypy[c] v1.20. +- Relax `setuptools` constraint to `setuptools>=68,<82.1`. + +### Fixed +- Correctly remove SIG remnant in utf-7 decoded string. (#718) (#716) + +## [3.4.6](https://github.com/Ousret/charset_normalizer/compare/3.4.5...3.4.6) (2026-03-15) + +### Changed +- Flattened the logic in `charset_normalizer.md` for higher performance. Removed `eligible(..)` and `feed(...)` + in favor of `feed_info(...)`. +- Raised upper bound for mypy[c] to 1.20, for our optimized version. +- Updated `UNICODE_RANGES_COMBINED` using Unicode blocks v17. + +### Fixed +- Edge case where noise difference between two candidates can be almost insignificant. (#672) +- CLI `--normalize` writing to wrong path when passing multiple files in. (#702) + +### Misc +- Freethreaded pre-built wheels now shipped in PyPI starting with 3.14t. (#616) + +## [3.4.5](https://github.com/Ousret/charset_normalizer/compare/3.4.4...3.4.5) (2026-03-06) + +### Changed +- Update `setuptools` constraint to `setuptools>=68,<=82`. +- Raised upper bound of mypyc for the optional pre-built extension to v1.19.1 + +### Fixed +- Add explicit link to lib math in our optimized build. (#692) +- Logger level not restored correctly for empty byte sequences. (#701) +- TypeError when passing bytearray to from_bytes. (#703) + +### Misc +- Applied safe micro-optimizations in both our noise detector and language detector. +- Rewrote the `query_yes_no` function (inside CLI) to avoid using ambiguous licensed code. +- Added `cd.py` submodule into mypyc optional compilation to reduce further the performance impact. + +## [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.11/site-packages/charset_normalizer-3.4.7.dist-info/RECORD b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/RECORD new file mode 100644 index 0000000..49040a6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/RECORD @@ -0,0 +1,36 @@ +../../../bin/normalizer,sha256=dJFwmZzO-u3zeY4qbukkMZsXoT_u8-ly71wrbMNzyWU,257 +81d243bd2c585b0f4821__mypyc.cpython-311-x86_64-linux-gnu.so,sha256=gpIx9m7J0jRbZd0AlebeKN_1Xwc7C5UeNi7DqVD8vgM,429672 +charset_normalizer-3.4.7.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +charset_normalizer-3.4.7.dist-info/METADATA,sha256=K8lK8L8LaZ1YmKvWLt3zEkpIxiCOC58xNhzFQrfQJxQ,40931 +charset_normalizer-3.4.7.dist-info/RECORD,, +charset_normalizer-3.4.7.dist-info/WHEEL,sha256=M5wZqvm0RPz434ksvS2jyqHKJ9fsZFXCH8JlrvbFuNs,190 +charset_normalizer-3.4.7.dist-info/entry_points.txt,sha256=ADSTKrkXZ3hhdOVFi6DcUEHQRS0xfxDIE_pEz4wLIXA,65 +charset_normalizer-3.4.7.dist-info/licenses/LICENSE,sha256=bQ1Bv-FwrGx9wkjJpj4lTQ-0WmDVCoJX0K-SxuJJuIc,1071 +charset_normalizer-3.4.7.dist-info/top_level.txt,sha256=c_vZbitqecT2GfK3zdxSTLCn8C-6pGnHQY5o_5Y32M0,47 +charset_normalizer/__init__.py,sha256=OKRxRv2Zhnqk00tqkN0c1BtJjm165fWXLydE52IKuHc,1590 +charset_normalizer/__main__.py,sha256=yzYxMR-IhKRHYwcSlavEv8oGdwxsR89mr2X09qXGdps,109 +charset_normalizer/__pycache__/__init__.cpython-311.pyc,, +charset_normalizer/__pycache__/__main__.cpython-311.pyc,, +charset_normalizer/__pycache__/api.cpython-311.pyc,, +charset_normalizer/__pycache__/cd.cpython-311.pyc,, +charset_normalizer/__pycache__/constant.cpython-311.pyc,, +charset_normalizer/__pycache__/legacy.cpython-311.pyc,, +charset_normalizer/__pycache__/md.cpython-311.pyc,, +charset_normalizer/__pycache__/models.cpython-311.pyc,, +charset_normalizer/__pycache__/utils.cpython-311.pyc,, +charset_normalizer/__pycache__/version.cpython-311.pyc,, +charset_normalizer/api.py,sha256=387F3n23MlMu-xfSbFULW2DLGsBmVrZVGhnkiGXeKBo,38844 +charset_normalizer/cd.cpython-311-x86_64-linux-gnu.so,sha256=q9uObNt5v2-3JR3GmbMsHY7cJIiCz9Eez4Hmvs0IR3M,15912 +charset_normalizer/cd.py,sha256=v0iPJweGsRegXywrM1LzUgqW9bJ1KFvIblQHP1jm5FQ,15174 +charset_normalizer/cli/__init__.py,sha256=D8I86lFk2-py45JvqxniTirSj_sFyE6sjaY_0-G1shc,136 +charset_normalizer/cli/__main__.py,sha256=E9FFSV1E2iOE_B2B1tJHQT9ExJqc60Ks_c-08sNawh8,11940 +charset_normalizer/cli/__pycache__/__init__.cpython-311.pyc,, +charset_normalizer/cli/__pycache__/__main__.cpython-311.pyc,, +charset_normalizer/constant.py,sha256=yvLAWDrdSC743Cu4amhwHLIO-FGuRTOTZouCzZKGikc,44431 +charset_normalizer/legacy.py,sha256=yBIFMNABNPE5JkdKOWyVo36fZtV9nm8bf37LrDWulz8,2661 +charset_normalizer/md.cpython-311-x86_64-linux-gnu.so,sha256=MvlOADQzK-FTt2QLGo1y7joPU_7evdj0LunrWj6uzXs,15912 +charset_normalizer/md.py,sha256=AYCdfDX79FrgoId3zXqmbCuDcbGr1NRuGqgJN94Rx9Q,30441 +charset_normalizer/models.py,sha256=FbaQnI6ECmVmyHRSvVM5fHNeMAQ3KSGdwLjGcQqWDws,12821 +charset_normalizer/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +charset_normalizer/utils.py,sha256=9cpi-_0-vC9pGDfuoarhC6VlF_Jxwx5Jsa_8I4w2D8k,12282 +charset_normalizer/version.py,sha256=2LxFuGp3BBuIwt95cp64y7v8bCNHcMAi08IfXt_47Co,115 diff --git a/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/WHEEL b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/WHEEL new file mode 100644 index 0000000..1c20eb8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/WHEEL @@ -0,0 +1,7 @@ +Wheel-Version: 1.0 +Generator: setuptools (82.0.1) +Root-Is-Purelib: false +Tag: cp311-cp311-manylinux_2_17_x86_64 +Tag: cp311-cp311-manylinux2014_x86_64 +Tag: cp311-cp311-manylinux_2_28_x86_64 + diff --git a/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/entry_points.txt b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/entry_points.txt new file mode 100644 index 0000000..65619e7 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/entry_points.txt @@ -0,0 +1,2 @@ +[console_scripts] +normalizer = charset_normalizer.cli:cli_detect diff --git a/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/licenses/LICENSE b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/licenses/LICENSE new file mode 100644 index 0000000..9725772 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.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.11/site-packages/charset_normalizer-3.4.7.dist-info/top_level.txt b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/top_level.txt new file mode 100644 index 0000000..89847be --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer-3.4.7.dist-info/top_level.txt @@ -0,0 +1,2 @@ +81d243bd2c585b0f4821__mypyc +charset_normalizer diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__init__.py b/venv/lib/python3.11/site-packages/charset_normalizer/__init__.py new file mode 100644 index 0000000..0d3a379 --- /dev/null +++ b/venv/lib/python3.11/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.11/site-packages/charset_normalizer/__main__.py b/venv/lib/python3.11/site-packages/charset_normalizer/__main__.py new file mode 100644 index 0000000..e0e76f7 --- /dev/null +++ b/venv/lib/python3.11/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.11/site-packages/charset_normalizer/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..526eb10 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/__main__.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..83157a9 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/__main__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/api.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/api.cpython-311.pyc new file mode 100644 index 0000000..54ab3ee Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/api.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/cd.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/cd.cpython-311.pyc new file mode 100644 index 0000000..891dd76 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/cd.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/constant.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/constant.cpython-311.pyc new file mode 100644 index 0000000..d50bc90 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/constant.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/legacy.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/legacy.cpython-311.pyc new file mode 100644 index 0000000..5418ca9 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/legacy.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/md.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/md.cpython-311.pyc new file mode 100644 index 0000000..f013523 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/md.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/models.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/models.cpython-311.pyc new file mode 100644 index 0000000..dbee9fb Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/models.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..98f5f46 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/version.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/version.cpython-311.pyc new file mode 100644 index 0000000..6d33793 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/__pycache__/version.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/api.py b/venv/lib/python3.11/site-packages/charset_normalizer/api.py new file mode 100644 index 0000000..50cb955 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer/api.py @@ -0,0 +1,988 @@ +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, + IANA_SUPPORTED_SIMILAR, + 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_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") +) + +# Pre-compute a reordered encoding list: multibyte first, then single-byte. +# This allows the mb_definitive_match optimization to fire earlier, skipping +# all single-byte encodings for genuine CJK content. Multibyte codecs +# hard-fail (UnicodeDecodeError) on single-byte data almost instantly, so +# testing them first costs negligible time for non-CJK files. +_mb_supported: list[str] = [] +_sb_supported: list[str] = [] + +for _supported_enc in IANA_SUPPORTED: + try: + if is_multi_byte_encoding(_supported_enc): + _mb_supported.append(_supported_enc) + else: + _sb_supported.append(_supported_enc) + except ImportError: + _sb_supported.append(_supported_enc) + +IANA_SUPPORTED_MB_FIRST: list[str] = _mb_supported + _sb_supported + + +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) + 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] = [] + soft_failure_skip: set[str] = set() + success_fast_tracked: set[str] = set() + + # Cache for decoded payload deduplication: hash(decoded_payload) -> (mean_mess_ratio, cd_ratios_merged, passed) + # When multiple encodings decode to the exact same string, we can skip the expensive + # mess_ratio and coherence_ratio analysis and reuse the results from the first encoding. + payload_result_cache: dict[int, tuple[float, list[tuple[str, float]], bool]] = {} + + # When a definitive result (chaos=0.0 and good coherence) is found after testing + # the prioritized encodings (ascii, utf_8), we can significantly reduce the remaining + # work. Encodings that target completely different language families (e.g., Cyrillic + # when the definitive match is Latin) are skipped entirely. + # Additionally, for same-family encodings that pass chaos probing, we reuse the + # definitive match's coherence ratios instead of recomputing them — a major savings + # since coherence_ratio accounts for ~30% of total time on slow Latin files. + definitive_match_found: bool = False + definitive_target_languages: set[str] = set() + # After the definitive match fires, we cap the number of additional same-family + # single-byte encodings that pass chaos probing. Once we've accumulated enough + # good candidates (N), further same-family SB encodings are unlikely to produce + # a better best() result and just waste mess_ratio + coherence_ratio time. + # The first encoding to trigger the definitive match is NOT counted (it's already in). + post_definitive_sb_success_count: int = 0 + POST_DEFINITIVE_SB_CAP: int = 7 + + # When a non-UTF multibyte encoding passes chaos probing with significant multibyte + # content (decoded length < 98% of raw length), skip all remaining single-byte encodings. + # Rationale: multi-byte decoders (CJK) have strict byte-sequence validation — if they + # decode without error AND pass chaos probing with substantial multibyte content, the + # data is genuinely multibyte encoded. Single-byte encodings will always decode (every + # byte maps to something) but waste time on mess_ratio before failing. + # The 98% threshold prevents false triggers on files that happen to have a few valid + # multibyte pairs (e.g., cp424/_ude_1.txt where big5 decodes with 99% ratio). + mb_definitive_match_found: bool = False + + 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_MB_FIRST: + 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 + + # Skip encodings similar to ones that already soft-failed (high mess ratio). + # Checked BEFORE the expensive decode attempt. + if encoding_iana in soft_failure_skip: + logger.log( + TRACE, + "%s is deemed too similar to a code page that was already considered unsuited. Continuing!", + encoding_iana, + ) + continue + + # Skip encodings that were already fast-tracked from a similar successful encoding. + if encoding_iana in success_fast_tracked: + logger.log( + TRACE, + "Skipping %s: already fast-tracked from a similar successful encoding.", + encoding_iana, + ) + continue + + try: + is_multi_byte_decoder: bool = is_multi_byte_encoding(encoding_iana) + except (ModuleNotFoundError, ImportError): # Defensive: + logger.log( + TRACE, + "Encoding %s does not provide an IncrementalDecoder", + encoding_iana, + ) + continue + + # When we've already found a definitive match (chaos=0.0 with good coherence) + # after testing the prioritized encodings, skip encodings that target + # completely different language families. This avoids running expensive + # mess_ratio + coherence_ratio on clearly unrelated candidates (e.g., Cyrillic + # when the definitive match is Latin-based). + if definitive_match_found: + if not is_multi_byte_decoder: + enc_languages = set(encoding_languages(encoding_iana)) + else: + enc_languages = set(mb_encoding_languages(encoding_iana)) + if not enc_languages.intersection(definitive_target_languages): + logger.log( + TRACE, + "Skipping %s: definitive match already found, this encoding targets different languages (%s vs %s).", + encoding_iana, + enc_languages, + definitive_target_languages, + ) + continue + + # After the definitive match, cap the number of additional same-family + # single-byte encodings that pass chaos probing. This avoids testing the + # tail of rare, low-value same-family encodings (mac_iceland, cp860, etc.) + # that almost never change best() but each cost ~1-2ms of mess_ratio + coherence. + if ( + definitive_match_found + and not is_multi_byte_decoder + and post_definitive_sb_success_count >= POST_DEFINITIVE_SB_CAP + ): + logger.log( + TRACE, + "Skipping %s: already accumulated %d same-family results after definitive match (cap=%d).", + encoding_iana, + post_definitive_sb_success_count, + POST_DEFINITIVE_SB_CAP, + ) + continue + + # When a multibyte encoding with significant multibyte content has already + # passed chaos probing, skip all single-byte encodings. They will either fail + # chaos probing (wasting mess_ratio time) or produce inferior results. + if mb_definitive_match_found and not is_multi_byte_decoder: + logger.log( + TRACE, + "Skipping single-byte %s: multi-byte definitive match already found.", + 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: + # UTF-7 BOM is encoded in modified Base64 whose byte boundary + # can overlap with the next character. Stripping raw SIG bytes + # before decoding may leave stray bytes that decode as garbage. + # Decode the full sequence and remove the leading BOM char instead. + # see https://github.com/jawah/charset_normalizer/issues/718 + # and https://github.com/jawah/charset_normalizer/issues/716 + if encoding_iana == "utf_7" and bom_or_sig_available: + decoded_payload = str( + sequences, + encoding=encoding_iana, + ) + if decoded_payload and decoded_payload[0] == "\ufeff": + decoded_payload = decoded_payload[1:] + 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 + + 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, + ) + + # Payload-hash deduplication: if another encoding already decoded to the + # exact same string, reuse its mess_ratio and coherence results entirely. + # This is strictly more general than the old IANA_SUPPORTED_SIMILAR approach + # because it catches ALL identical decoding, not just pre-mapped ones. + if decoded_payload is not None and not is_multi_byte_decoder: + payload_hash: int = hash(decoded_payload) + cached = payload_result_cache.get(payload_hash) + if cached is not None: + cached_mess, cached_cd, cached_passed = cached + if cached_passed: + # The previous encoding with identical output passed chaos probing. + fast_match = CharsetMatch( + sequences, + encoding_iana, + cached_mess, + bom_or_sig_available, + cached_cd, + ( + 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(fast_match) + success_fast_tracked.add(encoding_iana) + logger.log( + TRACE, + "%s fast-tracked (identical decoded payload to a prior encoding, chaos=%f %%).", + encoding_iana, + round(cached_mess * 100, ndigits=3), + ) + + if ( + encoding_iana in [specified_encoding, "ascii", "utf_8"] + and cached_mess < 0.1 + ): + if cached_mess == 0.0: + logger.debug( + "Encoding detection: %s is most likely the one.", + fast_match.encoding, + ) + if explain: + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + return CharsetMatches([fast_match]) + early_stop_results.append(fast_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: + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + return CharsetMatches([probable_result]) + + continue + else: + # The previous encoding with identical output failed chaos probing. + tested_but_soft_failure.append(encoding_iana) + logger.log( + TRACE, + "%s fast-skipped (identical decoded payload to a prior encoding that failed chaos probing).", + encoding_iana, + ) + # Prepare fallbacks for special encodings even when skipped. + if enable_fallback and encoding_iana in [ + "ascii", + "utf_8", + specified_encoding, + "utf_16", + "utf_32", + ]: + 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 + + 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) + if encoding_iana in IANA_SUPPORTED_SIMILAR: + soft_failure_skip.update(IANA_SUPPORTED_SIMILAR[encoding_iana]) + # Cache this soft-failure so identical decoding from other encodings + # can be skipped immediately. + if decoded_payload is not None and not is_multi_byte_decoder: + payload_result_cache.setdefault( + hash(decoded_payload), (mean_mess_ratio, [], False) + ) + 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 = [] + + # Run coherence detection on all chunks. We previously tried limiting to + # 1-2 chunks for post-definitive encodings to save time, but this caused + # coverage regressions by producing unrepresentative coherence scores. + # The SB cap and language-family skip optimizations provide sufficient + # speedup without sacrificing coherence accuracy. + if encoding_iana != "ascii": + # We shall skip the CD when its about ASCII + # Most of the time its not relevant to run "language-detection" on it. + 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) + else: + 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) + + # Cache the successful result for payload-hash deduplication. + if decoded_payload is not None and not is_multi_byte_decoder: + payload_result_cache.setdefault( + hash(decoded_payload), + (mean_mess_ratio, cd_ratios_merged, True), + ) + + # Count post-definitive same-family SB successes for the early termination cap. + # Only count low-mess encodings (< 2%) toward the cap. High-mess encodings are + # marginal results that shouldn't prevent better-quality candidates from being + # tested. For example, iso8859_4 (mess=0%) should not be skipped just because + # 7 high-mess Latin encodings (cp1252 at 8%, etc.) were tried first. + if ( + definitive_match_found + and not is_multi_byte_decoder + and mean_mess_ratio < 0.02 + ): + post_definitive_sb_success_count += 1 + + 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 = early_stop_results.best() # type: ignore[assignment] + logger.debug( + "Encoding detection: %s is most likely the one.", + probable_result.encoding, # type: ignore[union-attr] + ) + if explain: # Defensive: ensure exit path clean handler + logger.removeHandler(explain_handler) + logger.setLevel(previous_logger_level) + + return CharsetMatches([probable_result]) + + # Once we find a result with good coherence (>= 0.5) after testing the + # prioritized encodings (ascii, utf_8), activate "definitive mode": skip + # encodings that target completely different language families. This avoids + # running expensive mess_ratio + coherence_ratio on clearly unrelated + # candidates (e.g., Cyrillic encodings when the match is Latin-based). + # We require coherence >= 0.5 to avoid false positives (e.g., cp1251 decoding + # Hebrew text with 0.0 chaos but wrong language detection at coherence 0.33). + if not definitive_match_found and not is_multi_byte_decoder: + best_coherence = ( + max((v for _, v in cd_ratios_merged), default=0.0) + if cd_ratios_merged + else 0.0 + ) + if best_coherence >= 0.5 and "ascii" in tested and "utf_8" in tested: + definitive_match_found = True + definitive_target_languages.update(target_languages) + logger.log( + TRACE, + "Definitive match found: %s (chaos=%.3f, coherence=%.2f). Encodings targeting different language families will be skipped.", + encoding_iana, + mean_mess_ratio, + best_coherence, + ) + + # When a non-UTF multibyte encoding passes chaos probing with significant + # multibyte content (decoded < 98% of raw), activate mb_definitive_match. + # This skips all remaining single-byte encodings which would either soft-fail + # (running expensive mess_ratio for nothing) or produce inferior results. + if ( + not mb_definitive_match_found + and is_multi_byte_decoder + and multi_byte_bonus + and decoded_payload is not None + and len(decoded_payload) < length * 0.98 + and encoding_iana + not in { + "utf_8", + "utf_8_sig", + "utf_16", + "utf_16_be", + "utf_16_le", + "utf_32", + "utf_32_be", + "utf_32_le", + "utf_7", + } + and "ascii" in tested + and "utf_8" in tested + ): + mb_definitive_match_found = True + logger.log( + TRACE, + "Multi-byte definitive match: %s (chaos=%.3f, decoded=%d/%d=%.1f%%). Single-byte encodings will be skipped.", + encoding_iana, + mean_mess_ratio, + len(decoded_payload), + length, + len(decoded_payload) / length * 100, + ) + + 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.11/site-packages/charset_normalizer/cd.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/charset_normalizer/cd.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..15eb479 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/cd.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/cd.py b/venv/lib/python3.11/site-packages/charset_normalizer/cd.py new file mode 100644 index 0000000..9545d35 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer/cd.py @@ -0,0 +1,454 @@ +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, + _FREQUENCIES_SET, + _FREQUENCIES_RANK, +) +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( # Defensive: + "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]] = [] + + characters_set: frozenset[str] = frozenset(characters) + 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(_FREQUENCIES_SET[language] & characters_set) + + 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") # Defensive: + + character_approved_count: int = 0 + frequencies_language_set: frozenset[str] = _FREQUENCIES_SET[language] + lang_rank: dict[str, int] = _FREQUENCIES_RANK[language] + + ordered_characters_count: int = len(ordered_characters) + target_language_characters_count: int = len(FREQUENCIES[language]) + + large_alphabet: bool = target_language_characters_count > 26 + + expected_projection_ratio: float = ( + target_language_characters_count / ordered_characters_count + ) + + # Pre-built rank dict for ordered_characters (avoids repeated list slicing). + ordered_rank: dict[str, int] = { + char: rank for rank, char in enumerate(ordered_characters) + } + + # Pre-compute characters common to both orderings. + # Avoids repeated `c in ordered_rank` dict lookups in the inner counts. + common_chars: list[tuple[int, int]] = [ + (lr, ordered_rank[c]) for c, lr in lang_rank.items() if c in ordered_rank + ] + + # Pre-extract lr and orr arrays for faster iteration in the inner loop. + # Plain integer loops with local arrays are much faster under mypyc than + # generator expression sums over a list of tuples. + common_count: int = len(common_chars) + common_lr: list[int] = [p[0] for p in common_chars] + common_orr: list[int] = [p[1] for p in common_chars] + + 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 = lang_rank[character] + 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 + + # Count how many characters appear "before" in both orderings, + # and how many appear "at or after" in both orderings. + # Single pass over pre-extracted arrays — much faster under mypyc + # than two generator expression sums. + before_match_count: int = 0 + after_match_count: int = 0 + for i in range(common_count): + lr_i: int = common_lr[i] + orr_i: int = common_orr[i] + if lr_i < character_rank_in_language: + if orr_i < character_rank: + before_match_count += 1 + else: + if orr_i >= character_rank: + after_match_count += 1 + + after_len: int = target_language_characters_count - character_rank_in_language + + if character_rank_in_language == 0 and before_match_count <= 4: + character_approved_count += 1 + continue + + if after_len == 0 and after_match_count <= 4: + character_approved_count += 1 + continue + + if ( + character_rank_in_language > 0 + and before_match_count / character_rank_in_language >= 0.4 + ) or (after_len > 0 and after_match_count / after_len >= 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, list[str]] = {} + + # Fast path: track single-layer key to skip dict iteration for single-script text. + single_layer_key: str | None = None + multi_layer: bool = False + + # Cache the last character_range and its resolved layer to avoid repeated + # is_suspiciously_successive_range calls for consecutive same-range chars. + prev_character_range: str | None = None + prev_layer_target: str | None = None + + for character in decoded_sequence: + if character.isalpha() is False: + continue + + # ASCII fast-path: a-z and A-Z are always "Basic Latin". + # Avoids unicode_range() function call overhead for the most common case. + character_ord: int = ord(character) + if character_ord < 128: + character_range: str | None = "Basic Latin" + else: + character_range = unicode_range(character) + + if character_range is None: + continue + + # Fast path: same range as previous character → reuse cached layer target. + if character_range == prev_character_range: + if prev_layer_target is not None: + layers[prev_layer_target].append(character) + continue + + layer_target_range: str | None = None + + if multi_layer: + for discovered_range in layers: + if ( + is_suspiciously_successive_range(discovered_range, character_range) + is False + ): + layer_target_range = discovered_range + break + elif single_layer_key is not None: + if ( + is_suspiciously_successive_range(single_layer_key, character_range) + is False + ): + layer_target_range = single_layer_key + + if layer_target_range is None: + layer_target_range = character_range + + if layer_target_range not in layers: + layers[layer_target_range] = [] + if single_layer_key is None: + single_layer_key = layer_target_range + else: + multi_layer = True + + layers[layer_target_range].append(character) + + # Cache for next iteration + prev_character_range = character_range + prev_layer_target = layer_target_range + + return ["".join(chars).lower() for chars in 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 = len(layer) + + 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.11/site-packages/charset_normalizer/cli/__init__.py b/venv/lib/python3.11/site-packages/charset_normalizer/cli/__init__.py new file mode 100644 index 0000000..543a5a4 --- /dev/null +++ b/venv/lib/python3.11/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.11/site-packages/charset_normalizer/cli/__main__.py b/venv/lib/python3.11/site-packages/charset_normalizer/cli/__main__.py new file mode 100644 index 0000000..ad843c1 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer/cli/__main__.py @@ -0,0 +1,362 @@ +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: # Defensive: + """Ask a yes/no question via input() and return the answer as a bool.""" + prompt = " [Y/n] " if default == "yes" else " [y/N] " + + while True: + choice = input(question + prompt).strip().lower() + if not choice: + return default == "yes" + if choice in ("y", "yes"): + return True + if choice in ("n", "no"): + return False + print("Please respond with 'y' or '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: + cli_result = 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, + ) + x_.append(cli_result) + + 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: + cli_result.unicode_path = join(dir_path, ".".join(o_)) + + with open(cli_result.unicode_path, "wb") as fp: + fp.write(best_guess.output()) + except OSError as e: # Defensive: + 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__": # Defensive: + cli_detect() diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/cli/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/cli/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..28fe90f Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/cli/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/cli/__pycache__/__main__.cpython-311.pyc b/venv/lib/python3.11/site-packages/charset_normalizer/cli/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..ef723aa Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/cli/__pycache__/__main__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/constant.py b/venv/lib/python3.11/site-packages/charset_normalizer/constant.py new file mode 100644 index 0000000..e1297d2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer/constant.py @@ -0,0 +1,2050 @@ +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\x2d", + b"\x2b\x2f\x76\x38", + b"\x2b\x2f\x76\x39", + b"\x2b\x2f\x76\x2b", + b"\x2b\x2f\x76\x2f", + ], + "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/17.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), + "Todhri": range(67008, 67072), + "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), + "Sidetic": range(67904, 67936), + "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), + "Garay": range(68928, 69008), + "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), + "Tulu-Tigalari": range(70528, 70656), + "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), + "Myanmar Extended-C": range(71376, 71424), + "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), + "Sharada Supplement": range(72544, 72576), + "Sunuwar": range(72640, 72704), + "Bhaiksuki": range(72704, 72816), + "Marchen": range(72816, 72896), + "Masaram Gondi": range(72960, 73056), + "Gunjala Gondi": range(73056, 73136), + "Tolong Siki": range(73136, 73200), + "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), + "Egyptian Hieroglyphs Extended-A": range(78944, 82944), + "Anatolian Hieroglyphs": range(82944, 83584), + "Gurung Khema": range(90368, 90432), + "Bamum Supplement": range(92160, 92736), + "Mro": range(92736, 92784), + "Tangsa": range(92784, 92880), + "Bassa Vah": range(92880, 92928), + "Pahawh Hmong": range(92928, 93072), + "Kirat Rai": range(93504, 93568), + "Medefaidrin": range(93760, 93856), + "Beria Erfe": range(93856, 93920), + "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), + "Tangut Components Supplement": range(101760, 101888), + "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), + "Symbols for Legacy Computing Supplement": range(117760, 118464), + "Miscellaneous Symbols Supplement": range(118464, 118528), + "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), + "Ol Onal": range(124368, 124416), + "Tai Yo": range(124608, 124672), + "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(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 Unified Ideographs Extension I": range(191472, 192096), + "CJK Compatibility Ideographs Supplement": range(194560, 195104), + "CJK Unified Ideographs Extension G": range(196608, 201552), + "CJK Unified Ideographs Extension H": range(201552, 205744), + "CJK Unified Ideographs Extension J": range(205744, 210048), + "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: frozenset[str] = frozenset( + { + "<", + ">", + "=", + ":", + "/", + "&", + ";", + "{", + "}", + "[", + "]", + ",", + "|", + '"', + "-", + "(", + ")", + } +) + +# Sample character sets — replace with full lists if needed +COMMON_CHINESE_CHARACTERS = "的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年动同工也能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去把性好应开它合还因由其些然前外天政四日那社义事平形相全表间样与关各重新线内数正心反你明看原又么利比或但质气第向道命此变条只没结解问意建月公无系军很情者最立代想已通并提直题党程展五果料象员革位入常文总次品式活设及管特件长求老头基资边流路级少图山统接知较将组见计别她手角期根论运农指几九区强放决西被干做必战先回则任取据处队南给色光门即保治北造百规热领七海口东导器压志世金增争济阶油思术极交受联什认六共权收证改清己美再采转更单风切打白教速花带安场身车例真务具万每目至达走积示议声报斗完类八离华名确才科张信马节话米整空元况今集温传土许步群广石记需段研界拉林律叫且究观越织装影算低持音众书布复容儿须际商非验连断深难近矿千周委素技备半办青省列习响约支般史感劳便团往酸历市克何除消构府太准精值号率族维划选标写存候毛亲快效斯院查江型眼王按格养易置派层片始却专状育厂京识适属圆包火住调满县局照参红细引听该铁价严龙飞" + +COMMON_JAPANESE_CHARACTERS = "日一国年大十二本中長出三時行見月分後前生五間上東四今金九入学高円子外八六下来気小七山話女北午百書先名川千水半男西電校語土木聞食車何南万毎白天母火右読友左休父雨" + +COMMON_KOREAN_CHARACTERS = "一二三四五六七八九十百千萬上下左右中人女子大小山川日月火水木金土父母天地國名年時文校學生" + +# Combine all into a frozenset +COMMON_CJK_CHARACTERS = frozenset( + "".join( + [ + COMMON_CHINESE_CHARACTERS, + COMMON_JAPANESE_CHARACTERS, + COMMON_KOREAN_CHARACTERS, + ] + ) +) + +KO_NAMES: frozenset[str] = frozenset({"johab", "cp949", "euc_kr"}) +ZH_NAMES: frozenset[str] = frozenset({"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) + +# Bit flags for unified character classification. +# A single unicodedata.name() call sets all relevant flags at once. +_LATIN: int = 1 +_ACCENTUATED: int = 1 << 1 +_CJK: int = 1 << 2 +_HANGUL: int = 1 << 3 +_KATAKANA: int = 1 << 4 +_HIRAGANA: int = 1 << 5 +_THAI: int = 1 << 6 +_ARABIC: int = 1 << 7 +_ARABIC_ISOLATED_FORM: int = 1 << 8 + +_ACCENT_KEYWORDS: tuple[str, ...] = ( + "WITH GRAVE", + "WITH ACUTE", + "WITH CEDILLA", + "WITH DIAERESIS", + "WITH CIRCUMFLEX", + "WITH TILDE", + "WITH MACRON", + "WITH RING ABOVE", +) + +# Pre-built lookup structures for FREQUENCIES (computed once at import time). +# character -> rank mapping per language (replaces list .index() calls). +_FREQUENCIES_RANK: dict[str, dict[str, int]] = { + lang: {char: rank for rank, char in enumerate(chars)} + for lang, chars in FREQUENCIES.items() +} + +# frozenset per language (avoids rebuilding set() per call). +_FREQUENCIES_SET: dict[str, frozenset[str]] = { + lang: frozenset(chars) for lang, chars in FREQUENCIES.items() +} diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/legacy.py b/venv/lib/python3.11/site-packages/charset_normalizer/legacy.py new file mode 100644 index 0000000..293c1ef --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer/legacy.py @@ -0,0 +1,79 @@ +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 + +if TYPE_CHECKING: + from typing 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.11/site-packages/charset_normalizer/md.cpython-311-x86_64-linux-gnu.so b/venv/lib/python3.11/site-packages/charset_normalizer/md.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000..5512842 Binary files /dev/null and b/venv/lib/python3.11/site-packages/charset_normalizer/md.cpython-311-x86_64-linux-gnu.so differ diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/md.py b/venv/lib/python3.11/site-packages/charset_normalizer/md.py new file mode 100644 index 0000000..b41d9cf --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer/md.py @@ -0,0 +1,936 @@ +from __future__ import annotations + +import sys +from functools import lru_cache +from logging import getLogger + +if sys.version_info >= (3, 8): + from typing import final +else: + try: + from typing_extensions import final + except ImportError: + + def final(cls): # type: ignore[misc,no-untyped-def] + return cls + + +from .constant import ( + COMMON_CJK_CHARACTERS, + COMMON_SAFE_ASCII_CHARACTERS, + TRACE, + UNICODE_SECONDARY_RANGE_KEYWORD, + _ACCENTUATED, + _ARABIC, + _ARABIC_ISOLATED_FORM, + _CJK, + _HANGUL, + _HIRAGANA, + _KATAKANA, + _LATIN, + _THAI, +) +from .utils import ( + _character_flags, + is_emoticon, + is_punctuation, + is_separator, + is_symbol, + remove_accent, + unicode_range, +) + +# Combined bitmask for CJK/Hangul/Katakana/Hiragana/Thai glyph detection. +_GLYPH_MASK: int = _CJK | _HANGUL | _KATAKANA | _HIRAGANA | _THAI + + +@final +class CharInfo: + """Pre-computed character properties shared across all detectors. + + Instantiated once and reused via :meth:`update` on every character + in the hot loop so that redundant calls to str methods + (``isalpha``, ``isupper``, …) and cached utility functions + (``_character_flags``, ``is_punctuation``, …) are avoided when + several plugins need the same information. + """ + + __slots__ = ( + "character", + "printable", + "alpha", + "upper", + "lower", + "space", + "digit", + "is_ascii", + "case_variable", + "flags", + "accentuated", + "latin", + "is_cjk", + "is_arabic", + "is_glyph", + "punct", + "sym", + ) + + def __init__(self) -> None: + self.character: str = "" + self.printable: bool = False + self.alpha: bool = False + self.upper: bool = False + self.lower: bool = False + self.space: bool = False + self.digit: bool = False + self.is_ascii: bool = False + self.case_variable: bool = False + self.flags: int = 0 + self.accentuated: bool = False + self.latin: bool = False + self.is_cjk: bool = False + self.is_arabic: bool = False + self.is_glyph: bool = False + self.punct: bool = False + self.sym: bool = False + + def update(self, character: str) -> None: + """Update all properties for *character* (called once per character).""" + self.character = character + + # ASCII fast-path: for characters with ord < 128, we can skip + # _character_flags() entirely and derive most properties from ord. + o: int = ord(character) + if o < 128: + self.is_ascii = True + self.accentuated = False + self.is_cjk = False + self.is_arabic = False + self.is_glyph = False + # ASCII alpha: a-z (97-122) or A-Z (65-90) + if 65 <= o <= 90: + # Uppercase ASCII letter + self.alpha = True + self.upper = True + self.lower = False + self.space = False + self.digit = False + self.printable = True + self.case_variable = True + self.flags = _LATIN + self.latin = True + self.punct = False + self.sym = False + elif 97 <= o <= 122: + # Lowercase ASCII letter + self.alpha = True + self.upper = False + self.lower = True + self.space = False + self.digit = False + self.printable = True + self.case_variable = True + self.flags = _LATIN + self.latin = True + self.punct = False + self.sym = False + elif 48 <= o <= 57: + # ASCII digit 0-9 + self.alpha = False + self.upper = False + self.lower = False + self.space = False + self.digit = True + self.printable = True + self.case_variable = False + self.flags = 0 + self.latin = False + self.punct = False + self.sym = False + elif o == 32 or (9 <= o <= 13): + # Space, tab, newline, etc. + self.alpha = False + self.upper = False + self.lower = False + self.space = True + self.digit = False + self.printable = o == 32 + self.case_variable = False + self.flags = 0 + self.latin = False + self.punct = False + self.sym = False + else: + # Other ASCII (punctuation, symbols, control chars) + self.printable = character.isprintable() + self.alpha = False + self.upper = False + self.lower = False + self.space = False + self.digit = False + self.case_variable = False + self.flags = 0 + self.latin = False + self.punct = is_punctuation(character) if self.printable else False + self.sym = is_symbol(character) if self.printable else False + else: + # Non-ASCII path + self.is_ascii = False + self.printable = character.isprintable() + self.alpha = character.isalpha() + self.upper = character.isupper() + self.lower = character.islower() + self.space = character.isspace() + self.digit = character.isdigit() + self.case_variable = self.lower != self.upper + + # Flag-based classification (single unicodedata.name() call, lru-cached) + flags: int + if self.alpha: + flags = _character_flags(character) + else: + flags = 0 + self.flags = flags + self.accentuated = bool(flags & _ACCENTUATED) + self.latin = bool(flags & _LATIN) + self.is_cjk = bool(flags & _CJK) + self.is_arabic = bool(flags & _ARABIC) + self.is_glyph = bool(flags & _GLYPH_MASK) + + # Eagerly compute punct and sym (avoids property dispatch overhead + # on 300K+ accesses in the hot loop). + self.punct = is_punctuation(character) if self.printable else False + self.sym = is_symbol(character) if self.printable else False + + +class MessDetectorPlugin: + """ + Base abstract class used for mess detection plugins. + All detectors MUST extend and implement given methods. + """ + + __slots__ = () + + def feed_info(self, character: str, info: CharInfo) -> None: + """ + The main routine to be executed upon character. + Insert the logic in witch the text would be considered chaotic. + """ + raise NotImplementedError # Defensive: + + def reset(self) -> None: # Defensive: + """ + 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 # Defensive: + + +@final +class TooManySymbolOrPunctuationPlugin(MessDetectorPlugin): + __slots__ = ( + "_punctuation_count", + "_symbol_count", + "_character_count", + "_last_printable_char", + "_frenzy_symbol_in_word", + ) + + 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 feed_info(self, character: str, info: CharInfo) -> None: + """Optimized feed using pre-computed character info.""" + self._character_count += 1 + + if ( + character != self._last_printable_char + and character not in COMMON_SAFE_ASCII_CHARACTERS + ): + if info.punct: + self._punctuation_count += 1 + elif not info.digit and info.sym and not is_emoticon(character): + 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 + + +@final +class TooManyAccentuatedPlugin(MessDetectorPlugin): + __slots__ = ("_character_count", "_accentuated_count") + + def __init__(self) -> None: + self._character_count: int = 0 + self._accentuated_count: int = 0 + + def feed_info(self, character: str, info: CharInfo) -> None: + """Optimized feed using pre-computed character info.""" + self._character_count += 1 + + if info.accentuated: + 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 + + +@final +class UnprintablePlugin(MessDetectorPlugin): + __slots__ = ("_unprintable_count", "_character_count") + + def __init__(self) -> None: + self._unprintable_count: int = 0 + self._character_count: int = 0 + + def feed_info(self, character: str, info: CharInfo) -> None: + """Optimized feed using pre-computed character info.""" + if ( + not info.space + and not info.printable + and character != "\x1a" + and character != "\ufeff" + ): + 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: # Defensive: + return 0.0 + + return (self._unprintable_count * 8) / self._character_count + + +@final +class SuspiciousDuplicateAccentPlugin(MessDetectorPlugin): + __slots__ = ( + "_successive_count", + "_character_count", + "_last_latin_character", + "_last_was_accentuated", + ) + + def __init__(self) -> None: + self._successive_count: int = 0 + self._character_count: int = 0 + + self._last_latin_character: str | None = None + self._last_was_accentuated: bool = False + + def feed_info(self, character: str, info: CharInfo) -> None: + """Optimized feed using pre-computed character info.""" + self._character_count += 1 + if ( + self._last_latin_character is not None + and info.accentuated + and self._last_was_accentuated + ): + if info.upper and self._last_latin_character.isupper(): + self._successive_count += 1 + if remove_accent(character) == remove_accent(self._last_latin_character): + self._successive_count += 1 + self._last_latin_character = character + self._last_was_accentuated = info.accentuated + + def reset(self) -> None: # Abstract + self._successive_count = 0 + self._character_count = 0 + self._last_latin_character = None + self._last_was_accentuated = False + + @property + def ratio(self) -> float: + if self._character_count == 0: + return 0.0 + + return (self._successive_count * 2) / self._character_count + + +@final +class SuspiciousRange(MessDetectorPlugin): + __slots__ = ( + "_suspicious_successive_range_count", + "_character_count", + "_last_printable_seen", + "_last_printable_range", + ) + + def __init__(self) -> None: + self._suspicious_successive_range_count: int = 0 + self._character_count: int = 0 + self._last_printable_seen: str | None = None + self._last_printable_range: str | None = None + + def feed_info(self, character: str, info: CharInfo) -> None: + """Optimized feed using pre-computed character info.""" + self._character_count += 1 + + if info.space or info.punct or character in COMMON_SAFE_ASCII_CHARACTERS: + self._last_printable_seen = None + self._last_printable_range = None + return + + if self._last_printable_seen is None: + self._last_printable_seen = character + self._last_printable_range = unicode_range(character) + return + + unicode_range_a: str | None = self._last_printable_range + 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 + self._last_printable_range = unicode_range_b + + def reset(self) -> None: # Abstract + self._character_count = 0 + self._suspicious_successive_range_count = 0 + self._last_printable_seen = None + self._last_printable_range = 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 + + +@final +class SuperWeirdWordPlugin(MessDetectorPlugin): + __slots__ = ( + "_word_count", + "_bad_word_count", + "_foreign_long_count", + "_is_current_word_bad", + "_foreign_long_watch", + "_character_count", + "_bad_character_count", + "_buffer_length", + "_buffer_last_char", + "_buffer_last_char_accentuated", + "_buffer_accent_count", + "_buffer_glyph_count", + "_buffer_upper_count", + ) + + 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_length: int = 0 + self._buffer_last_char: str | None = None + self._buffer_last_char_accentuated: bool = False + self._buffer_accent_count: int = 0 + self._buffer_glyph_count: int = 0 + self._buffer_upper_count: int = 0 + + def feed_info(self, character: str, info: CharInfo) -> None: + """Optimized feed using pre-computed character info.""" + if info.alpha: + self._buffer_length += 1 + self._buffer_last_char = character + + if info.upper: + self._buffer_upper_count += 1 + + self._buffer_last_char_accentuated = info.accentuated + + if info.accentuated: + self._buffer_accent_count += 1 + if ( + not self._foreign_long_watch + and (not info.latin or info.accentuated) + and not info.is_glyph + ): + self._foreign_long_watch = True + if info.is_glyph: + self._buffer_glyph_count += 1 + return + if not self._buffer_length: + return + if info.space or info.punct or is_separator(character): + self._word_count += 1 + buffer_length: int = self._buffer_length + + self._character_count += buffer_length + + if buffer_length >= 4: + if self._buffer_accent_count / buffer_length >= 0.5: + self._is_current_word_bad = True + elif ( + self._buffer_last_char_accentuated + and self._buffer_last_char.isupper() # type: ignore[union-attr] + and self._buffer_upper_count != buffer_length + ): + 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: + probable_camel_cased: bool = ( + self._buffer_upper_count > 0 + and self._buffer_upper_count / buffer_length <= 0.3 + ) + + 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 += buffer_length + self._is_current_word_bad = False + + self._foreign_long_watch = False + self._buffer_length = 0 + self._buffer_last_char = None + self._buffer_last_char_accentuated = False + self._buffer_accent_count = 0 + self._buffer_glyph_count = 0 + self._buffer_upper_count = 0 + elif ( + character not in {"<", ">", "-", "=", "~", "|", "_"} + and not info.digit + and info.sym + ): + self._is_current_word_bad = True + self._buffer_length += 1 + self._buffer_last_char = character + self._buffer_last_char_accentuated = False + + def reset(self) -> None: # Abstract + self._buffer_length = 0 + self._buffer_last_char = None + self._buffer_last_char_accentuated = False + 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 + self._buffer_accent_count = 0 + self._buffer_glyph_count = 0 + self._buffer_upper_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 + + +@final +class CjkUncommonPlugin(MessDetectorPlugin): + """ + Detect messy CJK text that probably means nothing. + """ + + __slots__ = ("_character_count", "_uncommon_count") + + def __init__(self) -> None: + self._character_count: int = 0 + self._uncommon_count: int = 0 + + def feed_info(self, character: str, info: CharInfo) -> None: + """Optimized feed using pre-computed character info.""" + self._character_count += 1 + + if character not in COMMON_CJK_CHARACTERS: + self._uncommon_count += 1 + + 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 + + +@final +class ArchaicUpperLowerPlugin(MessDetectorPlugin): + __slots__ = ( + "_buf", + "_character_count_since_last_sep", + "_successive_upper_lower_count", + "_successive_upper_lower_count_final", + "_character_count", + "_last_alpha_seen", + "_last_alpha_seen_upper", + "_last_alpha_seen_lower", + "_current_ascii_only", + ) + + 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._last_alpha_seen_upper: bool = False + self._last_alpha_seen_lower: bool = False + self._current_ascii_only: bool = True + + def feed_info(self, character: str, info: CharInfo) -> None: + """Optimized feed using pre-computed character info.""" + is_concerned: bool = info.alpha and info.case_variable + chunk_sep: bool = not is_concerned + + if chunk_sep and self._character_count_since_last_sep > 0: + if ( + self._character_count_since_last_sep <= 64 + and not info.digit + and not self._current_ascii_only + ): + 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 and not info.is_ascii: + self._current_ascii_only = False + + if self._last_alpha_seen is not None: + if (info.upper and self._last_alpha_seen_lower) or ( + info.lower and self._last_alpha_seen_upper + ): + if self._buf: + 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 + self._last_alpha_seen_upper = info.upper + self._last_alpha_seen_lower = info.lower + + 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._last_alpha_seen_upper = False + self._last_alpha_seen_lower = False + self._buf = False + self._current_ascii_only = True + + @property + def ratio(self) -> float: + if self._character_count == 0: # Defensive: + return 0.0 + + return self._successive_upper_lower_count_final / self._character_count + + +@final +class ArabicIsolatedFormPlugin(MessDetectorPlugin): + __slots__ = ("_character_count", "_isolated_form_count") + + 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 feed_info(self, character: str, info: CharInfo) -> None: + """Optimized feed using pre-computed character info.""" + self._character_count += 1 + + if info.flags & _ARABIC_ISOLATED_FORM: + 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. + """ + + seq_len: int = len(decoded_sequence) + + if seq_len < 511: + step: int = 32 + elif seq_len < 1024: + step = 64 + else: + step = 128 + + # Create each detector as a named local variable (unrolled from the generic loop). + # This eliminates per-character iteration over the detector list and + # per-character eligible() virtual dispatch, while keeping every plugin class + # intact and fully readable. + d_sp: TooManySymbolOrPunctuationPlugin = TooManySymbolOrPunctuationPlugin() + d_ta: TooManyAccentuatedPlugin = TooManyAccentuatedPlugin() + d_up: UnprintablePlugin = UnprintablePlugin() + d_sda: SuspiciousDuplicateAccentPlugin = SuspiciousDuplicateAccentPlugin() + d_sr: SuspiciousRange = SuspiciousRange() + d_sw: SuperWeirdWordPlugin = SuperWeirdWordPlugin() + d_cu: CjkUncommonPlugin = CjkUncommonPlugin() + d_au: ArchaicUpperLowerPlugin = ArchaicUpperLowerPlugin() + d_ai: ArabicIsolatedFormPlugin = ArabicIsolatedFormPlugin() + + # Local references for feed_info methods called in the hot loop. + d_sp_feed = d_sp.feed_info + d_ta_feed = d_ta.feed_info + d_up_feed = d_up.feed_info + d_sda_feed = d_sda.feed_info + d_sr_feed = d_sr.feed_info + d_sw_feed = d_sw.feed_info + d_cu_feed = d_cu.feed_info + d_au_feed = d_au.feed_info + d_ai_feed = d_ai.feed_info + + # Single reusable CharInfo object (avoids per-character allocation). + info: CharInfo = CharInfo() + info_update = info.update + + mean_mess_ratio: float + + for block_start in range(0, seq_len, step): + for character in decoded_sequence[block_start : block_start + step]: + # Pre-compute all character properties once (shared across all plugins). + info_update(character) + + # Detectors with eligible() == always True + d_up_feed(character, info) + d_sw_feed(character, info) + d_au_feed(character, info) + + # Detectors with eligible() == isprintable + if info.printable: + d_sp_feed(character, info) + d_sr_feed(character, info) + + # Detectors with eligible() == isalpha + if info.alpha: + d_ta_feed(character, info) + # SuspiciousDuplicateAccent: isalpha() and is_latin() + if info.latin: + d_sda_feed(character, info) + # CjkUncommon: is_cjk() + if info.is_cjk: + d_cu_feed(character, info) + # ArabicIsolatedForm: is_arabic() + if info.is_arabic: + d_ai_feed(character, info) + + mean_mess_ratio = ( + d_sp.ratio + + d_ta.ratio + + d_up.ratio + + d_sda.ratio + + d_sr.ratio + + d_sw.ratio + + d_cu.ratio + + d_au.ratio + + d_ai.ratio + ) + + if mean_mess_ratio >= maximum_threshold: + break + else: + # Flush last word buffer in SuperWeirdWordPlugin via trailing newline. + info_update("\n") + d_sw_feed("\n", info) + d_au_feed("\n", info) + d_up_feed("\n", info) + + mean_mess_ratio = ( + d_sp.ratio + + d_ta.ratio + + d_up.ratio + + d_sda.ratio + + d_sr.ratio + + d_sw.ratio + + d_cu.ratio + + d_au.ratio + + d_ai.ratio + ) + + if debug: # Defensive: + logger = getLogger("charset_normalizer") + + logger.log( + TRACE, + "Mess-detector extended-analysis start. " + f"intermediary_mean_mess_ratio_calc={step} mean_mess_ratio={mean_mess_ratio} " + f"maximum_threshold={maximum_threshold}", + ) + + if seq_len > 16: + logger.log(TRACE, f"Starting with: {decoded_sequence[:16]}") + logger.log(TRACE, f"Ending with: {decoded_sequence[-16::]}") + + for dt in [d_sp, d_ta, d_up, d_sda, d_sr, d_sw, d_cu, d_au, d_ai]: + logger.log(TRACE, f"{dt.__class__}: {dt.ratio}") + + return round(mean_mess_ratio, 3) diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/models.py b/venv/lib/python3.11/site-packages/charset_normalizer/models.py new file mode 100644 index 0000000..382de15 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer/models.py @@ -0,0 +1,369 @@ +from __future__ import annotations + +from encodings.aliases import aliases +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 | bytearray, + 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 | bytearray = 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 0.5% difference --> Use Coherence + if chaos_difference < 0.005 and coherence_difference > 0.02: + return self.coherence > other.coherence + elif chaos_difference < 0.005 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") + # UTF-7 BOM is encoded in modified Base64 whose byte boundary + # can overlap with the next character, so raw-byte stripping + # is unreliable. Strip the decoded BOM character instead. + if ( + self._has_sig_or_bom + and self._encoding == "utf_7" + and self._string + and self._string[0] == "\ufeff" + ): + self._string = self._string[1:] + 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 | bytearray: + """ + 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) -> int: + """ + Retrieve a hash fingerprint of the decoded payload, used for deduplication. + """ + return hash(str(self)) + + +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.11/site-packages/charset_normalizer/py.typed b/venv/lib/python3.11/site-packages/charset_normalizer/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/charset_normalizer/utils.py b/venv/lib/python3.11/site-packages/charset_normalizer/utils.py new file mode 100644 index 0000000..0f529b5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer/utils.py @@ -0,0 +1,422 @@ +from __future__ import annotations + +import importlib +import logging +import unicodedata +from bisect import bisect_right +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, + _LATIN, + _CJK, + _HANGUL, + _KATAKANA, + _HIRAGANA, + _THAI, + _ARABIC, + _ARABIC_ISOLATED_FORM, + _ACCENT_KEYWORDS, + _ACCENTUATED, +) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def _character_flags(character: str) -> int: + """Compute all name-based classification flags with a single unicodedata.name() call.""" + try: + desc: str = unicodedata.name(character) + except ValueError: + return 0 + + flags: int = 0 + + if "LATIN" in desc: + flags |= _LATIN + if "CJK" in desc: + flags |= _CJK + if "HANGUL" in desc: + flags |= _HANGUL + if "KATAKANA" in desc: + flags |= _KATAKANA + if "HIRAGANA" in desc: + flags |= _HIRAGANA + if "THAI" in desc: + flags |= _THAI + if "ARABIC" in desc: + flags |= _ARABIC + if "ISOLATED FORM" in desc: + flags |= _ARABIC_ISOLATED_FORM + + for kw in _ACCENT_KEYWORDS: + if kw in desc: + flags |= _ACCENTUATED + break + + return flags + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_accentuated(character: str) -> bool: + return bool(_character_flags(character) & _ACCENTUATED) + + +@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)) + + +# Pre-built sorted lookup table for O(log n) binary search in unicode_range(). +# Each entry is (range_start, range_end_exclusive, range_name). +_UNICODE_RANGES_SORTED: list[tuple[int, int, str]] = sorted( + (ord_range.start, ord_range.stop, name) + for name, ord_range in UNICODE_RANGES_COMBINED.items() +) +_UNICODE_RANGE_STARTS: list[int] = [e[0] for e in _UNICODE_RANGES_SORTED] + + +@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) + + # Binary search: find the rightmost range whose start <= character_ord + idx = bisect_right(_UNICODE_RANGE_STARTS, character_ord) - 1 + if idx >= 0: + start, stop, name = _UNICODE_RANGES_SORTED[idx] + if character_ord < stop: + return name + + return None + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_latin(character: str) -> bool: + return bool(_character_flags(character) & _LATIN) + + +@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: + return bool(_character_flags(character) & _CJK) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_hiragana(character: str) -> bool: + return bool(_character_flags(character) & _HIRAGANA) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_katakana(character: str) -> bool: + return bool(_character_flags(character) & _KATAKANA) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_hangul(character: str) -> bool: + return bool(_character_flags(character) & _HANGUL) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_thai(character: str) -> bool: + return bool(_character_flags(character) & _THAI) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_arabic(character: str) -> bool: + return bool(_character_flags(character) & _ARABIC) + + +@lru_cache(maxsize=UTF8_MAXIMAL_ALLOCATION) +def is_arabic_isolated_form(character: str) -> bool: + return bool(_character_flags(character) & _ARABIC_ISOLATED_FORM) + + +@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 | bytearray, search_zone: int = 8192 +) -> str | None: + """ + Extract using ASCII-only decoder any specified encoding in the first n-bytes. + """ + if not isinstance(sequence, (bytes, bytearray)): + 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 | bytearray) -> 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(256): + 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 / 256 + + +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 | bytearray, + 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.11/site-packages/charset_normalizer/version.py b/venv/lib/python3.11/site-packages/charset_normalizer/version.py new file mode 100644 index 0000000..a93d367 --- /dev/null +++ b/venv/lib/python3.11/site-packages/charset_normalizer/version.py @@ -0,0 +1,8 @@ +""" +Expose version +""" + +from __future__ import annotations + +__version__ = "3.4.7" +VERSION = __version__.split(".") diff --git a/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/INSTALLER b/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/METADATA b/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/METADATA new file mode 100644 index 0000000..1fb06f0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/METADATA @@ -0,0 +1,84 @@ +Metadata-Version: 2.4 +Name: click +Version: 8.4.2 +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.11/site-packages/click-8.4.2.dist-info/RECORD b/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/RECORD new file mode 100644 index 0000000..63fd9fd --- /dev/null +++ b/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/RECORD @@ -0,0 +1,40 @@ +click-8.4.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +click-8.4.2.dist-info/METADATA,sha256=GUyd2B1Wf5CB8CbH5AEGD7r6e8FHyOClizZotApkwDE,2621 +click-8.4.2.dist-info/RECORD,, +click-8.4.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +click-8.4.2.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 +click/__init__.py,sha256=FId2fXCSJB3yeWD-e2uON-mBhFa2Yc9MvXGmHu8OXG0,4634 +click/__pycache__/__init__.cpython-311.pyc,, +click/__pycache__/_compat.cpython-311.pyc,, +click/__pycache__/_termui_impl.cpython-311.pyc,, +click/__pycache__/_textwrap.cpython-311.pyc,, +click/__pycache__/_utils.cpython-311.pyc,, +click/__pycache__/_winconsole.cpython-311.pyc,, +click/__pycache__/core.cpython-311.pyc,, +click/__pycache__/decorators.cpython-311.pyc,, +click/__pycache__/exceptions.cpython-311.pyc,, +click/__pycache__/formatting.cpython-311.pyc,, +click/__pycache__/globals.cpython-311.pyc,, +click/__pycache__/parser.cpython-311.pyc,, +click/__pycache__/shell_completion.cpython-311.pyc,, +click/__pycache__/termui.cpython-311.pyc,, +click/__pycache__/testing.cpython-311.pyc,, +click/__pycache__/types.cpython-311.pyc,, +click/__pycache__/utils.cpython-311.pyc,, +click/_compat.py,sha256=gPNtXQ9q-G6Qil2b-MC5CsHsGGcQ4u6YSWy9_tlmuhc,18879 +click/_termui_impl.py,sha256=CGdg24AeXijeGSzbu0Z7x3c4aaahVFjVBpEbbjhQ5K4,31730 +click/_textwrap.py,sha256=7Z0N7Vmn-66TNSTUwp6OXJbcUXRmYET9h9c2ucD8oQQ,6270 +click/_utils.py,sha256=eCZCtwJtsYD5QYkkNWJ8MY_8ABIjy8MczgMMyVY32rQ,996 +click/_winconsole.py,sha256=KSxfNbMlYRa6GOJuCLgsg2Pb3dVkgJNPqLJPae-Pa10,8543 +click/core.py,sha256=rZz76ihNTFV4Y2sxp3H-m93GxL2acD5Pqs0IobEvmuk,140616 +click/decorators.py,sha256=9e1Ndu4jhGAcP6RGdNPAwAWtuP9hEs4ETp1u3lKmH1o,19709 +click/exceptions.py,sha256=HvSY34G4auj_bYRR8-T8CU8Jwq_1-OcsRU4ezfozeEk,11862 +click/formatting.py,sha256=8SW2KGkvjfz9Q1NbeojMHuZBN0cfnQJDs4mqDP6oXms,10444 +click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923 +click/parser.py,sha256=oJ-fU_3mvxugIuNtHaCATZ56lgEmHRggjJiSqEgYrjA,19052 +click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +click/shell_completion.py,sha256=5tGGY5pV3mAZ17xT23OnuKrWqzEyyLVtrJ30npUxjkU,22618 +click/termui.py,sha256=Vn9ehmrQl92z2_6R4bVZOsHUI6j8LrT8u0RzNZUpCvY,33213 +click/testing.py,sha256=S9I-pspAlJH3RvZJWDQoJXb-M0nrAEJzXcUzrVXsT34,26458 +click/types.py,sha256=9G4DB-nBj-omA_XWsYwbQ3H9BkpH82wJj-kxIPScKmA,44788 +click/utils.py,sha256=XwrDxOzU__rnHn-rvJmJcD7ecbypUKMeDJQRjN2F-OA,20942 diff --git a/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/WHEEL b/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/venv/lib/python3.11/site-packages/click-8.4.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.11/site-packages/click-8.4.2.dist-info/licenses/LICENSE.txt b/venv/lib/python3.11/site-packages/click-8.4.2.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..d12a849 --- /dev/null +++ b/venv/lib/python3.11/site-packages/click-8.4.2.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.11/site-packages/click/__init__.py b/venv/lib/python3.11/site-packages/click/__init__.py new file mode 100644 index 0000000..64be7e0 --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/__init__.py @@ -0,0 +1,126 @@ +""" +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 .core import ParameterSource as ParameterSource +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 NoSuchCommand as NoSuchCommand +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 get_pager_file as get_pager_file +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.11/site-packages/click/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..ec3cad4 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/_compat.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/_compat.cpython-311.pyc new file mode 100644 index 0000000..72d1fe4 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/_compat.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/_termui_impl.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/_termui_impl.cpython-311.pyc new file mode 100644 index 0000000..72ff775 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/_termui_impl.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/_textwrap.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/_textwrap.cpython-311.pyc new file mode 100644 index 0000000..5bc9a30 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/_textwrap.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/_utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/_utils.cpython-311.pyc new file mode 100644 index 0000000..12607a4 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/_utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/_winconsole.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/_winconsole.cpython-311.pyc new file mode 100644 index 0000000..04b561b Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/_winconsole.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/core.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/core.cpython-311.pyc new file mode 100644 index 0000000..77e9e82 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/core.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/decorators.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/decorators.cpython-311.pyc new file mode 100644 index 0000000..bee6a15 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/decorators.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/exceptions.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/exceptions.cpython-311.pyc new file mode 100644 index 0000000..0307e3c Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/exceptions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/formatting.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/formatting.cpython-311.pyc new file mode 100644 index 0000000..51e8f79 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/formatting.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/globals.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/globals.cpython-311.pyc new file mode 100644 index 0000000..d4702da Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/globals.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/parser.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/parser.cpython-311.pyc new file mode 100644 index 0000000..f225a1a Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/parser.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/shell_completion.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/shell_completion.cpython-311.pyc new file mode 100644 index 0000000..6f617c6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/shell_completion.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/termui.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/termui.cpython-311.pyc new file mode 100644 index 0000000..7197e40 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/termui.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/testing.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/testing.cpython-311.pyc new file mode 100644 index 0000000..cacafed Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/testing.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/types.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/types.cpython-311.pyc new file mode 100644 index 0000000..d0d3bd5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/types.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/__pycache__/utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/click/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..dd37aca Binary files /dev/null and b/venv/lib/python3.11/site-packages/click/__pycache__/utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/click/_compat.py b/venv/lib/python3.11/site-packages/click/_compat.py new file mode 100644 index 0000000..134c4f3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/_compat.py @@ -0,0 +1,626 @@ +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 + elif hasattr(stream, "color"): + # ._termui_impl.MaybeStripAnsi handles stripping ansi itself, + # so we don't need to strip it here + return False + 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.11/site-packages/click/_termui_impl.py b/venv/lib/python3.11/site-packages/click/_termui_impl.py new file mode 100644 index 0000000..fadae94 --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/_termui_impl.py @@ -0,0 +1,945 @@ +""" +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 io +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 strip_ansi +from ._compat import term_len +from ._compat import WIN +from .exceptions import ClickException +from .utils import echo +from .utils import KeepOpenFile + +V = t.TypeVar("V") + + +class _BufferedTextPagerStream(t.Protocol): + buffer: t.BinaryIO + + +def _has_binary_buffer( + stream: t.BinaryIO | t.TextIO, +) -> t.TypeGuard[_BufferedTextPagerStream]: + # TextIO is wider than TextIOWrapper; text-only streams such as StringIO + # are valid TextIO values but do not expose a binary buffer to wrap. + return getattr(stream, "buffer", None) is not None + + +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 "{d}{day_label} {h:02}:{m:02}:{s:02}".format( + d=t, + day_label=_("d"), + h=hours, + m=minutes, + s=seconds, + ) + 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() + + +class MaybeStripAnsi(io.TextIOWrapper): + def __init__(self, stream: t.IO[bytes], *, color: bool, **kwargs: t.Any): + super().__init__(stream, **kwargs) + self.color = color + + def write(self, text: str) -> int: + if not self.color: + text = strip_ansi(text) + return super().write(text) + + +def _pager_contextmanager( + color: bool | None = None, +) -> t.ContextManager[tuple[t.BinaryIO | t.TextIO, str, bool]]: + """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, color) + + # Split using POSIX mode (the default) so that quote characters are + # stripped from tokens and quoted Windows paths are preserved. + # Non-POSIX mode retains quotes in tokens, and wrapping tokens + # with shlex.quote re-introduces quoting issues on Windows. + pager_cmd_parts = shlex.split(os.environ.get("PAGER", "")) + if pager_cmd_parts: + if WIN: + return _tempfilepager(pager_cmd_parts, color) + return _pipepager(pager_cmd_parts, color) + + if os.environ.get("TERM") in ("dumb", "emacs"): + return _nullpager(stdout, color) + if WIN or sys.platform.startswith("os2"): + return _tempfilepager(["more"], color) + return _pipepager(["less"], color) + + +@contextlib.contextmanager +def get_pager_file(color: bool | None = None) -> t.Generator[t.TextIO, None, None]: + """Context manager. + + Yields a writable file-like object which can be used as an output pager. + + .. versionadded:: 8.4.0 + + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + with _pager_contextmanager(color=color) as (stream, encoding, color): + # Split streams by capabilities rather than the abstract TextIO / + # BinaryIO annotations: buffered text streams can be unwrapped to bytes, + # while other streams are yielded as-is. + wrapper: MaybeStripAnsi | None = None + if _has_binary_buffer(stream): + # Text stream backed by a binary buffer. + wrapper = MaybeStripAnsi(stream.buffer, color=color, encoding=encoding) + stream = wrapper + try: + # Narrow the BinaryIO | TextIO union that _pager_contextmanager + # yields; the caller writes text to the pager. + yield t.cast(t.TextIO, stream) + finally: + try: + stream.flush() + finally: + # Hand the binary buffer back to the pager that produced it + # rather than letting this TextIOWrapper close it on garbage + # collection. The pager owns the buffer's lifecycle: subprocess + # pipes and temp files are closed by their own helpers, while a + # borrowed stdout must stay open for the caller. detach() runs + # even if flush() raised, so the buffer is never closed here. + if wrapper is not None: + wrapper.detach() + + +@contextlib.contextmanager +def _pipepager( + cmd_parts: list[str], color: bool | None = None +) -> t.Iterator[tuple[t.BinaryIO | t.TextIO, str, bool]]: + """Page through text by feeding it to another program. + + Invokes the pager via :class:`subprocess.Popen` with an ``argv`` list + produced by :func:`shlex.split`. The command is resolved to an absolute + path with :func:`shutil.which` as recommended by the + :mod:`subprocess` docs for Windows compatibility. + + Invoking a pager through this might support colors: if piping to + ``less`` and the user hasn't decided on colors, ``LESS=-R`` is set + automatically. + """ + # Split the command into the invoked CLI and its parameters. + if not cmd_parts: + # No usable pager: fall back to stdout through _nullpager so it gets the + # same borrowed-stream handling and the caller's stream is not closed. + stdout = _default_text_stdout() or StringIO() + with _nullpager(stdout, color) as rv: + yield rv + return + + import shutil + + cmd = cmd_parts[0] + cmd_params = cmd_parts[1:] + + cmd_filepath = shutil.which(cmd) + if not cmd_filepath: + # No usable pager: fall back to stdout through _nullpager so it gets the + # same borrowed-stream handling and the caller's stream is not closed. + stdout = _default_text_stdout() or StringIO() + with _nullpager(stdout, color) as rv: + yield rv + return + + # 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 + + if color is None: + color = False + + c = subprocess.Popen( + [str(cmd_path)] + cmd_params, + shell=False, + stdin=subprocess.PIPE, + env=env, + errors="replace", + text=True, + ) + stdin = t.cast(t.BinaryIO, c.stdin) + encoding = get_best_encoding(stdin) + try: + yield stdin, encoding, color + 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: + 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 + + +@contextlib.contextmanager +def _tempfilepager( + cmd_parts: list[str], color: bool | None = None +) -> t.Iterator[tuple[t.BinaryIO | t.TextIO, str, bool]]: + """Page through text by invoking a program on a temporary file. + + Used as the primary pager strategy on Windows (where piping to + ``more`` adds spurious ``\\r\\n``), and as a fallback on other + platforms. The command is resolved to an absolute path with + :func:`shutil.which`. + """ + # Split the command into the invoked CLI and its parameters. + if not cmd_parts: + # No usable pager: fall back to stdout through _nullpager so it gets the + # same borrowed-stream handling and the caller's stream is not closed. + stdout = _default_text_stdout() or StringIO() + with _nullpager(stdout, color) as rv: + yield rv + return + + import shutil + import subprocess + + cmd = cmd_parts[0] + + cmd_filepath = shutil.which(cmd) + if not cmd_filepath: + # No usable pager: fall back to stdout through _nullpager so it gets the + # same borrowed-stream handling and the caller's stream is not closed. + stdout = _default_text_stdout() or StringIO() + with _nullpager(stdout, color) as rv: + yield rv + return + + # 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 tempfile + + encoding = get_best_encoding(sys.stdout) + if color is None: + color = False + # On Windows, NamedTemporaryFile cannot be opened by another process + # while Python still has it open, so we use delete=False and clean up manually + # rather than using a contextmanager here. + f = tempfile.NamedTemporaryFile(mode="wb", delete=False) + try: + yield t.cast(t.BinaryIO, f), encoding, color + f.flush() + f.close() + subprocess.call([str(cmd_path), f.name]) + finally: + os.unlink(f.name) + + +@contextlib.contextmanager +def _nullpager( + stream: t.TextIO, color: bool | None = None +) -> t.Iterator[tuple[t.TextIO, str, bool]]: + """Simply print unformatted text. This is the ultimate fallback. Don't close the + output stream in this case, since it's coming from elsewhere rather than our + internal helpers. + + The stream is wrapped in :class:`~click.utils.KeepOpenFile` so that, as a + borrowed stream, it is not closed by a ``with`` block. The wrapper that + :func:`get_pager_file` builds around it is detached rather than closed. + """ + encoding = get_best_encoding(stream) + + if color is None: + color = False + + yield KeepOpenFile(stream), encoding, color # type: ignore[misc] + + +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: + """Open files in the user's editor.""" + import shlex + import subprocess + + editor = self.get_editor() + environ: dict[str, str] | None = None + + if self.env: + environ = os.environ.copy() + environ.update(self.env) + + try: + # Split in POSIX mode (the default) for the same reasons as + # in pager(): strips quotes from tokens and preserves quoted + # Windows paths. + c = subprocess.Popen( + args=shlex.split(editor) + list(filenames), + env=environ, + ) + 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", "/select,", url] + try: + return subprocess.call(args) + except OSError: + return 127 + else: + try: + os.startfile(url) # type: ignore[attr-defined] + except OSError: + return 127 + return 0 + 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() + + +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.11/site-packages/click/_textwrap.py b/venv/lib/python3.11/site-packages/click/_textwrap.py new file mode 100644 index 0000000..82840f2 --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/_textwrap.py @@ -0,0 +1,188 @@ +from __future__ import annotations + +import collections.abc as cabc +import textwrap +from contextlib import contextmanager + +from ._compat import _ansi_re +from ._compat import term_len + + +def _truncate_visible(text: str, n: int) -> str: + """Return the longest prefix of ``text`` containing at most ``n`` visible + characters. + + ANSI escape sequences inside the prefix are kept intact and do not count + toward the visible width. A cut is never placed inside an escape sequence. + """ + if n <= 0: + return "" + + visible = 0 + i = 0 + cut = 0 + end = len(text) + while i < end: + m = _ansi_re.match(text, i) + if m is not None: + i = m.end() + continue + visible += 1 + i += 1 + cut = i + if visible >= n: + break + return text[:cut] + + +class TextWrapper(textwrap.TextWrapper): + """``textwrap.TextWrapper`` variant that measures widths by visible + character count. + + ANSI escape sequences embedded in chunks, indents, or the placeholder are + excluded from the width budget. Without this, styled help text (a styled + ``Usage:`` prefix, a colorized option name, ...) would be wrapped earlier + than its visible length warrants and tokens would split mid-word. + """ + + 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 = _truncate_visible(last, space_left) + res = last[len(cut) :] + cur_line.append(cut) + reversed_chunks[-1] = res + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + def _wrap_chunks(self, chunks: list[str]) -> list[str]: + """Wrap chunks counting widths in visible characters. + + Mirrors the algorithm of :meth:`textwrap.TextWrapper._wrap_chunks` + with every width measurement routed through + :func:`click._compat.term_len` instead of :func:`len`, so ANSI escape + bytes in chunks, indents, or the placeholder do not inflate the count. + + .. seealso:: + :class:`textwrap.TextWrapper` in the Python standard library documentation: + https://docs.python.org/3/library/textwrap.html#textwrap.TextWrapper + + Reference implementation in CPython: + https://github.com/python/cpython/blob/main/Lib/textwrap.py + """ + lines: list[str] = [] + if self.width <= 0: + raise ValueError(f"invalid width {self.width!r} (must be > 0)") + if self.max_lines is not None: + if self.max_lines > 1: + indent = self.subsequent_indent + else: + indent = self.initial_indent + if term_len(indent) + term_len(self.placeholder.lstrip()) > self.width: + raise ValueError("placeholder too large for max width") + + chunks.reverse() + + while chunks: + cur_line: list[str] = [] + cur_len = 0 + + if lines: + indent = self.subsequent_indent + else: + indent = self.initial_indent + + width = self.width - term_len(indent) + + if self.drop_whitespace and chunks[-1].strip() == "" and lines: + del chunks[-1] + + while chunks: + n = term_len(chunks[-1]) + + if cur_len + n <= width: + cur_line.append(chunks.pop()) + cur_len += n + + else: + break + + if chunks and term_len(chunks[-1]) > width: + self._handle_long_word(chunks, cur_line, cur_len, width) + cur_len = sum(map(term_len, cur_line)) + + if self.drop_whitespace and cur_line and cur_line[-1].strip() == "": + cur_len -= term_len(cur_line[-1]) + del cur_line[-1] + + if cur_line: + if ( + self.max_lines is None + or len(lines) + 1 < self.max_lines + or ( + not chunks + or self.drop_whitespace + and len(chunks) == 1 + and not chunks[0].strip() + ) + and cur_len <= width + ): + lines.append(indent + "".join(cur_line)) + else: + while cur_line: + if ( + cur_line[-1].strip() + and cur_len + term_len(self.placeholder) <= width + ): + cur_line.append(self.placeholder) + lines.append(indent + "".join(cur_line)) + break + cur_len -= term_len(cur_line[-1]) + del cur_line[-1] + else: + if lines: + prev_line = lines[-1].rstrip() + if ( + term_len(prev_line) + term_len(self.placeholder) + <= self.width + ): + lines[-1] = prev_line + self.placeholder + break + lines.append(indent + self.placeholder.lstrip()) + break + + return lines + + @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.11/site-packages/click/_utils.py b/venv/lib/python3.11/site-packages/click/_utils.py new file mode 100644 index 0000000..05ee2e9 --- /dev/null +++ b/venv/lib/python3.11/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: t.Literal[Sentinel.UNSET] = Sentinel.UNSET +"""Sentinel used to indicate that a value is not set.""" + +FLAG_NEEDS_VALUE: t.Literal[Sentinel.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.TypeAlias = t.Literal[Sentinel.UNSET] +"""Type hint for the :data:`UNSET` sentinel value.""" + +T_FLAG_NEEDS_VALUE: t.TypeAlias = t.Literal[Sentinel.FLAG_NEEDS_VALUE] +"""Type hint for the :data:`FLAG_NEEDS_VALUE` sentinel value.""" diff --git a/venv/lib/python3.11/site-packages/click/_winconsole.py b/venv/lib/python3.11/site-packages/click/_winconsole.py new file mode 100644 index 0000000..d25178d --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/_winconsole.py @@ -0,0 +1,297 @@ +# 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 gettext import gettext as _ + +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(_("Windows error: {error}").format(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 _("Windows error: {error}").format(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.11/site-packages/click/core.py b/venv/lib/python3.11/site-packages/click/core.py new file mode 100644 index 0000000..d7ecbef --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/core.py @@ -0,0 +1,3639 @@ +from __future__ import annotations + +import collections.abc as cabc +import enum +import errno +import inspect +import os +import sys +import typing as t +from abc import ABC +from abc import abstractmethod +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 NoSuchCommand +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 typing_extensions import Self + + 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 _format_deprecated_label(deprecated: bool | str) -> str: + """Return the parenthesized deprecation label shown in help text.""" + label = _("deprecated").upper() + if isinstance(deprecated, str): + return f"({label}: {deprecated})" + return f"({label})" + + +def _format_deprecated_suffix(deprecated: bool | str) -> str: + """Return the trailing reason for a ``DeprecationWarning`` message, + prefixed with a space, or an empty string when no reason was given. + """ + if isinstance(deprecated, str): + return f" {deprecated}" + return "" + + +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.Generator[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.IntEnum): + """This is an :class:`~enum.IntEnum` 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. + + Members are ordered from most explicit to least explicit source. + This allows comparison to check if a value was explicitly provided: + + .. code-block:: python + + source = ctx.get_parameter_source("port") + if source < click.ParameterSource.DEFAULT_MAP: + ... # value was explicitly set + + .. versionchanged:: 8.3.3 + Use :class:`~enum.IntEnum` and reorder members from most to + least explicit. Supports comparison operators. + + .. versionchanged:: 8.0 + Use :class:`~enum.Enum` and drop the ``validate`` method. + + .. versionchanged:: 8.0 + Added the ``PROMPT`` value. + """ + + PROMPT = enum.auto() + """Used a prompt to confirm a default or provide a 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_MAP = enum.auto() + """Used a default provided by :attr:`Context.default_map`.""" + DEFAULT = enum.auto() + """Used the default specified by the parameter.""" + + +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 that 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 + + parent: Context | None + command: Command + info_name: str | None + params: dict[str, t.Any] + args: list[str] + _protected_args: list[str] + _opt_prefixes: set[str] + obj: t.Any + _meta: dict[str, t.Any] + default_map: cabc.MutableMapping[str, t.Any] | None + invoked_subcommand: str | None + terminal_width: int | None + max_content_width: int | None + allow_extra_args: bool + allow_interspersed_args: bool + ignore_unknown_options: bool + help_option_names: list[str] + token_normalize_func: t.Callable[[str], str] | None + resilient_parsing: bool + auto_envvar_prefix: str | None + color: bool | None + show_default: bool | None + _close_callbacks: list[t.Callable[[], t.Any]] + _depth: int + _parameter_source: dict[str, ParameterSource] + _param_default_explicit: dict[str, bool] + _exit_stack: ExitStack + + 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 = {} + #: the leftover arguments. + self.args = [] + #: 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 = [] + #: the collected prefixes of the command's options. + self._opt_prefixes = 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 = obj + self._meta = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = 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 = show_default + + self._close_callbacks = [] + self._depth = 0 + self._parameter_source = {} + # Tracks whether the option that currently owns each parameter slot in + # :attr:`params` had its ``default`` set explicitly by the user. Used + # to tie-break feature-switch groups where multiple options share a + # parameter name and both fall back to their default value. + # Refs: https://github.com/pallets/click/issues/3403 + self._param_default_explicit = {} + 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) -> Self: + 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.Generator[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 + + def _default_map_has(self, name: str | None) -> bool: + """Check if :attr:`default_map` contains a real value for ``name``. + + Returns ``False`` when the key is absent, the map is ``None``, + ``name`` is ``None``, or the stored value is the internal + :data:`UNSET` sentinel. + """ + return ( + name is not None + and self.default_map is not None + and name in self.default_map + and self.default_map[name] is not UNSET + ) + + @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 not self._default_map_has(name): + return None + + # Assert to make the type checker happy. + assert self.default_map is not None + value = self.default_map[name] + + if call and callable(value): + return value() + + return value + + 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(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 + + name: str | None + context_settings: cabc.MutableMapping[str, t.Any] + callback: t.Callable[..., t.Any] | None + params: list[Parameter] + help: str | None + epilog: str | None + options_metavar: str | None + short_help: str | None + add_help_option: bool + _help_option: Option | None + no_args_is_help: bool + hidden: bool + deprecated: bool | str + + 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 = 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 = 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 ordering 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: + text = f"{_(text)} {_format_deprecated_label(self.deprecated)}" + + 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: + label = _format_deprecated_label(self.deprecated) + text = f"{_(text)} {label}" if text else label + + 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: + message = _( + "DeprecationWarning: The command {name!r} is deprecated.{extra_message}" + ).format( + name=self.name, + extra_message=_format_deprecated_suffix(self.deprecated), + ) + 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) + 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] + + commands: cabc.MutableMapping[str, Command] + invoke_without_command: bool + subcommand_metavar: str + chain: bool + _result_callback: t.Callable[..., t.Any] | None + + 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 = 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: + # When the group can run without a subcommand, the leading command + # token is optional, so wrap it in brackets to reflect that. + if chain: + if invoke_without_command: + subcommand_metavar = "[COMMAND1] [ARGS]... [COMMAND2 [ARGS]...]..." + else: + subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." + elif invoke_without_command: + subcommand_metavar = "[COMMAND] [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]) + + # 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) + raise NoSuchCommand(cmd_name, possibilities=self.commands, ctx=ctx) + 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. + """ + + sources: list[Group] + + 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 = 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: cabc.Iterable[V]) -> cabc.Iterator[V]: + """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(ABC): + 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" + + name: str + opts: list[str] + secondary_opts: list[str] + # `Parameter.type` is annotated in `__init__` to avoid confusing mypy + required: bool + callback: t.Callable[[Context, Parameter, t.Any], t.Any] | None + nargs: int + multiple: bool + expose_value: bool + default: t.Any | t.Callable[[], t.Any] | None + _default_explicit: bool + is_eager: bool + metavar: str | None + envvar: str | cabc.Sequence[str] | None + _custom_shell_complete: ( + t.Callable[[Context, Parameter, str], list[CompletionItem] | list[str]] | None + ) + deprecated: bool | str + + def __init__( + self, + param_decls: cabc.Sequence[str] | None = None, + type: types.ParamType[t.Any] | 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, self.opts, self.secondary_opts = self._parse_decls( + param_decls or (), expose_value + ) + self.type: types.ParamType[t.Any] = 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 = default + # Whether the user passed ``default`` explicitly to the constructor. + # Captured before any auto-derived default (like ``False`` for boolean + # flags in :class:`Option`) replaces the :data:`UNSET` sentinel, so it + # remains ``False`` when the default was inferred rather than chosen. + # Refs: https://github.com/pallets/click/issues/3403 + self._default_explicit = default is not UNSET + 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}>" + + @abstractmethod + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str, list[str], list[str]]: ... + + @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 + + 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) + + if value is None and not ctx._default_map_has(self.name): + value = self.default + + if call and callable(value): + value = value() + + return value + + @abstractmethod + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: ... + + 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) + # 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) + if default_map_value is not None or ctx._default_map_has(self.name): + value = default_map_value + source = ParameterSource.DEFAULT_MAP + + # A string from default_map must be split for multi-value + # parameters, matching value_from_envvar behavior. + if isinstance(value, str) and self.nargs != 1: + value = self.type.split_envvar_value(value) + + 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: + """ + # Capture the slot's existing state before we mutate + # ``_parameter_source`` so the write decision below can compare our + # incoming source against the source of the option that already wrote + # the slot (if any). + existing_value = ctx.params.get(self.name, UNSET) + existing_source = ctx.get_parameter_source(self.name) + existing_default_explicit = ctx._param_default_explicit.get(self.name, False) + + with augment_usage_errors(ctx, param=self): + value, source = self.consume_value(ctx, opts) + + # Record the source before processing so eager callbacks and type + # conversion can inspect it. Restored after arbitration if this + # option loses a feature-switch group. + ctx.set_parameter_source(self.name, source) + + # Display a deprecation warning if necessary. + if ( + self.deprecated + and value is not UNSET + and source < ParameterSource.DEFAULT_MAP + ): + 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=_format_deprecated_suffix(self.deprecated), + ) + 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 + + # Arbitrate the slot when several parameters target the same variable + # name (feature-switch groups). See: https://github.com/pallets/click/issues/3403 + slot_empty = existing_value is UNSET + more_explicit = existing_source is not None and source < existing_source + same_source = existing_source is not None and source == existing_source + auto_would_downgrade_explicit = ( + same_source + and source == ParameterSource.DEFAULT + and existing_default_explicit + and not self._default_explicit + ) + is_winner = ( + slot_empty + or more_explicit + or (same_source and not auto_would_downgrade_explicit) + ) + + if is_winner: + if self.expose_value: + ctx.params[self.name] = value + ctx._param_default_explicit[self.name] = self._default_explicit + elif existing_source is not None: + # Lost arbitration; restore the winning option's source. + ctx.set_parameter_source(self.name, existing_source) + # else: ctx.params[self.name] was populated by code that bypassed + # handle_parse_result (from another option's callback for example). Keep + # the provisional source recorded before process_value so downstream + # lookups don't return ``None``. + + return value, args + + def get_help_record(self, ctx: Context) -> tuple[str, str] | None: + return None + + def get_usage_pieces(self, ctx: Context) -> list[str]: + return [] + + def get_error_hint(self, ctx: Context | None) -> str: + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + + .. versionchanged:: 8.4.0 + ``ctx`` can be ``None``. + """ + 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[t.Any].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.4.0 + Non-basic ``flag_value`` types (not ``str``, ``int``, ``float``, or + ``bool``) are passed through unchanged instead of being stringified. + Previously, ``type=click.UNPROCESSED`` was required to preserve them. + + .. 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, for basic Python + types (``str``, ``int``, ``float``, ``bool``). + """ + + param_type_name = "option" + + prompt: str | None + confirmation_prompt: bool | str + prompt_required: bool + hide_input: bool + hidden: bool + + _flag_needs_value: bool + is_flag: bool + is_bool_flag: bool + flag_value: t.Any + + count: bool + allow_from_autoenv: bool + help: str | None + show_default: bool | str | None + show_choices: bool + show_envvar: bool + + 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] | 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 not self.name: + raise TypeError("'name' is required with 'prompt=True'.") + + prompt_text = self.name.replace("_", " ").capitalize() + elif prompt is False: + prompt_text = None + else: + prompt_text = prompt + + if deprecated: + label = _format_deprecated_label(deprecated) + help = f"{help} {label}" if help else label + + 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 to determine whether or not it needs + # value, we need to check if `flag_value` or `default` was set. Either one is + # sufficient. + # Ref: https://github.com/pallets/click/issues/3084 + elif is_flag is False and not self._flag_needs_value: + self._flag_needs_value = flag_value is not UNSET or 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[t.Any] = 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: + guessed = types.convert_type(None, flag_value) + if ( + isinstance(guessed, types.StringParamType) + and not isinstance(flag_value, str) + and flag_value is not None + ): + # The flag_value type couldn't be auto-detected + # (not str, int, float, or bool). Since flag_value + # is a programmer-provided Python object, not CLI + # input, pass it through unchanged instead of + # stringifying it. + self.type = types.UNPROCESSED + else: + self.type = guessed + + self.is_flag = bool(is_flag) + self.is_bool_flag = self.is_flag and isinstance(self.type, types.BoolParamType) + self.flag_value = 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 + + # The alignment of default to the flag_value is resolved lazily in + # get_default() to prevent callable flag_values (like classes) from + # being instantiated. Refs: + # https://github.com/pallets/click/issues/3121 + # https://github.com/pallets/click/issues/3024#issuecomment-3146199461 + # https://github.com/pallets/click/pull/3030/commits/06847da + + # 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_default( + self, ctx: Context, call: bool = True + ) -> t.Any | t.Callable[[], t.Any] | None: + """Return the default value for this option. + + For non-boolean flag options, ``default=True`` is treated as a sentinel + meaning "activate this flag by default" and is resolved to + :attr:`flag_value`. For example, with ``--upper/--lower`` feature + switches where ``flag_value="upper"`` and ``default=True``, the default + resolves to ``"upper"``. + + .. caution:: + This substitution only applies to non-boolean flags + (:attr:`is_bool_flag` is ``False``). For boolean flags, ``True`` is + a legitimate Python value and ``default=True`` is returned as-is. + + .. versionchanged:: 8.3.3 + ``default=True`` is no longer substituted with ``flag_value`` for + boolean flags, fixing negative boolean flags like + ``flag_value=False, default=True``. + """ + value = super().get_default(ctx, call=False) + + # Resolve default=True to flag_value lazily (here instead of + # __init__) to prevent callable flag_values (like classes) from + # being instantiated by the callable check below. + if value is True and self.is_flag and not self.is_bool_flag: + value = self.flag_value + elif call and callable(value): + value = value() + + return value + + def get_error_hint(self, ctx: Context | None) -> 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, 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(_("Name '{name}' defined twice").format(name=name)) + 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( + _( + "Boolean option {decl!r} cannot use the" + " same flag for true/false." + ).format(decl=decl) + ) + 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 "", opts, secondary_opts + raise TypeError( + _( + "Could not determine name for option with declarations {decls!r}" + ).format(decls=decls) + ) + + if not opts and not secondary_opts: + raise TypeError( + _( + "No options defined but a name was passed ({name})." + " Did you mean to declare an argument instead? Did" + " you mean to pass '--{name}'?" + ).format(name=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 + ): + 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 isinstance(default_value, str) and 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 given, provide this to `prompt` as well, + # otherwise we use `prompt`'s default behavior + prompt_kwargs: t.Any = {} + if self.show_default is not None: + 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: + 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 < 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 isinstance(value, cabc.Iterable) + and source < 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 >= 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() + + 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() + # Types like ``Choice`` and ``DateTime`` already surround their metavar + # with square brackets to enumerate the allowed values. Reuse those + # outer brackets as the optional-argument indicator instead of wrapping + # the metavar in a second pair, which would produce ``[[a|b|c]]``. + already_bracketed = var.startswith("[") and var.endswith("]") + if self.deprecated: + var += "!" + if not self.required and not already_bracketed: + var = f"[{var}]" + if self.nargs != 1: + var += "..." + return var + + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str, list[str], list[str]]: + if not decls: + if not expose_value: + return "", [], [] + 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" + " {length}: {decls}." + ).format(length=len(decls), 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 | None) -> str: + if ctx is not None: + return f"'{self.make_metavar(ctx)}'" + return f"'{self.human_readable_name}'" + + 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.11/site-packages/click/decorators.py b/venv/lib/python3.11/site-packages/click/decorators.py new file mode 100644 index 0000000..db6a45e --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/decorators.py @@ -0,0 +1,575 @@ +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. If the detected (or given) name does + not match an installed distribution, Click resolves it as an import + (top-level module) name via + :func:`importlib.metadata.packages_distributions`, so e.g. ``PIL`` + resolves to the ``Pillow`` distribution. + + :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=``. + + .. versionchanged:: 8.4.2 + When ``package_name`` does not match an installed distribution, + Click now resolves it as an import (top-level module). + """ + 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 + nonlocal package_name + + 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: + # The given name didn't match an installed distribution. + # Try resolving it as an import (top-level module) name, + # e.g. ``PIL`` is provided by the ``Pillow`` distribution. + distributions = importlib.metadata.packages_distributions().get( + package_name, [] + ) + if len(distributions) == 1: + package_name = distributions[0] + version = importlib.metadata.version(package_name) + elif len(distributions) > 1: + raise RuntimeError( + f"{package_name!r} maps to multiple installed" + f" distributions ({', '.join(distributions)})." + " Pass 'package_name' to disambiguate." + ) from None + else: + 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.11/site-packages/click/exceptions.py b/venv/lib/python3.11/site-packages/click/exceptions.py new file mode 100644 index 0000000..6272c38 --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/exceptions.py @@ -0,0 +1,378 @@ +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 + + +def _format_possibilities(possibilities: list[str]) -> str: + possibility_str = ", ".join(repr(p) for p in sorted(possibilities)) + return ngettext( + "Did you mean {possibility}?", + "(Did you mean one of: {possibilities}?)", + len(possibilities), + ).format(possibility=possibility_str, possibilities=possibility_str) + + +class ClickException(Exception): + """An exception that Click can handle and show to the user.""" + + #: The exit code for this exception. + exit_code: t.ClassVar[int] = 1 + + show_color: t.Final[bool | None] + message: t.Final[str] + + 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 = 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: t.ClassVar[int] = 2 + + ctx: Context | None + cmd: t.Final[Command | None] + + def __init__(self, message: str, ctx: Context | None = None) -> None: + super().__init__(message) + self.ctx = ctx + self.cmd = 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 + ): + help_names = self.ctx.command.get_help_option_names(self.ctx) + # Pick the longest name (like ``--help`` over ``-h``) for + # readability in error messages. + hint = _("Try '{command} {option}' for help.").format( + command=self.ctx.command_path, + option=max(help_names, key=len), + ) + 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. + """ + + param: Parameter | None + param_hint: cabc.Sequence[str] | str | None + + 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) + 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'``. + """ + + param_type: t.Final[str | None] + + 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) + 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 + """ + + option_name: t.Final[str] + possibilities: t.Final[list[str] | None] + + def __init__( + self, + option_name: str, + message: str | None = None, + possibilities: cabc.Iterable[str] | None = None, + ctx: Context | None = None, + ) -> None: + if message is None: + message = _("No such option {name!r}.").format(name=option_name) + + super().__init__(message, ctx) + self.option_name = option_name + + if possibilities: + from difflib import get_close_matches + + possibilities_ = get_close_matches(option_name, possibilities) + else: + possibilities_ = None + self.possibilities = possibilities_ + + def format_message(self) -> str: + if not self.possibilities: + return self.message + return f"{self.message} {_format_possibilities(self.possibilities)}" + + +class NoSuchCommand(UsageError): + """Raised if Click attempted to handle a command that does not exist. + + .. versionadded:: 8.4.0 + """ + + command_name: t.Final[str] + possibilities: t.Final[list[str] | None] + + def __init__( + self, + command_name: str, + message: str | None = None, + possibilities: cabc.Iterable[str] | None = None, + ctx: Context | None = None, + ) -> None: + if message is None: + message = _("No such command {name!r}.").format(name=command_name) + + super().__init__(message, ctx) + self.command_name = command_name + + if possibilities: + from difflib import get_close_matches + + possibilities_ = get_close_matches(command_name, possibilities) + else: + possibilities_ = None + self.possibilities = possibilities_ + + def format_message(self) -> str: + if not self.possibilities: + return self.message + return f"{self.message} {_format_possibilities(self.possibilities)}" + + +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. + """ + + option_name: t.Final[str] + + 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): + ctx: Context + + def __init__(self, ctx: Context) -> None: + 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.""" + + ui_filename: t.Final[str] + filename: t.Final[str] + + def __init__(self, filename: str, hint: str | None = None) -> None: + if hint is None: + hint = _("unknown error") + + super().__init__(hint) + self.ui_filename = 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",) + + exit_code: t.Final[int] + + def __init__(self, code: int = 0) -> None: + self.exit_code = code diff --git a/venv/lib/python3.11/site-packages/click/formatting.py b/venv/lib/python3.11/site-packages/click/formatting.py new file mode 100644 index 0000000..c4aa2de --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/formatting.py @@ -0,0 +1,320 @@ +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. + + .. versionchanged:: 8.4.0 + Width is measured in visible characters. ANSI escape sequences in + ``text``, ``initial_indent``, or ``subsequent_indent`` no longer + count toward the width budget, so styled input wraps based on what + the user sees instead of raw byte length. + """ + 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. + """ + + indent_increment: int + width: int + current_indent: int + buffer: list[str] + + 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 = 0 + self.buffer = [] + + 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 = "{usage} ".format(usage=_("Usage:")) + + usage_prefix = f"{prefix:>{self.current_indent}}{prog} " + text_width = self.width - self.current_indent + + if not args: + # Without args, the prefix's trailing space and the wrap_text + # call that would normally place args on the line are both + # unnecessary. Emit just the prefix line. + self.write(usage_prefix.rstrip(" ")) + self.write("\n") + return + + 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.Iterable[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.Generator[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.Generator[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.Iterable[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.11/site-packages/click/globals.py b/venv/lib/python3.11/site-packages/click/globals.py new file mode 100644 index 0000000..a2f9172 --- /dev/null +++ b/venv/lib/python3.11/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.11/site-packages/click/parser.py b/venv/lib/python3.11/site-packages/click/parser.py new file mode 100644 index 0000000..4fcbf7c --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/parser.py @@ -0,0 +1,533 @@ +""" +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 | T_UNSET] | T_UNSET], 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[str]) -> str | T_UNSET: + try: + if spos is None: + return c.popleft() + else: + return c.pop() + except IndexError: + return UNSET + + while nargs_spec: + if spos is None: + nargs = nargs_spec.popleft() + else: + nargs = nargs_spec.pop() + + if nargs == 1: + rv.append(_fetch(args)) + elif nargs > 1: + x: list[str | T_UNSET] = [_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( + _("Invalid start character for option ({option})").format( + 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 | T_UNSET] | T_UNSET, + state: _ParsingState, + ) -> None: + if self.nargs > 1: + assert isinstance(value, cabc.Sequence) + holes = sum(x is UNSET for x in value) + 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: + raise NoSuchOption(opt, possibilities=self._long_opt, 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 large. 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_UNSET | T_FLAG_NEEDS_VALUE: + nargs = option.nargs + + value: str | cabc.Sequence[str] | T_UNSET | 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.11/site-packages/click/py.typed b/venv/lib/python3.11/site-packages/click/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/venv/lib/python3.11/site-packages/click/shell_completion.py b/venv/lib/python3.11/site-packages/click/shell_completion.py new file mode 100644 index 0000000..468ee77 --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/shell_completion.py @@ -0,0 +1,705 @@ +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, +) -> t.Literal[0, 1]: + """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) + + # Write bytes, otherwise Windows text stdout translates LF to CRLF and breaks. + if instruction == "source": + echo(comp.source().encode(), nl=False) + return 0 + + if instruction == "complete": + echo(comp.complete().encode()) + return 0 + + return 1 + + +if t.TYPE_CHECKING: + from typing_extensions import TypeVar + + # `Any` is used as default for backwards compatibility (instead of e.g. `str`) + _ValueT_co = TypeVar("_ValueT_co", covariant=True, default=t.Any) +else: + _ValueT_co = t.TypeVar("_ValueT_co", covariant=True) + + +class CompletionItem(t.Generic[_ValueT_co]): + """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: _ValueT_co, + type: str = "plain", + help: str | None = None, + **kwargs: t.Any, + ) -> None: + self.value: _ValueT_co = 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 _SourceVarsDict(t.TypedDict): + complete_func: str + complete_var: str + prog_name: str + + +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. + """ + + cli: Command + ctx_args: cabc.MutableMapping[str, t.Any] + prog_name: str + complete_var: str + + 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) -> _SourceVarsDict: + """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[str]]: + """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]) -> 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: t.ClassVar[str] = "bash" + source_template: t.ClassVar[str] = _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[t.Any]) -> str: + return f"{item.type},{item.value}" + + +class ZshComplete(ShellComplete): + """Shell completion for Zsh.""" + + name: t.ClassVar[str] = "zsh" + source_template: t.ClassVar[str] = _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]) -> 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: t.ClassVar[str] = "fish" + source_template: t.ClassVar[str] = _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]) -> str: + """ + .. versionchanged:: 8.4.2 + Escape newlines and replace tabs with spaces in the help text to + fix completion errors with multi-line help strings. + """ + # According to https://fishshell.com/docs/current/cmds/complete.html + # Command substitutions found in ARGUMENTS should return a newline- + # separated list of arguments, and each argument may optionally have a tab + # character followed by the argument description. + if item.help: + help_ = item.help.replace("\n", "\\n").replace("\t", " ") + return f"{item.type},{item.value}\t{help_}" + + return f"{item.type},{item.value}" + + +_available_shells: t.Final[dict[str, type[ShellComplete]]] = { + "bash": BashComplete, + "fish": FishComplete, + "zsh": ZshComplete, +} + +_ShellCompleteT = t.TypeVar("_ShellCompleteT", bound="ShellComplete") + + +def add_completion_class( + cls: type[_ShellCompleteT], name: str | None = None +) -> type[_ShellCompleteT]: + """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 + + +@t.overload +def get_completion_class(shell: t.Literal["bash"]) -> type[BashComplete]: ... +@t.overload +def get_completion_class(shell: t.Literal["fish"]) -> type[FishComplete]: ... +@t.overload +def get_completion_class(shell: t.Literal["zsh"]) -> type[ZshComplete]: ... +@t.overload +def get_completion_class(shell: str) -> type[ShellComplete] | None: ... +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 + + 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.11/site-packages/click/termui.py b/venv/lib/python3.11/site-packages/click/termui.py new file mode 100644 index 0000000..9bc88db --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/termui.py @@ -0,0 +1,945 @@ +from __future__ import annotations + +import collections.abc as cabc +import inspect +import io +import itertools +import re +import sys +import typing as t +from contextlib import AbstractContextManager +from contextlib import redirect_stdout +from gettext import gettext as _ + +from ._compat import isatty +from ._compat import strip_ansi +from ._compat import WIN +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" + + +_HIDDEN_INPUT_MASK = "'***'" + + +def _mask_hidden_input(message: str, value: str) -> str: + """Replace occurrences of ``value`` in ``message`` with a fixed mask. + + Both ``repr(value)`` (the form built-in :class:`ParamType` errors use + via ``{value!r}``) and the raw value are masked. The raw-value pass + uses word-boundary lookarounds so a substring like ``"1"`` does not + match inside ``"10"``, and ``"ent"`` does not match inside + ``"Authentication"``. The empty string is skipped to avoid matching + at every boundary. + """ + message = message.replace(repr(value), _HIDDEN_INPUT_MASK) + if value: + message = re.sub( + rf"(? str: + import getpass + + return getpass.getpass(prompt) + + +def _readline_prompt(func: t.Callable[[str], str], text: str, err: bool) -> str: + """Call a prompt function, passing the full prompt on non-Windows so + readline can handle line editing and cursor positioning correctly. + + On Windows the prompt is written separately via :func:`echo` for + colorama support, with only the last character passed to *func*. + """ + if WIN: + # 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 func(text[-1:]) + if err: + with redirect_stdout(sys.stderr): + return func(text) + return func(text) + + +def _build_prompt( + text: str, + suffix: str, + show_default: bool | str = False, + default: t.Any | None = None, + show_choices: bool = True, + type: ParamType[t.Any] | 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 isinstance(show_default, str): + default = f"({show_default})" + 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] | t.Any | None = None, + value_proc: t.Callable[[str], t.Any] | None = None, + prompt_suffix: str = ": ", + show_default: bool | str = 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. + If this value is a string, it shows that string + in parentheses instead of the actual value. + :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.3 + ``show_default`` can be a string to show a custom value instead + of the actual default, matching the help text behavior. + + .. 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: + return _readline_prompt(f, text, err) + 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: + message = _mask_hidden_input(e.message, value) if hide_input else e.message + echo(_("Error: {message}").format(message=message), 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: + value = _readline_prompt(visible_prompt_func, prompt, err).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 get_pager_file( + color: bool | None = None, +) -> t.ContextManager[t.TextIO]: + """Context manager. + + Yields a writable file-like object which can be used as an output pager. + + .. versionadded:: 8.4.0 + + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + from ._termui_impl import get_pager_file + + color = resolve_color_default(color) + + return get_pager_file(color=color) + + +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. + """ + + 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) + + with get_pager_file(color=color) as pager: + for text in itertools.chain(text_generator, "\n"): + pager.write(text) + # Flush after each write so a slow generator streams to the pager + # incrementally rather than staying invisible until the pipe buffer + # fills (~8 KB). + pager.flush() + + +@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(_("Unknown color {colour!r}").format(colour=fg)) from None + + if bg: + try: + bits.append(f"\033[{_interpret_color(bg, 10)}m") + except KeyError: + raise TypeError(_("Unknown color {colour!r}").format(colour=bg)) 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.11/site-packages/click/testing.py b/venv/lib/python3.11/site-packages/click/testing.py new file mode 100644 index 0000000..19fae4a --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/testing.py @@ -0,0 +1,772 @@ +from __future__ import annotations + +import collections.abc as cabc +import contextlib +import io +import os +import pdb +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 + +if sys.platform == "win32": + CaptureMode: t.TypeAlias = t.Literal["sys"] # pyright: ignore[reportRedeclaration] +else: + CaptureMode: t.TypeAlias = t.Literal["sys", "fd"] # pyright: ignore[reportRedeclaration] +ExceptionInfo: t.TypeAlias = tuple[type[BaseException], BaseException, TracebackType] + + +class EchoingStdin: + _input: t.BinaryIO + _output: t.BinaryIO + _paused: bool + + 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.Generator[None]: + if stream is None: + yield + else: + stream._paused = True + yield + stream._paused = False + + +class _FDCapture: + """Redirect a file descriptor to a temporary file for capture. + + Saves the current target of *targetfd* via :func:`os.dup`, then + redirects it to a temporary file via :func:`os.dup2`. On + :meth:`stop`, restores the original ``fd`` and returns the captured + bytes. Inspired by Pytest's ``FDCapture``. + + .. versionadded:: 8.4.0 + """ + + _targetfd: int + saved_fd: int + _tmpfile: t.BinaryIO | None + + def __init__(self, targetfd: int) -> None: + self._targetfd = targetfd + self.saved_fd = -1 + self._tmpfile = None + + def start(self) -> None: + self.saved_fd = os.dup(self._targetfd) + self._tmpfile = tempfile.TemporaryFile(buffering=0) + os.dup2(self._tmpfile.fileno(), self._targetfd) + + def stop(self) -> bytes: + assert self._tmpfile is not None, "_FDCapture.start() was not called" + os.dup2(self.saved_fd, self._targetfd) + os.close(self.saved_fd) + self.saved_fd = -1 + self._tmpfile.seek(0) + data = self._tmpfile.read() + self._tmpfile.close() + self._tmpfile = None + return data + + +class BytesIOCopy(io.BytesIO): + """Patch ``io.BytesIO`` to let the written stream be copied to another. + + .. versionadded:: 8.2 + """ + + copy_to: io.BytesIO + + 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 + """ + + output: io.BytesIO + stdout: BytesIOCopy + stderr: BytesIOCopy + + def __init__(self) -> None: + self.output = io.BytesIO() + self.stdout = BytesIOCopy(copy_to=self.output) + self.stderr = BytesIOCopy(copy_to=self.output) + + +class _NamedTextIOWrapper(io.TextIOWrapper): + """A :class:`~io.TextIOWrapper` with custom ``name`` and ``mode`` + that does not close its underlying buffer. + + When ``CliRunner`` runs in ``fd`` mode, ``_original_fd`` is patched to + point at the saved (pre-redirection) ``fd``, so C-level consumers that call + :meth:`fileno` (like ``faulthandler`` or ``subprocess``) keep working. In + the default ``sys`` mode ``_original_fd`` stays at ``-1`` and + :meth:`fileno` raises :exc:`io.UnsupportedOperation`, matching the + pre-``8.3.3`` behavior. + """ + + _name: str + _mode: str + _original_fd: int + + def __init__( + self, + buffer: t.BinaryIO, + name: str, + mode: str, + **kwargs: t.Any, + ) -> None: + super().__init__(buffer, **kwargs) + self._name = name + self._mode = mode + self._original_fd = -1 + + def close(self) -> None: + """The buffer this object contains belongs to some other object, + so prevent the default ``__del__`` implementation from closing + that buffer. + + .. versionadded:: 8.3.2 + """ + + def fileno(self) -> int: + """Return the file descriptor of the saved original stream when + ``CliRunner`` runs in ``fd`` mode. Otherwise delegate to + :class:`~io.TextIOWrapper`, which raises + :exc:`io.UnsupportedOperation` for a ``BytesIO``-backed buffer. + """ + if self._original_fd >= 0: + return self._original_fd + return super().fileno() + + @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``. + """ + + runner: CliRunner + stdout_bytes: bytes + stderr_bytes: bytes + output_bytes: bytes + return_value: t.Any + exit_code: int + exception: BaseException | None + exc_info: ExceptionInfo | None + + 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: ExceptionInfo | None = 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`. + :param capture: Selects the output capture strategy. ``sys`` (default) + captures Python-level writes only and leaves + :meth:`sys.stdout.fileno` raising :exc:`io.UnsupportedOperation`, so + user code that calls :func:`os.dup2` on ``sys.stdout.fileno()`` cannot + clobber the host runner's stdout. ``fd`` redirects file descriptors + ``1`` and ``2`` via :func:`os.dup2` to a temporary file, also catching + output from stale stream references, C extensions, and subprocesses. + ``fd`` is not supported on Windows. + + .. versionchanged:: 8.4.0 + Added the ``capture`` parameter. The default ``sys`` mode no longer + exposes the original fd through :meth:`fileno`, reverting the change + introduced in ``8.3.3`` that broke Pytest's ``fd``-level capture + teardown. Use ``capture="fd"`` to restore that behavior with proper + isolation. :issue:`3384` + + .. versionchanged:: 8.2 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 8.2 + ``mix_stderr`` parameter has been removed. + """ + + charset: str + env: cabc.Mapping[str, str | None] + echo_stdin: bool + catch_exceptions: bool + capture: CaptureMode + + def __init__( + self, + charset: str = "utf-8", + env: cabc.Mapping[str, str | None] | None = None, + echo_stdin: bool = False, + catch_exceptions: bool = True, + capture: CaptureMode = "sys", + ) -> None: + if capture not in {"sys", "fd"}: + raise ValueError( + f"capture={capture!r} is not valid. Choose from 'sys' or 'fd'." + ) + if capture == "fd" and sys.platform == "win32": + raise ValueError( + f"capture={capture!r} is not supported on Windows. Use 'sys'." + ) + self.charset = charset + self.env = env or {} + self.echo_stdin = echo_stdin + self.catch_exceptions = catch_exceptions + self.capture = capture + + 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.Generator[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 + old_pdb_init = pdb.Pdb.__init__ + 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 + + def _patched_pdb_init( + self: pdb.Pdb, + completekey: str = "tab", + stdin: t.IO[str] | None = None, + stdout: t.IO[str] | None = None, + **kwargs: t.Any, + ) -> None: + """Default ``pdb.Pdb`` to real terminal streams during + ``CliRunner`` isolation. + + Without this patch, ``pdb.Pdb.__init__`` inherits from + ``cmd.Cmd`` which falls back to ``sys.stdin``/``sys.stdout`` + when no explicit streams are provided. During isolation + those are ``BytesIO``-backed wrappers, so the debugger + reads from an empty buffer and writes to captured output, + making interactive debugging impossible. + + By defaulting to ``sys.__stdin__``/``sys.__stdout__`` (the + original terminal streams Python preserves regardless of + redirection), debuggers can interact with the user while + ``click.echo`` output is still captured normally. + + This covers ``pdb.set_trace()``, ``breakpoint()``, + ``pdb.post_mortem()``, and debuggers that subclass + ``pdb.Pdb`` (ipdb, pdbpp). Explicit ``stdin``/``stdout`` + arguments are honored and not overridden. Debuggers that + do not subclass ``pdb.Pdb`` (pudb, debugpy) are not + covered. + """ + if stdin is None: + stdin = sys.__stdin__ + if stdout is None: + stdout = sys.__stdout__ + old_pdb_init( + self, completekey=completekey, stdin=stdin, stdout=stdout, **kwargs + ) + + pdb.Pdb.__init__ = _patched_pdb_init # type: ignore[assignment] + + 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 + pdb.Pdb.__init__ = old_pdb_init # type: ignore[method-assign] + + 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 + + # Set up fd capture before isolation replaces sys.stdout and sys.stderr. + cap_out: _FDCapture | None = None + cap_err: _FDCapture | None = None + + if self.capture == "fd": + cap_out = _FDCapture(1) + cap_err = _FDCapture(2) + try: + cap_out.start() + cap_err.start() + except OSError: + cap_out = cap_err = None + + with self.isolation(input=input, env=env, color=color) as outstreams: + # Point the captured streams' fileno() at the saved (original) + # fd so that C-level consumers like faulthandler keep working + # while fd 1/2 are redirected to the capture tmpfile. + if cap_out is not None and cap_err is not None: + sys.stdout._original_fd = cap_out.saved_fd # type: ignore[union-attr] + sys.stderr._original_fd = cap_err.saved_fd # type: ignore[union-attr] + + 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() + + # Stop fd capture and merge the captured bytes into + # the stdout/stderr BytesIO streams. BytesIOCopy mirrors + # those writes into outstreams[2] automatically. + if cap_out is not None and cap_err is not None: + fd_out = cap_out.stop() + fd_err = cap_err.stop() + if fd_out: + outstreams[0].write(fd_out) + if fd_err: + outstreams[1].write(fd_err) + + 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.Generator[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.11/site-packages/click/types.py b/venv/lib/python3.11/site-packages/click/types.py new file mode 100644 index 0000000..1e9872e --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/types.py @@ -0,0 +1,1374 @@ +from __future__ import annotations + +import abc +import collections.abc as cabc +import enum +import os +import stat +import sys +import typing as t +import uuid +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 + +_ValueT = t.TypeVar("_ValueT") +_ValueT_contra = t.TypeVar("_ValueT_contra", contravariant=True) +_ValueT_co = t.TypeVar("_ValueT_co", covariant=True) + +_FloatValueT = t.TypeVar("_FloatValueT", bound=float) +_FloatValueT_co = t.TypeVar("_FloatValueT_co", bound=float, covariant=True) + + +class ParamTypeInfoDict(t.TypedDict): + param_type: str + name: str + + +class ParamType(t.Generic[_ValueT_co], abc.ABC): + """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. + + .. versionchanged:: 8.4.0 + Now a generic abstract base class. Parameterize with the + converted value type (``ParamType[int]`` for an integer-returning + type) so that :meth:`convert` and downstream consumers carry the + narrowed return type. + """ + + is_composite: t.ClassVar[bool] = False + arity: int = 1 # read-only + + #: 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) -> ParamTypeInfoDict: + """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, + ) -> _ValueT_co | None: + if value is not None: + return self.convert(value, param, ctx) + return None + + 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 + ) -> _ValueT_co: + """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``. + """ + # The default returns the value as-is so subclasses that only customize + # metadata are not forced to redeclare ``convert``. + return t.cast("_ValueT_co", 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[_ValueT_co]): + is_composite: t.ClassVar[bool] = True + + @property + @abc.abstractmethod + def arity(self) -> int: ... # type: ignore[override] + + +if t.TYPE_CHECKING: + # on Python 3.10 this will raise a TypeError + + class FuncParamTypeInfoDict( + ParamTypeInfoDict, + t.Generic[_ValueT_contra, _ValueT_co], + ): + func: t.Callable[[_ValueT_contra], _ValueT_co] +else: + + class FuncParamTypeInfoDict(ParamTypeInfoDict): + func: t.Callable[[t.Any], t.Any] + + +class FuncParamType(ParamType[_ValueT_co], t.Generic[_ValueT_contra, _ValueT_co]): + name: str + func: t.Callable[[_ValueT_contra], _ValueT_co] + + def __init__(self, func: t.Callable[[_ValueT_contra], _ValueT_co]) -> None: + self.name = func.__name__ + self.func = func + + def to_info_dict(self) -> FuncParamTypeInfoDict[_ValueT_contra, _ValueT_co]: + return {"func": self.func, **super().to_info_dict()} + + def convert( + self, value: _ValueT_contra, param: Parameter | None, ctx: Context | None + ) -> _ValueT_co: + try: + return self.func(value) + except ValueError as exc: + message = str(exc) + + if not message: + try: + message = str(value) + except UnicodeError: + message = t.cast("bytes", value).decode("utf-8", "replace") + + self.fail(message, param, ctx) + + +class UnprocessedParamType(ParamType[t.Any]): + name = "text" + + def convert( + self, value: _ValueT, param: Parameter | None, ctx: Context | None + ) -> _ValueT: + return value + + def __repr__(self) -> str: + return "UNPROCESSED" + + +class StringParamType(ParamType[str]): + name = "text" + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> str: + if isinstance(value, bytes): + enc = _get_argv_encoding() + try: + return value.decode(enc) + except UnicodeError: + fs_enc = sys.getfilesystemencoding() + if fs_enc != enc: + try: + return value.decode(fs_enc) + except UnicodeError: + return value.decode("utf-8", "replace") + else: + return value.decode("utf-8", "replace") + return str(value) + + def __repr__(self) -> str: + return "STRING" + + +if t.TYPE_CHECKING: + # on Python 3.10 this will raise a TypeError + + class ChoiceInfoDict(ParamTypeInfoDict, t.Generic[_ValueT_co]): + choices: tuple[_ValueT_co, ...] + case_sensitive: bool +else: + + class ChoiceInfoDict(ParamTypeInfoDict): + choices: tuple[t.Any, ...] + case_sensitive: bool + + +class Choice(ParamType[_ValueT_co], t.Generic[_ValueT_co]): + """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.4.0 + Now generic in the choice value type. Parameterize with the type of + the choice values (``Choice[HashType]`` for an enum, ``Choice[str]`` + for plain strings) to enable type-checked consumers. + + .. 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: str = "choice" + + choices: tuple[_ValueT_co, ...] + case_sensitive: bool + + def __init__( + self, choices: cabc.Iterable[_ValueT_co], case_sensitive: bool = True + ) -> None: + self.choices = tuple(choices) + self.case_sensitive = case_sensitive + + def to_info_dict(self) -> ChoiceInfoDict[_ValueT_co]: + return { + "choices": self.choices, + "case_sensitive": self.case_sensitive, + **super().to_info_dict(), + } + + def _normalized_mapping( + self, ctx: Context | None = None + ) -> cabc.Mapping[_ValueT_co, 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: object, 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[attr-defined] + 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 + ) -> _ValueT_co: + """ + 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 _("Choice({choices})").format(choices=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 = [self.normalize_choice(choice, ctx) for choice in 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 DateTimeInfoDict(ParamTypeInfoDict): + formats: cabc.Sequence[str] + + +class DateTime(ParamType[datetime]): + """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" + + formats: cabc.Sequence[str] + + def __init__(self, formats: cabc.Sequence[str] | None = None): + self.formats = formats or [ + "%Y-%m-%d", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", + ] + + def to_info_dict(self) -> DateTimeInfoDict: + return {"formats": self.formats, **super().to_info_dict()} + + def get_metavar(self, param: Parameter, ctx: Context) -> str: + 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 + ) -> datetime: + 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[_ValueT_co], t.Generic[_ValueT_contra, _ValueT_co] +): + _number_class: t.Callable[[_ValueT_contra], _ValueT_co] + + def convert( + self, value: _ValueT_contra, param: Parameter | None, ctx: Context | None + ) -> _ValueT_co: + 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, + ) + + +if t.TYPE_CHECKING: + # on Python 3.10 this will raise a TypeError + + class NumberRangeInfoDict(ParamTypeInfoDict, t.Generic[_FloatValueT_co]): + min: _FloatValueT_co | None + max: _FloatValueT_co | None + min_open: bool + max_open: bool + clamp: bool +else: + + class NumberRangeInfoDict(ParamTypeInfoDict): + min: t.Any | None + max: t.Any | None + min_open: bool + max_open: bool + clamp: bool + + +class _NumberRangeBase( + _NumberParamTypeBase[_ValueT_contra, _FloatValueT_co], + t.Generic[_ValueT_contra, _FloatValueT_co], +): + min: _FloatValueT_co | None + max: _FloatValueT_co | None + min_open: bool + max_open: bool + clamp: bool + + def __init__( + self, + min: _FloatValueT_co | None = None, + max: _FloatValueT_co | 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) -> NumberRangeInfoDict[_FloatValueT_co]: + return { + "min": self.min, + "max": self.max, + "min_open": self.min_open, + "max_open": self.max_open, + "clamp": self.clamp, + **super().to_info_dict(), + } + + def convert( + self, value: _ValueT_contra, param: Parameter | None, ctx: Context | None + ) -> _FloatValueT_co: + import operator + + rv = super().convert(value, param, ctx) + min = self.min + max = self.max + lt_min: bool = min is not None and ( + operator.le if self.min_open else operator.lt + )(rv, min) + gt_max: bool = max is not None and ( + operator.ge if self.max_open else operator.gt + )(rv, max) + + if self.clamp: + if min is not None and lt_min: + return self._clamp(min, 1, self.min_open) + + if max is not None and gt_max: + return self._clamp(max, -1, self.max_open) + + 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 + + @abc.abstractmethod + def _clamp( + # Covariant type variables cannot be used in input positions, so we use a + # separate method-scoped type variable instead. + self: _NumberRangeBase[t.Any, _FloatValueT], + bound: _FloatValueT, + dir: t.Literal[1, -1], + open: bool, + ) -> _FloatValueT: + """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. + """ + ... + + 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[t.SupportsInt | t.SupportsIndex, int]): + name = "integer" + _number_class = int + + def __repr__(self) -> str: + return "INT" + + +class IntRange(_NumberRangeBase[int, int], 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(self, bound: int, dir: t.Literal[1, -1], open: bool) -> int: + if not open: + return bound + + return bound + dir + + +class FloatParamType(_NumberParamTypeBase[t.SupportsFloat | t.SupportsIndex, float]): + name = "float" + _number_class = float + + def __repr__(self) -> str: + return "FLOAT" + + +class FloatRange(_NumberRangeBase[float, float], 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[bool]): + 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[uuid.UUID]): + name = "uuid" + + def convert( + self, value: uuid.UUID | str, param: Parameter | None, ctx: Context | None + ) -> uuid.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 FileInfoDict(ParamTypeInfoDict): + mode: str + encoding: str | None + + +class File(ParamType[t.IO[t.Any]]): + """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 + + mode: str + encoding: str | None + errors: str | None + lazy: bool | None + atomic: bool + + 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) -> FileInfoDict: + return { + "mode": self.mode, + "encoding": self.encoding, + **super().to_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 + + 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.TypeIs[t.IO[t.Any]]: + return hasattr(value, "read") or hasattr(value, "write") + + +class PathInfoDict(ParamTypeInfoDict): + exists: bool + file_okay: bool + dir_okay: bool + writable: bool + readable: bool + allow_dash: bool + + +class Path(ParamType[str | bytes | os.PathLike[str]]): + """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 + + exists: bool + file_okay: bool + dir_okay: bool + readable: bool + writable: bool + executable: bool + resolve_path: bool + allow_dash: bool + name: str + + 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 | None = None, + executable: bool = False, + ) -> None: + 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: type | None = path_type + + if self.file_okay and not self.dir_okay: + self.name = _("file") + elif self.dir_okay and not self.file_okay: + self.name = _("directory") + else: + self.name = _("path") + + def to_info_dict(self) -> PathInfoDict: + return { + "exists": self.exists, + "file_okay": self.file_okay, + "dir_okay": self.dir_okay, + "writable": self.writable, + "readable": self.readable, + "allow_dash": self.allow_dash, + **super().to_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 TupleInfoDict(ParamTypeInfoDict): + types: cabc.Sequence[ParamTypeInfoDict] + + +class Tuple(CompositeParamType[tuple[t.Any, ...]]): + """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[t.Any]]) -> None: + self.types: cabc.Sequence[ParamType[t.Any]] = [convert_type(ty) for ty in types] + + def to_info_dict(self) -> TupleInfoDict: + return { + "types": [ty.to_info_dict() for ty in self.types], + **super().to_info_dict(), + } + + @property + def name(self) -> str: # type: ignore[override] + return f"<{' '.join(ty.name for ty in self.types)}>" + + @property + def arity(self) -> int: # type: ignore[override] + return len(self.types) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> tuple[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 _guess_type( + ty: type[t.Any] | ParamType[t.Any] | None, + default: t.Any | None, +) -> type[t.Any] | tuple[type[t.Any], ...] | ParamType[t.Any] | None: + """Infer a type from *ty* or *default*. + + Returns *ty* unchanged when it is not ``None``. Otherwise inspects + *default* to produce a ``type``, a ``tuple`` of types (for tuple + defaults), or ``None``. + """ + if ty is not None: + return ty + + if default is None: + return None + + if not isinstance(default, (tuple, list)): + return type(default) + + # If the default is empty, return None so convert_type falls + # through to STRING. + if not default: + return None + + item = default[0] + + # A sequence of iterables needs to detect the inner types. + # Can't call convert_type recursively because that would + # incorrectly unwind the tuple to a single type. + if isinstance(item, (tuple, list)): + return tuple(map(type, item)) + + return type(item) + + +@t.overload +def convert_type(ty: None, default: None = None) -> StringParamType: ... +@t.overload +def convert_type( + ty: type | ParamType[t.Any], default: t.Any | None = None +) -> ParamType[t.Any]: ... +@t.overload +def convert_type( + ty: t.Any | None, default: t.Any | None = None +) -> ParamType[t.Any]: ... +def convert_type( + ty: t.Any | None = None, default: t.Any | None = None +) -> ParamType[t.Any]: + """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 = _guess_type(ty, default) + is_guessed = guessed is not ty + + if isinstance(guessed, tuple): + return Tuple(guessed) + + if isinstance(guessed, ParamType): + return guessed + + if guessed is str or guessed is None: + return STRING + + if guessed is int: + return INT + + if guessed is float: + return FLOAT + + if guessed is bool: + return BOOL + + if is_guessed: + return STRING + + if __debug__: + try: + if issubclass(guessed, ParamType): + raise AssertionError( + f"Attempted to use an uninstantiated parameter type ({guessed})." + ) + except TypeError: + # guessed is an instance (correct), so issubclass fails. + pass + + return FuncParamType(guessed) + + +#: 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 provided. +#: +#: .. versionadded:: 4.0 +UNPROCESSED: t.Final[UnprocessedParamType] = UnprocessedParamType() + +#: A unicode string parameter type which is the implicit default. This +#: can also be selected by using ``str`` as type. +STRING: t.Final[StringParamType] = StringParamType() + +#: An integer parameter. This can also be selected by using ``int`` as +#: type. +INT: t.Final[IntParamType] = IntParamType() + +#: A floating point value parameter. This can also be selected by using +#: ``float`` as type. +FLOAT: t.Final[FloatParamType] = FloatParamType() + +#: A boolean parameter. This is the default for boolean flags. This can +#: also be selected by using ``bool`` as a type. +BOOL: t.Final[BoolParamType] = BoolParamType() + +#: A UUID parameter. +UUID: t.Final[UUIDParameterType] = UUIDParameterType() + + +class OptionHelpExtra(t.TypedDict, total=False): + envvars: tuple[str, ...] + default: str + range: str + required: str diff --git a/venv/lib/python3.11/site-packages/click/utils.py b/venv/lib/python3.11/site-packages/click/utils.py new file mode 100644 index 0000000..c0cb22d --- /dev/null +++ b/venv/lib/python3.11/site-packages/click/utils.py @@ -0,0 +1,653 @@ +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 gettext import gettext as _ +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. + + :meta private: + """ + # 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. + """ + + name: str + mode: str + encoding: str | None + errors: str | None + atomic: bool + _f: t.IO[t.Any] | None + should_close: bool + + def __init__( + self, + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, + ) -> None: + self.name = os.fspath(filename) + self.mode = mode + self.encoding = encoding + self.errors = errors + self.atomic = atomic + + 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: + """Proxy a file object but keep it open across a ``with`` block. + + Wraps a borrowed file (such as ``sys.stdin`` or ``sys.stdout``) so that + leaving a ``with`` block does not close it, as used by :func:`open_file` + for the ``-`` filename. The caller stays responsible for the file: an + explicit :meth:`close` still passes through to the wrapped object. + + Dunder methods are proxied explicitly: implicit special-method lookups + bypass :meth:`__getattr__`, because Python resolves them on the type rather + than the instance. + """ + + _file: t.IO[t.Any] + + def __init__(self, file: t.IO[t.Any]) -> None: + self._file = 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: object = 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 + + match message: + case str() | bytes() | bytearray(): + out = message + case None: + out = "" + case _: + out = str(message) + + if nl: + 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(_("Unknown standard stream '{name}'").format(name=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(_("Unknown standard stream '{name}'").format(name=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. + """ + + wrapped: t.IO[t.Any] + + 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.11/site-packages/discord/__init__.py b/venv/lib/python3.11/site-packages/discord/__init__.py new file mode 100644 index 0000000..120069b --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/__init__.py @@ -0,0 +1,123 @@ +""" +Discord API Wrapper +~~~~~~~~~~~~~~~~~~~ + +A basic wrapper for the Discord API. + +:copyright: (c) 2015-2021 Rapptz & (c) 2021-present Pycord Development +:license: MIT, see LICENSE for more details. +""" + +__title__ = "pycord" +__author__ = "Pycord Development" +__license__ = "MIT" +__copyright__ = "Copyright 2015-2021 Rapptz & Copyright 2021-present Pycord Development" + +__path__ = __import__("pkgutil").extend_path(__path__, __name__) + +import logging +from typing import TYPE_CHECKING + +# We need __version__ to be imported first +# isort: off +from ._version import * + +# isort: on + + +from . import abc, opus, sinks, ui, utils +from .activity import * +from .appinfo import * +from .application_role_connection import * +from .asset import * +from .audit_logs import * +from .automod import * +from .bot import * +from .channel import * +from .client import * +from .cog import * +from .collectibles import * +from .colour import * +from .commands import * +from .components import * +from .embeds import * +from .emoji import * +from .enums import * +from .errors import * +from .file import * +from .flags import * +from .guild import * +from .http import * +from .incidents import * +from .integrations import * +from .interactions import * +from .invite import * +from .member import * +from .mentions import * +from .message import * +from .monetization import * +from .object import * +from .onboarding import * +from .partial_emoji import * +from .permissions import * +from .player import * +from .poll import * +from .primary_guild import * +from .raw_models import * +from .reaction import * +from .role import * +from .scheduled_events import * +from .shard import * +from .soundboard import * +from .stage_instance import * +from .sticker import * +from .team import * +from .template import * +from .threads import * +from .user import * +from .webhook import * +from .welcome_screen import * +from .widget import * + +if TYPE_CHECKING: + from typing import Generic, TypeVar + + from typing_extensions import deprecated + + from discord.voice import VoiceClient as VoiceClientC + from discord.voice import VoiceProtocol as VoiceProtocolC + + C = TypeVar("C", bound=Client) + + @deprecated( + "discord.VoiceClient is deprecated in favour of discord.voice.VoiceClient since 2.7 and will be removed in 3.0", + ) + class VoiceClient(VoiceClientC): ... + + @deprecated( + "discord.VoiceProtocol is deprecated in favour of discord.voice.VoiceProtocol since 2.7 and will be removed in 3.0", + ) + class VoiceProtocol(VoiceProtocolC[C], Generic[C]): ... + +else: + from .utils import warn_deprecated + + def __getattr__(name: str) -> object: + if name == "VoiceClient": + warn_deprecated( + "discord.VoiceClient", "discord.voice.VoiceClient", "2.7", "3.0" + ) + from .voice import VoiceClient + + return VoiceClient + if name == "VoiceProtocol": + warn_deprecated( + "discord.VoiceProtocol", "discord.voice.VoiceProtocol", "2.7", "3.0" + ) + from .voice import VoiceProtocol + + return VoiceProtocol + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/venv/lib/python3.11/site-packages/discord/__main__.py b/venv/lib/python3.11/site-packages/discord/__main__.py new file mode 100644 index 0000000..ed34bdf --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/__main__.py @@ -0,0 +1,376 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +import argparse +import importlib.metadata +import platform +import sys +from pathlib import Path +from typing import Tuple + +import aiohttp + +import discord + + +def show_version() -> None: + entries = [ + "- Python v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format( + sys.version_info + ) + ] + + version_info = discord.version_info + entries.append( + "- py-cord v{0.major}.{0.minor}.{0.micro}-{0.releaselevel}".format(version_info) + ) + if version_info.releaselevel != "final": + version = importlib.metadata.version("py-cord") + if version: + entries.append(f" - py-cord importlib.metadata: v{version}") + + entries.append(f"- aiohttp v{aiohttp.__version__}") + uname = platform.uname() + entries.append("- system info: {0.system} {0.release} {0.version}".format(uname)) + print("\n".join(entries)) + + +def core(parser, args) -> None: + if args.version: + show_version() + + +_bot_template = """#!/usr/bin/env python3 + +from discord.ext import commands +import discord +import config + +class Bot(commands.{base}): + def __init__(self, **kwargs): + super().__init__(command_prefix=commands.when_mentioned_or('{prefix}'), **kwargs) + for cog in config.cogs: + try: + self.load_extension(cog) + except Exception as exc: + print(f'Could not load extension {{cog}} due to {{exc.__class__.__name__}}: {{exc}}') + + async def on_ready(self): + print(f'Logged on as {{self.user}} (ID: {{self.user.id}})') + + +bot = Bot() + +# write general commands here + +bot.run(config.token) +""" + +_gitignore_template = """# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# Our configuration files +config.py +""" + +_cog_template = '''from discord.ext import commands +import discord + +class {name}(commands.Cog{attrs}): + """The description for {name} goes here.""" + + def __init__(self, bot): + self.bot = bot +{extra} +def setup(bot): + bot.add_cog({name}(bot)) +''' + +_cog_extras = """ + def cog_unload(self): + # clean up logic goes here + pass + + async def cog_check(self, ctx): + # checks that apply to every command in here + return True + + async def bot_check(self, ctx): + # checks that apply to every command to the bot + return True + + async def bot_check_once(self, ctx): + # check that apply to every command but is guaranteed to be called only once + return True + + async def cog_command_error(self, ctx, error): + # error handling to every command in here + pass + + async def cog_before_invoke(self, ctx): + # called before a command is called here + pass + + async def cog_after_invoke(self, ctx): + # called after a command is called here + pass + +""" + + +# certain file names and directory names are forbidden +# see: https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx +# although some of this doesn't apply to Linux, we might as well be consistent +_base_table = { + "<": "-", + ">": "-", + ":": "-", + '"': "-", + # '/': '-', these are fine + # '\\': '-', + "|": "-", + "?": "-", + "*": "-", +} + +# NUL (0) and 1-31 are disallowed +_base_table.update((chr(i), None) for i in range(32)) + +_translation_table = str.maketrans(_base_table) + + +def to_path(parser, name, *, replace_spaces=False) -> Path: + if isinstance(name, Path): + return name + + if sys.platform == "win32": + forbidden = ( + "CON", + "PRN", + "AUX", + "NUL", + "COM1", + "COM2", + "COM3", + "COM4", + "COM5", + "COM6", + "COM7", + "COM8", + "COM9", + "LPT1", + "LPT2", + "LPT3", + "LPT4", + "LPT5", + "LPT6", + "LPT7", + "LPT8", + "LPT9", + ) + if len(name) <= 4 and name.upper() in forbidden: + parser.error("invalid directory name given, use a different one") + + name = name.translate(_translation_table) + if replace_spaces: + name = name.replace(" ", "-") + return Path(name) + + +def newbot(parser, args) -> None: + new_directory = to_path(parser, args.directory) / to_path(parser, args.name) + + # as a note exist_ok for Path is a 3.5+ only feature + # since we already checked above that we're >3.5 + try: + new_directory.mkdir(exist_ok=True, parents=True) + except OSError as exc: + parser.error(f"could not create our bot directory ({exc})") + + cogs = new_directory / "cogs" + + try: + cogs.mkdir(exist_ok=True) + init = cogs / "__init__.py" + init.touch() + except OSError as exc: + print(f"warning: could not create cogs directory ({exc})") + + try: + with open(str(new_directory / "config.py"), "w", encoding="utf-8") as fp: + fp.write('token = "place your token here"\ncogs = []\n') + except OSError as exc: + parser.error(f"could not create config file ({exc})") + + try: + with open(str(new_directory / "bot.py"), "w", encoding="utf-8") as fp: + base = "Bot" if not args.sharded else "AutoShardedBot" + fp.write(_bot_template.format(base=base, prefix=args.prefix)) + except OSError as exc: + parser.error(f"could not create bot file ({exc})") + + if not args.no_git: + try: + with open(str(new_directory / ".gitignore"), "w", encoding="utf-8") as fp: + fp.write(_gitignore_template) + except OSError as exc: + print(f"warning: could not create .gitignore file ({exc})") + + print("successfully made bot at", new_directory) + + +def newcog(parser, args) -> None: + cog_dir = to_path(parser, args.directory) + try: + cog_dir.mkdir(exist_ok=True) + except OSError as exc: + print(f"warning: could not create cogs directory ({exc})") + + directory = cog_dir / to_path(parser, args.name) + directory = directory.with_suffix(".py") + try: + with open(str(directory), "w", encoding="utf-8") as fp: + attrs = "" + extra = _cog_extras if args.full else "" + if args.class_name: + name = args.class_name + else: + name = str(directory.stem) + if "-" in name or "_" in name: + translation = str.maketrans("-_", " ") + name = name.translate(translation).title().replace(" ", "") + else: + name = name.title() + + if args.display_name: + attrs += f', name="{args.display_name}"' + if args.hide_commands: + attrs += ", command_attrs=dict(hidden=True)" + fp.write(_cog_template.format(name=name, extra=extra, attrs=attrs)) + except OSError as exc: + parser.error(f"could not create cog file ({exc})") + else: + print("successfully made cog at", directory) + + +def add_newbot_args(subparser: argparse._SubParsersAction) -> None: + parser = subparser.add_parser( + "newbot", help="creates a command bot project quickly" + ) + parser.set_defaults(func=newbot) + + parser.add_argument("name", help="the bot project name") + parser.add_argument( + "directory", + help="the directory to place it in (default: .)", + nargs="?", + default=Path.cwd(), + ) + parser.add_argument( + "--prefix", help="the bot prefix (default: $)", default="$", metavar="" + ) + parser.add_argument( + "--sharded", help="whether to use AutoShardedBot", action="store_true" + ) + parser.add_argument( + "--no-git", + help="do not create a .gitignore file", + action="store_true", + dest="no_git", + ) + + +def add_newcog_args(subparser: argparse._SubParsersAction) -> None: + parser = subparser.add_parser("newcog", help="creates a new cog template quickly") + parser.set_defaults(func=newcog) + + parser.add_argument("name", help="the cog name") + parser.add_argument( + "directory", + help="the directory to place it in (default: cogs)", + nargs="?", + default=Path("cogs"), + ) + parser.add_argument( + "--class-name", + help="the class name of the cog (default: )", + dest="class_name", + ) + parser.add_argument("--display-name", help="the cog name (default: )") + parser.add_argument( + "--hide-commands", + help="whether to hide all commands in the cog", + action="store_true", + ) + parser.add_argument( + "--full", help="add all special methods as well", action="store_true" + ) + + +def parse_args() -> Tuple[argparse.ArgumentParser, argparse.Namespace]: + parser = argparse.ArgumentParser( + prog="discord", description="Tools for helping with Pycord" + ) + parser.add_argument( + "-v", "--version", action="store_true", help="shows the library version" + ) + parser.set_defaults(func=core) + + subparser = parser.add_subparsers(dest="subcommand", title="subcommands") + add_newbot_args(subparser) + add_newcog_args(subparser) + return parser, parser.parse_args() + + +def main() -> None: + parser, args = parse_args() + args.func(parser, args) + + +if __name__ == "__main__": + main() diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..24a1664 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/__main__.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/__main__.cpython-311.pyc new file mode 100644 index 0000000..e4a6ff9 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/__main__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/_version.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/_version.cpython-311.pyc new file mode 100644 index 0000000..4203596 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/_version.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/abc.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/abc.cpython-311.pyc new file mode 100644 index 0000000..f918de7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/abc.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/activity.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/activity.cpython-311.pyc new file mode 100644 index 0000000..af56c36 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/activity.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/appinfo.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/appinfo.cpython-311.pyc new file mode 100644 index 0000000..3507564 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/appinfo.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/application_role_connection.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/application_role_connection.cpython-311.pyc new file mode 100644 index 0000000..7192e47 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/application_role_connection.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/asset.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/asset.cpython-311.pyc new file mode 100644 index 0000000..08feff6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/asset.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/audit_logs.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/audit_logs.cpython-311.pyc new file mode 100644 index 0000000..c969583 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/audit_logs.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/automod.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/automod.cpython-311.pyc new file mode 100644 index 0000000..72c49e7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/automod.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/backoff.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/backoff.cpython-311.pyc new file mode 100644 index 0000000..c5e31fd Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/backoff.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/bot.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/bot.cpython-311.pyc new file mode 100644 index 0000000..08c97a7 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/bot.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/channel.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/channel.cpython-311.pyc new file mode 100644 index 0000000..37cb844 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/channel.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/client.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/client.cpython-311.pyc new file mode 100644 index 0000000..17d2129 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/client.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/cog.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/cog.cpython-311.pyc new file mode 100644 index 0000000..dcb9f0d Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/cog.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/collectibles.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/collectibles.cpython-311.pyc new file mode 100644 index 0000000..b02d008 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/collectibles.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/colour.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/colour.cpython-311.pyc new file mode 100644 index 0000000..d6d5506 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/colour.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/components.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/components.cpython-311.pyc new file mode 100644 index 0000000..18b6a45 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/components.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/context_managers.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/context_managers.cpython-311.pyc new file mode 100644 index 0000000..c05bd69 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/context_managers.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/embeds.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/embeds.cpython-311.pyc new file mode 100644 index 0000000..1ad3093 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/embeds.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/emoji.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/emoji.cpython-311.pyc new file mode 100644 index 0000000..2ee3dbb Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/emoji.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/enums.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/enums.cpython-311.pyc new file mode 100644 index 0000000..71a9d3c Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/enums.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/errors.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/errors.cpython-311.pyc new file mode 100644 index 0000000..274d73b Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/errors.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/file.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/file.cpython-311.pyc new file mode 100644 index 0000000..2c90b01 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/file.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/flags.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/flags.cpython-311.pyc new file mode 100644 index 0000000..90f0cde Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/flags.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/gateway.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/gateway.cpython-311.pyc new file mode 100644 index 0000000..f89f284 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/gateway.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/guild.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/guild.cpython-311.pyc new file mode 100644 index 0000000..d32710e Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/guild.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/http.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/http.cpython-311.pyc new file mode 100644 index 0000000..5313115 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/http.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/incidents.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/incidents.cpython-311.pyc new file mode 100644 index 0000000..2b340a3 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/incidents.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/integrations.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/integrations.cpython-311.pyc new file mode 100644 index 0000000..e30d7c4 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/integrations.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/interactions.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/interactions.cpython-311.pyc new file mode 100644 index 0000000..29e847d Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/interactions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/invite.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/invite.cpython-311.pyc new file mode 100644 index 0000000..f89672c Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/invite.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/iterators.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/iterators.cpython-311.pyc new file mode 100644 index 0000000..683441d Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/iterators.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/member.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/member.cpython-311.pyc new file mode 100644 index 0000000..3b3f45a Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/member.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/mentions.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/mentions.cpython-311.pyc new file mode 100644 index 0000000..652b9b0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/mentions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/message.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/message.cpython-311.pyc new file mode 100644 index 0000000..53e61fa Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/message.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/mixins.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/mixins.cpython-311.pyc new file mode 100644 index 0000000..7865ff4 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/mixins.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/monetization.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/monetization.cpython-311.pyc new file mode 100644 index 0000000..8c05645 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/monetization.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/object.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/object.cpython-311.pyc new file mode 100644 index 0000000..e33a5c5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/object.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/oggparse.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/oggparse.cpython-311.pyc new file mode 100644 index 0000000..b48a9bd Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/oggparse.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/onboarding.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/onboarding.cpython-311.pyc new file mode 100644 index 0000000..1cff619 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/onboarding.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/opus.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/opus.cpython-311.pyc new file mode 100644 index 0000000..d000f38 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/opus.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/partial_emoji.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/partial_emoji.cpython-311.pyc new file mode 100644 index 0000000..850ff89 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/partial_emoji.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/permissions.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/permissions.cpython-311.pyc new file mode 100644 index 0000000..eb3f682 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/permissions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/player.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/player.cpython-311.pyc new file mode 100644 index 0000000..919f4fb Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/player.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/poll.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/poll.cpython-311.pyc new file mode 100644 index 0000000..d062de6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/poll.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/primary_guild.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/primary_guild.cpython-311.pyc new file mode 100644 index 0000000..9e60c7e Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/primary_guild.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/raw_models.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/raw_models.cpython-311.pyc new file mode 100644 index 0000000..92c2edc Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/raw_models.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/reaction.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/reaction.cpython-311.pyc new file mode 100644 index 0000000..be03052 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/reaction.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/role.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/role.cpython-311.pyc new file mode 100644 index 0000000..5d2fb9f Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/role.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/scheduled_events.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/scheduled_events.cpython-311.pyc new file mode 100644 index 0000000..b672d5e Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/scheduled_events.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/shard.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/shard.cpython-311.pyc new file mode 100644 index 0000000..baca326 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/shard.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/soundboard.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/soundboard.cpython-311.pyc new file mode 100644 index 0000000..943b778 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/soundboard.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/stage_instance.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/stage_instance.cpython-311.pyc new file mode 100644 index 0000000..0023ff1 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/stage_instance.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/state.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/state.cpython-311.pyc new file mode 100644 index 0000000..374c40f Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/state.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/sticker.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/sticker.cpython-311.pyc new file mode 100644 index 0000000..041914c Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/sticker.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/team.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/team.cpython-311.pyc new file mode 100644 index 0000000..9c7886e Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/team.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/template.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/template.cpython-311.pyc new file mode 100644 index 0000000..2c71ee2 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/template.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/threads.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/threads.cpython-311.pyc new file mode 100644 index 0000000..c7e90a6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/threads.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/user.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/user.cpython-311.pyc new file mode 100644 index 0000000..6f3ff77 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/user.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/utils.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/utils.cpython-311.pyc new file mode 100644 index 0000000..06d3e65 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/utils.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/welcome_screen.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/welcome_screen.cpython-311.pyc new file mode 100644 index 0000000..2ffdcc0 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/welcome_screen.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/__pycache__/widget.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/__pycache__/widget.cpython-311.pyc new file mode 100644 index 0000000..f82c3d3 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/__pycache__/widget.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/_version.py b/venv/lib/python3.11/site-packages/discord/_version.py new file mode 100644 index 0000000..ddc7a10 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/_version.py @@ -0,0 +1,165 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import datetime +import re +import warnings +from importlib.metadata import PackageNotFoundError, version + +from typing_extensions import TypedDict, deprecated + +__all__ = ("__version__", "VersionInfo", "version_info") + +from typing import Literal, NamedTuple + +try: + __version__ = version("py-cord") +except PackageNotFoundError: + # Package is not installed + try: + from setuptools_scm import get_version # type: ignore[import] + + __version__ = get_version() + except ImportError: + # setuptools_scm is not installed + __version__ = "0.0.0" + warnings.warn( + ( + "Package is not installed, and setuptools_scm is not installed. " + f"As a fallback, {__name__}.__version__ will be set to {__version__}" + ), + RuntimeWarning, + stacklevel=2, + ) + + +class AdvancedVersionInfo(TypedDict): + serial: int + build: int | None + commit: str | None + date: datetime.date | None + + +class VersionInfo(NamedTuple): + major: int + minor: int + micro: int + releaselevel: Literal["alpha", "beta", "candidate", "final"] + + # We can't set instance attributes on a NamedTuple, so we have to use a + # global variable to store the advanced version info. + @property + def advanced(self) -> AdvancedVersionInfo: + return _advanced + + @advanced.setter + def advanced(self, value: object) -> None: + global _advanced + _advanced = value + + @property + @deprecated( + "VersionInfo.release_level is deprecated since version 2.4, consider using releaselevel instead." + ) + def release_level(self) -> Literal["alpha", "beta", "candidate", "final"]: + return self.releaselevel + + @property + @deprecated( + 'VersionInfo.serial is deprecated since version 2.4, consider using .advanced["serial"] instead.' + ) + def serial(self) -> int: + return self.advanced["serial"] + + @property + @deprecated( + 'VersionInfo.build is deprecated since version 2.4, consider using .advanced["build"] instead.' + ) + def build(self) -> int | None: + return self.advanced["build"] + + @property + @deprecated( + 'VersionInfo.commit is deprecated since version 2.4, consider using .advanced["commit"] instead.' + ) + def commit(self) -> str | None: + return self.advanced["commit"] + + @property + @deprecated( + 'VersionInfo.date is deprecated since version 2.4, consider using .advanced["date"] instead.' + ) + def date(self) -> datetime.date | None: + return self.advanced["date"] + + +version_regex = re.compile( + r"^(?P\d+)(?:\.(?P\d+))?(?:\.(?P\d+))?" + r"(?:(?Prc|a|b)(?P\d+))?" + r"(?:\.dev(?P\d+))?" + r"(?:\+(?:(?:g(?P[a-fA-F0-9]{4,40})(?:\.d(?P\d{4}\d{2}\d{2})|))|d(?P\d{4}\d{2}\d{2})))?$" +) +version_match = version_regex.match(__version__) +if version_match is None: + raise RuntimeError(f"Invalid version string: {__version__}") +raw_info = version_match.groupdict() + +level_info: Literal["alpha", "beta", "candidate", "final"] + +if raw_info["level"] == "a": + level_info = "alpha" +elif raw_info["level"] == "b": + level_info = "beta" +elif raw_info["level"] == "rc": + level_info = "candidate" +elif raw_info["level"] is None: + level_info = "final" +else: + raise RuntimeError("Invalid release level") + +if (raw_date := raw_info["date"] or raw_info["date1"]) is not None: + date_info = datetime.date( + int(raw_date[:4]), + int(raw_date[4:6]), + int(raw_date[6:]), + ) +else: + date_info = None + +version_info: VersionInfo = VersionInfo( + major=int(raw_info["major"] or 0) or None, + minor=int(raw_info["minor"] or 0) or None, + micro=int(raw_info["patch"] or 0) or None, + releaselevel=level_info, +) + +_advanced = AdvancedVersionInfo( + serial=raw_info["serial"], + build=int(raw_info["build"] or 0) or None, + commit=raw_info["commit"], + date=date_info, +) diff --git a/venv/lib/python3.11/site-packages/discord/abc.py b/venv/lib/python3.11/site-packages/discord/abc.py new file mode 100644 index 0000000..4b6ca92 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/abc.py @@ -0,0 +1,2101 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import asyncio +import copy +import time +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Iterable, + Protocol, + Sequence, + TypeAlias, + TypeVar, + Union, + overload, + runtime_checkable, +) + +from . import utils +from .context_managers import Typing +from .enums import ChannelType +from .errors import ClientException, InvalidArgument +from .file import File, VoiceMessage +from .flags import ChannelFlags, MessageFlags +from .invite import Invite +from .iterators import HistoryIterator, MessagePinIterator +from .mentions import AllowedMentions +from .object import Object +from .partial_emoji import PartialEmoji, _EmojiTag +from .permissions import PermissionOverwrite, Permissions +from .role import Role +from .scheduled_events import ScheduledEvent +from .sticker import GuildSticker, StickerItem +from .utils import warn_deprecated + +__all__ = ( + "Snowflake", + "User", + "PrivateChannel", + "GuildChannel", + "Messageable", + "Connectable", + "Mentionable", +) + +if TYPE_CHECKING: + from datetime import datetime + + from .asset import Asset + from .channel import ( + CategoryChannel, + DMChannel, + GroupChannel, + PartialMessageable, + StageChannel, + TextChannel, + VoiceChannel, + ) + from .client import Client + from .embeds import Embed + from .enums import InviteTarget + from .guild import Guild + from .member import Member + from .message import Message, MessageReference, PartialMessage + from .poll import Poll + from .state import ConnectionState + from .threads import Thread + from .types.channel import Channel as ChannelPayload + from .types.channel import GuildChannel as GuildChannelPayload + from .types.channel import OverwriteType + from .types.channel import PermissionOverwrite as PermissionOverwritePayload + from .ui.view import BaseView + from .user import ClientUser + + PartialMessageableChannel: TypeAlias = ( + TextChannel + | VoiceChannel + | StageChannel + | Thread + | DMChannel + | PartialMessageable + ) + MessageableChannel: TypeAlias = PartialMessageableChannel | GroupChannel + SnowflakeTime = Union["Snowflake", datetime] + + from .voice import VoiceClient, VoiceProtocol + + T = TypeVar("T", bound=VoiceProtocol) + +MISSING = utils.MISSING + + +async def _single_delete_strategy( + messages: Iterable[Message], *, reason: str | None = None +): + for m in messages: + await m.delete(reason=reason) + + +async def _purge_messages_helper( + channel: TextChannel | StageChannel | Thread | VoiceChannel, + *, + limit: int | None = 100, + check: Callable[[Message], bool] = MISSING, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + around: SnowflakeTime | None = None, + oldest_first: bool | None = False, + bulk: bool = True, + reason: str | None = None, +) -> list[Message]: + if check is MISSING: + check = lambda m: True + + iterator = channel.history( + limit=limit, + before=before, + after=after, + oldest_first=oldest_first, + around=around, + ) + ret: list[Message] = [] + count = 0 + + minimum_time = int((time.time() - 14 * 24 * 60 * 60) * 1000.0 - 1420070400000) << 22 + strategy = channel.delete_messages if bulk else _single_delete_strategy + + async for message in iterator: + if count == 100: + to_delete = ret[-100:] + await strategy(to_delete, reason=reason) + count = 0 + await asyncio.sleep(1) + + if not check(message): + continue + + if message.id < minimum_time: + # older than 14 days old + if count == 1: + await ret[-1].delete(reason=reason) + elif count >= 2: + to_delete = ret[-count:] + await strategy(to_delete, reason=reason) + + count = 0 + strategy = _single_delete_strategy + + count += 1 + ret.append(message) + + # Some messages remaining to poll + if count >= 2: + # more than 2 messages -> bulk delete + to_delete = ret[-count:] + await strategy(to_delete, reason=reason) + elif count == 1: + # delete a single message + await ret[-1].delete(reason=reason) + + return ret + + +@runtime_checkable +class Snowflake(Protocol): + """An ABC that details the common operations on a Discord model. + + Almost all :ref:`Discord models ` meet this + abstract base class. + + If you want to create a snowflake on your own, consider using + :class:`.Object`. + + Attributes + ---------- + id: :class:`int` + The model's unique ID. + """ + + __slots__ = () + id: int + + +@runtime_checkable +class User(Snowflake, Protocol): + """An ABC that details the common operations on a Discord user. + + The following implement this ABC: + + - :class:`~discord.User` + - :class:`~discord.ClientUser` + - :class:`~discord.Member` + + This ABC must also implement :class:`~discord.abc.Snowflake`. + + Attributes + ---------- + name: :class:`str` + The user's username. + discriminator: :class:`str` + The user's discriminator. + + .. note:: + + If the user has migrated to the new username system, this will always be "0". + global_name: :class:`str` + The user's global name. + + .. versionadded:: 2.5 + avatar: :class:`~discord.Asset` + The avatar asset the user has. + bot: :class:`bool` + If the user is a bot account. + """ + + __slots__ = () + + name: str + discriminator: str + global_name: str | None + avatar: Asset | None + bot: bool + + @property + def display_name(self) -> str: + """Returns the user's display name.""" + raise NotImplementedError + + @property + def mention(self) -> str: + """Returns a string that allows you to mention the given user.""" + raise NotImplementedError + + +@runtime_checkable +class PrivateChannel(Snowflake, Protocol): + """An ABC that details the common operations on a private Discord channel. + + The following implement this ABC: + + - :class:`~discord.DMChannel` + - :class:`~discord.GroupChannel` + + This ABC must also implement :class:`~discord.abc.Snowflake`. + + Attributes + ---------- + me: :class:`~discord.ClientUser` + The user presenting yourself. + """ + + __slots__ = () + + me: ClientUser + + +class _Overwrites: + __slots__ = ("id", "allow", "deny", "type") + + ROLE = 0 + MEMBER = 1 + + def __init__(self, data: PermissionOverwritePayload): + self.id: int = int(data["id"]) + self.allow: int = int(data.get("allow", 0)) + self.deny: int = int(data.get("deny", 0)) + self.type: OverwriteType = data["type"] + + def _asdict(self) -> PermissionOverwritePayload: + return { + "id": self.id, + "allow": str(self.allow), + "deny": str(self.deny), + "type": self.type, + } + + def is_role(self) -> bool: + return self.type == self.ROLE + + def is_member(self) -> bool: + return self.type == self.MEMBER + + +GCH = TypeVar("GCH", bound="GuildChannel") + + +class GuildChannel: + """An ABC that details the common operations on a Discord guild channel. + + The following implement this ABC: + + - :class:`~discord.TextChannel` + - :class:`~discord.VoiceChannel` + - :class:`~discord.CategoryChannel` + - :class:`~discord.StageChannel` + - :class:`~discord.ForumChannel` + + This ABC must also implement :class:`~discord.abc.Snowflake`. + + Attributes + ---------- + name: :class:`str` + The channel name. + guild: :class:`~discord.Guild` + The guild the channel belongs to. + position: :class:`int` + The position in the channel list. This is a number that starts at 0. + e.g. the top channel is position 0. + """ + + __slots__ = () + + id: int + name: str + guild: Guild + type: ChannelType + position: int + category_id: int | None + flags: ChannelFlags + _state: ConnectionState + _overwrites: list[_Overwrites] + + if TYPE_CHECKING: + + def __init__( + self, *, state: ConnectionState, guild: Guild, data: dict[str, Any] + ): ... + + def __str__(self) -> str: + return self.name + + @property + def _sorting_bucket(self) -> int: + raise NotImplementedError + + def _update(self, guild: Guild, data: dict[str, Any]) -> None: + raise NotImplementedError + + async def _move( + self, + position: int, + parent_id: Any | None = None, + lock_permissions: bool = False, + *, + reason: str | None, + ) -> None: + if position < 0: + raise InvalidArgument("Channel position cannot be less than 0.") + + http = self._state.http + bucket = self._sorting_bucket + channels: list[GuildChannel] = [ + c for c in self.guild.channels if c._sorting_bucket == bucket + ] + + channels.sort(key=lambda c: c.position) + + try: + # remove ourselves from the channel list + channels.remove(self) + except ValueError: + # not there somehow lol + return + else: + index = next( + (i for i, c in enumerate(channels) if c.position >= position), + len(channels), + ) + # add ourselves at our designated position + channels.insert(index, self) + + payload = [] + for index, c in enumerate(channels): + d: dict[str, Any] = {"id": c.id, "position": index} + if parent_id is not MISSING and c.id == self.id: + d.update(parent_id=parent_id, lock_permissions=lock_permissions) + payload.append(d) + + await http.bulk_channel_update(self.guild.id, payload, reason=reason) + + async def _edit( + self, options: dict[str, Any], reason: str | None + ) -> ChannelPayload | None: + try: + parent = options.pop("category") + except KeyError: + parent_id = MISSING + else: + parent_id = parent and parent.id + + try: + options["rate_limit_per_user"] = options.pop("slowmode_delay") + except KeyError: + pass + + try: + options["default_thread_rate_limit_per_user"] = options.pop( + "default_thread_slowmode_delay" + ) + except KeyError: + pass + + try: + options["flags"] = options.pop("flags").value + except KeyError: + pass + + try: + options["available_tags"] = [ + tag.to_dict() for tag in options.pop("available_tags") + ] + except KeyError: + pass + + try: + rtc_region = options.pop("rtc_region") + except KeyError: + pass + else: + options["rtc_region"] = None if rtc_region is None else str(rtc_region) + + try: + video_quality_mode = options.pop("video_quality_mode") + except KeyError: + pass + else: + options["video_quality_mode"] = int(video_quality_mode) + + lock_permissions = options.pop("sync_permissions", False) + + try: + position = options.pop("position") + except KeyError: + if parent_id is not MISSING: + if lock_permissions: + category = self.guild.get_channel(parent_id) + if category: + options["permission_overwrites"] = [ + c._asdict() for c in category._overwrites + ] + options["parent_id"] = parent_id + elif lock_permissions and self.category_id is not None: + # if we're syncing permissions on a pre-existing channel category without changing it + # we need to update the permissions to point to the pre-existing category + category = self.guild.get_channel(self.category_id) + if category: + options["permission_overwrites"] = [ + c._asdict() for c in category._overwrites + ] + else: + await self._move( + position, + parent_id=parent_id, + lock_permissions=lock_permissions, + reason=reason, + ) + + overwrites = options.get("overwrites") + if overwrites is not None: + perms = [] + for target, perm in overwrites.items(): + if not isinstance(perm, PermissionOverwrite): + raise InvalidArgument( + "Expected PermissionOverwrite received" + f" {perm.__class__.__name__}" + ) + + allow, deny = perm.pair() + payload = { + "allow": allow.value, + "deny": deny.value, + "id": target.id, + "type": ( + _Overwrites.ROLE + if isinstance(target, Role) + else _Overwrites.MEMBER + ), + } + + perms.append(payload) + options["permission_overwrites"] = perms + + try: + ch_type = options["type"] + except KeyError: + pass + else: + if not isinstance(ch_type, ChannelType): + raise InvalidArgument("type field must be of type ChannelType") + options["type"] = ch_type.value + + try: + default_reaction_emoji = options["default_reaction_emoji"] + except KeyError: + pass + else: + if isinstance( + default_reaction_emoji, _EmojiTag + ): # GuildEmoji, PartialEmoji + default_reaction_emoji = default_reaction_emoji._to_partial() + elif isinstance(default_reaction_emoji, int): + default_reaction_emoji = PartialEmoji( + name=None, id=default_reaction_emoji + ) + elif isinstance(default_reaction_emoji, str): + default_reaction_emoji = PartialEmoji.from_str(default_reaction_emoji) + elif default_reaction_emoji is None: + pass + else: + raise InvalidArgument( + "default_reaction_emoji must be of type: GuildEmoji | int | str | None" + ) + + options["default_reaction_emoji"] = ( + default_reaction_emoji._to_forum_reaction_payload() + if default_reaction_emoji + else None + ) + + if options: + return await self._state.http.edit_channel( + self.id, reason=reason, **options + ) + + def _fill_overwrites(self, data: GuildChannelPayload) -> None: + self._overwrites = [] + everyone_index = 0 + everyone_id = self.guild.id + + for index, overridden in enumerate(data.get("permission_overwrites", [])): + overwrite = _Overwrites(overridden) + self._overwrites.append(overwrite) + + if overwrite.type == _Overwrites.MEMBER: + continue + + if overwrite.id == everyone_id: + # the @everyone role is not guaranteed to be the first one + # in the list of permission overwrites, however the permission + # resolution code kind of requires that it is the first one in + # the list since it is special. So we need the index so we can + # swap it to be the first one. + everyone_index = index + + # do the swap + tmp = self._overwrites + if tmp: + tmp[everyone_index], tmp[0] = tmp[0], tmp[everyone_index] + + @property + def changed_roles(self) -> list[Role]: + """Returns a list of roles that have been overridden from + their default values in the :attr:`~discord.Guild.roles` attribute. + """ + ret = [] + g = self.guild + for overwrite in filter(lambda o: o.is_role(), self._overwrites): + role = g.get_role(overwrite.id) + if role is None: + continue + + role = copy.copy(role) + role.permissions.handle_overwrite(overwrite.allow, overwrite.deny) + ret.append(role) + return ret + + @property + def mention(self) -> str: + """The string that allows you to mention the channel.""" + return f"<#{self.id}>" + + @property + def jump_url(self) -> str: + """Returns a URL that allows the client to jump to the channel. + + .. versionadded:: 2.0 + """ + return f"https://discord.com/channels/{self.guild.id}/{self.id}" + + @property + def created_at(self) -> datetime: + """Returns the channel's creation time in UTC.""" + return utils.snowflake_time(self.id) + + def overwrites_for(self, obj: Role | User) -> PermissionOverwrite: + """Returns the channel-specific overwrites for a member or a role. + + Parameters + ---------- + obj: Union[:class:`~discord.Role`, :class:`~discord.abc.User`] + The role or user denoting + whose overwrite to get. + + Returns + ------- + :class:`~discord.PermissionOverwrite` + The permission overwrites for this object. + """ + + if isinstance(obj, User): + predicate = lambda p: p.is_member() + elif isinstance(obj, Role): + predicate = lambda p: p.is_role() + else: + predicate = lambda p: True + + for overwrite in filter(predicate, self._overwrites): + if overwrite.id == obj.id: + allow = Permissions(overwrite.allow) + deny = Permissions(overwrite.deny) + return PermissionOverwrite.from_pair(allow, deny) + + return PermissionOverwrite() + + @property + def overwrites(self) -> dict[Role | Member, PermissionOverwrite]: + """Returns all of the channel's overwrites. + + This is returned as a dictionary where the key contains the target which + can be either a :class:`~discord.Role` or a :class:`~discord.Member` and the value is the + overwrite as a :class:`~discord.PermissionOverwrite`. + + Returns + ------- + Dict[Union[:class:`~discord.Role`, :class:`~discord.Member`], :class:`~discord.PermissionOverwrite`] + The channel's permission overwrites. + """ + ret = {} + for ow in self._overwrites: + allow = Permissions(ow.allow) + deny = Permissions(ow.deny) + overwrite = PermissionOverwrite.from_pair(allow, deny) + target = None + + if ow.is_role(): + target = self.guild.get_role(ow.id) + elif ow.is_member(): + target = self.guild.get_member(ow.id) + + # TODO: There is potential data loss here in the non-chunked + # case, i.e. target is None because get_member returned nothing. + # This can be fixed with a slight breaking change to the return type, + # i.e. adding discord.Object to the list of it + # However, for now this is an acceptable compromise. + if target is not None: + ret[target] = overwrite + return ret + + @property + def category(self) -> CategoryChannel | None: + """The category this channel belongs to. + + If there is no category then this is ``None``. + """ + return self.guild.get_channel(self.category_id) # type: ignore + + @property + def permissions_synced(self) -> bool: + """Whether the permissions for this channel are synced with the + category it belongs to. + + If there is no category then this is ``False``. + + .. versionadded:: 1.3 + """ + if self.category_id is None: + return False + + category = self.guild.get_channel(self.category_id) + return bool(category and category.overwrites == self.overwrites) + + def permissions_for(self, obj: Member | Role, /) -> Permissions: + """Handles permission resolution for the :class:`~discord.Member` + or :class:`~discord.Role`. + + This function takes into consideration the following cases: + + - Guild owner + - Guild roles + - Channel overrides + - Member overrides + + If a :class:`~discord.Role` is passed, then it checks the permissions + someone with that role would have, which is essentially: + + - The default role permissions + - The permissions of the role used as a parameter + - The default role permission overwrites + - The permission overwrites of the role used as a parameter + + .. versionchanged:: 2.0 + The object passed in can now be a role object. + + Parameters + ---------- + obj: Union[:class:`~discord.Member`, :class:`~discord.Role`] + The object to resolve permissions for. This could be either + a member or a role. If it's a role then member overwrites + are not computed. + + Returns + ------- + :class:`~discord.Permissions` + The resolved permissions for the member or role. + """ + + # The current cases can be explained as: + # Guild owner get all permissions -- no questions asked. Otherwise... + # The @everyone role gets the first application. + # After that, the applied roles that the user has in the channel + # (or otherwise) are then OR'd together. + # After the role permissions are resolved, the member permissions + # have to take into effect. + # After all that is done, you have to do the following: + + # If manage permissions is True, then all permissions are set to True. + + # The operation first takes into consideration the denied + # and then the allowed. + + if self.guild.owner_id == obj.id: + return Permissions.all() + + default = self.guild.default_role + base = Permissions(default.permissions.value if default else 0) + + # Handle the role case first + if isinstance(obj, Role): + base.value |= obj._permissions + + if base.administrator: + return Permissions.all() + + # Apply @everyone allow/deny first since it's special + try: + maybe_everyone = self._overwrites[0] + if maybe_everyone.id == self.guild.id: + base.handle_overwrite( + allow=maybe_everyone.allow, deny=maybe_everyone.deny + ) + except IndexError: + pass + + if obj.is_default(): + return base + + overwrite = utils.get(self._overwrites, type=_Overwrites.ROLE, id=obj.id) + if overwrite is not None: + base.handle_overwrite(overwrite.allow, overwrite.deny) + + return base + + roles = obj._roles + get_role = self.guild.get_role + + # Apply guild roles that the member has. + for role_id in roles: + role = get_role(role_id) + if role is not None: + base.value |= role._permissions + + # Guild-wide Administrator -> True for everything + # Bypass all channel-specific overrides + if base.administrator: + return Permissions.all() + + # Apply @everyone allow/deny first since it's special + try: + maybe_everyone = self._overwrites[0] + if maybe_everyone.id == self.guild.id: + base.handle_overwrite( + allow=maybe_everyone.allow, deny=maybe_everyone.deny + ) + remaining_overwrites = self._overwrites[1:] + else: + remaining_overwrites = self._overwrites + except IndexError: + remaining_overwrites = self._overwrites + + denies = 0 + allows = 0 + + # Apply channel specific role permission overwrites + for overwrite in remaining_overwrites: + if overwrite.is_role() and roles.has(overwrite.id): + denies |= overwrite.deny + allows |= overwrite.allow + + base.handle_overwrite(allow=allows, deny=denies) + + # Apply member specific permission overwrites + for overwrite in remaining_overwrites: + if overwrite.is_member() and overwrite.id == obj.id: + base.handle_overwrite(allow=overwrite.allow, deny=overwrite.deny) + break + + # if you can't send a message in a channel then you can't have certain + # permissions as well + if not base.send_messages: + base.send_tts_messages = False + base.mention_everyone = False + base.embed_links = False + base.attach_files = False + + # if you can't read a channel then you have no permissions there + if not base.read_messages: + denied = Permissions.all_channel() + base.value &= ~denied.value + + return base + + async def delete(self, *, reason: str | None = None) -> None: + """|coro| + + Deletes the channel. + + You must have :attr:`~discord.Permissions.manage_channels` permission to use this. + + Parameters + ---------- + reason: Optional[:class:`str`] + The reason for deleting this channel. + Shows up on the audit log. + + Raises + ------ + ~discord.Forbidden + You do not have proper permissions to delete the channel. + ~discord.NotFound + The channel was not found or was already deleted. + ~discord.HTTPException + Deleting the channel failed. + """ + await self._state.http.delete_channel(self.id, reason=reason) + + @overload + async def set_permissions( + self, + target: Member | Role, + *, + overwrite: PermissionOverwrite | None = ..., + reason: str | None = ..., + ) -> None: ... + + @overload + async def set_permissions( + self, + target: Member | Role, + *, + reason: str | None = ..., + **permissions: bool, + ) -> None: ... + + async def set_permissions( + self, target, *, overwrite=MISSING, reason=None, **permissions + ): + r"""|coro| + + Sets the channel specific permission overwrites for a target in the + channel. + + The ``target`` parameter should either be a :class:`~discord.Member` or a + :class:`~discord.Role` that belongs to guild. + + The ``overwrite`` parameter, if given, must either be ``None`` or + :class:`~discord.PermissionOverwrite`. For convenience, you can pass in + keyword arguments denoting :class:`~discord.Permissions` attributes. If this is + done, then you cannot mix the keyword arguments with the ``overwrite`` + parameter. + + If the ``overwrite`` parameter is ``None``, then the permission + overwrites are deleted. + + You must have the :attr:`~discord.Permissions.manage_roles` permission to use this. + + .. note:: + + This method *replaces* the old overwrites with the ones given. + + Examples + ---------- + + Setting allow and deny: :: + + await message.channel.set_permissions(message.author, read_messages=True, + send_messages=False) + + Deleting overwrites :: + + await channel.set_permissions(member, overwrite=None) + + Using :class:`~discord.PermissionOverwrite` :: + + overwrite = discord.PermissionOverwrite() + overwrite.send_messages = False + overwrite.read_messages = True + await channel.set_permissions(member, overwrite=overwrite) + + Parameters + ----------- + target: Union[:class:`~discord.Member`, :class:`~discord.Role`] + The member or role to overwrite permissions for. + overwrite: Optional[:class:`~discord.PermissionOverwrite`] + The permissions to allow and deny to the target, or ``None`` to + delete the overwrite. + \*\*permissions + A keyword argument list of permissions to set for ease of use. + Cannot be mixed with ``overwrite``. + reason: Optional[:class:`str`] + The reason for doing this action. Shows up on the audit log. + + Raises + ------- + ~discord.Forbidden + You do not have permissions to edit channel specific permissions. + ~discord.HTTPException + Editing channel specific permissions failed. + ~discord.NotFound + The role or member being edited is not part of the guild. + ~discord.InvalidArgument + The overwrite parameter invalid or the target type was not + :class:`~discord.Role` or :class:`~discord.Member`. + """ + + http = self._state.http + + if isinstance(target, User): + perm_type = _Overwrites.MEMBER + elif isinstance(target, Role): + perm_type = _Overwrites.ROLE + else: + raise InvalidArgument("target parameter must be either Member or Role") + + if overwrite is MISSING: + if len(permissions) == 0: + raise InvalidArgument("No overwrite provided.") + try: + overwrite = PermissionOverwrite(**permissions) + except (ValueError, TypeError): + raise InvalidArgument("Invalid permissions given to keyword arguments.") + elif len(permissions) > 0: + raise InvalidArgument("Cannot mix overwrite and keyword arguments.") + + # TODO: wait for event + + if overwrite is None: + await http.delete_channel_permissions(self.id, target.id, reason=reason) + elif isinstance(overwrite, PermissionOverwrite): + allow, deny = overwrite.pair() + await http.edit_channel_permissions( + self.id, target.id, allow.value, deny.value, perm_type, reason=reason + ) + else: + raise InvalidArgument("Invalid overwrite type provided.") + + async def _clone_impl( + self: GCH, + base_attrs: dict[str, Any], + *, + name: str | None = None, + reason: str | None = None, + ) -> GCH: + base_attrs["permission_overwrites"] = [x._asdict() for x in self._overwrites] + base_attrs["parent_id"] = self.category_id + base_attrs["name"] = name or self.name + guild_id = self.guild.id + cls = self.__class__ + data = await self._state.http.create_channel( + guild_id, self.type.value, reason=reason, **base_attrs + ) + obj = cls(state=self._state, guild=self.guild, data=data) + + # temporarily add it to the cache + self.guild._channels[obj.id] = obj # type: ignore + return obj + + async def clone( + self: GCH, *, name: str | None = None, reason: str | None = None + ) -> GCH: + """|coro| + + Clones this channel. This creates a channel with the same properties + as this channel. + + You must have the :attr:`~discord.Permissions.manage_channels` permission to + do this. + + .. versionadded:: 1.1 + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the new channel. If not provided, defaults to this + channel name. + reason: Optional[:class:`str`] + The reason for cloning this channel. Shows up on the audit log. + + Returns + ------- + :class:`.abc.GuildChannel` + The channel that was created. + + Raises + ------ + ~discord.Forbidden + You do not have the proper permissions to create this channel. + ~discord.HTTPException + Creating the channel failed. + """ + raise NotImplementedError + + @overload + async def move( + self, + *, + beginning: bool, + offset: int = MISSING, + category: Snowflake | None = MISSING, + sync_permissions: bool = MISSING, + reason: str | None = MISSING, + ) -> None: ... + + @overload + async def move( + self, + *, + end: bool, + offset: int = MISSING, + category: Snowflake | None = MISSING, + sync_permissions: bool = MISSING, + reason: str = MISSING, + ) -> None: ... + + @overload + async def move( + self, + *, + before: Snowflake, + offset: int = MISSING, + category: Snowflake | None = MISSING, + sync_permissions: bool = MISSING, + reason: str = MISSING, + ) -> None: ... + + @overload + async def move( + self, + *, + after: Snowflake, + offset: int = MISSING, + category: Snowflake | None = MISSING, + sync_permissions: bool = MISSING, + reason: str = MISSING, + ) -> None: ... + + async def move(self, **kwargs) -> None: + """|coro| + + A rich interface to help move a channel relative to other channels. + + If exact position movement is required, ``edit`` should be used instead. + + You must have the :attr:`~discord.Permissions.manage_channels` permission to + do this. + + .. note:: + + Voice channels will always be sorted below text channels. + This is a Discord limitation. + + .. versionadded:: 1.7 + + Parameters + ---------- + beginning: :class:`bool` + Whether to move the channel to the beginning of the + channel list (or category if given). + This is mutually exclusive with ``end``, ``before``, and ``after``. + end: :class:`bool` + Whether to move the channel to the end of the + channel list (or category if given). + This is mutually exclusive with ``beginning``, ``before``, and ``after``. + before: :class:`~discord.abc.Snowflake` + The channel that should be before our current channel. + This is mutually exclusive with ``beginning``, ``end``, and ``after``. + after: :class:`~discord.abc.Snowflake` + The channel that should be after our current channel. + This is mutually exclusive with ``beginning``, ``end``, and ``before``. + offset: :class:`int` + The number of channels to offset the move by. For example, + an offset of ``2`` with ``beginning=True`` would move + it 2 after the beginning. A positive number moves it below + while a negative number moves it above. Note that this + number is relative and computed after the ``beginning``, + ``end``, ``before``, and ``after`` parameters. + category: Optional[:class:`~discord.abc.Snowflake`] + The category to move this channel under. + If ``None`` is given then it moves it out of the category. + This parameter is ignored if moving a category channel. + sync_permissions: :class:`bool` + Whether to sync the permissions with the category (if given). + reason: :class:`str` + The reason for the move. + + Raises + ------ + InvalidArgument + An invalid position was given or a bad mix of arguments was passed. + Forbidden + You do not have permissions to move the channel. + HTTPException + Moving the channel failed. + """ + + if not kwargs: + return + + beginning, end = kwargs.get("beginning"), kwargs.get("end") + before, after = kwargs.get("before"), kwargs.get("after") + offset = kwargs.get("offset", 0) + if sum(bool(a) for a in (beginning, end, before, after)) > 1: + raise InvalidArgument( + "Only one of [before, after, end, beginning] can be used." + ) + + bucket = self._sorting_bucket + parent_id = kwargs.get("category", MISSING) + channels: list[GuildChannel] + if parent_id not in (MISSING, None): + parent_id = parent_id.id + channels = [ + ch + for ch in self.guild.channels + if ch._sorting_bucket == bucket and ch.category_id == parent_id + ] + else: + channels = [ + ch + for ch in self.guild.channels + if ch._sorting_bucket == bucket and ch.category_id == self.category_id + ] + + channels.sort(key=lambda c: (c.position, c.id)) + + try: + # Try to remove ourselves from the channel list + channels.remove(self) + except ValueError: + # If we're not there then it's probably due to not being in the category + pass + + index = None + if beginning: + index = 0 + elif end: + index = len(channels) + elif before: + index = next((i for i, c in enumerate(channels) if c.id == before.id), None) + elif after: + index = next( + (i + 1 for i, c in enumerate(channels) if c.id == after.id), None + ) + + if index is None: + raise InvalidArgument("Could not resolve appropriate move position") + + channels.insert(max((index + offset), 0), self) + payload = [] + lock_permissions = kwargs.get("sync_permissions", False) + reason = kwargs.get("reason") + for index, channel in enumerate(channels): + d = {"id": channel.id, "position": index} + if parent_id is not MISSING and channel.id == self.id: + d.update(parent_id=parent_id, lock_permissions=lock_permissions) + payload.append(d) + + await self._state.http.bulk_channel_update( + self.guild.id, payload, reason=reason + ) + + async def create_invite( + self, + *, + reason: str | None = None, + max_age: int = 0, + max_uses: int = 0, + temporary: bool = False, + unique: bool = True, + target_event: ScheduledEvent | None = None, + target_type: InviteTarget | None = None, + target_user: User | None = None, + target_application_id: int | None = None, + roles: list[Role | Object] | None = None, + target_users_file: File | None = None, + ) -> Invite: + """|coro| + + Creates an instant invite from a text or voice channel. + + You must have the :attr:`~discord.Permissions.create_instant_invite` permission to + do this. + + Parameters + ---------- + max_age: :class:`int` + How long the invite should last in seconds. If it's 0 then the invite + doesn't expire. Defaults to ``0``. + max_uses: :class:`int` + How many uses the invite could be used for. If it's 0 then there + are unlimited uses. Defaults to ``0``. + temporary: :class:`bool` + Denotes that the invite grants temporary membership + (i.e. they get kicked after they disconnect). Defaults to ``False``. + unique: :class:`bool` + Indicates if a unique invite URL should be created. Defaults to True. + If this is set to ``False`` then it will return a previously created + invite. + reason: Optional[:class:`str`] + The reason for creating this invite. Shows up on the audit log. + target_type: Optional[:class:`.InviteTarget`] + The type of target for the voice channel invite, if any. + + .. versionadded:: 2.0 + + target_user: Optional[:class:`User`] + The user whose stream to display for this invite, required if `target_type` is `TargetType.stream`. + The user must be streaming in the channel. + + .. versionadded:: 2.0 + + target_application_id: Optional[:class:`int`] + The id of the embedded application for the invite, required if `target_type` is + `TargetType.embedded_application`. + + .. versionadded:: 2.0 + + target_event: Optional[:class:`.ScheduledEvent`] + The scheduled event object to link to the event. + Shortcut to :meth:`.Invite.set_scheduled_event` + + See :meth:`.Invite.set_scheduled_event` for more + info on event invite linking. + + .. versionadded:: 2.0 + + roles: Optional[List[Union[:class:`.Role`, :class:`.Object`]]] + The roles to give a user when joining through this invite. + + You must have the :attr:`~Permissions.manage_roles` permission to do this and roles cannot be higher than your own. + + .. versionadded:: 2.8 + + target_users_file: Optional[:class:`File`] + A CSV file with a single column of user IDs for all the users able to accept this invite. + + You can use :func:`utils.users_to_csv` to generate a virtual CSV file from a sequence of user IDs. + + .. versionadded:: 2.8 + + Returns + ------- + :class:`~discord.Invite` + The invite that was created. + + Raises + ------ + ~discord.HTTPException + Invite creation failed. + + ~discord.NotFound + The channel that was passed is a category or an invalid channel. + """ + + data = await self._state.http.create_invite( + self.id, + reason=reason, + max_age=max_age, + max_uses=max_uses, + temporary=temporary, + unique=unique, + target_type=target_type.value if target_type else None, + target_user_id=target_user.id if target_user else None, + target_application_id=target_application_id, + roles=[str(r.id) for r in roles] if roles else None, + target_users_file=target_users_file, + ) + invite = Invite.from_incomplete(data=data, state=self._state) + + if target_event: + invite.set_scheduled_event(target_event) + return invite + + async def invites(self) -> list[Invite]: + """|coro| + + Returns a list of all active instant invites from this channel. + + You must have :attr:`~discord.Permissions.manage_channels` to get this information. + + Returns + ------- + List[:class:`~discord.Invite`] + The list of invites that are currently active. + + Raises + ------ + ~discord.Forbidden + You do not have proper permissions to get the information. + ~discord.HTTPException + An error occurred while fetching the information. + """ + + state = self._state + data = await state.http.invites_from_channel(self.id) + guild = self.guild + return [ + Invite(state=state, data=invite, channel=self, guild=guild) + for invite in data + ] + + +class Messageable: + """An ABC that details the common operations on a model that can send messages. + + The following implement this ABC: + + - :class:`~discord.TextChannel` + - :class:`~discord.VoiceChannel` + - :class:`~discord.StageChannel` + - :class:`~discord.DMChannel` + - :class:`~discord.GroupChannel` + - :class:`~discord.User` + - :class:`~discord.Member` + - :class:`~discord.ext.commands.Context` + - :class:`~discord.Thread` + - :class:`~discord.ApplicationContext` + """ + + __slots__ = () + _state: ConnectionState + + async def _get_channel(self) -> MessageableChannel: + raise NotImplementedError + + @overload + async def send( + self, + content: str | None = ..., + *, + tts: bool = ..., + embed: Embed = ..., + file: File = ..., + stickers: Sequence[GuildSticker | StickerItem] = ..., + delete_after: float = ..., + nonce: int | str = ..., + enforce_nonce: bool = ..., + allowed_mentions: AllowedMentions = ..., + reference: Message | MessageReference | PartialMessage = ..., + mention_author: bool = ..., + view: BaseView = ..., + poll: Poll = ..., + suppress: bool = ..., + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: ... + + @overload + async def send( + self, + content: str | None = ..., + *, + tts: bool = ..., + embed: Embed = ..., + files: list[File] = ..., + stickers: Sequence[GuildSticker | StickerItem] = ..., + delete_after: float = ..., + nonce: int | str = ..., + enforce_nonce: bool = ..., + allowed_mentions: AllowedMentions = ..., + reference: Message | MessageReference | PartialMessage = ..., + mention_author: bool = ..., + view: BaseView = ..., + poll: Poll = ..., + suppress: bool = ..., + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: ... + + @overload + async def send( + self, + content: str | None = ..., + *, + tts: bool = ..., + embeds: list[Embed] = ..., + file: File = ..., + stickers: Sequence[GuildSticker | StickerItem] = ..., + delete_after: float = ..., + nonce: int | str = ..., + enforce_nonce: bool = ..., + allowed_mentions: AllowedMentions = ..., + reference: Message | MessageReference | PartialMessage = ..., + mention_author: bool = ..., + view: BaseView = ..., + poll: Poll = ..., + suppress: bool = ..., + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: ... + + @overload + async def send( + self, + content: str | None = ..., + *, + tts: bool = ..., + embeds: list[Embed] = ..., + files: list[File] = ..., + stickers: Sequence[GuildSticker | StickerItem] = ..., + delete_after: float = ..., + nonce: int | str = ..., + enforce_nonce: bool = ..., + allowed_mentions: AllowedMentions = ..., + reference: Message | MessageReference | PartialMessage = ..., + mention_author: bool = ..., + view: BaseView = ..., + poll: Poll = ..., + suppress: bool = ..., + suppress_embeds: bool = ..., + silent: bool = ..., + ) -> Message: ... + + async def send( + self, + content=None, + *, + tts=None, + embed=None, + embeds=None, + file=None, + files=None, + stickers=None, + delete_after=None, + nonce=None, + enforce_nonce=None, + allowed_mentions=None, + reference=None, + mention_author=None, + view=None, + poll=None, + suppress=None, + suppress_embeds=None, + silent=None, + ): + """|coro| + + Sends a message to the destination with the content given. + + The content must be a type that can convert to a string through ``str(content)``. + If the content is set to ``None`` (the default), then the ``embed`` parameter must + be provided. + + To upload a single file, the ``file`` parameter should be used with a + single :class:`~discord.File` object. To upload multiple files, the ``files`` + parameter should be used with a :class:`list` of :class:`~discord.File` objects. + **Specifying both parameters will lead to an exception**. + + To upload a single embed, the ``embed`` parameter should be used with a + single :class:`~discord.Embed` object. To upload multiple embeds, the ``embeds`` + parameter should be used with a :class:`list` of :class:`~discord.Embed` objects. + **Specifying both parameters will lead to an exception**. + + Parameters + ---------- + content: Optional[:class:`str`] + The content of the message to send. + tts: :class:`bool` + Indicates if the message should be sent using text-to-speech. + embed: :class:`~discord.Embed` + The rich embed for the content. + file: :class:`~discord.File` + The file to upload. + files: List[:class:`~discord.File`] + A list of files to upload. Must be a maximum of 10. + nonce: Union[:class:`str`, :class:`int`] + The nonce to use for sending this message. If the message was successfully sent, + then the message will have a nonce with this value. + enforce_nonce: Optional[:class:`bool`] + Whether :attr:`nonce` is enforced to be validated. + + .. versionadded:: 2.5 + delete_after: :class:`float` + If provided, the number of seconds to wait in the background + before deleting the message we just sent. If the deletion fails, + then it is silently ignored. + allowed_mentions: :class:`~discord.AllowedMentions` + Controls the mentions being processed in this message. If this is + passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`. + The merging behaviour only overrides attributes that have been explicitly passed + to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`. + If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions` + are used instead. + + .. versionadded:: 1.4 + + reference: Union[:class:`~discord.Message`, :class:`~discord.MessageReference`, :class:`~discord.PartialMessage`] + A reference to the :class:`~discord.Message` being replied to or forwarded. This can be created using + :meth:`~discord.Message.to_reference`. + When replying, you can control whether this mentions the author of the referenced message using the + :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions`` or by + setting ``mention_author``. + + .. versionadded:: 1.6 + + mention_author: Optional[:class:`bool`] + If set, overrides the :attr:`~discord.AllowedMentions.replied_user` attribute of ``allowed_mentions``. + + .. versionadded:: 1.6 + view: :class:`discord.ui.BaseView` + A Discord UI View to add to the message. + embeds: List[:class:`~discord.Embed`] + A list of embeds to upload. Must be a maximum of 10. + + .. versionadded:: 2.0 + stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] + A list of stickers to upload. Must be a maximum of 3. + + .. versionadded:: 2.0 + suppress: :class:`bool` + Whether to suppress embeds for the message. + + .. deprecated:: 2.8 + suppress_embeds: :class:`bool` + Whether to suppress embeds for the message. + + .. versionadded:: 2.8 + silent: :class:`bool` + Whether to suppress push and desktop notifications for the message. + + .. versionadded:: 2.4 + poll: :class:`Poll` + The poll to send. + + .. versionadded:: 2.6 + + Returns + ------- + :class:`~discord.Message` + The message that was sent. + + Raises + ------ + ~discord.HTTPException + Sending the message failed. + ~discord.Forbidden + You do not have the proper permissions to send the message. + ~discord.InvalidArgument + The ``files`` list is not of the appropriate size, + you specified both ``file`` and ``files``, + or you specified both ``embed`` and ``embeds``, + or the ``reference`` object is not a :class:`~discord.Message`, + :class:`~discord.MessageReference` or :class:`~discord.PartialMessage`. + """ + + channel = await self._get_channel() + state = self._state + content = str(content) if content is not None else None + + if embed is not None and embeds is not None: + raise InvalidArgument( + "cannot pass both embed and embeds parameter to send()" + ) + + if embed is not None: + embed = embed.to_dict() + + elif embeds is not None: + if len(embeds) > 10: + raise InvalidArgument( + "embeds parameter must be a list of up to 10 elements" + ) + embeds = [embed.to_dict() for embed in embeds] + + if suppress is not None: + warn_deprecated("suppress", "suppress_embeds", "2.8") + if suppress_embeds is None: + suppress_embeds = suppress + + flags = MessageFlags( + suppress_embeds=bool(suppress_embeds), + suppress_notifications=bool(silent), + ) + + if stickers is not None: + stickers = [sticker.id for sticker in stickers] + + if allowed_mentions is None: + allowed_mentions = ( + state.allowed_mentions and state.allowed_mentions.to_dict() + ) + elif state.allowed_mentions is not None: + allowed_mentions = state.allowed_mentions.merge(allowed_mentions).to_dict() + else: + allowed_mentions = allowed_mentions.to_dict() + + if mention_author is not None: + allowed_mentions = allowed_mentions or AllowedMentions().to_dict() + allowed_mentions["replied_user"] = bool(mention_author) + + _reference = None + if reference is not None: + try: + _reference = reference.to_message_reference_dict() + from .message import MessageReference + + if not isinstance(reference, MessageReference): + utils.warn_deprecated( + f"Passing {type(reference).__name__} to reference", + "MessageReference", + "2.7", + "3.0", + ) + except AttributeError: + raise InvalidArgument( + "reference parameter must be Message, MessageReference, or" + " PartialMessage" + ) from None + + if view: + if not hasattr(view, "__discord_ui_view__"): + raise InvalidArgument( + f"view parameter must be BaseView not {view.__class__!r}" + ) + + components = view.to_components() + if view.is_components_v2(): + if embeds or content: + raise TypeError( + "cannot send embeds or content with a view using v2 component logic" + ) + flags.is_components_v2 = True + else: + components = None + + if poll: + poll = poll.to_dict() + + if file is not None and files is not None: + raise InvalidArgument("cannot pass both file and files parameter to send()") + + if file is not None: + if not isinstance(file, File): + raise InvalidArgument("file parameter must be File") + files = [file] + elif files is not None: + if len(files) > 10: + raise InvalidArgument( + "files parameter must be a list of up to 10 elements" + ) + elif not all(isinstance(file, File) for file in files): + raise InvalidArgument("files parameter must be a list of File") + + if files is not None: + flags = flags + MessageFlags( + is_voice_message=any(isinstance(f, VoiceMessage) for f in files) + ) + try: + data = await state.http.send_files( + channel.id, + files=files, + content=content, + tts=tts, + embed=embed, + embeds=embeds, + nonce=nonce, + enforce_nonce=enforce_nonce, + allowed_mentions=allowed_mentions, + message_reference=_reference, + stickers=stickers, + components=components, + flags=flags.value, + poll=poll, + ) + finally: + for f in files: + f.close() + else: + data = await state.http.send_message( + channel.id, + content, + tts=tts, + embed=embed, + embeds=embeds, + nonce=nonce, + enforce_nonce=enforce_nonce, + allowed_mentions=allowed_mentions, + message_reference=_reference, + stickers=stickers, + components=components, + flags=flags.value, + poll=poll, + ) + + ret = state.create_message(channel=channel, data=data) + if view: + if view.is_dispatchable(): + state.store_view(view, ret.id) + view.message = ret + view._refresh(ret.components) + + if delete_after is not None: + await ret.delete(delay=delete_after) + return ret + + async def trigger_typing(self) -> None: + """|coro| + + Triggers a *typing* indicator to the destination. + + *Typing* indicator will go away after 10 seconds, or after a message is sent. + """ + + channel = await self._get_channel() + await self._state.http.send_typing(channel.id) + + def typing(self) -> Typing: + """Returns a context manager that allows you to type for an indefinite period of time. + + This is useful for denoting long computations in your bot. + + .. note:: + + This is both a regular context manager and an async context manager. + This means that both ``with`` and ``async with`` work with this. + + Example Usage: :: + + async with channel.typing(): + # simulate something heavy + await asyncio.sleep(10) + + await channel.send('done!') + """ + return Typing(self) + + async def fetch_message(self, id: int, /) -> Message: + """|coro| + + Retrieves a single :class:`~discord.Message` from the destination. + + Parameters + ---------- + id: :class:`int` + The message ID to look for. + + Returns + ------- + :class:`~discord.Message` + The message asked for. + + Raises + ------ + ~discord.NotFound + The specified message was not found. + ~discord.Forbidden + You do not have the permissions required to get a message. + ~discord.HTTPException + Retrieving the message failed. + """ + + channel = await self._get_channel() + data = await self._state.http.get_message(channel.id, id) + return self._state.create_message(channel=channel, data=data) + + def pins( + self, + *, + limit: int | None = 50, + before: SnowflakeTime | None = None, + ) -> MessagePinIterator: + """Returns a :class:`~discord.MessagePinIterator` that enables receiving the destination's pinned messages. + + You must have :attr:`~discord.Permissions.read_message_history` permissions to use this. + + .. warning:: + + Starting from version 3.0, `await channel.pins()` will no longer return a list of :class:`Message`. See examples below for new usage instead. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of pinned messages to retrieve. + If ``None``, retrieves every pinned message in the channel. + before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages pinned before this datetime. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + + Yields + ------ + :class:`~discord.MessagePin` + The pinned message. + + Raises + ------ + ~discord.Forbidden + You do not have permissions to get pinned messages. + ~discord.HTTPException + The request to get pinned messages failed. + + Examples + -------- + + Usage :: + + counter = 0 + async for pin in channel.pins(limit=250): + if pin.message.author == client.user: + counter += 1 + + Flattening into a list: :: + + pins = await channel.pins(limit=None).flatten() + # pins is now a list of MessagePin... + + All parameters are optional. + """ + return MessagePinIterator( + self, + limit=limit, + before=before, + ) + + def can_send(self, *objects) -> bool: + """Returns a :class:`bool` indicating whether you have the permissions to send the object(s). + + Returns + ------- + :class:`bool` + Indicates whether you have the permissions to send the object(s). + + Raises + ------ + TypeError + An invalid type has been passed. + """ + mapping = { + "Message": "send_messages", + "Embed": "embed_links", + "File": "attach_files", + "GuildEmoji": "use_external_emojis", + "GuildSticker": "use_external_stickers", + } + # Can't use channel = await self._get_channel() since its async + if hasattr(self, "permissions_for"): + channel = self + elif hasattr(self, "channel") and type(self.channel).__name__ not in ( + "DMChannel", + "GroupChannel", + ): + channel = self.channel + else: + return True # Permissions don't exist for User DMs + + objects = (None,) + objects # Makes sure we check for send_messages first + + for obj in objects: + try: + if obj is None: + permission = mapping["Message"] + else: + permission = ( + mapping.get(type(obj).__name__) or mapping[obj.__name__] + ) + + if type(obj).__name__ == "GuildEmoji": + if ( + obj._to_partial().is_unicode_emoji + or obj.guild_id == channel.guild.id + ): + continue + elif type(obj).__name__ == "GuildSticker": + if obj.guild_id == channel.guild.id: + continue + + except (KeyError, AttributeError): + raise TypeError(f"The object {obj} is of an invalid type.") + + if not getattr(channel.permissions_for(channel.guild.me), permission): + return False + + return True + + def history( + self, + *, + limit: int | None = 100, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + around: SnowflakeTime | None = None, + oldest_first: bool | None = None, + ) -> HistoryIterator: + """Returns an :class:`~discord.AsyncIterator` that enables receiving the destination's message history. + + You must have :attr:`~discord.Permissions.read_message_history` permissions to use this. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of messages to retrieve. + If ``None``, retrieves every message in the channel. Note, however, + that this would make it a slow operation. + before: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages before this date or message. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + after: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages after this date or message. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + around: Optional[Union[:class:`~discord.abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve messages around this date or message. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + When using this argument, the maximum limit is 101. Note that if the limit is an + even number, then this will return at most limit + 1 messages. + oldest_first: Optional[:class:`bool`] + If set to ``True``, return messages in oldest->newest order. Defaults to ``True`` if + ``after`` is specified, otherwise ``False``. + + Yields + ------ + :class:`~discord.Message` + The message with the message data parsed. + + Raises + ------ + ~discord.Forbidden + You do not have permissions to get channel message history. + ~discord.HTTPException + The request to get message history failed. + + Examples + -------- + + Usage :: + + counter = 0 + async for message in channel.history(limit=200): + if message.author == client.user: + counter += 1 + + Flattening into a list: :: + + messages = await channel.history(limit=123).flatten() + # messages is now a list of Message... + + All parameters are optional. + """ + return HistoryIterator( + self, + limit=limit, + before=before, + after=after, + around=around, + oldest_first=oldest_first, + ) + + +class Connectable(Protocol): + """An ABC that details the common operations on a channel that can + connect to a voice server. + + The following implement this ABC: + + - :class:`~discord.VoiceChannel` + - :class:`~discord.StageChannel` + + Note + ---- + This ABC is not decorated with :func:`typing.runtime_checkable`, so will fail :func:`isinstance`/:func:`issubclass` + checks. + """ + + __slots__ = () + _state: ConnectionState + id: int + + def _get_voice_client_key(self) -> tuple[int, str]: + raise NotImplementedError + + def _get_voice_state_pair(self) -> tuple[int, int]: + raise NotImplementedError + + async def connect( + self, + *, + timeout: float = 60.0, + reconnect: bool = True, + cls: Callable[[Client, Connectable], T] = MISSING, + ) -> T: + """|coro| + + Connects to voice and creates a :class:`VoiceClient` to establish + your connection to the voice server. + + This requires :attr:`Intents.voice_states`. + + Parameters + ---------- + timeout: :class:`float` + The timeout in seconds to wait for the voice endpoint. + reconnect: :class:`bool` + Whether the bot should automatically attempt + a reconnect if a part of the handshake fails + or the gateway goes down. + cls: Type[:class:`VoiceProtocol`] + A type that subclasses :class:`~discord.VoiceProtocol` to connect with. + Defaults to :class:`~discord.VoiceClient`. + + Returns + ------- + :class:`~discord.VoiceProtocol` + A voice client that is fully connected to the voice server. + + Raises + ------ + asyncio.TimeoutError + Could not connect to the voice channel in time. + ~discord.ClientException + You are already connected to a voice channel. + ~discord.opus.OpusNotLoaded + The opus library has not been loaded. + """ + + # import directly from _types so if the user does not have davey + # it won't error here + from .voice._types import VoiceProtocol + + if cls is MISSING: + # if the user passes no cls, then actually import VoiceClient + from .voice import VoiceClient + + cls = VoiceClient # pyright: ignore[reportAssignmentType] + + key_id, _ = self._get_voice_client_key() + state = self._state + + if state._get_voice_client(key_id): + raise ClientException("Already connected to a voice channel.") + + client = state._get_client() + voice = cls(client, self) + + if not isinstance(voice, VoiceProtocol): + raise TypeError("Type must meet VoiceProtocol abstract base class.") + + state._add_voice_client(key_id, voice) + + try: + await voice.connect(timeout=timeout, reconnect=reconnect) + except asyncio.TimeoutError: + try: + await voice.disconnect(force=True) + except Exception: + # we don't care if disconnect failed because connection failed + pass + raise # re-raise + + return voice + + +class Mentionable: + # TODO: documentation, methods if needed + pass diff --git a/venv/lib/python3.11/site-packages/discord/activity.py b/venv/lib/python3.11/site-packages/discord/activity.py new file mode 100644 index 0000000..81128a5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/activity.py @@ -0,0 +1,881 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING, Any, Union, overload + +from .asset import Asset +from .colour import Colour +from .enums import ActivityType, try_enum +from .partial_emoji import PartialEmoji +from .utils import _get_as_snowflake + +__all__ = ( + "BaseActivity", + "Activity", + "Streaming", + "Game", + "Spotify", + "CustomActivity", +) + +"""If you're curious, this is the current schema for an activity. + +It's fairly long so I will document it here: + +All keys are optional. + +state: str (max: 128), +details: str (max: 128) +timestamps: dict + start: int (min: 1) + end: int (min: 1) +assets: dict + large_image: str (max: 32) + large_text: str (max: 128) + small_image: str (max: 32) + small_text: str (max: 128) +party: dict + id: str (max: 128), + size: List[int] (max-length: 2) + elem: int (min: 1) +secrets: dict + match: str (max: 128) + join: str (max: 128) + spectate: str (max: 128) +instance: bool +application_id: str +name: str (max: 128) +url: str +type: int +sync_id: str +session_id: str +flags: int +buttons: list[dict] + label: str (max: 32) + url: str (max: 512) +NOTE: Bots cannot access a user's activity button URLs. When received through the +gateway, the type of the buttons field will be list[str]. + +There are also activity flags which are mostly uninteresting for the library atm. + +t.ActivityFlags = { + INSTANCE: 1, + JOIN: 2, + SPECTATE: 4, + JOIN_REQUEST: 8, + SYNC: 16, + PLAY: 32 +} +""" + +if TYPE_CHECKING: + from .types.activity import Activity as ActivityPayload + from .types.activity import ActivityAssets, ActivityParty, ActivityTimestamps + + +class BaseActivity: + """The base activity that all user-settable activities inherit from. + A user-settable activity is one that can be used in :meth:`Client.change_presence`. + + The following types currently count as user-settable: + + - :class:`Activity` + - :class:`Game` + - :class:`Streaming` + - :class:`CustomActivity` + + Note that although these types are considered user-settable by the library, + Discord typically ignores certain combinations of activity depending on + what is currently set. This behaviour may change in the future so there are + no guarantees on whether Discord will actually let you set these types. + + .. versionadded:: 1.3 + """ + + __slots__ = ("_created_at",) + + def __init__(self, **kwargs): + self._created_at: float | None = kwargs.pop("created_at", None) + + @property + def created_at(self) -> datetime.datetime | None: + """When the user started doing this activity in UTC. + + .. versionadded:: 1.3 + """ + if self._created_at is not None: + return datetime.datetime.fromtimestamp( + self._created_at / 1000, tz=datetime.timezone.utc + ) + + def to_dict(self) -> ActivityPayload: + raise NotImplementedError + + +class Activity(BaseActivity): + """Represents an activity in Discord. + + This could be an activity such as streaming, playing, listening + or watching. + + For memory optimisation purposes, some activities are offered in slimmed + down versions: + + - :class:`Game` + - :class:`Streaming` + + Attributes + ---------- + application_id: Optional[:class:`int`] + The application ID of the game. + name: Optional[:class:`str`] + The name of the activity. + url: Optional[:class:`str`] + A stream URL that the activity could be doing. + type: :class:`ActivityType` + The type of activity currently being done. + state: Optional[:class:`str`] + The user's current party status or text used for a custom status. + details: Optional[:class:`str`] + The detail of the user's current activity. + timestamps: Dict[:class:`str`, :class:`int`] + A dictionary of timestamps. It contains the following optional keys: + + - ``start``: Corresponds to when the user started doing the + activity in milliseconds since Unix epoch. + - ``end``: Corresponds to when the user will finish doing the + activity in milliseconds since Unix epoch. + + assets: Dict[:class:`str`, :class:`str`] + A dictionary representing the images and their hover text of an activity. + It contains the following optional keys: + + - ``large_image``: A string representing the ID for the large image asset. + - ``large_text``: A string representing the text when hovering over the large image asset. + - ``small_image``: A string representing the ID for the small image asset. + - ``small_text``: A string representing the text when hovering over the small image asset. + + party: Dict[:class:`str`, Union[:class:`str`, List[:class:`int`]]] + A dictionary representing the activity party. It contains the following optional keys: + + - ``id``: A string representing the party ID. + - ``size``: A list of up to two integer elements denoting (current_size, maximum_size). + buttons: Union[List[Dict[:class:`str`, :class:`str`]], List[:class:`str`]] + A list of dictionaries representing custom buttons shown in a rich presence. + Each dictionary contains the following keys: + + - ``label``: A string representing the text shown on the button. + - ``url``: A string representing the URL opened upon clicking the button. + + .. note:: + + Bots cannot access a user's activity button URLs. Therefore, the type of this attribute + will be List[:class:`str`] when received through the gateway. + + .. versionadded:: 2.0 + + emoji: Optional[:class:`PartialEmoji`] + The emoji that belongs to this activity. + """ + + __slots__ = ( + "state", + "details", + "_created_at", + "timestamps", + "assets", + "party", + "flags", + "sync_id", + "session_id", + "type", + "name", + "url", + "application_id", + "emoji", + "buttons", + ) + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.state: str | None = kwargs.pop("state", None) + self.details: str | None = kwargs.pop("details", None) + self.timestamps: ActivityTimestamps = kwargs.pop("timestamps", {}) + self.assets: ActivityAssets = kwargs.pop("assets", {}) + self.party: ActivityParty = kwargs.pop("party", {}) + self.application_id: int | None = _get_as_snowflake(kwargs, "application_id") + self.url: str | None = kwargs.pop("url", None) + self.flags: int = kwargs.pop("flags", 0) + self.sync_id: str | None = kwargs.pop("sync_id", None) + self.session_id: str | None = kwargs.pop("session_id", None) + self.buttons: list[str] = kwargs.pop("buttons", []) + + activity_type = kwargs.pop("type", -1) + self.type: ActivityType = ( + activity_type + if isinstance(activity_type, ActivityType) + else try_enum(ActivityType, activity_type) + ) + self.name: str | None = kwargs.pop( + "name", "Custom Status" if self.type == ActivityType.custom else None + ) + + emoji = kwargs.pop("emoji", None) + self.emoji: PartialEmoji | None = ( + PartialEmoji.from_dict(emoji) if emoji is not None else None + ) + + def __repr__(self) -> str: + attrs = ( + ("type", self.type), + ("name", self.name), + ("state", self.state), + ("url", self.url), + ("details", self.details), + ("application_id", self.application_id), + ("session_id", self.session_id), + ("emoji", self.emoji), + ) + inner = " ".join("%s=%r" % t for t in attrs) + return f"" + + def to_dict(self) -> dict[str, Any]: + ret: dict[str, Any] = {} + for attr in self.__slots__: + value = getattr(self, attr, None) + if value is None: + continue + + if isinstance(value, dict) and len(value) == 0: + continue + + ret[attr] = value + ret["type"] = int(self.type) + if self.emoji: + ret["emoji"] = self.emoji.to_dict() + return ret + + @property + def start(self) -> datetime.datetime | None: + """When the user started doing this activity in UTC, if applicable.""" + try: + timestamp = self.timestamps["start"] / 1000 + except KeyError: + return None + else: + return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc) + + @property + def end(self) -> datetime.datetime | None: + """When the user will stop doing this activity in UTC, if applicable.""" + try: + timestamp = self.timestamps["end"] / 1000 + except KeyError: + return None + else: + return datetime.datetime.fromtimestamp(timestamp, tz=datetime.timezone.utc) + + @property + def large_image_url(self) -> str | None: + """Returns a URL pointing to the large image asset of this activity if applicable.""" + if self.application_id is None: + return None + + try: + large_image = self.assets["large_image"] + except KeyError: + return None + else: + return f"{Asset.BASE}/app-assets/{self.application_id}/{large_image}.png" + + @property + def small_image_url(self) -> str | None: + """Returns a URL pointing to the small image asset of this activity if applicable.""" + if self.application_id is None: + return None + + try: + small_image = self.assets["small_image"] + except KeyError: + return None + else: + return f"{Asset.BASE}/app-assets/{self.application_id}/{small_image}.png" + + @property + def large_image_text(self) -> str | None: + """Returns the large image asset hover text of this activity if applicable.""" + return self.assets.get("large_text", None) + + @property + def small_image_text(self) -> str | None: + """Returns the small image asset hover text of this activity if applicable.""" + return self.assets.get("small_text", None) + + +class Game(BaseActivity): + """A slimmed down version of :class:`Activity` that represents a Discord game. + + This is typically displayed via **Playing** on the official Discord client. + + .. container:: operations + + .. describe:: x == y + + Checks if two games are equal. + + .. describe:: x != y + + Checks if two games are not equal. + + .. describe:: hash(x) + + Returns the game's hash. + + .. describe:: str(x) + + Returns the game's name. + + Parameters + ---------- + name: :class:`str` + The game's name. + + Attributes + ---------- + name: :class:`str` + The game's name. + """ + + __slots__ = ("name", "_end", "_start") + + def __init__(self, name: str, **extra): + super().__init__(**extra) + self.name: str = name + + try: + timestamps: ActivityTimestamps = extra["timestamps"] + except KeyError: + self._start = 0 + self._end = 0 + else: + self._start = timestamps.get("start", 0) + self._end = timestamps.get("end", 0) + + @property + def type(self) -> ActivityType: + """Returns the game's type. This is for compatibility with :class:`Activity`. + + It always returns :attr:`ActivityType.playing`. + """ + return ActivityType.playing + + @property + def start(self) -> datetime.datetime | None: + """When the user started playing this game in UTC, if applicable.""" + if self._start: + return datetime.datetime.fromtimestamp( + self._start / 1000, tz=datetime.timezone.utc + ) + return None + + @property + def end(self) -> datetime.datetime | None: + """When the user will stop playing this game in UTC, if applicable.""" + if self._end: + return datetime.datetime.fromtimestamp( + self._end / 1000, tz=datetime.timezone.utc + ) + return None + + def __str__(self) -> str: + return str(self.name) + + def __repr__(self) -> str: + return f"" + + def to_dict(self) -> dict[str, Any]: + timestamps: dict[str, Any] = {} + if self._start: + timestamps["start"] = self._start + + if self._end: + timestamps["end"] = self._end + + return { + "type": ActivityType.playing.value, + "name": str(self.name), + "timestamps": timestamps, + } + + def __eq__(self, other: Any) -> bool: + return isinstance(other, Game) and other.name == self.name + + def __hash__(self) -> int: + return hash(self.name) + + +class Streaming(BaseActivity): + """A slimmed down version of :class:`Activity` that represents a Discord streaming status. + + This is typically displayed via **Streaming** on the official Discord client. + + .. container:: operations + + .. describe:: x == y + + Checks if two streams are equal. + + .. describe:: x != y + + Checks if two streams are not equal. + + .. describe:: hash(x) + + Returns the stream's hash. + + .. describe:: str(x) + + Returns the stream's name. + + Attributes + ---------- + platform: Optional[:class:`str`] + Where the user is streaming from (ie. YouTube, Twitch). + + .. versionadded:: 1.3 + + name: Optional[:class:`str`] + The stream's name. + details: Optional[:class:`str`] + An alias for :attr:`name` + game: Optional[:class:`str`] + The game being streamed. + + .. versionadded:: 1.3 + + url: :class:`str` + The stream's URL. + assets: Dict[:class:`str`, :class:`str`] + A dictionary comprised of similar keys than those in :attr:`Activity.assets`. + """ + + __slots__ = ("platform", "name", "game", "url", "details", "assets") + + def __init__(self, *, name: str | None, url: str, **extra: Any): + super().__init__(**extra) + self.platform: str | None = name + self.name: str | None = extra.pop("details", name) + self.game: str | None = extra.pop("state", None) + self.url: str = url + self.details: str | None = extra.pop("details", self.name) # compatibility + self.assets: ActivityAssets = extra.pop("assets", {}) + + @property + def type(self) -> ActivityType: + """Returns the game's type. This is for compatibility with :class:`Activity`. + + It always returns :attr:`ActivityType.streaming`. + """ + return ActivityType.streaming + + def __str__(self) -> str: + return str(self.name) + + def __repr__(self) -> str: + return f"" + + @property + def twitch_name(self) -> str | None: + """If provided, the twitch name of the user streaming. + + This corresponds to the ``large_image`` key of the :attr:`Streaming.assets` + dictionary if it starts with ``twitch:``. Typically this is set by the Discord client. + """ + + try: + name = self.assets["large_image"] + except KeyError: + return None + else: + return name[7:] if name[:7] == "twitch:" else None + + def to_dict(self) -> dict[str, Any]: + ret: dict[str, Any] = { + "type": ActivityType.streaming.value, + "name": str(self.name), + "url": str(self.url), + "assets": self.assets, + } + if self.details: + ret["details"] = self.details + return ret + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, Streaming) + and other.name == self.name + and other.url == self.url + ) + + def __hash__(self) -> int: + return hash(self.name) + + +class Spotify: + """Represents a Spotify listening activity from Discord. This is a special case of + :class:`Activity` that makes it easier to work with the Spotify integration. + + .. container:: operations + + .. describe:: x == y + + Checks if two activities are equal. + + .. describe:: x != y + + Checks if two activities are not equal. + + .. describe:: hash(x) + + Returns the activity's hash. + + .. describe:: str(x) + + Returns the string 'Spotify'. + """ + + __slots__ = ( + "_state", + "_details", + "_timestamps", + "_assets", + "_party", + "_sync_id", + "_session_id", + "_created_at", + ) + + def __init__(self, **data): + self._state: str = data.pop("state", "") + self._details: str = data.pop("details", "") + self._timestamps: dict[str, int] = data.pop("timestamps", {}) + self._assets: ActivityAssets = data.pop("assets", {}) + self._party: ActivityParty = data.pop("party", {}) + self._sync_id: str = data.pop("sync_id") + self._session_id: str = data.pop("session_id") + self._created_at: float | None = data.pop("created_at", None) + + @property + def type(self) -> ActivityType: + """Returns the activity's type. This is for compatibility with :class:`Activity`. + + It always returns :attr:`ActivityType.listening`. + """ + return ActivityType.listening + + @property + def created_at(self) -> datetime.datetime | None: + """When the user started listening in UTC. + + .. versionadded:: 1.3 + """ + if self._created_at is not None: + return datetime.datetime.fromtimestamp( + self._created_at / 1000, tz=datetime.timezone.utc + ) + + @property + def colour(self) -> Colour: + """Returns the Spotify integration colour, as a :class:`Colour`. + + There is an alias for this named :attr:`color` + """ + return Colour(0x1DB954) + + @property + def color(self) -> Colour: + """Returns the Spotify integration colour, as a :class:`Colour`. + + There is an alias for this named :attr:`colour` + """ + return self.colour + + def to_dict(self) -> dict[str, Any]: + return { + "flags": 48, # SYNC | PLAY + "name": "Spotify", + "assets": self._assets, + "party": self._party, + "sync_id": self._sync_id, + "session_id": self._session_id, + "timestamps": self._timestamps, + "details": self._details, + "state": self._state, + } + + @property + def name(self) -> str: + """The activity's name. This will always return "Spotify".""" + return "Spotify" + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, Spotify) + and other._session_id == self._session_id + and other._sync_id == self._sync_id + and other.start == self.start + ) + + def __hash__(self) -> int: + return hash(self._session_id) + + def __str__(self) -> str: + return "Spotify" + + def __repr__(self) -> str: + return ( + "" + ) + + @property + def title(self) -> str: + """The title of the song being played.""" + return self._details + + @property + def artists(self) -> list[str]: + """The artists of the song being played.""" + return self._state.split("; ") + + @property + def artist(self) -> str: + """The artist of the song being played. + + This does not attempt to split the artist information into + multiple artists. Useful if there's only a single artist. + """ + return self._state + + @property + def album(self) -> str: + """The album that the song being played belongs to.""" + return self._assets.get("large_text", "") + + @property + def album_cover_url(self) -> str: + """The album cover image URL from Spotify's CDN.""" + large_image = self._assets.get("large_image", "") + if large_image[:8] != "spotify:": + return "" + album_image_id = large_image[8:] + return f"https://i.scdn.co/image/{album_image_id}" + + @property + def track_id(self) -> str: + """The track ID used by Spotify to identify this song.""" + return self._sync_id + + @property + def track_url(self) -> str: + """The track URL to listen on Spotify. + + .. versionadded:: 2.0 + """ + return f"https://open.spotify.com/track/{self.track_id}" + + @property + def start(self) -> datetime.datetime: + """When the user started playing this song in UTC.""" + return datetime.datetime.fromtimestamp( + self._timestamps["start"] / 1000, tz=datetime.timezone.utc + ) + + @property + def end(self) -> datetime.datetime: + """When the user will stop playing this song in UTC.""" + return datetime.datetime.fromtimestamp( + self._timestamps["end"] / 1000, tz=datetime.timezone.utc + ) + + @property + def duration(self) -> datetime.timedelta: + """The duration of the song being played.""" + return self.end - self.start + + @property + def party_id(self) -> str: + """The party ID of the listening party.""" + return self._party.get("id", "") + + +class CustomActivity(BaseActivity): + """Represents a Custom activity from Discord. + + .. container:: operations + + .. describe:: x == y + + Checks if two activities are equal. + + .. describe:: x != y + + Checks if two activities are not equal. + + .. describe:: hash(x) + + Returns the activity's hash. + + .. describe:: str(x) + + Returns the custom status text. + + .. versionadded:: 1.3 + + Attributes + ---------- + name: Optional[:class:`str`] + The custom activity's name. + emoji: Optional[:class:`PartialEmoji`] + The emoji to pass to the activity, if any. + state: Optional[:class:`str`] + The text used for the custom activity. + """ + + __slots__ = ("name", "emoji", "state") + + def __init__( + self, name: str | None, *, emoji: PartialEmoji | None = None, **extra: Any + ): + super().__init__(**extra) + self.name: str | None = name + self.state: str | None = extra.pop("state", name) + if self.name == "Custom Status": + self.name = self.state + + self.emoji: PartialEmoji | None + if emoji is None: + self.emoji = emoji + elif isinstance(emoji, dict): + self.emoji = PartialEmoji.from_dict(emoji) + elif isinstance(emoji, str): + self.emoji = PartialEmoji(name=emoji) + elif isinstance(emoji, PartialEmoji): + self.emoji = emoji + else: + raise TypeError( + "Expected str, PartialEmoji, or None, received" + f" {type(emoji)!r} instead." + ) + + @property + def type(self) -> ActivityType: + """Returns the activity's type. This is for compatibility with :class:`Activity`. + + It always returns :attr:`ActivityType.custom`. + """ + return ActivityType.custom + + def to_dict(self) -> dict[str, Any]: + if self.name == self.state: + o = { + "type": ActivityType.custom.value, + "state": self.name, + "name": "Custom Status", + } + else: + o = { + "type": ActivityType.custom.value, + "name": self.name, + } + + if self.emoji: + o["emoji"] = self.emoji.to_dict() + return o + + def __eq__(self, other: Any) -> bool: + return ( + isinstance(other, CustomActivity) + and other.name == self.name + and other.emoji == self.emoji + ) + + def __hash__(self) -> int: + return hash((self.name, str(self.emoji))) + + def __str__(self) -> str: + if not self.emoji: + return str(self.name) + if self.name: + return f"{self.emoji} {self.name}" + return str(self.emoji) + + def __repr__(self) -> str: + return f"" + + +ActivityTypes = Union[Activity, Game, CustomActivity, Streaming, Spotify] + + +@overload +def create_activity(data: ActivityPayload) -> ActivityTypes: ... + + +@overload +def create_activity(data: None) -> None: ... + + +def create_activity(data: ActivityPayload | None) -> ActivityTypes | None: + if not data: + return None + + game_type = try_enum(ActivityType, data.get("type", -1)) + if game_type is ActivityType.playing: + if "application_id" in data or "session_id" in data: + return Activity(**data) + return Game(**data) + elif game_type is ActivityType.custom: + try: + name = data.pop("name") + except KeyError: + return Activity(**data) + else: + # we removed the name key from data already + return CustomActivity(name=name, **data) # type: ignore + elif game_type is ActivityType.streaming: + if "url" in data: + # the url won't be None here + return Streaming(**data) # type: ignore + return Activity(**data) + elif ( + game_type is ActivityType.listening + and "sync_id" in data + and "session_id" in data + ): + return Spotify(**data) + return Activity(**data) diff --git a/venv/lib/python3.11/site-packages/discord/appinfo.py b/venv/lib/python3.11/site-packages/discord/appinfo.py new file mode 100644 index 0000000..e3a415e --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/appinfo.py @@ -0,0 +1,650 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any + +from . import utils +from .asset import Asset +from .enums import ApplicationEventWebhookStatus, try_enum +from .flags import ApplicationFlags +from .permissions import Permissions + +if TYPE_CHECKING: + from .guild import Guild + from .state import ConnectionState + from .types.appinfo import AppInfo as AppInfoPayload + from .types.appinfo import AppInstallParams as AppInstallParamsPayload + from .types.appinfo import PartialAppInfo as PartialAppInfoPayload + from .types.appinfo import Team as TeamPayload + from .user import User + +__all__ = ( + "AppInfo", + "PartialAppInfo", + "AppInstallParams", + "IntegrationTypesConfig", + "ApplicationEventWebhookStatus", +) + + +class AppInfo: + """Represents the application info for the bot provided by Discord. + + Attributes + ---------- + id: :class:`int` + The application ID. + name: :class:`str` + The application name. + owner: :class:`User` + The application owner. + team: Optional[:class:`Team`] + The application's team. + + .. versionadded:: 1.3 + + description: :class:`str` + The application description. + bot_public: :class:`bool` + Whether the bot can be invited by anyone or if it is locked + to the application owner. + bot_require_code_grant: :class:`bool` + Whether the bot requires the completion of the full OAuth2 code + grant flow to join. + rpc_origins: Optional[List[:class:`str`]] + A list of RPC origin URLs, if RPC is enabled. + + verify_key: :class:`str` + The hex encoded key for verification in interactions. + + .. versionadded:: 1.3 + + guild_id: Optional[:class:`int`] + If this application is a game sold on Discord, + this field will be the guild to which it has been linked to. + + .. versionadded:: 1.3 + + primary_sku_id: Optional[:class:`int`] + If this application is a game sold on Discord, + this field will be the id of the "Game SKU" that is created, + if it exists. + + .. versionadded:: 1.3 + + slug: Optional[:class:`str`] + If this application is a game sold on Discord, + this field will be the URL slug that links to the store page. + + .. versionadded:: 1.3 + + terms_of_service_url: Optional[:class:`str`] + The application's terms of service URL, if set. + + .. versionadded:: 2.0 + + privacy_policy_url: Optional[:class:`str`] + The application's privacy policy URL, if set. + + .. versionadded:: 2.0 + + approximate_guild_count: Optional[:class:`int`] + The approximate count of guilds to which the app has been added, if any. + + .. versionadded:: 2.7 + + approximate_user_install_count: Optional[:class:`int`] + The approximate count of users who have installed the application, if any. + + .. versionadded:: 2.7 + + redirect_uris: Optional[List[:class:`str`]] + The list of redirect URIs for the application, if set. + + .. versionadded:: 2.7 + + interactions_endpoint_url: Optional[:class:`str`] + The interactions endpoint URL for the application, if set. + + .. versionadded:: 2.7 + + role_connections_verification_url: Optional[:class:`str`] + The role connection verification URL for the application, if set. + + .. versionadded:: 2.7 + + install_params: Optional[:class:`AppInstallParams`] + The settings for the application's default in-app authorization link, if set. + + .. versionchanged:: 2.8 + Fixed incorrect type documentation. + + integration_types_config: Optional[:class:`IntegrationTypesConfig`] + Per-installation context configuration for guild (``0``) and user (``1``) contexts. + + .. versionadded:: 2.8 + + event_webhooks_url: Optional[:class:`str`] + The URL used to receive application event webhooks, if set. + + .. versionadded:: 2.8 + + event_webhooks_status: Optional[:class:`ApplicationEventWebhookStatus`] + The status of event webhooks for the application, if set. + + .. versionadded:: 2.8 + + event_webhooks_types: Optional[List[:class:`str`]] + List of event webhook types subscribed to, if set. + + .. versionadded:: 2.8 + + tags: Optional[List[:class:`str`]] + The list of tags describing the content and functionality of the app, if set. + + Maximium of 5 tags. + + .. versionadded:: 2.7 + + custom_install_url: Optional[:class:`str`] + The default custom authorization URL for the application, if set. + + .. versionadded:: 2.7 + + approximate_user_authorization_count: Optional[:class:`int`] + The approximate count of users who have authorized the application, if any. + + .. versionadded:: 2.8 + + bot: Optional[:class:`User`] + The bot user associated with this application, if any. + + .. versionadded:: 2.8 + """ + + __slots__ = ( + "_state", + "description", + "id", + "name", + "rpc_origins", + "bot_public", + "bot_require_code_grant", + "owner", + "bot", + "_icon", + "_summary", + "verify_key", + "team", + "guild_id", + "primary_sku_id", + "slug", + "_cover_image", + "terms_of_service_url", + "privacy_policy_url", + "approximate_guild_count", + "approximate_user_install_count", + "approximate_user_authorization_count", + "_flags", + "redirect_uris", + "interactions_endpoint_url", + "role_connections_verification_url", + "event_webhooks_url", + "event_webhooks_status", + "event_webhooks_types", + "integration_types_config", + "install_params", + "tags", + "custom_install_url", + ) + + def __init__(self, state: ConnectionState, data: AppInfoPayload): + from .team import Team + + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self.name: str = data["name"] + self.description: str = data["description"] + self._icon: str | None = data.get("icon") + self.rpc_origins: list[str] | None = data.get("rpc_origins") + self.bot_public: bool = data.get("bot_public", True) + self.bot_require_code_grant: bool = data.get("bot_require_code_grant", False) + self.owner: User | None = ( + state.create_user(owner) + if (owner := data.get("owner")) is not None + else None + ) + + team: TeamPayload | None = data.get("team") + self.team: Team | None = Team(state, team) if team else None + + self._summary: str | None = data.get("summary") + self.verify_key: str = data["verify_key"] + self.bot: User | None = ( + state.create_user(bot) if (bot := data.get("bot")) is not None else None + ) + + self.guild_id: int | None = utils._get_as_snowflake(data, "guild_id") + + self.primary_sku_id: int | None = utils._get_as_snowflake( + data, "primary_sku_id" + ) + self.slug: str | None = data.get("slug") + self._cover_image: str | None = data.get("cover_image") + self.terms_of_service_url: str | None = data.get("terms_of_service_url") + self.privacy_policy_url: str | None = data.get("privacy_policy_url") + self.approximate_guild_count: int | None = data.get("approximate_guild_count") + self.approximate_user_install_count: int | None = data.get( + "approximate_user_install_count" + ) + self.approximate_user_authorization_count: int | None = data.get( + "approximate_user_authorization_count" + ) + self._flags: int = data.get("flags", 0) + self.redirect_uris: list[str] = data.get("redirect_uris", []) + self.interactions_endpoint_url: str | None = data.get( + "interactions_endpoint_url" + ) + self.role_connections_verification_url: str | None = data.get( + "role_connections_verification_url" + ) + self.event_webhooks_url: str | None = data.get("event_webhooks_url") + self.event_webhooks_status: ApplicationEventWebhookStatus | None = ( + try_enum(ApplicationEventWebhookStatus, status) + if (status := data.get("event_webhooks_status")) is not None + else None + ) + self.event_webhooks_types: list[str] | None = data.get("event_webhooks_types") + + self.install_params: AppInstallParams | None = ( + AppInstallParams(install_params) + if (install_params := data.get("install_params")) is not None + else None + ) + self.tags: list[str] = data.get("tags", []) + self.custom_install_url: str | None = data.get("custom_install_url") + self.integration_types_config: IntegrationTypesConfig | None = ( + IntegrationTypesConfig.from_payload(data.get("integration_types_config")) + ) + + def __repr__(self) -> str: + return ( + f"<{self.__class__.__name__} id={self.id} name={self.name!r} " + f"description={self.description!r} public={self.bot_public} " + f"owner={self.owner!r}>" + ) + + @property + def flags(self) -> ApplicationFlags: + """The public application flags. + + Returns an :class:`ApplicationFlags` instance. + + .. versionadded:: 2.8 + """ + return ApplicationFlags._from_value(self._flags) + + async def edit( + self, + *, + description: str | None = utils.MISSING, + icon: bytes | None = utils.MISSING, + cover_image: bytes | None = utils.MISSING, + tags: list[str] | None = utils.MISSING, + terms_of_service_url: str | None = utils.MISSING, + privacy_policy_url: str | None = utils.MISSING, + interactions_endpoint_url: str | None = utils.MISSING, + role_connections_verification_url: str | None = utils.MISSING, + install_params: AppInstallParams | None = utils.MISSING, + custom_install_url: str | None = utils.MISSING, + integration_types_config: IntegrationTypesConfig | None = utils.MISSING, + flags: ApplicationFlags | None = utils.MISSING, + event_webhooks_url: str | None = utils.MISSING, + event_webhooks_status: ApplicationEventWebhookStatus = utils.MISSING, + event_webhooks_types: list[str] | None = utils.MISSING, + ) -> AppInfo: + """|coro| + + Edit the current application's settings. + + .. versionadded:: 2.8 + + Parameters + ---------- + description: Optional[:class:`str`] + The new application description or ``None`` to clear. + icon: Optional[:class:`bytes`] + New icon image. If ``bytes`` is given it will be base64 encoded automatically. Pass ``None`` to clear. + cover_image: Optional[:class:`bytes`] + New cover image for the store embed. If ``bytes`` is given it will be base64 encoded automatically. Pass ``None`` to clear. + tags: Optional[List[:class:`str`]] + List of tags for the application (max 5). Pass ``None`` to clear. + terms_of_service_url: Optional[:class:`str`] + The application's Terms of Service URL. Pass ``None`` to clear. + privacy_policy_url: Optional[:class:`str`] + The application's Privacy Policy URL. Pass ``None`` to clear. + interactions_endpoint_url: Optional[:class:`str`] + The interactions endpoint callback URL. Pass ``None`` to clear. + role_connections_verification_url: Optional[:class:`str`] + The role connection verification URL for the application. Pass ``None`` to clear. + install_params: Optional[:class:`AppInstallParams`] + Settings for the application's default in-app authorization link. Pass ``None`` to clear. Omit entirely to leave unchanged. + custom_install_url: Optional[:class:`str`] + The default custom authorization URL for the application. Pass ``None`` to clear. + integration_types_config: Optional[:class:`IntegrationTypesConfig`] + Object specifying per-installation context configuration (guild and/or user). You may set contexts individually + and omit others to leave them unchanged. Pass the object with a context explicitly set to ``None`` to clear just that + context, or pass ``None`` to clear the entire integration types configuration. + flags: Optional[:class:`ApplicationFlags`] + Application public flags. Pass ``None`` to clear (not typical). + event_webhooks_url: Optional[:class:`str`] + Event webhooks callback URL for receiving application webhook events. Pass ``None`` to clear. + event_webhooks_status: :class:`ApplicationEventWebhookStatus` + The desired webhook status. + event_webhooks_types: Optional[List[:class:`str`]] + List of webhook event types to subscribe to. Pass ``None`` to clear. + + Returns + ------- + :class:`.AppInfo` + The updated application information. + """ + payload: dict[str, Any] = {} + if description is not utils.MISSING: + payload["description"] = description + if icon is not utils.MISSING: + if icon is None: + payload["icon"] = None + else: + payload["icon"] = utils._bytes_to_base64_data(icon) + if cover_image is not utils.MISSING: + if cover_image is None: + payload["cover_image"] = None + else: + payload["cover_image"] = utils._bytes_to_base64_data(cover_image) + if tags is not utils.MISSING: + payload["tags"] = tags + if terms_of_service_url is not utils.MISSING: + payload["terms_of_service_url"] = terms_of_service_url + if privacy_policy_url is not utils.MISSING: + payload["privacy_policy_url"] = privacy_policy_url + if interactions_endpoint_url is not utils.MISSING: + payload["interactions_endpoint_url"] = interactions_endpoint_url + if role_connections_verification_url is not utils.MISSING: + payload["role_connections_verification_url"] = ( + role_connections_verification_url + ) + if install_params is not utils.MISSING: + if install_params is None: + payload["install_params"] = None + else: + payload["install_params"] = install_params._to_payload() + if custom_install_url is not utils.MISSING: + payload["custom_install_url"] = custom_install_url + if integration_types_config is not utils.MISSING: + if integration_types_config is None: + payload["integration_types_config"] = None + else: + payload["integration_types_config"] = ( + integration_types_config._to_payload() + ) + if flags is not utils.MISSING: + payload["flags"] = None if flags is None else flags.value + if event_webhooks_url is not utils.MISSING: + payload["event_webhooks_url"] = event_webhooks_url + if event_webhooks_status is not utils.MISSING: + payload["event_webhooks_status"] = event_webhooks_status.value + if event_webhooks_types is not utils.MISSING: + payload["event_webhooks_types"] = event_webhooks_types + + data = await self._state.http.edit_current_application_info(payload) + return AppInfo(self._state, data) + + @property + def icon(self) -> Asset | None: + """Retrieves the application's icon asset, if any.""" + if self._icon is None: + return None + return Asset._from_icon(self._state, self.id, self._icon, path="app") + + @property + def cover_image(self) -> Asset | None: + """Retrieves the cover image on a store embed, if any. + + This is only available if the application is a game sold on Discord. + """ + if self._cover_image is None: + return None + return Asset._from_cover_image(self._state, self.id, self._cover_image) + + @property + def guild(self) -> Guild | None: + """If this application is a game sold on Discord, + this field will be the guild to which it has been linked. + + .. versionadded:: 1.3 + """ + return self._state._get_guild(self.guild_id) + + @property + def summary(self) -> str | None: + """If this application is a game sold on Discord, + this field will be the summary field for the store page of its primary SKU. + + It currently returns an empty string. + + .. versionadded:: 1.3 + .. deprecated:: 2.7 + """ + utils.warn_deprecated( + "summary", + "description", + reference="https://docs.discord.com/developers/resources/application#application-object-application-structure", + ) + return self._summary + + +class PartialAppInfo: + """Represents a partial AppInfo given by :func:`~discord.abc.GuildChannel.create_invite` + + .. versionadded:: 2.0 + + Attributes + ---------- + id: :class:`int` + The application ID. + name: :class:`str` + The application name. + description: :class:`str` + The application description. + rpc_origins: Optional[List[:class:`str`]] + A list of RPC origin URLs, if RPC is enabled. + summary: :class:`str` + If this application is a game sold on Discord, + this field will be the summary field for the store page of its primary SKU. + verify_key: :class:`str` + The hex encoded key for verification in interactions. + terms_of_service_url: Optional[:class:`str`] + The application's terms of service URL, if set. + privacy_policy_url: Optional[:class:`str`] + The application's privacy policy URL, if set. + """ + + __slots__ = ( + "_state", + "id", + "name", + "description", + "rpc_origins", + "summary", + "verify_key", + "terms_of_service_url", + "privacy_policy_url", + "_icon", + ) + + def __init__(self, *, state: ConnectionState, data: PartialAppInfoPayload): + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self.name: str = data["name"] + self._icon: str | None = data.get("icon") + self.description: str = data["description"] + self.rpc_origins: list[str] | None = data.get("rpc_origins") + self.summary: str = data["summary"] + self.verify_key: str = data["verify_key"] + self.terms_of_service_url: str | None = data.get("terms_of_service_url") + self.privacy_policy_url: str | None = data.get("privacy_policy_url") + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} id={self.id} name={self.name!r} description={self.description!r}>" + + @property + def icon(self) -> Asset | None: + """Retrieves the application's icon asset, if any.""" + if self._icon is None: + return None + return Asset._from_icon(self._state, self.id, self._icon, path="app") + + +class AppInstallParams: + """Represents the settings for the custom authorization URL of an application. + + .. versionadded:: 2.7 + + Attributes + ---------- + scopes: List[:class:`str`] + The list of OAuth2 scopes for adding the application to a guild. + permissions: :class:`Permissions` + The permissions to request for the bot role in the guild. + """ + + __slots__ = ("scopes", "permissions") + + def __init__(self, data: AppInstallParamsPayload) -> None: + self.scopes: list[str] = data.get("scopes", []) + self.permissions: Permissions = Permissions(int(data["permissions"])) + + def _to_payload(self) -> dict[str, object]: + """Serialize this object into an application install params payload. + + .. versionadded:: 2.8 + + Returns + ------- + Dict[str, Any] + A dict with ``scopes`` and ``permissions`` (string form) suitable for the API. + """ + if self.permissions.value > 0 and "bot" not in self.scopes: + raise ValueError( + "'bot' must be in install_params.scopes if permissions are requested" + ) + return { + "scopes": list(self.scopes), + "permissions": str(self.permissions.value), + } + + +class IntegrationTypesConfig: + """Represents per-installation context configuration for an application. + + This object is used to build the payload for the ``integration_types_config`` field when editing an application. + + .. versionadded:: 2.8 + + Parameters + ---------- + guild: Optional[:class:`AppInstallParams`] + The configuration for the guild installation context. Omit to leave unchanged; pass ``None`` to clear. + user: Optional[:class:`AppInstallParams`] + The configuration for the user installation context. Omit to leave unchanged; pass ``None`` to clear. + """ + + __slots__ = ("guild", "user") + + def __init__( + self, + *, + guild: AppInstallParams | None = utils.MISSING, + user: AppInstallParams | None = utils.MISSING, + ) -> None: + self.guild: AppInstallParams | None = guild + self.user: AppInstallParams | None = user + + @staticmethod + def _get_ctx( + raw: dict[int | str, dict[str, object] | None] | None, key: int + ) -> dict[str, object] | None: + if raw is None: + return None + if key in raw: + return raw[key] + skey = str(key) + return raw.get(skey) + + @staticmethod + def _decode_ctx(value: dict[str, Any] | None) -> AppInstallParams | None: + if value is None: + return None + params = value.get("oauth2_install_params") + if not params: + return None + return AppInstallParams(params) + + @classmethod + def from_payload( + cls, data: dict[int | str, dict[str, Any] | None] | None + ) -> IntegrationTypesConfig | None: + if data is None: + return None + guild_ctx = cls._decode_ctx(cls._get_ctx(data, 0)) + user_ctx = cls._decode_ctx(cls._get_ctx(data, 1)) + return cls(guild=guild_ctx, user=user_ctx) + + def _encode_install_params( + self, value: AppInstallParams | None + ) -> dict[str, dict[str, Any]] | None: + if value is None: + return None + return {"oauth2_install_params": value._to_payload()} + + def _to_payload(self) -> dict[int, dict[str, dict[str, Any]] | None]: + """Serialize this configuration into the payload expected by the API. + + Returns + ------- + Dict[int, Dict[str, Dict[str, Any]] | None] + Mapping of integration context IDs to encoded install parameters, or ``None`` to clear. + + .. versionadded:: 2.8 + """ + payload: dict[int, dict[str, dict[str, Any]] | None] = {} + if self.guild is not utils.MISSING: + payload[0] = self._encode_install_params(self.guild) + if self.user is not utils.MISSING: + payload[1] = self._encode_install_params(self.user) + return payload diff --git a/venv/lib/python3.11/site-packages/discord/application_role_connection.py b/venv/lib/python3.11/site-packages/discord/application_role_connection.py new file mode 100644 index 0000000..6cf9bd9 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/application_role_connection.py @@ -0,0 +1,128 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +from .enums import ApplicationRoleConnectionMetadataType, try_enum +from .utils import MISSING + +if TYPE_CHECKING: + from .types.application_role_connection import ( + ApplicationRoleConnectionMetadata as ApplicationRoleConnectionMetadataPayload, + ) + +__all__ = ("ApplicationRoleConnectionMetadata",) + + +class ApplicationRoleConnectionMetadata: + r"""Represents role connection metadata for a Discord application. + + .. versionadded:: 2.4 + + Parameters + ---------- + type: :class:`ApplicationRoleConnectionMetadataType` + The type of metadata value. + key: :class:`str` + The key for this metadata field. + May only be the ``a-z``, ``0-9``, or ``_`` characters, with a maximum of 50 characters. + name: :class:`str` + The name for this metadata field. Maximum 100 characters. + description: :class:`str` + The description for this metadata field. Maximum 200 characters. + name_localizations: Optional[Dict[:class:`str`, :class:`str`]] + The name localizations for this metadata field. The values of this should be ``"locale": "name"``. + See `here `_ for a list of valid locales. + description_localizations: Optional[Dict[:class:`str`, :class:`str`]] + The description localizations for this metadata field. The values of this should be ``"locale": "name"``. + See `here `_ for a list of valid locales. + """ + + __slots__ = ( + "type", + "key", + "name", + "description", + "name_localizations", + "description_localizations", + ) + + def __init__( + self, + *, + type: ApplicationRoleConnectionMetadataType, + key: str, + name: str, + description: str, + name_localizations: dict[str, str] = MISSING, + description_localizations: dict[str, str] = MISSING, + ): + self.type: ApplicationRoleConnectionMetadataType = type + self.key: str = key + self.name: str = name + self.name_localizations: dict[str, str] = name_localizations + self.description: str = description + self.description_localizations: dict[str, str] = description_localizations + + def __repr__(self): + return ( + "" + ) + + def __str__(self): + return self.name + + @classmethod + def from_dict( + cls, data: ApplicationRoleConnectionMetadataPayload + ) -> ApplicationRoleConnectionMetadata: + return cls( + type=try_enum(ApplicationRoleConnectionMetadataType, data["type"]), + key=data["key"], + name=data["name"], + description=data["description"], + name_localizations=data.get("name_localizations"), + description_localizations=data.get("description_localizations"), + ) + + def to_dict(self) -> ApplicationRoleConnectionMetadataPayload: + data = { + "type": self.type.value, + "key": self.key, + "name": self.name, + "description": self.description, + } + if self.name_localizations is not MISSING: + data["name_localizations"] = self.name_localizations + if self.description_localizations is not MISSING: + data["description_localizations"] = self.description_localizations + return data diff --git a/venv/lib/python3.11/site-packages/discord/asset.py b/venv/lib/python3.11/site-packages/discord/asset.py new file mode 100644 index 0000000..659d6dd --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/asset.py @@ -0,0 +1,526 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import io +import os +from typing import TYPE_CHECKING, Any, Literal + +import yarl + +from . import utils +from .errors import DiscordException, InvalidArgument + +__all__ = ("Asset",) + +if TYPE_CHECKING: + ValidStaticFormatTypes = Literal["webp", "jpeg", "jpg", "png"] + ValidAssetFormatTypes = Literal["webp", "jpeg", "jpg", "png", "gif"] + from .state import ConnectionState + + +VALID_STATIC_FORMATS = frozenset({"jpeg", "jpg", "webp", "png"}) +VALID_ASSET_FORMATS = VALID_STATIC_FORMATS | {"gif"} + + +MISSING = utils.MISSING + + +class AssetMixin: + url: str + _state: Any | None + + async def read(self) -> bytes: + """|coro| + + Retrieves the content of this asset as a :class:`bytes` object. + + Returns + ------- + :class:`bytes` + The content of the asset. + + Raises + ------ + DiscordException + There was no internal connection state. + HTTPException + Downloading the asset failed. + NotFound + The asset was deleted. + """ + if self._state is None: + raise DiscordException("Invalid state (no ConnectionState provided)") + + return await self._state.http.get_from_cdn(self.url) + + async def save( + self, + fp: str | bytes | os.PathLike | io.BufferedIOBase, + *, + seek_begin: bool = True, + ) -> int: + """|coro| + + Saves this asset into a file-like object. + + Parameters + ---------- + fp: Union[:class:`io.BufferedIOBase`, :class:`os.PathLike`] + The file-like object to save this attachment to or the filename + to use. If a filename is passed then a file is created with that + filename and used instead. + seek_begin: :class:`bool` + Whether to seek to the beginning of the file after saving is + successfully done. + + Returns + ------- + :class:`int` + The number of bytes written. + + Raises + ------ + DiscordException + There was no internal connection state. + HTTPException + Downloading the asset failed. + NotFound + The asset was deleted. + """ + + data = await self.read() + if isinstance(fp, io.BufferedIOBase): + written = fp.write(data) + if seek_begin: + fp.seek(0) + return written + else: + with open(fp, "wb") as f: + return f.write(data) + + +class Asset(AssetMixin): + """Represents a CDN asset on Discord. + + .. container:: operations + + .. describe:: str(x) + + Returns the URL of the CDN asset. + + .. describe:: len(x) + + Returns the length of the CDN asset's URL. + + .. describe:: x == y + + Checks if the asset is equal to another asset. + + .. describe:: x != y + + Checks if the asset is not equal to another asset. + + .. describe:: hash(x) + + Returns the asset's url's hash. + + This is equivalent to hash(:attr:`url`). + """ + + __slots__: tuple[str, ...] = ( + "_state", + "_url", + "_animated", + "_key", + ) + + BASE = "https://cdn.discordapp.com" + + def __init__(self, state, *, url: str, key: str, animated: bool = False): + self._state = state + self._url = url + self._animated = animated + self._key = key + + @classmethod + def _from_default_avatar(cls, state, index: int) -> Asset: + return cls( + state, + url=f"{cls.BASE}/embed/avatars/{index}.png", + key=str(index), + animated=False, + ) + + @classmethod + def _from_avatar(cls, state, user_id: int, avatar: str) -> Asset: + animated = avatar.startswith("a_") + format = "gif" if animated else "png" + return cls( + state, + url=f"{cls.BASE}/avatars/{user_id}/{avatar}.{format}?size=1024", + key=avatar, + animated=animated, + ) + + @classmethod + def _from_avatar_decoration( + cls, state, user_id: int, avatar_decoration: str + ) -> Asset: + animated = avatar_decoration.startswith("a_") + endpoint = ( + "avatar-decoration-presets" + # if avatar_decoration.startswith(("v3", "v2")) + # else f"avatar-decorations/{user_id}" + ) + return cls( + state, + url=f"{cls.BASE}/{endpoint}/{avatar_decoration}.png?size=1024", + key=avatar_decoration, + animated=animated, + ) + + @classmethod + def _from_user_primary_guild_tag( + cls, state: ConnectionState, identity_guild_id: int, badge_id: str + ) -> Asset: + """Creates an Asset for a user's primary guild (tag) badge. + + Parameters + ---------- + state: ConnectionState + The connection state. + identity_guild_id: int + The ID of the guild. + badge_id: str + The badge hash/id. + + Returns + ------- + :class:`Asset` + The primary guild badge asset. + """ + return cls( + state, + url=f"{Asset.BASE}/guild-tag-badges/{identity_guild_id}/{badge_id}.png?size=256", + key=badge_id, + animated=False, + ) + + @classmethod + def _from_guild_avatar( + cls, state, guild_id: int, member_id: int, avatar: str + ) -> Asset: + animated = avatar.startswith("a_") + format = "gif" if animated else "png" + return cls( + state, + url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/avatars/{avatar}.{format}?size=1024", + key=avatar, + animated=animated, + ) + + @classmethod + def _from_guild_banner( + cls, state, guild_id: int, member_id: int, banner: str + ) -> Asset: + animated = banner.startswith("a_") + format = "gif" if animated else "png" + return cls( + state, + url=f"{cls.BASE}/guilds/{guild_id}/users/{member_id}/banners/{banner}.{format}?size=512", + key=banner, + animated=animated, + ) + + @classmethod + def _from_icon(cls, state, object_id: int, icon_hash: str, path: str) -> Asset: + return cls( + state, + url=f"{cls.BASE}/{path}-icons/{object_id}/{icon_hash}.png?size=1024", + key=icon_hash, + animated=False, + ) + + @classmethod + def _from_cover_image(cls, state, object_id: int, cover_image_hash: str) -> Asset: + return cls( + state, + url=f"{cls.BASE}/app-assets/{object_id}/store/{cover_image_hash}.png?size=1024", + key=cover_image_hash, + animated=False, + ) + + @classmethod + def _from_collectible( + cls, state: ConnectionState, asset: str, animated: bool = False + ) -> Asset: + name = "static.png" if not animated else "asset.webm" + return cls( + state, + url=f"{cls.BASE}/assets/collectibles/{asset}{name}", + key=asset, + animated=animated, + ) + + @classmethod + def _from_guild_image(cls, state, guild_id: int, image: str, path: str) -> Asset: + animated = False + format = "png" + if path == "banners": + animated = image.startswith("a_") + format = "gif" if animated else "png" + + return cls( + state, + url=f"{cls.BASE}/{path}/{guild_id}/{image}.{format}?size=1024", + key=image, + animated=animated, + ) + + @classmethod + def _from_guild_icon(cls, state, guild_id: int, icon_hash: str) -> Asset: + animated = icon_hash.startswith("a_") + format = "gif" if animated else "png" + return cls( + state, + url=f"{cls.BASE}/icons/{guild_id}/{icon_hash}.{format}?size=1024", + key=icon_hash, + animated=animated, + ) + + @classmethod + def _from_sticker_banner(cls, state, banner: int) -> Asset: + return cls( + state, + url=f"{cls.BASE}/app-assets/710982414301790216/store/{banner}.png", + key=str(banner), + animated=False, + ) + + @classmethod + def _from_user_banner(cls, state, user_id: int, banner_hash: str) -> Asset: + animated = banner_hash.startswith("a_") + format = "gif" if animated else "png" + return cls( + state, + url=f"{cls.BASE}/banners/{user_id}/{banner_hash}.{format}?size=512", + key=banner_hash, + animated=animated, + ) + + @classmethod + def _from_scheduled_event_image( + cls, state, event_id: int, cover_hash: str + ) -> Asset: + return cls( + state, + url=f"{cls.BASE}/guild-events/{event_id}/{cover_hash}.png", + key=cover_hash, + animated=False, + ) + + @classmethod + def _from_soundboard_sound(cls, state, sound_id: int) -> Asset: + return cls( + state, + url=f"{cls.BASE}/soundboard-sounds/{sound_id}", + key=str(sound_id), + ) + + def __str__(self) -> str: + return self._url + + def __len__(self) -> int: + return len(self._url) + + def __repr__(self): + shorten = self._url.replace(self.BASE, "") + return f"" + + def __eq__(self, other): + return isinstance(other, Asset) and self._url == other._url + + def __hash__(self): + return hash(self._url) + + @property + def url(self) -> str: + """Returns the underlying URL of the asset.""" + return self._url + + @property + def key(self) -> str: + """Returns the identifying key of the asset.""" + return self._key + + def is_animated(self) -> bool: + """Returns whether the asset is animated.""" + return self._animated + + def replace( + self, + *, + size: int = MISSING, + format: ValidAssetFormatTypes = MISSING, + static_format: ValidStaticFormatTypes = MISSING, + ) -> Asset: + """Returns a new asset with the passed components replaced. + + Parameters + ---------- + size: :class:`int` + The new size of the asset. + format: :class:`str` + The new format to change it to. Must be either + 'webp', 'jpeg', 'jpg', 'png', or 'gif' if it's animated. + static_format: :class:`str` + The new format to change it to if the asset isn't animated. + Must be either 'webp', 'jpeg', 'jpg', or 'png'. + + Returns + ------- + :class:`Asset` + The newly updated asset. + + Raises + ------ + InvalidArgument + An invalid size or format was passed. + """ + url = yarl.URL(self._url) + path, _ = os.path.splitext(url.path) + + if format is not MISSING: + if self._animated: + if format not in VALID_ASSET_FORMATS: + raise InvalidArgument( + f"format must be one of {VALID_ASSET_FORMATS}" + ) + url = url.with_path(f"{path}.{format}") + elif static_format is MISSING: + if format not in VALID_STATIC_FORMATS: + raise InvalidArgument( + f"format must be one of {VALID_STATIC_FORMATS}" + ) + url = url.with_path(f"{path}.{format}") + + if static_format is not MISSING and not self._animated: + if static_format not in VALID_STATIC_FORMATS: + raise InvalidArgument( + f"static_format must be one of {VALID_STATIC_FORMATS}" + ) + url = url.with_path(f"{path}.{static_format}") + + if size is not MISSING: + if not utils.valid_icon_size(size): + raise InvalidArgument("size must be a power of 2 between 16 and 4096") + url = url.with_query(size=size) + else: + url = url.with_query(url.raw_query_string) + + url = str(url) + return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + + def with_size(self, size: int, /) -> Asset: + """Returns a new asset with the specified size. + + Parameters + ---------- + size: :class:`int` + The new size of the asset. + + Returns + ------- + :class:`Asset` + The new updated asset. + + Raises + ------ + InvalidArgument + The asset had an invalid size. + """ + if not utils.valid_icon_size(size): + raise InvalidArgument("size must be a power of 2 between 16 and 4096") + + url = str(yarl.URL(self._url).with_query(size=size)) + return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + + def with_format(self, format: ValidAssetFormatTypes, /) -> Asset: + """Returns a new asset with the specified format. + + Parameters + ---------- + format: :class:`str` + The new format of the asset. + + Returns + ------- + :class:`Asset` + The new updated asset. + + Raises + ------ + InvalidArgument + The asset has an invalid format. + """ + + if self._animated: + if format not in VALID_ASSET_FORMATS: + raise InvalidArgument(f"format must be one of {VALID_ASSET_FORMATS}") + elif format not in VALID_STATIC_FORMATS: + raise InvalidArgument(f"format must be one of {VALID_STATIC_FORMATS}") + + url = yarl.URL(self._url) + path, _ = os.path.splitext(url.path) + url = str(url.with_path(f"{path}.{format}").with_query(url.raw_query_string)) + return Asset(state=self._state, url=url, key=self._key, animated=self._animated) + + def with_static_format(self, format: ValidStaticFormatTypes, /) -> Asset: + """Returns a new asset with the specified static format. + + This only changes the format if the underlying asset is + not animated. Otherwise, the asset is not changed. + + Parameters + ---------- + format: :class:`str` + The new static format of the asset. + + Returns + ------- + :class:`Asset` + The new updated asset. + + Raises + ------ + InvalidArgument + The asset had an invalid format. + """ + + if self._animated: + return self + return self.with_format(format) diff --git a/venv/lib/python3.11/site-packages/discord/audit_logs.py b/venv/lib/python3.11/site-packages/discord/audit_logs.py new file mode 100644 index 0000000..b2f6a72 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/audit_logs.py @@ -0,0 +1,719 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import datetime +from functools import cached_property +from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generator, TypeVar + +from . import enums, utils +from .asset import Asset +from .automod import AutoModAction, AutoModTriggerMetadata +from .colour import Colour +from .invite import Invite +from .mixins import Hashable +from .object import Object +from .permissions import PermissionOverwrite, Permissions + +__all__ = ( + "AuditLogDiff", + "AuditLogChanges", + "AuditLogEntry", +) + + +if TYPE_CHECKING: + from . import abc + from .emoji import GuildEmoji + from .guild import Guild + from .member import Member + from .role import Role + from .scheduled_events import ScheduledEvent + from .stage_instance import StageInstance + from .state import ConnectionState + from .sticker import GuildSticker + from .threads import Thread + from .types.audit_log import AuditLogChange as AuditLogChangePayload + from .types.audit_log import AuditLogEntry as AuditLogEntryPayload + from .types.automod import AutoModAction as AutoModActionPayload + from .types.automod import AutoModTriggerMetadata as AutoModTriggerMetadataPayload + from .types.channel import PermissionOverwrite as PermissionOverwritePayload + from .types.role import Role as RolePayload + from .types.snowflake import Snowflake + from .user import User + + +def _transform_permissions(entry: AuditLogEntry, data: str) -> Permissions: + return Permissions(int(data)) + + +def _transform_color(entry: AuditLogEntry, data: int) -> Colour: + return Colour(data) + + +def _transform_snowflake(entry: AuditLogEntry, data: Snowflake) -> int: + return int(data) + + +def _transform_channel( + entry: AuditLogEntry, data: Snowflake | None +) -> abc.GuildChannel | Object | None: + if data is None: + return None + return entry.guild.get_channel(int(data)) or Object(id=data) + + +def _transform_channels( + entry: AuditLogEntry, data: list[Snowflake] | None +) -> list[abc.GuildChannel | Object] | None: + if data is None: + return None + return [_transform_channel(entry, channel) for channel in data] + + +def _transform_roles( + entry: AuditLogEntry, data: list[Snowflake] | None +) -> list[Role | Object] | None: + if data is None: + return None + return [entry.guild.get_role(int(r)) or Object(id=r) for r in data] + + +def _transform_member_id( + entry: AuditLogEntry, data: Snowflake | None +) -> Member | User | None: + if data is None: + return None + return entry._get_member(int(data)) + + +def _transform_guild_id(entry: AuditLogEntry, data: Snowflake | None) -> Guild | None: + if data is None: + return None + return entry._state._get_guild(data) + + +def _transform_overwrites( + entry: AuditLogEntry, data: list[PermissionOverwritePayload] +) -> list[tuple[Object, PermissionOverwrite]]: + overwrites = [] + for elem in data: + allow = Permissions(int(elem["allow"])) + deny = Permissions(int(elem["deny"])) + ow = PermissionOverwrite.from_pair(allow, deny) + + ow_type = elem["type"] + ow_id = int(elem["id"]) + target = None + if ow_type == 0: + target = entry.guild.get_role(ow_id) + elif ow_type == 1: + target = entry._get_member(ow_id) + + if target is None: + target = Object(id=ow_id) + + overwrites.append((target, ow)) + + return overwrites + + +def _transform_icon(entry: AuditLogEntry, data: str | None) -> Asset | None: + if data is None: + return None + return Asset._from_guild_icon(entry._state, entry.guild.id, data) + + +def _transform_avatar(entry: AuditLogEntry, data: str | None) -> Asset | None: + if data is None: + return None + return Asset._from_avatar(entry._state, entry._target_id, data) # type: ignore + + +def _transform_scheduled_event_image( + entry: AuditLogEntry, data: str | None +) -> Asset | None: + if data is None: + return None + return Asset._from_scheduled_event_image(entry._state, entry._target_id, data) + + +def _guild_hash_transformer( + path: str, +) -> Callable[[AuditLogEntry, str | None], Asset | None]: + def _transform(entry: AuditLogEntry, data: str | None) -> Asset | None: + if data is None: + return None + return Asset._from_guild_image(entry._state, entry.guild.id, data, path=path) + + return _transform + + +T = TypeVar("T", bound=enums.Enum) + + +def _enum_transformer(enum: type[T]) -> Callable[[AuditLogEntry, int], T]: + def _transform(entry: AuditLogEntry, data: int) -> T: + return enums.try_enum(enum, data) + + return _transform + + +def _transform_type( + entry: AuditLogEntry, data: int +) -> enums.ChannelType | enums.StickerType: + if entry.action.name.startswith("sticker_"): + return enums.try_enum(enums.StickerType, data) + else: + return enums.try_enum(enums.ChannelType, data) + + +def _transform_actions( + entry: AuditLogEntry, data: list[AutoModActionPayload] | None +) -> list[AutoModAction] | None: + if data is None: + return None + else: + return [AutoModAction.from_dict(d) for d in data] + + +def _transform_trigger_metadata( + entry: AuditLogEntry, data: AutoModTriggerMetadataPayload | None +) -> AutoModTriggerMetadata | None: + if data is None: + return None + else: + return AutoModTriggerMetadata.from_dict(data) + + +def _transform_communication_disabled_until( + entry: AuditLogEntry, data: str +) -> datetime.datetime | None: + if data: + return datetime.datetime.fromisoformat(data) + return None + + +class AuditLogDiff: + def __len__(self) -> int: + return len(self.__dict__) + + def __iter__(self) -> Generator[tuple[str, Any]]: + yield from self.__dict__.items() + + def __repr__(self) -> str: + values = " ".join("%s=%r" % item for item in self.__dict__.items()) + return f"" + + if TYPE_CHECKING: + + def __getattr__(self, item: str) -> Any: ... + + def __setattr__(self, key: str, value: Any) -> Any: ... + + +Transformer = Callable[["AuditLogEntry", Any], Any] + + +class AuditLogChanges: + TRANSFORMERS: ClassVar[dict[str, tuple[str | None, Transformer | None]]] = { + "verification_level": (None, _enum_transformer(enums.VerificationLevel)), + "explicit_content_filter": (None, _enum_transformer(enums.ContentFilter)), + "allow": (None, _transform_permissions), + "deny": (None, _transform_permissions), + "permissions": (None, _transform_permissions), + "id": (None, _transform_snowflake), + "color": ("colour", _transform_color), + "owner_id": ("owner", _transform_member_id), + "inviter_id": ("inviter", _transform_member_id), + "channel_id": ("channel", _transform_channel), + "afk_channel_id": ("afk_channel", _transform_channel), + "system_channel_id": ("system_channel", _transform_channel), + "widget_channel_id": ("widget_channel", _transform_channel), + "rules_channel_id": ("rules_channel", _transform_channel), + "public_updates_channel_id": ("public_updates_channel", _transform_channel), + "permission_overwrites": ("overwrites", _transform_overwrites), + "splash_hash": ("splash", _guild_hash_transformer("splashes")), + "banner_hash": ("banner", _guild_hash_transformer("banners")), + "discovery_splash_hash": ( + "discovery_splash", + _guild_hash_transformer("discovery-splashes"), + ), + "icon_hash": ("icon", _transform_icon), + "avatar_hash": ("avatar", _transform_avatar), + "rate_limit_per_user": ("slowmode_delay", None), + "guild_id": ("guild", _transform_guild_id), + "tags": ("emoji", None), + "default_message_notifications": ( + "default_notifications", + _enum_transformer(enums.NotificationLevel), + ), + "rtc_region": (None, _enum_transformer(enums.VoiceRegion)), + "video_quality_mode": (None, _enum_transformer(enums.VideoQualityMode)), + "privacy_level": (None, _enum_transformer(enums.StagePrivacyLevel)), + "format_type": (None, _enum_transformer(enums.StickerFormatType)), + "type": (None, _transform_type), + "status": (None, _enum_transformer(enums.ScheduledEventStatus)), + "entity_type": ( + "location_type", + _enum_transformer(enums.ScheduledEventLocationType), + ), + "command_id": ("command_id", _transform_snowflake), + "image_hash": ("image", _transform_scheduled_event_image), + "trigger_type": (None, _enum_transformer(enums.AutoModTriggerType)), + "event_type": (None, _enum_transformer(enums.AutoModEventType)), + "actions": (None, _transform_actions), + "trigger_metadata": (None, _transform_trigger_metadata), + "exempt_roles": (None, _transform_roles), + "exempt_channels": (None, _transform_channels), + "communication_disabled_until": (None, _transform_communication_disabled_until), + } + + def __init__( + self, + entry: AuditLogEntry, + data: list[AuditLogChangePayload], + *, + state: ConnectionState, + ): + self.before = AuditLogDiff() + self.after = AuditLogDiff() + + for elem in sorted(data, key=lambda i: i["key"]): + attr = elem["key"] + + # special cases for role/trigger_metadata add/remove + if attr == "$add": + self._handle_role(self.before, self.after, entry, elem["new_value"]) # type: ignore + continue + elif attr == "$remove": + self._handle_role(self.after, self.before, entry, elem["new_value"]) # type: ignore + continue + elif attr in [ + "$add_keyword_filter", + "$add_regex_patterns", + "$add_allow_list", + ]: + self._handle_trigger_metadata( + self.before, self.after, entry, elem["new_value"], attr # type: ignore + ) + continue + elif attr in [ + "$remove_keyword_filter", + "$remove_regex_patterns", + "$remove_allow_list", + ]: + self._handle_trigger_metadata( + self.after, self.before, entry, elem["new_value"], attr # type: ignore + ) + continue + + try: + key, transformer = self.TRANSFORMERS[attr] + except (ValueError, KeyError): + transformer = None + else: + if key: + attr = key + + transformer: Transformer | None + + try: + before = elem["old_value"] + except KeyError: + before = None + else: + if transformer: + before = transformer(entry, before) + + if attr == "location" and hasattr(self.before, "location_type"): + from .scheduled_events import ScheduledEventLocation + + if ( + self.before.location_type + is enums.ScheduledEventLocationType.external + ): + before = ScheduledEventLocation(state=state, value=before) + elif hasattr(self.before, "channel"): + before = ScheduledEventLocation( + state=state, value=self.before.channel + ) + + setattr(self.before, attr, before) + + try: + after = elem["new_value"] + except KeyError: + after = None + else: + if transformer: + after = transformer(entry, after) + + if attr == "location" and hasattr(self.after, "location_type"): + from .scheduled_events import ScheduledEventLocation + + if ( + self.after.location_type + is enums.ScheduledEventLocationType.external + ): + after = ScheduledEventLocation(state=state, value=after) + elif hasattr(self.after, "channel"): + after = ScheduledEventLocation( + state=state, value=self.after.channel + ) + + setattr(self.after, attr, after) + + # add an alias + if hasattr(self.after, "colour"): + self.after.color = self.after.colour + self.before.color = self.before.colour + if hasattr(self.after, "expire_behavior"): + self.after.expire_behaviour = self.after.expire_behavior + self.before.expire_behaviour = self.before.expire_behavior + + def __repr__(self) -> str: + return f"" + + def _handle_role( + self, + first: AuditLogDiff, + second: AuditLogDiff, + entry: AuditLogEntry, + elem: list[RolePayload], + ) -> None: + if not hasattr(first, "roles"): + setattr(first, "roles", []) + + data = [] + g: Guild = entry.guild # type: ignore + + for e in elem: + role_id = int(e["id"]) + role = g.get_role(role_id) + + if role is None: + role = Object(id=role_id) + role.name = e["name"] # type: ignore + + data.append(role) + + setattr(second, "roles", data) + + def _handle_trigger_metadata( + self, + first: AuditLogDiff, + second: AuditLogDiff, + entry: AuditLogEntry, + elem: list[AutoModTriggerMetadataPayload], + attr: str, + ) -> None: + if not hasattr(first, "trigger_metadata"): + setattr(first, "trigger_metadata", None) + + key = attr.split("_", 1)[-1] + data = {key: elem} + tm = AutoModTriggerMetadata.from_dict(data) + + setattr(second, "trigger_metadata", tm) + + +class _AuditLogProxyMemberPrune: + delete_member_days: int + members_removed: int + + +class _AuditLogProxyMemberMoveOrMessageDelete: + channel: abc.GuildChannel + count: int + + +class _AuditLogProxyMemberDisconnect: + count: int + + +class _AuditLogProxyPinAction: + channel: abc.GuildChannel + message_id: int + + +class _AuditLogProxyStageInstanceAction: + channel: abc.GuildChannel + + +class AuditLogEntry(Hashable): + r"""Represents an Audit Log entry. + + You retrieve these via :meth:`Guild.audit_logs`. + + .. container:: operations + + .. describe:: x == y + + Checks if two entries are equal. + + .. describe:: x != y + + Checks if two entries are not equal. + + .. describe:: hash(x) + + Returns the entry's hash. + + .. versionchanged:: 1.7 + Audit log entries are now comparable and hashable. + + Attributes + ----------- + action: :class:`AuditLogAction` + The action that was done. + user: Optional[:class:`abc.User`] + The user who initiated this action. Usually a :class:`Member`\, unless gone + then it's a :class:`User`. + id: :class:`int` + The entry ID. + target: Any + The target that got changed. The exact type of this depends on + the action being done. + reason: Optional[:class:`str`] + The reason this action was done. + extra: Any + Extra information that this entry has that might be useful. + For most actions, this is ``None``. However, in some cases it + contains extra information. See :class:`AuditLogAction` for + which actions have this field filled out. + """ + + def __init__( + self, *, users: dict[int, User], data: AuditLogEntryPayload, guild: Guild + ): + self._state = guild._state + self.guild = guild + self._users = users + self._from_data(data) + + def _from_data(self, data: AuditLogEntryPayload) -> None: + self.action = enums.try_enum(enums.AuditLogAction, data["action_type"]) + self.id = int(data["id"]) + + # this key is technically not usually present + self.reason = data.get("reason") + self.extra = data.get("options") + + if isinstance(self.action, enums.AuditLogAction) and self.extra: + if self.action is enums.AuditLogAction.member_prune: + # member prune has two keys with useful information + self.extra: _AuditLogProxyMemberPrune = type( + "_AuditLogProxy", (), {k: int(v) for k, v in self.extra.items()} + )() + elif ( + self.action is enums.AuditLogAction.member_move + or self.action is enums.AuditLogAction.message_delete + ): + channel_id = int(self.extra["channel_id"]) + elems = { + "count": int(self.extra["count"]), + "channel": self.guild.get_channel(channel_id) + or Object(id=channel_id), + } + self.extra: _AuditLogProxyMemberMoveOrMessageDelete = type( + "_AuditLogProxy", (), elems + )() + elif self.action is enums.AuditLogAction.member_disconnect: + # The member disconnect action has a dict with some information + elems = { + "count": int(self.extra["count"]), + } + self.extra: _AuditLogProxyMemberDisconnect = type( + "_AuditLogProxy", (), elems + )() + elif self.action.name.endswith("pin"): + # the pin actions have a dict with some information + channel_id = int(self.extra["channel_id"]) + elems = { + "channel": self.guild.get_channel(channel_id) + or Object(id=channel_id), + "message_id": int(self.extra["message_id"]), + } + self.extra: _AuditLogProxyPinAction = type( + "_AuditLogProxy", (), elems + )() + elif self.action.name.startswith("overwrite_"): + # the overwrite_ actions have a dict with some information + instance_id = int(self.extra["id"]) + the_type = self.extra.get("type") + if the_type == "1": + self.extra = self._get_member(instance_id) + elif the_type == "0": + role = self.guild.get_role(instance_id) + if role is None: + role = Object(id=instance_id) + role.name = self.extra.get("role_name") # type: ignore + self.extra: Role = role + elif self.action.name.startswith("stage_instance"): + channel_id = int(self.extra["channel_id"]) + elems = { + "channel": self.guild.get_channel(channel_id) + or Object(id=channel_id) + } + self.extra: _AuditLogProxyStageInstanceAction = type( + "_AuditLogProxy", (), elems + )() + + self.extra: ( + _AuditLogProxyMemberPrune + | _AuditLogProxyMemberMoveOrMessageDelete + | _AuditLogProxyMemberDisconnect + | _AuditLogProxyPinAction + | _AuditLogProxyStageInstanceAction + | Member + | User + | None + | Role + ) + + # this key is not present when the above is present, typically. + # It's a list of { new_value: a, old_value: b, key: c } + # where new_value and old_value are not guaranteed to be there depending + # on the action type, so let's just fetch it for now and only turn it + # into meaningful data when requested + self._changes = data.get("changes", []) + + self.user = self._get_member(utils._get_as_snowflake(data, "user_id")) # type: ignore + self._target_id = utils._get_as_snowflake(data, "target_id") + + def _get_member(self, user_id: int) -> Member | User | None: + return self.guild.get_member(user_id) or self._users.get(user_id) + + def __repr__(self) -> str: + return f"" + + @property + def created_at(self) -> datetime.datetime: + """Returns the entry's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def target( + self, + ) -> ( + Guild + | abc.GuildChannel + | Member + | User + | Role + | Invite + | GuildEmoji + | StageInstance + | GuildSticker + | Thread + | Object + | None + ): + try: + converter = getattr(self, f"_convert_target_{self.action.target_type}") + except AttributeError: + return Object(id=self._target_id) + else: + return converter(self._target_id) + + @property + def category(self) -> enums.AuditLogActionCategory: + """The category of the action, if applicable.""" + return self.action.category + + @cached_property + def changes(self) -> AuditLogChanges: + """The list of changes this entry has.""" + obj = AuditLogChanges(self, self._changes, state=self._state) + del self._changes + return obj + + @property + def before(self) -> AuditLogDiff: + """The target's prior state.""" + return self.changes.before + + @property + def after(self) -> AuditLogDiff: + """The target's subsequent state.""" + return self.changes.after + + def _convert_target_guild(self, target_id: int) -> Guild: + return self.guild + + def _convert_target_channel(self, target_id: int) -> abc.GuildChannel | Object: + return self.guild.get_channel(target_id) or Object(id=target_id) + + def _convert_target_user(self, target_id: int) -> Member | User | None: + return self._get_member(target_id) + + def _convert_target_role(self, target_id: int) -> Role | Object: + return self.guild.get_role(target_id) or Object(id=target_id) + + def _convert_target_invite(self, target_id: int) -> Invite: + # invites have target_id set to null + # so figure out which change has the full invite data + changeset = ( + self.before + if self.action is enums.AuditLogAction.invite_delete + else self.after + ) + + fake_payload = { + "max_age": changeset.max_age, + "max_uses": changeset.max_uses, + "code": changeset.code, + "temporary": changeset.temporary, + "uses": changeset.uses, + } + + obj = Invite(state=self._state, data=fake_payload, guild=self.guild, channel=changeset.channel) # type: ignore + try: + obj.inviter = changeset.inviter + except AttributeError: + pass + return obj + + def _convert_target_emoji(self, target_id: int) -> GuildEmoji | Object: + return self._state.get_emoji(target_id) or Object(id=target_id) + + def _convert_target_message(self, target_id: int) -> Member | User | None: + return self._get_member(target_id) + + def _convert_target_stage_instance(self, target_id: int) -> StageInstance | Object: + return self.guild.get_stage_instance(target_id) or Object(id=target_id) + + def _convert_target_sticker(self, target_id: int) -> GuildSticker | Object: + return self._state.get_sticker(target_id) or Object(id=target_id) + + def _convert_target_thread(self, target_id: int) -> Thread | Object: + return self.guild.get_thread(target_id) or Object(id=target_id) + + def _convert_target_scheduled_event( + self, target_id: int + ) -> ScheduledEvent | Object: + return self.guild.get_scheduled_event(target_id) or Object(id=target_id) diff --git a/venv/lib/python3.11/site-packages/discord/automod.py b/venv/lib/python3.11/site-packages/discord/automod.py new file mode 100644 index 0000000..5f2e7de --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/automod.py @@ -0,0 +1,560 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from datetime import timedelta +from functools import cached_property +from typing import TYPE_CHECKING + +from . import utils +from .enums import ( + AutoModActionType, + AutoModEventType, + AutoModKeywordPresetType, + AutoModTriggerType, + try_enum, +) +from .mixins import Hashable +from .object import Object + +__all__ = ( + "AutoModRule", + "AutoModAction", + "AutoModActionMetadata", + "AutoModTriggerMetadata", +) + +if TYPE_CHECKING: + from .abc import Snowflake + from .channel import ForumChannel, TextChannel, VoiceChannel + from .guild import Guild + from .member import Member + from .role import Role + from .state import ConnectionState + from .types.automod import AutoModAction as AutoModActionPayload + from .types.automod import AutoModActionMetadata as AutoModActionMetadataPayload + from .types.automod import AutoModRule as AutoModRulePayload + from .types.automod import AutoModTriggerMetadata as AutoModTriggerMetadataPayload + +MISSING = utils.MISSING + + +class AutoModActionMetadata: + """Represents an action's metadata. + + Depending on the action's type, different attributes will be used. + + .. versionadded:: 2.0 + + Attributes + ---------- + channel_id: :class:`int` + The ID of the channel to send the message to. + Only for actions of type :attr:`AutoModActionType.send_alert_message`. + timeout_duration: :class:`datetime.timedelta` + How long the member that triggered the action should be timed out for. + Only for actions of type :attr:`AutoModActionType.timeout`. + custom_message: :class:`str` + An additional message shown to members when their message is blocked. + Maximum 150 characters. + Only for actions of type :attr:`AutoModActionType.block_message`. + """ + + # maybe add a table of action types and attributes? + + __slots__ = ( + "channel_id", + "timeout_duration", + "custom_message", + ) + + def __init__( + self, + channel_id: int = MISSING, + timeout_duration: timedelta = MISSING, + custom_message: str = MISSING, + ): + self.channel_id: int = channel_id + self.timeout_duration: timedelta = timeout_duration + self.custom_message: str = custom_message + + def to_dict(self) -> dict: + data = {} + + if self.channel_id is not MISSING: + data["channel_id"] = self.channel_id + + if self.timeout_duration is not MISSING: + data["duration_seconds"] = self.timeout_duration.total_seconds() + + if self.custom_message is not MISSING: + data["custom_message"] = self.custom_message + + return data + + @classmethod + def from_dict(cls, data: AutoModActionMetadataPayload): + kwargs = {} + + if (channel_id := data.get("channel_id")) is not None: + kwargs["channel_id"] = int(channel_id) + + if (duration_seconds := data.get("duration_seconds")) is not None: + # might need an explicit int cast + kwargs["timeout_duration"] = timedelta(seconds=duration_seconds) + + if (custom_message := data.get("custom_message")) is not None: + kwargs["custom_message"] = custom_message + + return cls(**kwargs) + + def __repr__(self) -> str: + repr_attrs = ( + "channel_id", + "timeout_duration", + "custom_message", + ) + inner = [] + + for attr in repr_attrs: + if (value := getattr(self, attr)) is not MISSING: + inner.append(f"{attr}={value}") + inner = " ".join(inner) + + return f"" + + +class AutoModAction: + """Represents an action for a guild's auto moderation rule. + + .. versionadded:: 2.0 + + Attributes + ---------- + type: :class:`AutoModActionType` + The action's type. + metadata: :class:`AutoModActionMetadata` + The action's metadata. + """ + + # note that AutoModActionType.timeout is only valid for trigger type 1? + + __slots__ = ( + "type", + "metadata", + ) + + def __init__(self, action_type: AutoModActionType, metadata: AutoModActionMetadata): + self.type: AutoModActionType = action_type + self.metadata: AutoModActionMetadata = metadata + + def to_dict(self) -> dict: + return { + "type": self.type.value, + "metadata": self.metadata.to_dict(), + } + + @classmethod + def from_dict(cls, data: AutoModActionPayload): + return cls( + try_enum(AutoModActionType, data["type"]), + AutoModActionMetadata.from_dict(data["metadata"]), + ) + + def __repr__(self) -> str: + return f"" + + +class AutoModTriggerMetadata: + r"""Represents a rule's trigger metadata, defining additional data used to determine when a rule triggers. + + Depending on the trigger type, different metadata attributes will be used: + + +-----------------------------+--------------------------------------------------------------------------------+ + | Attribute | Trigger Types | + +=============================+================================================================================+ + | :attr:`keyword_filter` | :attr:`AutoModTriggerType.keyword` | + +-----------------------------+--------------------------------------------------------------------------------+ + | :attr:`regex_patterns` | :attr:`AutoModTriggerType.keyword` | + +-----------------------------+--------------------------------------------------------------------------------+ + | :attr:`presets` | :attr:`AutoModTriggerType.keyword_preset` | + +-----------------------------+--------------------------------------------------------------------------------+ + | :attr:`allow_list` | :attr:`AutoModTriggerType.keyword`\, :attr:`AutoModTriggerType.keyword_preset` | + +-----------------------------+--------------------------------------------------------------------------------+ + | :attr:`mention_total_limit` | :attr:`AutoModTriggerType.mention_spam` | + +-----------------------------+--------------------------------------------------------------------------------+ + + Each attribute has limits that may change based on the trigger type. + See `here `__ + for information on attribute limits. + + .. versionadded:: 2.0 + + Attributes + ---------- + keyword_filter: List[:class:`str`] + A list of substrings to filter. + + regex_patterns: List[:class:`str`] + A list of regex patterns to filter using Rust-flavored regex, which is not + fully compatible with regex syntax supported by the builtin `re` module. + + .. versionadded:: 2.4 + + presets: List[:class:`AutoModKeywordPresetType`] + A list of preset keyword sets to filter. + + allow_list: List[:class:`str`] + A list of substrings to allow, overriding keyword and regex matches. + + .. versionadded:: 2.4 + + mention_total_limit: :class:`int` + The total number of unique role and user mentions allowed. + + .. versionadded:: 2.4 + """ + + __slots__ = ( + "keyword_filter", + "regex_patterns", + "presets", + "allow_list", + "mention_total_limit", + ) + + def __init__( + self, + keyword_filter: list[str] = MISSING, + regex_patterns: list[str] = MISSING, + presets: list[AutoModKeywordPresetType] = MISSING, + allow_list: list[str] = MISSING, + mention_total_limit: int = MISSING, + ): + self.keyword_filter = keyword_filter + self.regex_patterns = regex_patterns + self.presets = presets + self.allow_list = allow_list + self.mention_total_limit = mention_total_limit + + def to_dict(self) -> dict: + data = {} + + if self.keyword_filter is not MISSING: + data["keyword_filter"] = self.keyword_filter + + if self.regex_patterns is not MISSING: + data["regex_patterns"] = self.regex_patterns + + if self.presets is not MISSING: + data["presets"] = [wordset.value for wordset in self.presets] + + if self.allow_list is not MISSING: + data["allow_list"] = self.allow_list + + if self.mention_total_limit is not MISSING: + data["mention_total_limit"] = self.mention_total_limit + + return data + + @classmethod + def from_dict(cls, data: AutoModTriggerMetadataPayload): + kwargs = {} + + if (keyword_filter := data.get("keyword_filter")) is not None: + kwargs["keyword_filter"] = keyword_filter + + if (regex_patterns := data.get("regex_patterns")) is not None: + kwargs["regex_patterns"] = regex_patterns + + if (presets := data.get("presets")) is not None: + kwargs["presets"] = [ + try_enum(AutoModKeywordPresetType, wordset) for wordset in presets + ] + + if (allow_list := data.get("allow_list")) is not None: + kwargs["allow_list"] = allow_list + + if (mention_total_limit := data.get("mention_total_limit")) is not None: + kwargs["mention_total_limit"] = mention_total_limit + + return cls(**kwargs) + + def __repr__(self) -> str: + repr_attrs = ( + "keyword_filter", + "regex_patterns", + "presets", + "allow_list", + "mention_total_limit", + ) + inner = [] + + for attr in repr_attrs: + if (value := getattr(self, attr)) is not MISSING: + inner.append(f"{attr}={value}") + inner = " ".join(inner) + + return f"" + + +class AutoModRule(Hashable): + """Represents a guild's auto moderation rule. + + .. versionadded:: 2.0 + + .. container:: operations + + .. describe:: x == y + + Checks if two rules are equal. + + .. describe:: x != y + + Checks if two rules are not equal. + + .. describe:: hash(x) + + Returns the rule's hash. + + .. describe:: str(x) + + Returns the rule's name. + + Attributes + ---------- + id: :class:`int` + The rule's ID. + name: :class:`str` + The rule's name. + creator_id: :class:`int` + The ID of the user who created this rule. + event_type: :class:`AutoModEventType` + Indicates in what context the rule is checked. + trigger_type: :class:`AutoModTriggerType` + Indicates what type of information is checked to determine whether the rule is triggered. + trigger_metadata: :class:`AutoModTriggerMetadata` + The rule's trigger metadata. + actions: List[:class:`AutoModAction`] + The actions to perform when the rule is triggered. + enabled: :class:`bool` + Whether this rule is enabled. + exempt_role_ids: List[:class:`int`] + The IDs of the roles that are exempt from this rule. + exempt_channel_ids: List[:class:`int`] + The IDs of the channels that are exempt from this rule. + """ + + __slots__ = ( + "__dict__", + "_state", + "id", + "guild_id", + "name", + "creator_id", + "event_type", + "trigger_type", + "trigger_metadata", + "actions", + "enabled", + "exempt_role_ids", + "exempt_channel_ids", + ) + + def __init__( + self, + *, + state: ConnectionState, + data: AutoModRulePayload, + ): + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self.guild_id: int = int(data["guild_id"]) + self.name: str = data["name"] + self.creator_id: int = int(data["creator_id"]) + self.event_type: AutoModEventType = try_enum( + AutoModEventType, data["event_type"] + ) + self.trigger_type: AutoModTriggerType = try_enum( + AutoModTriggerType, data["trigger_type"] + ) + self.trigger_metadata: AutoModTriggerMetadata = ( + AutoModTriggerMetadata.from_dict(data["trigger_metadata"]) + ) + self.actions: list[AutoModAction] = [ + AutoModAction.from_dict(d) for d in data["actions"] + ] + self.enabled: bool = data["enabled"] + self.exempt_role_ids: list[int] = [int(r) for r in data["exempt_roles"]] + self.exempt_channel_ids: list[int] = [int(c) for c in data["exempt_channels"]] + + def __repr__(self) -> str: + return f"" + + def __str__(self) -> str: + return self.name + + @property + def guild(self) -> Guild | None: + """The guild this rule belongs to.""" + return self._state._get_guild(self.guild_id) + + @property + def creator(self) -> Member | None: + """The member who created this rule.""" + if self.guild is None: + return None + return self.guild.get_member(self.creator_id) + + @cached_property + def exempt_roles(self) -> list[Role | Object]: + """The roles that are exempt + from this rule. + + If a role is not found in the guild's cache, + then it will be returned as an :class:`Object`. + """ + if self.guild is None: + return [Object(role_id) for role_id in self.exempt_role_ids] + return [ + self.guild.get_role(role_id) or Object(role_id) + for role_id in self.exempt_role_ids + ] + + @cached_property + def exempt_channels( + self, + ) -> list[TextChannel | ForumChannel | VoiceChannel | Object]: + """The channels that are exempt from this rule. + + If a channel is not found in the guild's cache, + then it will be returned as an :class:`Object`. + """ + if self.guild is None: + return [Object(channel_id) for channel_id in self.exempt_channel_ids] + return [ + self.guild.get_channel(channel_id) or Object(channel_id) + for channel_id in self.exempt_channel_ids + ] + + async def delete(self, reason: str | None = None) -> None: + """|coro| + + Deletes this rule. + + Parameters + ---------- + reason: Optional[:class:`str`] + The reason for deleting this rule. Shows up in the audit log. + + Raises + ------ + Forbidden + You do not have the Manage Guild permission. + HTTPException + The operation failed. + """ + await self._state.http.delete_auto_moderation_rule( + self.guild_id, self.id, reason=reason + ) + + async def edit( + self, + *, + name: str = MISSING, + event_type: AutoModEventType = MISSING, + trigger_metadata: AutoModTriggerMetadata = MISSING, + actions: list[AutoModAction] = MISSING, + enabled: bool = MISSING, + exempt_roles: list[Snowflake] = MISSING, + exempt_channels: list[Snowflake] = MISSING, + reason: str | None = None, + ) -> AutoModRule | None: + """|coro| + + Edits this rule. + + Parameters + ---------- + name: :class:`str` + The rule's new name. + event_type: :class:`AutoModEventType` + The new context in which the rule is checked. + trigger_metadata: :class:`AutoModTriggerMetadata` + The new trigger metadata. + actions: List[:class:`AutoModAction`] + The new actions to perform when the rule is triggered. + enabled: :class:`bool` + Whether this rule is enabled. + exempt_roles: List[:class:`abc.Snowflake`] + The roles that will be exempt from this rule. + exempt_channels: List[:class:`abc.Snowflake`] + The channels that will be exempt from this rule. + reason: Optional[:class:`str`] + The reason for editing this rule. Shows up in the audit log. + + Returns + ------- + Optional[:class:`.AutoModRule`] + The newly updated rule, if applicable. This is only returned + when fields are updated. + + Raises + ------ + Forbidden + You do not have the Manage Guild permission. + HTTPException + The operation failed. + """ + http = self._state.http + payload = {} + + if name is not MISSING: + payload["name"] = name + + if event_type is not MISSING: + payload["event_type"] = event_type.value + + if trigger_metadata is not MISSING: + payload["trigger_metadata"] = trigger_metadata.to_dict() + + if actions is not MISSING: + payload["actions"] = [a.to_dict() for a in actions] + + if enabled is not MISSING: + payload["enabled"] = enabled + + # Maybe consider enforcing limits on the number of exempt roles/channels? + if exempt_roles is not MISSING: + payload["exempt_roles"] = [r.id for r in exempt_roles] + + if exempt_channels is not MISSING: + payload["exempt_channels"] = [c.id for c in exempt_channels] + + if payload: + data = await http.edit_auto_moderation_rule( + self.guild_id, self.id, payload, reason=reason + ) + return AutoModRule(state=self._state, data=data) diff --git a/venv/lib/python3.11/site-packages/discord/backoff.py b/venv/lib/python3.11/site-packages/discord/backoff.py new file mode 100644 index 0000000..819c7d3 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/backoff.py @@ -0,0 +1,101 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import random +import time +from typing import Callable, Generic, Literal, TypeVar, overload + +T = TypeVar("T", bool, Literal[True], Literal[False]) + +__all__ = ("ExponentialBackoff",) + + +class ExponentialBackoff(Generic[T]): + """An implementation of the exponential backoff algorithm + + Provides a convenient interface to implement an exponential backoff + for reconnecting or retrying transmissions in a distributed network. + + Once instantiated, the delay method will return the next interval to + wait for when retrying a connection or transmission. The maximum + delay increases exponentially with each retry up to a maximum of + 2^10 * base, and is reset if no more attempts are needed in a period + of 2^11 * base seconds. + + Parameters + ---------- + base: :class:`int` + The base delay in seconds. The first retry-delay will be up to + this many seconds. + integral: :class:`bool` + Set to ``True`` if whole periods of base is desirable, otherwise any + number in between may be returned. + """ + + def __init__(self, base: int = 1, *, integral: T = False): + self._base: int = base + + self._exp: int = 0 + self._max: int = 10 + self._reset_time: int = base * 2**11 + self._last_invocation: float = time.monotonic() + + # Use our own random instance to avoid messing with global one + rand = random.Random() + rand.seed() + + self._randfunc: Callable[..., int | float] = rand.randrange if integral else rand.uniform # type: ignore + + @overload + def delay(self: ExponentialBackoff[Literal[False]]) -> float: ... + + @overload + def delay(self: ExponentialBackoff[Literal[True]]) -> int: ... + + @overload + def delay(self: ExponentialBackoff[bool]) -> int | float: ... + + def delay(self) -> int | float: + """Compute the next delay + + Returns the next delay to wait according to the exponential + backoff algorithm. This is a value between 0 and base * 2^exp + where exponent starts off at 1 and is incremented at every + invocation of this method up to a maximum of 10. + + If a period of more than base * 2^11 has passed since the last + retry, the exponent is reset to 1. + """ + invocation = time.monotonic() + interval = invocation - self._last_invocation + self._last_invocation = invocation + + if interval > self._reset_time: + self._exp = 0 + + self._exp = min(self._exp + 1, self._max) + return self._randfunc(0, self._base * 2**self._exp) diff --git a/venv/lib/python3.11/site-packages/discord/bin/libopus-0.x64.dll b/venv/lib/python3.11/site-packages/discord/bin/libopus-0.x64.dll new file mode 100644 index 0000000..74a8e35 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/bin/libopus-0.x64.dll differ diff --git a/venv/lib/python3.11/site-packages/discord/bin/libopus-0.x86.dll b/venv/lib/python3.11/site-packages/discord/bin/libopus-0.x86.dll new file mode 100644 index 0000000..ee71317 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/bin/libopus-0.x86.dll differ diff --git a/venv/lib/python3.11/site-packages/discord/bot.py b/venv/lib/python3.11/site-packages/discord/bot.py new file mode 100644 index 0000000..6c3632d --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/bot.py @@ -0,0 +1,1685 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import asyncio +import collections +import collections.abc +import copy +import inspect +import logging +import sys +import traceback +from abc import ABC, abstractmethod +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Coroutine, + Generator, + Literal, + Mapping, + TypeVar, +) + +from .client import Client +from .cog import CogMixin +from .commands import ( + ApplicationCommand, + ApplicationContext, + AutocompleteContext, + MessageCommand, + SlashCommand, + SlashCommandGroup, + UserCommand, + command, +) +from .enums import IntegrationType, InteractionContextType, InteractionType, TeamRole +from .errors import CheckFailure, DiscordException +from .interactions import Interaction +from .shard import AutoShardedClient +from .types import interactions +from .user import User +from .utils import MISSING, async_all, find, get + +if TYPE_CHECKING: + from typing_extensions import Never + + from .cog import Cog + from .commands import Option + from .ext.commands import Cooldown + from .member import Member + from .permissions import Permissions + +C = TypeVar("C", bound=MessageCommand | SlashCommand | UserCommand) +CoroFunc = Callable[..., Coroutine[Any, Any, Any]] +CFT = TypeVar("CFT", bound=CoroFunc) + +__all__ = ( + "ApplicationCommandMixin", + "Bot", + "AutoShardedBot", +) + +_log = logging.getLogger(__name__) + + +class ApplicationCommandMixin(ABC): + """A mixin that implements common functionality for classes that need + application command compatibility. + + Attributes + ---------- + application_commands: :class:`dict` + A mapping of command id string to :class:`.ApplicationCommand` objects. + pending_application_commands: :class:`list` + A list of commands that have been added but not yet registered. This is read-only and is modified via other + methods. + """ + + def __init__(self, *args, **kwargs) -> None: + super().__init__(*args, **kwargs) + self._pending_application_commands = [] + self._application_commands = {} + + @property + def all_commands(self): + return self._application_commands + + @property + def pending_application_commands(self): + return self._pending_application_commands + + @property + def commands(self) -> list[ApplicationCommand | Any]: + commands = self.application_commands + if self._bot._supports_prefixed_commands and hasattr( + self._bot, "prefixed_commands" + ): + commands += getattr(self._bot, "prefixed_commands") + return commands + + @property + def application_commands(self) -> list[ApplicationCommand]: + return list(self._application_commands.values()) + + def add_application_command(self, command: ApplicationCommand) -> None: + """Adds an :class:`.ApplicationCommand` into the internal list of commands. + + This is usually not called, instead the :meth:`command` or + other shortcut decorators are used instead. + + .. versionadded:: 2.0 + + Parameters + ---------- + command: :class:`.ApplicationCommand` + The command to add. + """ + if isinstance(command, SlashCommand) and command.is_subcommand: + raise TypeError("The provided command is a sub-command of group") + + if self._bot.debug_guilds and command.guild_ids is None: + command.guild_ids = self._bot.debug_guilds + if self._bot.default_command_contexts and command.contexts is None: + command.contexts = self._bot.default_command_contexts + if ( + self._bot.default_command_integration_types + and command.integration_types is None + ): + command.integration_types = self._bot.default_command_integration_types + + for cmd in self.pending_application_commands: + if cmd == command: + command.id = cmd.id + self._application_commands[command.id] = command + break + self._pending_application_commands.append(command) + + def remove_application_command( + self, command: ApplicationCommand + ) -> ApplicationCommand | None: + """Remove an :class:`.ApplicationCommand` from the internal list + of commands. + + .. versionadded:: 2.0 + + Parameters + ---------- + command: :class:`.ApplicationCommand` + The command to remove. + + Returns + ------- + Optional[:class:`.ApplicationCommand`] + The command that was removed. If the command has not been added, + ``None`` is returned instead. + """ + if command.id: + self._application_commands.pop(command.id, None) + + if command in self._pending_application_commands: + self._pending_application_commands.remove(command) + return command + + @property + def get_command(self): + """Shortcut for :meth:`.get_application_command`. + + .. note:: + Overridden in :class:`ext.commands.Bot`. + + .. versionadded:: 2.0 + """ + # TODO: Do something like we did in self.commands for this + return self.get_application_command + + def get_application_command( + self, + name: str, + guild_ids: list[int] | None = None, + type: type[ApplicationCommand] = ApplicationCommand, + ) -> ApplicationCommand | None: + """Get an :class:`.ApplicationCommand` from the internal list + of commands. + + .. versionadded:: 2.0 + + Parameters + ---------- + name: :class:`str` + The qualified name of the command to get. + guild_ids: List[:class:`int`] + The guild ids associated to the command to get. + type: Type[:class:`.ApplicationCommand`] + The type of the command to get. Defaults to :class:`.ApplicationCommand`. + + Returns + ------- + Optional[:class:`.ApplicationCommand`] + The command that was requested. If not found, returns ``None``. + """ + commands = self._application_commands.values() + for command in commands: + if command.name == name and isinstance(command, type): + if guild_ids is not None and command.guild_ids != guild_ids: + return + return command + elif (names := name.split())[0] == command.name and isinstance( + command, SlashCommandGroup + ): + while len(names) > 1: + command = get(commands, name=names.pop(0)) + if not isinstance(command, SlashCommandGroup) or ( + guild_ids is not None and command.guild_ids != guild_ids + ): + return + commands = command.subcommands + command = get(commands, name=names.pop()) + if not isinstance(command, type) or ( + guild_ids is not None and command.guild_ids != guild_ids + ): + return + return command + + async def get_desynced_commands( + self, + guild_id: int | None = None, + prefetched: list[interactions.ApplicationCommand] | None = None, + ) -> list[dict[str, Any]]: + """|coro| + + Gets the list of commands that are desynced from discord. If ``guild_id`` is specified, it will only return + guild commands that are desynced from said guild, else it will return global commands. + + .. note:: + This function is meant to be used internally, and should only be used if you want to override the default + command registration behavior. + + .. versionadded:: 2.0 + + Parameters + ---------- + guild_id: Optional[:class:`int`] + The guild id to get the desynced commands for, else global commands if unspecified. + prefetched: Optional[List[:class:`.ApplicationCommand`]] + If you already fetched the commands, you can pass them here to be used. Not recommended for typical usage. + + Returns + ------- + List[Dict[:class:`str`, Any]] + A list of the desynced commands. Each will come with at least the ``cmd`` and ``action`` keys, which + respectively contain the command and the action to perform. Other keys may also be present depending on + the action, including ``id``. + """ + + # We can suggest the user to upsert, edit, delete, or bulk upsert the commands + + def _check_command(cmd: ApplicationCommand, match: Mapping[str, Any]) -> bool: + if isinstance(cmd, SlashCommandGroup): + if len(cmd.subcommands) != len(match.get("options", [])): + return True + for i, subcommand in enumerate(cmd.subcommands): + match_ = next( + ( + data + for data in match["options"] + if data["name"] == subcommand.name + ), + MISSING, + ) + if match_ is not MISSING and _check_command(subcommand, match_): + return True + else: + as_dict = cmd.to_dict() + to_check = { + "nsfw": None, + "default_member_permissions": None, + "name": None, + "description": None, + "name_localizations": None, + "description_localizations": None, + "options": [ + "type", + "name", + "description", + "autocomplete", + "choices", + "name_localizations", + "description_localizations", + ], + "contexts": None, + "integration_types": None, + } + for check, value in to_check.items(): + if type(to_check[check]) == list: + # We need to do some falsy conversion here + # The API considers False (autocomplete) and [] (choices) to be falsy values + falsy_vals = (False, []) + for opt in value: + cmd_vals = ( + [val.get(opt, MISSING) for val in as_dict[check]] + if check in as_dict + else [] + ) + for i, val in enumerate(cmd_vals): + if val in falsy_vals: + cmd_vals[i] = MISSING + if match.get( + check, MISSING + ) is not MISSING and cmd_vals != [ + val.get(opt, MISSING) for val in match[check] + ]: + # We have a difference + return True + elif (attr := getattr(cmd, check, None)) != ( + found := match.get(check) + ): + # We might have a difference + if "localizations" in check and bool(attr) == bool(found): + # unlike other attrs, localizations are MISSING by default + continue + elif ( + check == "default_permission" + and attr is True + and found is None + ): + # This is a special case + # TODO: Remove for perms v2 + continue + return True + return False + + return_value = [] + cmds = self.pending_application_commands.copy() + + if guild_id is None: + pending = [cmd for cmd in cmds if cmd.guild_ids is None] + else: + pending = [ + cmd + for cmd in cmds + if cmd.guild_ids is not None and guild_id in cmd.guild_ids + ] + + registered_commands: list[interactions.ApplicationCommand] = [] + if prefetched is not None: + registered_commands = prefetched + elif self._bot.user: + if guild_id is None: + registered_commands = await self._bot.http.get_global_commands( + self._bot.user.id + ) + else: + registered_commands = await self._bot.http.get_guild_commands( + self._bot.user.id, guild_id + ) + + registered_commands_dict = {cmd["name"]: cmd for cmd in registered_commands} + # First let's check if the commands we have locally are the same as the ones on discord + for cmd in pending: + match = registered_commands_dict.get(cmd.name) + if match is None: + # We don't have this command registered + return_value.append({"command": cmd, "action": "upsert"}) + elif _check_command(cmd, match): + return_value.append( + { + "command": cmd, + "action": "edit", + "id": int(registered_commands_dict[cmd.name]["id"]), + } + ) + else: + # We have this command registered but it's the same + return_value.append( + {"command": cmd, "action": None, "id": int(match["id"])} + ) + + # Now let's see if there are any commands on discord that we need to delete + for cmd, value_ in registered_commands_dict.items(): + match = get(pending, name=registered_commands_dict[cmd]["name"]) + if match is None: + # We have this command registered but not in our list + return_value.append( + { + "command": registered_commands_dict[cmd]["name"], + "id": int(value_["id"]), + "action": "delete", + } + ) + + continue + + return return_value + + async def register_command( + self, + command: ApplicationCommand, + force: bool = True, + guild_ids: list[int] | None = None, + ) -> None: + """|coro| + + Registers a command. If the command has ``guild_ids`` set, or if the ``guild_ids`` parameter is passed, + the command will be registered as a guild command for those guilds. + + Parameters + ---------- + command: :class:`~.ApplicationCommand` + The command to register. + force: :class:`bool` + Whether to force the command to be registered. If this is set to False, the command will only be registered + if it seems to already be registered and up to date with our internal cache. Defaults to True. + guild_ids: :class:`list` + A list of guild ids to register the command for. If this is not set, the command's + :attr:`ApplicationCommand.guild_ids` attribute will be used. + + Returns + ------- + :class:`~.ApplicationCommand` + The command that was registered + """ + # TODO: Write this + raise NotImplementedError + + async def register_commands( + self, + commands: list[ApplicationCommand] | None = None, + guild_id: int | None = None, + method: Literal["individual", "bulk", "auto"] = "bulk", + force: bool = False, + delete_existing: bool = True, + ) -> list[interactions.ApplicationCommand]: + """|coro| + + Register a list of commands. + + .. versionadded:: 2.0 + + Parameters + ---------- + commands: Optional[List[:class:`~.ApplicationCommand`]] + A list of commands to register. If this is not set (``None``), then all commands will be registered. + guild_id: Optional[int] + If this is set, the commands will be registered as a guild command for the respective guild. If it is not + set, the commands will be registered according to their :attr:`ApplicationCommand.guild_ids` attribute. + method: Literal['individual', 'bulk', 'auto'] + The method to use when registering the commands. If this is set to "individual", then each command will be + registered individually. If this is set to "bulk", then all commands will be registered in bulk. If this is + set to "auto", then the method will be determined automatically. Defaults to "bulk". + force: :class:`bool` + Registers the commands regardless of the state of the command on Discord. This uses one less API call, but + can result in hitting rate limits more often. Defaults to False. + delete_existing: :class:`bool` + Whether to delete existing commands that are not in the list of commands to register. Defaults to True. + """ + if commands is None: + commands = self.pending_application_commands + + commands = [copy.copy(cmd) for cmd in commands] + + if guild_id is not None: + for cmd in commands: + to_rep_with = [guild_id] + cmd.guild_ids = to_rep_with + + is_global = guild_id is None + + registered = [] + + if is_global: + pending = list(filter(lambda c: c.guild_ids is None, commands)) + registration_methods = { + "bulk": self._bot.http.bulk_upsert_global_commands, + "upsert": self._bot.http.upsert_global_command, + "delete": self._bot.http.delete_global_command, + "edit": self._bot.http.edit_global_command, + } + + def _register( + method: Literal["bulk", "upsert", "delete", "edit"], *args, **kwargs + ): + return registration_methods[method]( + self._bot.user and self._bot.user.id, *args, **kwargs + ) + + else: + pending = list( + filter( + lambda c: c.guild_ids is not None and guild_id in c.guild_ids, + commands, + ) + ) + registration_methods = { + "bulk": self._bot.http.bulk_upsert_guild_commands, + "upsert": self._bot.http.upsert_guild_command, + "delete": self._bot.http.delete_guild_command, + "edit": self._bot.http.edit_guild_command, + } + + def _register( + method: Literal["bulk", "upsert", "delete", "edit"], *args, **kwargs + ): + return registration_methods[method]( + self._bot.user and self._bot.user.id, guild_id, *args, **kwargs + ) + + def register( + method: Literal["bulk", "upsert", "delete", "edit"], + *args, + cmd_name: str = None, + guild_id: int | None = None, + **kwargs, + ): + if kwargs.pop("_log", True): + if method == "bulk": + _log.debug( + f"Bulk updating commands {[c['name'] for c in args[0]]} for" + f" guild {guild_id}" + ) + elif method == "upsert": + _log.debug(f"Creating command {cmd_name} for guild {guild_id}") # type: ignore + elif method == "edit": + _log.debug(f"Editing command {cmd_name} for guild {guild_id}") # type: ignore + elif method == "delete": + _log.debug(f"Deleting command {cmd_name} for guild {guild_id}") # type: ignore + return _register(method, *args, **kwargs) + + pending_actions = [] + + if not force: + prefetched_commands: list[interactions.ApplicationCommand] = [] + if self._bot.user: + if guild_id is None: + prefetched_commands = await self._bot.http.get_global_commands( + self._bot.user.id + ) + else: + prefetched_commands = await self._bot.http.get_guild_commands( + self._bot.user.id, guild_id + ) + desynced = await self.get_desynced_commands( + guild_id=guild_id, prefetched=prefetched_commands + ) + + for cmd in desynced: + if cmd["action"] == "delete": + pending_actions.append( + { + "action": "delete" if delete_existing else None, + "command": collections.namedtuple("Command", ["name"])( + name=cmd["command"] + ), + "id": cmd["id"], + } + ) + continue + # We can assume the command item is a command, since it's only a string if action is delete + match = get(pending, name=cmd["command"].name, type=cmd["command"].type) + if match is None: + continue + if cmd["action"] == "edit": + pending_actions.append( + { + "action": "edit", + "command": match, + "id": cmd["id"], + } + ) + elif cmd["action"] == "upsert": + pending_actions.append( + { + "action": "upsert", + "command": match, + } + ) + elif cmd["action"] is None: + pending_actions.append( + { + "action": None, + "command": match, + } + ) + else: + raise ValueError(f"Unknown action: {cmd['action']}") + filtered_no_action = list( + filter(lambda c: c["action"] is not None, pending_actions) + ) + filtered_deleted = list( + filter(lambda a: a["action"] != "delete", pending_actions) + ) + if method == "bulk" or ( + method == "auto" and len(filtered_deleted) == len(pending) + ): + # Either the method is bulk or all the commands need to be modified, so we can just do a bulk upsert + data = [cmd["command"].to_dict() for cmd in filtered_deleted] + # If there's nothing to update, don't bother + if len(filtered_no_action) == 0: + _log.debug("Skipping bulk command update: Commands are up to date") + registered = prefetched_commands + else: + _log.debug( + "Bulk updating commands %s for guild %s", + {c["command"].name: c["action"] for c in pending_actions}, + guild_id, + ) + registered = await register("bulk", data, _log=False) + else: + if not filtered_no_action: + registered = [] + for cmd in filtered_no_action: + if cmd["action"] == "delete": + await register( + "delete", + cmd["id"], + cmd_name=cmd["command"].name, + guild_id=guild_id, + ) + continue + if cmd["action"] == "edit": + registered.append( + await register( + "edit", + cmd["id"], + cmd["command"].to_dict(), + cmd_name=cmd["command"].name, + guild_id=guild_id, + ) + ) + elif cmd["action"] == "upsert": + registered.append( + await register( + "upsert", + cmd["command"].to_dict(), + cmd_name=cmd["command"].name, + guild_id=guild_id, + ) + ) + else: + raise ValueError(f"Unknown action: {cmd['action']}") + + # TODO: Our lists dont work sometimes, see if that can be fixed so we can avoid this second API call + if method != "bulk": + if self._bot.user: + if guild_id is None: + registered = await self._bot.http.get_global_commands( + self._bot.user.id + ) + else: + registered = await self._bot.http.get_guild_commands( + self._bot.user.id, guild_id + ) + else: + data = [cmd.to_dict() for cmd in pending] + registered = await register("bulk", data, guild_id=guild_id) + + for i in registered: + cmd = get( + self.pending_application_commands, + name=i["name"], + type=i.get("type"), + ) + if not cmd: + raise ValueError( + f"Registered command {i['name']}, type {i.get('type')} not found in" + " pending commands" + ) + cmd.id = i["id"] + self._application_commands[cmd.id] = cmd + + return registered + + async def sync_commands( + self, + commands: list[ApplicationCommand] | None = None, + method: Literal["individual", "bulk", "auto"] = "bulk", + force: bool = False, + guild_ids: list[int] | None = None, + register_guild_commands: bool = True, + check_guilds: list[int] | None = [], + delete_existing: bool = True, + ) -> None: + """|coro| + + Registers all commands that have been added through :meth:`.add_application_command`. This method cleans up all + commands over the API and should sync them with the internal cache of commands. It attempts to register the + commands in the most efficient way possible, unless ``force`` is set to ``True``, in which case it will always + register all commands. + + By default, this coroutine is called inside the :func:`.on_connect` event. If you choose to override the + :func:`.on_connect` event, then you should invoke this coroutine as well such as the following: + + .. code-block:: python + + @bot.event + async def on_connect(): + if bot.auto_sync_commands: + await bot.sync_commands() + print(f"{bot.user.name} connected.") + + .. note:: + If you remove all guild commands from a particular guild, the library may not be able to detect and update + the commands accordingly, as it would have to individually check for each guild. To force the library to + unregister a guild's commands, call this function with ``commands=[]`` and ``guild_ids=[guild_id]``. + + .. versionadded:: 2.0 + + Parameters + ---------- + commands: Optional[List[:class:`~.ApplicationCommand`]] + A list of commands to register. If this is not set (None), then all commands will be registered. + method: Literal['individual', 'bulk', 'auto'] + The method to use when registering the commands. If this is set to "individual", then each command will be + registered individually. If this is set to "bulk", then all commands will be registered in bulk. If this is + set to "auto", then the method will be determined automatically. Defaults to "bulk". + force: :class:`bool` + Registers the commands regardless of the state of the command on Discord. This uses one less API call, but + can result in hitting rate limits more often. Defaults to False. + guild_ids: Optional[List[:class:`int`]] + A list of guild ids to register the commands for. If this is not set, the commands' + :attr:`~.ApplicationCommand.guild_ids` attribute will be used. + register_guild_commands: :class:`bool` + Whether to register guild commands. Defaults to True. + check_guilds: Optional[List[:class:`int`]] + A list of guilds ids to check for commands to unregister, since the bot would otherwise have to check all + guilds. Unlike ``guild_ids``, this does not alter the commands' :attr:`~.ApplicationCommand.guild_ids` + attribute, instead it adds the guild ids to a list of guilds to sync commands for. If + ``register_guild_commands`` is set to False, then this parameter is ignored. + delete_existing: :class:`bool` + Whether to delete existing commands that are not in the list of commands to register. Defaults to True. + """ + + check_guilds = list(set((check_guilds or []) + (self._bot.debug_guilds or []))) + + if commands is None: + commands = self.pending_application_commands + + if guild_ids is not None: + for cmd in commands: + cmd.guild_ids = guild_ids + + global_commands = [cmd for cmd in commands if cmd.guild_ids is None] + registered_commands = await self.register_commands( + global_commands, method=method, force=force, delete_existing=delete_existing + ) + + registered_guild_commands: dict[int, list[interactions.ApplicationCommand]] = {} + + if register_guild_commands: + cmd_guild_ids: list[int] = [] + for cmd in commands: + if cmd.guild_ids is not None: + cmd_guild_ids.extend(cmd.guild_ids) + if check_guilds is not None: + cmd_guild_ids.extend(check_guilds) + for guild_id in set(cmd_guild_ids): + guild_commands = [ + cmd + for cmd in commands + if cmd.guild_ids is not None and guild_id in cmd.guild_ids + ] + app_cmds = await self.register_commands( + guild_commands, + guild_id=guild_id, + method=method, + force=force, + delete_existing=delete_existing, + ) + registered_guild_commands[guild_id] = app_cmds + + for i in registered_commands: + cmd = get( + self.pending_application_commands, + name=i["name"], + guild_ids=None, + type=i.get("type"), + ) + if cmd: + cmd.id = i["id"] + self._application_commands[cmd.id] = cmd + + if register_guild_commands and registered_guild_commands: + for guild_id, guild_cmds in registered_guild_commands.items(): + for i in guild_cmds: + cmd = find( + lambda cmd: cmd.name == i["name"] + and cmd.type == i.get("type") + and cmd.guild_ids is not None + and (guild_id := i.get("guild_id")) + and guild_id in cmd.guild_ids, + self.pending_application_commands, + ) + if not cmd: + # command has not been added yet + continue + cmd.id = i["id"] + self._application_commands[cmd.id] = cmd + + async def process_application_commands( + self, interaction: Interaction, auto_sync: bool | None = None + ) -> None: + """|coro| + + This function processes the commands that have been registered + to the bot and other groups. Without this coroutine, none of the + commands will be triggered. + + By default, this coroutine is called inside the :func:`.on_interaction` + event. If you choose to override the :func:`.on_interaction` event, then + you should invoke this coroutine as well. + + This function finds a registered command matching the interaction id from + application commands and invokes it. If no matching command was + found, it replies to the interaction with a default message. + + .. versionadded:: 2.0 + + Parameters + ---------- + interaction: :class:`discord.Interaction` + The interaction to process + auto_sync: Optional[:class:`bool`] + Whether to automatically sync and unregister the command if it is not found in the internal cache. This will + invoke the :meth:`~.Bot.sync_commands` method on the context of the command, either globally or per-guild, + based on the type of the command, respectively. Defaults to :attr:`.Bot.auto_sync_commands`. + """ + if auto_sync is None: + auto_sync = self._bot.auto_sync_commands + # TODO: find out why the isinstance check below doesn't stop the type errors below + if interaction.type not in ( + InteractionType.application_command, + InteractionType.auto_complete, + ): + return + + command: ApplicationCommand | None = None + try: + if interaction.data: + command = self._application_commands[interaction.data["id"]] # type: ignore + except KeyError: + for cmd in self.application_commands + self.pending_application_commands: + if interaction.data: + guild_id = interaction.data.get("guild_id") + if guild_id: + guild_id = int(guild_id) + if cmd.name == interaction.data["name"] and ( # type: ignore + guild_id == cmd.guild_ids + or ( + isinstance(cmd.guild_ids, list) + and guild_id in cmd.guild_ids + ) + ): + command = cmd + break + else: + if auto_sync and interaction.data: + guild_id = interaction.data.get("guild_id") + if guild_id is None: + await self.sync_commands() + else: + await self.sync_commands(check_guilds=[guild_id]) + return self._bot.dispatch("unknown_application_command", interaction) + + if interaction.type is InteractionType.auto_complete: + return self._bot.dispatch( + "application_command_auto_complete", interaction, command + ) + + ctx = await self.get_application_context(interaction) + if command: + interaction.command = command + await self.invoke_application_command(ctx) + + async def on_application_command_auto_complete( + self, interaction: Interaction, command: ApplicationCommand + ) -> None: + async def callback() -> None: + ctx = await self.get_autocomplete_context(interaction) + interaction.command = command + return await command.invoke_autocomplete_callback(ctx) + + autocomplete_task = self._bot.loop.create_task(callback()) + try: + await self._bot.wait_for( + "application_command_auto_complete", + check=lambda i, c: c == command, + timeout=3, + ) + except asyncio.TimeoutError: + return + else: + if not autocomplete_task.done(): + autocomplete_task.cancel() + + def slash_command( + self, + *, + checks: list[Callable[[ApplicationContext], bool]] | None = MISSING, + cog: Cog | None = MISSING, + contexts: set[InteractionContextType] | None = MISSING, + cooldown: Cooldown | None = MISSING, + default_member_permissions: Permissions | None = MISSING, + description: str | None = MISSING, + description_localizations: dict[str, str] | None = MISSING, + guild_ids: list[int] | None = MISSING, + guild_only: bool | None = MISSING, + integration_types: set[IntegrationType] | None = MISSING, + name: str | None = MISSING, + name_localizations: dict[str, str] | None = MISSING, + nsfw: bool | None = MISSING, + options: list[Option] | None = MISSING, + parent: SlashCommandGroup | None = MISSING, + **kwargs: Never, + ) -> Callable[..., SlashCommand]: + """A shortcut decorator for adding a slash command to the bot. + This is equivalent to using :meth:`application_command`, providing + the :class:`SlashCommand` class. + + .. versionadded:: 2.0 + + Returns + ------- + Callable[..., :class:`SlashCommand`] + A decorator that converts the provided function into a :class:`.SlashCommand`, + adds it to the bot, and returns it. + """ + return self.application_command( + cls=SlashCommand, + checks=checks, + cog=cog, + contexts=contexts, + cooldown=cooldown, + default_member_permissions=default_member_permissions, + description=description, + description_localizations=description_localizations, + guild_ids=guild_ids, + guild_only=guild_only, + integration_types=integration_types, + name=name, + name_localizations=name_localizations, + nsfw=nsfw, + options=options, + parent=parent, + **kwargs, + ) + + def user_command( + self, + *, + checks: list[Callable[[ApplicationContext], bool]] | None = MISSING, + cog: Cog | None = MISSING, + contexts: set[InteractionContextType] | None = MISSING, + cooldown: Cooldown | None = MISSING, + default_member_permissions: Permissions | None = MISSING, + guild_ids: list[int] | None = MISSING, + guild_only: bool | None = MISSING, + integration_types: set[IntegrationType] | None = MISSING, + name: str | None = MISSING, + name_localizations: dict[str, str] | None = MISSING, + nsfw: bool | None = MISSING, + **kwargs: Never, + ) -> Callable[..., UserCommand]: + """A shortcut decorator for adding a user command to the bot. + This is equivalent to using :meth:`application_command`, providing + the :class:`UserCommand` class. + + .. versionadded:: 2.0 + + Returns + ------- + Callable[..., :class:`UserCommand`] + A decorator that converts the provided function into a :class:`.UserCommand`, + adds it to the bot, and returns it. + """ + return self.application_command( + cls=UserCommand, + checks=checks, + cog=cog, + contexts=contexts, + cooldown=cooldown, + default_member_permissions=default_member_permissions, + guild_ids=guild_ids, + guild_only=guild_only, + integration_types=integration_types, + name=name, + name_localizations=name_localizations, + nsfw=nsfw, + **kwargs, + ) + + def message_command( + self, + *, + checks: list[Callable[[ApplicationContext], bool]] | None = MISSING, + cog: Cog | None = MISSING, + contexts: set[InteractionContextType] | None = MISSING, + cooldown: Cooldown | None = MISSING, + default_member_permissions: Permissions | None = MISSING, + guild_ids: list[int] | None = MISSING, + guild_only: bool | None = MISSING, + integration_types: set[IntegrationType] | None = MISSING, + name: str | None = MISSING, + name_localizations: dict[str, str] | None = MISSING, + nsfw: bool | None = MISSING, + **kwargs: Never, + ) -> Callable[..., MessageCommand]: + """A shortcut decorator for adding a message command to the bot. + This is equivalent to using :meth:`application_command`, providing + the :class:`MessageCommand` class. + + .. versionadded:: 2.0 + + Returns + ------- + Callable[..., :class:`MessageCommand`] + A decorator that converts the provided function into a :class:`.MessageCommand`, + adds it to the bot, and returns it. + """ + return self.application_command( + cls=MessageCommand, + checks=checks, + cog=cog, + contexts=contexts, + cooldown=cooldown, + default_member_permissions=default_member_permissions, + guild_ids=guild_ids, + guild_only=guild_only, + integration_types=integration_types, + name=name, + name_localizations=name_localizations, + nsfw=nsfw, + **kwargs, + ) + + def application_command( + self, + *, + cls: type[C] = SlashCommand, + checks: list[Callable[[ApplicationContext], bool]] | None = MISSING, + cog: Cog | None = MISSING, + contexts: set[InteractionContextType] | None = MISSING, + cooldown: Cooldown | None = MISSING, + default_member_permissions: Permissions | None = MISSING, + description: str | None = MISSING, + description_localizations: dict[str, str] | None = MISSING, + guild_ids: list[int] | None = MISSING, + guild_only: bool | None = MISSING, + integration_types: set[IntegrationType] | None = MISSING, + name: str | None = MISSING, + name_localizations: dict[str, str] | None = MISSING, + nsfw: bool | None = MISSING, + options: list[Option] | None = MISSING, + parent: SlashCommandGroup | None = MISSING, + **kwargs: Any, + ) -> Callable[..., C]: + """A shortcut decorator that converts the provided function into + an application command via :func:`command` and adds it to + the internal command list via :meth:`~.Bot.add_application_command`. + + .. versionadded:: 2.0 + + Parameters + ---------- + cls: Type[:class:`ApplicationCommand`] + The factory class that will be used to create the command. + By default, this is :class:`.SlashCommand`. Should a custom + class be provided, it must be a subclass of either + :class:`SlashCommand`, :class:`MessageCommand` or :class:`UserCommand`. + + Returns + ------- + Callable[..., :class:`ApplicationCommand`] + A decorator that converts the provided function into an :class:`.ApplicationCommand`, + adds it to the bot, and returns it. + """ + + params = { + "checks": checks, + "cog": cog, + "contexts": contexts, + "cooldown": cooldown, + "default_member_permissions": default_member_permissions, + "description": description, + "description_localizations": description_localizations, + "guild_ids": guild_ids, + "guild_only": guild_only, + "integration_types": integration_types, + "name": name, + "name_localizations": name_localizations, + "nsfw": nsfw, + "options": options, + "parent": parent, + **kwargs, + } + kwargs = {k: v for k, v in params.items() if v is not MISSING} + + def decorator(func) -> C: + result = command(cls=cls, **kwargs)(func) + self.add_application_command(result) + return result + + return decorator + + def command(self, **kwargs): + """An alias for :meth:`application_command`. + + .. note:: + + This decorator is overridden by :class:`discord.ext.commands.Bot`. + + .. versionadded:: 2.0 + + Returns + ------- + Callable[..., :class:`ApplicationCommand`] + A decorator that converts the provided function into an :class:`.ApplicationCommand`, + adds it to the bot, and returns it. + """ + return self.application_command(**kwargs) + + def create_group( + self, + name: str, + description: str | None = None, + guild_ids: list[int] | None = None, + **kwargs, + ) -> SlashCommandGroup: + """A shortcut method that creates a slash command group with no subcommands and adds it to the internal + command list via :meth:`add_application_command`. + + .. versionadded:: 2.0 + + Parameters + ---------- + name: :class:`str` + The name of the group to create. + description: Optional[:class:`str`] + The description of the group to create. + guild_ids: Optional[List[:class:`int`]] + A list of the IDs of each guild this group should be added to, making it a guild command. + This will be a global command if ``None`` is passed. + kwargs: + Any additional keyword arguments to pass to :class:`.SlashCommandGroup`. + + Returns + ------- + SlashCommandGroup + The slash command group that was created. + """ + description = description or "No description provided." + group = SlashCommandGroup(name, description, guild_ids, **kwargs) + self.add_application_command(group) + return group + + def group( + self, + name: str | None = None, + description: str | None = None, + guild_ids: list[int] | None = None, + ) -> Callable[[type[SlashCommandGroup]], SlashCommandGroup]: + """A shortcut decorator that initializes the provided subclass of :class:`.SlashCommandGroup` + and adds it to the internal command list via :meth:`add_application_command`. + + .. versionadded:: 2.0 + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the group to create. This will resolve to the name of the decorated class if ``None`` is passed. + description: Optional[:class:`str`] + The description of the group to create. + guild_ids: Optional[List[:class:`int`]] + A list of the IDs of each guild this group should be added to, making it a guild command. + This will be a global command if ``None`` is passed. + + Returns + ------- + Callable[[Type[SlashCommandGroup]], SlashCommandGroup] + The slash command group that was created. + """ + + def inner(cls: type[SlashCommandGroup]) -> SlashCommandGroup: + group = cls( + name or cls.__name__, + ( + description or inspect.cleandoc(cls.__doc__).splitlines()[0] + if cls.__doc__ is not None + else "No description provided" + ), + guild_ids=guild_ids, + ) + self.add_application_command(group) + return group + + return inner + + slash_group = group + + def walk_application_commands(self) -> Generator[ApplicationCommand]: + """An iterator that recursively walks through all application commands and subcommands. + + Yields + ------ + :class:`.ApplicationCommand` + An application command from the internal list of application commands. + """ + for command in self.application_commands: + if isinstance(command, SlashCommandGroup): + yield from command.walk_commands() + yield command + + async def get_application_context( + self, interaction: Interaction, cls: Any = ApplicationContext + ) -> ApplicationContext: + r"""|coro| + + Returns the invocation context from the interaction. + + This is a more low-level counter-part for :meth:`.process_application_commands` + to allow users more fine-grained control over the processing. + + Parameters + ----------- + interaction: :class:`discord.Interaction` + The interaction to get the invocation context from. + cls + The factory class that will be used to create the context. + By default, this is :class:`.ApplicationContext`. Should a custom + class be provided, it must be similar enough to + :class:`.ApplicationContext`\'s interface. + + Returns + -------- + :class:`.ApplicationContext` + The invocation context. The type of this can change via the + ``cls`` parameter. + """ + return cls(self, interaction) + + async def get_autocomplete_context( + self, interaction: Interaction, cls: Any = AutocompleteContext + ) -> AutocompleteContext: + r"""|coro| + + Returns the autocomplete context from the interaction. + + This is a more low-level counter-part for :meth:`.process_application_commands` + to allow users more fine-grained control over the processing. + + Parameters + ----------- + interaction: :class:`discord.Interaction` + The interaction to get the invocation context from. + cls + The factory class that will be used to create the context. + By default, this is :class:`.AutocompleteContext`. Should a custom + class be provided, it must be similar enough to + :class:`.AutocompleteContext`\'s interface. + + Returns + -------- + :class:`.AutocompleteContext` + The autocomplete context. The type of this can change via the + ``cls`` parameter. + """ + return cls(self, interaction) + + async def invoke_application_command(self, ctx: ApplicationContext) -> None: + """|coro| + + Invokes the application command given under the invocation + context and handles all the internal event dispatch mechanisms. + + Parameters + ---------- + ctx: :class:`.ApplicationCommand` + The invocation context to invoke. + """ + self._bot.dispatch("application_command", ctx) + try: + if await self._bot.can_run(ctx, call_once=True): + await ctx.command.invoke(ctx) + else: + raise CheckFailure("The global check once functions failed.") + except DiscordException as exc: + await ctx.command.dispatch_error(ctx, exc) + else: + self._bot.dispatch("application_command_completion", ctx) + + @property + @abstractmethod + def _bot(self) -> Bot | AutoShardedBot: ... + + +class BotBase(ApplicationCommandMixin, CogMixin, ABC): + _supports_prefixed_commands = False + + def __init__(self, description=None, *args, **options): + super().__init__(*args, **options) + self.__cogs = {} # TYPE: Dict[str, Cog] + self.__extensions = {} # TYPE: Dict[str, types.ModuleType] + self._checks = [] # TYPE: List[Check] + self._check_once = [] + self._before_invoke = None + self._after_invoke = None + self.description = inspect.cleandoc(description) if description else "" + self.owner_id = options.get("owner_id") + self.owner_ids = options.get("owner_ids", set()) + self.auto_sync_commands = options.get("auto_sync_commands", True) + + self.debug_guilds = options.pop("debug_guilds", None) + self.default_command_contexts = options.pop( + "default_command_contexts", + { + InteractionContextType.guild, + InteractionContextType.bot_dm, + InteractionContextType.private_channel, + }, + ) + + self.default_command_integration_types = options.pop( + "default_command_integration_types", + { + IntegrationType.guild_install, + }, + ) + + if self.owner_id and self.owner_ids: + raise TypeError("Both owner_id and owner_ids are set.") + + if self.owner_ids and not isinstance( + self.owner_ids, collections.abc.Collection + ): + raise TypeError( + f"owner_ids must be a collection not {self.owner_ids.__class__!r}" + ) + if not isinstance(self.default_command_contexts, collections.abc.Collection): + raise TypeError( + f"default_command_contexts must be a collection not {self.default_command_contexts.__class__!r}" + ) + if not isinstance( + self.default_command_integration_types, collections.abc.Collection + ): + raise TypeError( + f"default_command_integration_types must be a collection not {self.default_command_integration_types.__class__!r}" + ) + self.default_command_contexts = set(self.default_command_contexts) + self.default_command_integration_types = set( + self.default_command_integration_types + ) + + self._checks = [] + self._check_once = [] + self._before_invoke = None + self._after_invoke = None + + async def on_connect(self): + if self.auto_sync_commands: + await self.sync_commands() + + async def on_interaction(self, interaction): + await self.process_application_commands(interaction) + + async def on_application_command_error( + self, context: ApplicationContext, exception: DiscordException + ) -> None: + """|coro| + + The default command error handler provided by the bot. + + By default, this prints to :data:`sys.stderr` however it could be + overridden to have a different implementation. + + This only fires if you do not specify any listeners for command error. + """ + if self._event_handlers.get("on_application_command_error", None): + return + command = context.command + if command and command.has_error_handler(): + return + + cog = context.cog + if cog and cog.has_error_handler(): + return + + print(f"Ignoring exception in command {context.command}:", file=sys.stderr) + traceback.print_exception( + type(exception), exception, exception.__traceback__, file=sys.stderr + ) + + # global check registration + # TODO: Remove these from commands.Bot + + def check(self, func): + """A decorator that adds a global check to the bot. A global check is similar to a :func:`.check` that is + applied on a per-command basis except it is run before any command checks have been verified and applies to + every command the bot has. + + .. note:: + + This function can either be a regular function or a coroutine. Similar to a command :func:`.check`, this + takes a single parameter of type :class:`.Context` and can only raise exceptions inherited from + :exc:`.ApplicationCommandError`. + + Example + ------- + .. code-block:: python3 + + @bot.check + def check_commands(ctx): + return ctx.command.qualified_name in allowed_commands + """ + # T was used instead of Check to ensure the type matches on return + self.add_check(func) # type: ignore + return func + + def add_check(self, func, *, call_once: bool = False) -> None: + """Adds a global check to the bot. This is the non-decorator interface to :meth:`.check` and + :meth:`.check_once`. + + Parameters + ---------- + func + The function that was used as a global check. + call_once: :class:`bool` + If the function should only be called once per :meth:`.Bot.invoke` call. + """ + + if call_once: + self._check_once.append(func) + else: + self._checks.append(func) + + def remove_check(self, func, *, call_once: bool = False) -> None: + """Removes a global check from the bot. + This function is idempotent and will not raise an exception + if the function is not in the global checks. + + Parameters + ---------- + func + The function to remove from the global checks. + call_once: :class:`bool` + If the function was added with ``call_once=True`` in + the :meth:`.Bot.add_check` call or using :meth:`.check_once`. + """ + checks = self._check_once if call_once else self._checks + + try: + checks.remove(func) + except ValueError: + pass + + def check_once(self, func): + """A decorator that adds a "call once" global check to the bot. Unlike regular global checks, this one is called + only once per :meth:`.Bot.invoke` call. Regular global checks are called whenever a command is called or + :meth:`.Command.can_run` is called. This type of check bypasses that and ensures that it's called only once, + even inside the default help command. + + .. note:: + + When using this function the :class:`.Context` sent to a group subcommand may only parse the parent command + and not the subcommands due to it being invoked once per :meth:`.Bot.invoke` call. + + .. note:: + + This function can either be a regular function or a coroutine. Similar to a command :func:`.check`, + this takes a single parameter of type :class:`.Context` and can only raise exceptions inherited from + :exc:`.ApplicationCommandError`. + + Example + ------- + .. code-block:: python3 + + @bot.check_once + def whitelist(ctx): + return ctx.message.author.id in my_whitelist + """ + self.add_check(func, call_once=True) + return func + + async def can_run( + self, ctx: ApplicationContext, *, call_once: bool = False + ) -> bool: + data = self._check_once if call_once else self._checks + + if not data: + return True + + # type-checker doesn't distinguish between functions and methods + return await async_all(f(ctx) for f in data) # type: ignore + + def before_invoke(self, coro): + """A decorator that registers a coroutine as a pre-invoke hook. + A pre-invoke hook is called directly before the command is + called. This makes it a useful function to set up database + connections or any type of set up required. + This pre-invoke hook takes a sole parameter, a :class:`.Context`. + + .. note:: + + The :meth:`~.Bot.before_invoke` and :meth:`~.Bot.after_invoke` hooks are + only called if all checks and argument parsing procedures pass + without error. If any check or argument parsing procedures fail + then the hooks are not called. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the pre-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not asyncio.iscoroutinefunction(coro): + raise TypeError("The pre-invoke hook must be a coroutine.") + + self._before_invoke = coro + return coro + + def after_invoke(self, coro): + r"""A decorator that registers a coroutine as a post-invoke hook. + A post-invoke hook is called directly after the command is + called. This makes it a useful function to clean-up database + connections or any type of clean up required. + This post-invoke hook takes a sole parameter, a :class:`.Context`. + + .. note:: + + Similar to :meth:`~.Bot.before_invoke`\, this is not called unless + checks and argument parsing procedures succeed. This hook is, + however, **always** called regardless of the internal command + callback raising an error (i.e. :exc:`.CommandInvokeError`\). + This makes it ideal for clean-up scenarios. + + Parameters + ----------- + coro: :ref:`coroutine ` + The coroutine to register as the post-invoke hook. + + Raises + ------- + TypeError + The coroutine passed is not actually a coroutine. + + """ + if not asyncio.iscoroutinefunction(coro): + raise TypeError("The post-invoke hook must be a coroutine.") + + self._after_invoke = coro + return coro + + async def is_owner(self, user: User | Member) -> bool: + """|coro| + + Checks if a :class:`~discord.User` or :class:`~discord.Member` is the owner of + this bot. + + If an :attr:`owner_id` is not set, it is fetched automatically + through the use of :meth:`~.Bot.application_info`, returning + the application owner, or all non read-only team members. + + .. versionchanged:: 1.3 + The function also checks if the application is team-owned if + :attr:`owner_ids` is not set. + + Parameters + ---------- + user: Union[:class:`.abc.User`, :class:`.member.Member`] + The user to check for. + + Returns + ------- + :class:`bool` + Whether the user is the owner. + """ + + if self.owner_id: + return user.id == self.owner_id + elif self.owner_ids: + return user.id in self.owner_ids + else: + app = await self.application_info() # type: ignore + if app.team: + self.owner_ids = ids = { + m.id for m in app.team.members if m.role is not TeamRole.read_only + } + return user.id in ids + else: + self.owner_id = owner_id = app.owner.id + return user.id == owner_id + + +class Bot(BotBase, Client): + """Represents a discord bot. + + This class is a subclass of :class:`discord.Client` and as a result + anything that you can do with a :class:`discord.Client` you can do with + this bot. + + This class also subclasses ``ApplicationCommandMixin`` to provide the functionality + to manage commands. + + .. versionadded:: 2.0 + + Attributes + ---------- + description: :class:`str` + The content prefixed into the default help message. + owner_id: Optional[:class:`int`] + The user ID that owns the bot. If this is not set and is then queried via + :meth:`.is_owner` then it is fetched automatically using + :meth:`~.Bot.application_info`, returning the application owner. + owner_ids: Optional[Collection[:class:`int`]] + The user IDs that owns the bot. This is similar to :attr:`owner_id`. + If this is not set and the application is team based, then it is + fetched automatically using :meth:`~.Bot.application_info`, + returning all non read-only team members. + For performance reasons it is recommended to use a :class:`set` + for the collection. You cannot set both ``owner_id`` and ``owner_ids``. + + .. versionadded:: 1.3 + debug_guilds: Optional[List[:class:`int`]] + Guild IDs of guilds to use for testing commands. + The bot will not create any global commands if debug guild IDs are passed. + + .. versionadded:: 2.0 + auto_sync_commands: :class:`bool` + Whether to automatically sync slash commands. This will call :meth:`~.Bot.sync_commands` in :func:`discord.on_connect`, and in + :attr:`.process_application_commands` if the command is not found. Defaults to ``True``. + + .. versionadded:: 2.0 + default_command_contexts: Collection[:class:`InteractionContextType`] + The default context types that the bot will use for commands. + Defaults to a set containing :attr:`InteractionContextType.guild`, :attr:`InteractionContextType.bot_dm`, and + :attr:`InteractionContextType.private_channel`. + + .. versionadded:: 2.6 + default_command_integration_types: Collection[:class:`IntegrationType`]] + The default integration types that the bot will use for commands. + Defaults to a set containing :attr:`IntegrationType.guild_install`. + + .. versionadded:: 2.6 + """ + + @property + def _bot(self) -> Bot: + return self + + +class AutoShardedBot(BotBase, AutoShardedClient): + """This is similar to :class:`.Bot` except that it is inherited from + :class:`discord.AutoShardedClient` instead. + + .. versionadded:: 2.0 + """ + + @property + def _bot(self) -> AutoShardedBot: + return self diff --git a/venv/lib/python3.11/site-packages/discord/channel.py b/venv/lib/python3.11/site-packages/discord/channel.py new file mode 100644 index 0000000..2a6d518 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/channel.py @@ -0,0 +1,3641 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import datetime +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Iterable, + Mapping, + NamedTuple, + Sequence, + TypeVar, + overload, +) + +from typing_extensions import deprecated + +import discord.abc + +from . import utils +from .asset import Asset +from .emoji import GuildEmoji +from .enums import ( + ChannelType, + EmbeddedActivity, + InviteTarget, + SortOrder, + StagePrivacyLevel, +) +from .enums import ThreadArchiveDuration as ThreadArchiveDurationEnum +from .enums import ( + VideoQualityMode, + VoiceChannelEffectAnimationType, + VoiceRegion, + try_enum, +) +from .errors import ClientException, InvalidArgument +from .file import File +from .flags import ChannelFlags, MessageFlags +from .invite import Invite +from .iterators import ArchivedThreadIterator +from .mixins import Hashable +from .object import Object +from .partial_emoji import PartialEmoji, _EmojiTag +from .permissions import PermissionOverwrite, Permissions +from .soundboard import PartialSoundboardSound, SoundboardSound +from .stage_instance import StageInstance +from .threads import Thread +from .utils import MISSING + +__all__ = ( + "TextChannel", + "VoiceChannel", + "StageChannel", + "DMChannel", + "CategoryChannel", + "GroupChannel", + "PartialMessageable", + "ForumChannel", + "MediaChannel", + "ForumTag", + "VoiceChannelEffectSendEvent", +) + +if TYPE_CHECKING: + from .abc import Snowflake, SnowflakeTime + from .embeds import Embed + from .guild import Guild + from .guild import GuildChannel as GuildChannelType + from .member import Member, VoiceState + from .mentions import AllowedMentions + from .message import EmojiInputType, Message, PartialMessage + from .role import Role + from .state import ConnectionState + from .sticker import GuildSticker, StickerItem + from .types.channel import CategoryChannel as CategoryChannelPayload + from .types.channel import DMChannel as DMChannelPayload + from .types.channel import ForumChannel as ForumChannelPayload + from .types.channel import ForumTag as ForumTagPayload + from .types.channel import GroupDMChannel as GroupChannelPayload + from .types.channel import StageChannel as StageChannelPayload + from .types.channel import TextChannel as TextChannelPayload + from .types.channel import VoiceChannel as VoiceChannelPayload + from .types.channel import VoiceChannelEffectSendEvent as VoiceChannelEffectSend + from .types.snowflake import SnowflakeList + from .types.threads import ThreadArchiveDuration + from .ui.view import BaseView + from .user import BaseUser, ClientUser, User + from .webhook import Webhook + + +class ForumTag(Hashable): + """Represents a forum tag that can be added to a thread inside a :class:`ForumChannel` + . + .. versionadded:: 2.3 + + .. container:: operations + + .. describe:: x == y + + Checks if two forum tags are equal. + + .. describe:: x != y + + Checks if two forum tags are not equal. + + .. describe:: hash(x) + + Returns the forum tag's hash. + + .. describe:: str(x) + + Returns the forum tag's name. + + Attributes + ---------- + id: :class:`int` + The tag ID. + Note that if the object was created manually then this will be ``0``. + name: :class:`str` + The name of the tag. Can only be up to 20 characters. + moderated: :class:`bool` + Whether this tag can only be added or removed by a moderator with + the :attr:`~Permissions.manage_threads` permission. + emoji: :class:`PartialEmoji` + The emoji that is used to represent this tag. + Note that if the emoji is a custom emoji, it will *not* have name information. + """ + + __slots__ = ("name", "id", "moderated", "emoji") + + def __init__( + self, *, name: str, emoji: EmojiInputType, moderated: bool = False + ) -> None: + self.name: str = name + self.id: int = 0 + self.moderated: bool = moderated + self.emoji: PartialEmoji + if isinstance(emoji, _EmojiTag): + self.emoji = emoji._to_partial() + elif isinstance(emoji, str): + self.emoji = PartialEmoji.from_str(emoji) + else: + raise TypeError( + "emoji must be a GuildEmoji, PartialEmoji, or str and not" + f" {emoji.__class__!r}" + ) + + def __repr__(self) -> str: + return ( + "" + ) + + def __str__(self) -> str: + return self.name + + @classmethod + def from_data(cls, *, state: ConnectionState, data: ForumTagPayload) -> ForumTag: + self = cls.__new__(cls) + self.name = data["name"] + self.id = int(data["id"]) + self.moderated = data.get("moderated", False) + + emoji_name = data["emoji_name"] or "" + emoji_id = utils._get_as_snowflake(data, "emoji_id") or None + self.emoji = PartialEmoji.with_state(state=state, name=emoji_name, id=emoji_id) + return self + + def to_dict(self) -> dict[str, Any]: + payload: dict[str, Any] = { + "name": self.name, + "moderated": self.moderated, + } | self.emoji._to_forum_reaction_payload() + + if self.id: + payload["id"] = self.id + + return payload + + +class _TextChannel(discord.abc.GuildChannel, Hashable): + __slots__ = ( + "name", + "id", + "guild", + "topic", + "_state", + "nsfw", + "category_id", + "position", + "slowmode_delay", + "_overwrites", + "_type", + "last_message_id", + "default_auto_archive_duration", + "default_thread_slowmode_delay", + "default_reaction_emoji", + "default_sort_order", + "available_tags", + "flags", + ) + + def __init__( + self, + *, + state: ConnectionState, + guild: Guild, + data: TextChannelPayload | ForumChannelPayload, + ): + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self._update(guild, data) + + @property + def _repr_attrs(self) -> tuple[str, ...]: + return "id", "name", "position", "category_id" + + def __repr__(self) -> str: + attrs = [(val, getattr(self, val)) for val in self._repr_attrs] + joined = " ".join("%s=%r" % t for t in attrs) + return f"<{self.__class__.__name__} {joined}>" + + def _update( + self, guild: Guild, data: TextChannelPayload | ForumChannelPayload + ) -> None: + # This data will always exist + self.guild: Guild = guild + self.name: str = data["name"] + self.category_id: int | None = utils._get_as_snowflake(data, "parent_id") + self._type: int = data["type"] + # This data may be missing depending on how this object is being created/updated + if not data.pop("_invoke_flag", False): + self.topic: str | None = data.get("topic") + self.position: int = data.get("position") + self.nsfw: bool = data.get("nsfw", False) + # Does this need coercion into `int`? No idea yet. + self.slowmode_delay: int = data.get("rate_limit_per_user", 0) + self.default_auto_archive_duration: ThreadArchiveDuration = data.get( + "default_auto_archive_duration", 1440 + ) + self.default_thread_slowmode_delay: int | None = data.get( + "default_thread_rate_limit_per_user" + ) + self.last_message_id: int | None = utils._get_as_snowflake( + data, "last_message_id" + ) + self.flags: ChannelFlags = ChannelFlags._from_value(data.get("flags", 0)) + self._fill_overwrites(data) + + @property + def type(self) -> ChannelType: + """The channel's Discord type.""" + return try_enum(ChannelType, self._type) + + @property + def _sorting_bucket(self) -> int: + return ChannelType.text.value + + @utils.copy_doc(discord.abc.GuildChannel.permissions_for) + def permissions_for(self, obj: Member | Role, /) -> Permissions: + base = super().permissions_for(obj) + + # text channels do not have voice related permissions + denied = Permissions.voice() + base.value &= ~denied.value + return base + + @property + def members(self) -> list[Member]: + """Returns all members that can see this channel.""" + return [m for m in self.guild.members if self.permissions_for(m).read_messages] + + @property + def threads(self) -> list[Thread]: + """Returns all the threads that you can see. + + .. versionadded:: 2.0 + """ + return [ + thread + for thread in self.guild._threads.values() + if thread.parent_id == self.id + ] + + def is_nsfw(self) -> bool: + """Checks if the channel is NSFW.""" + return self.nsfw + + @property + def last_message(self) -> Message | None: + """Fetches the last message from this channel in cache. + + The message might not be valid or point to an existing message. + + .. admonition:: Reliable Fetching + :class: helpful + + For a slightly more reliable method of fetching the + last message, consider using either :meth:`history` + or :meth:`fetch_message` with the :attr:`last_message_id` + attribute. + + Returns + ------- + Optional[:class:`Message`] + The last message in this channel or ``None`` if not found. + """ + return ( + self._state._get_message(self.last_message_id) + if self.last_message_id + else None + ) + + async def edit(self, **options) -> _TextChannel: + """Edits the channel.""" + raise NotImplementedError + + @utils.copy_doc(discord.abc.GuildChannel.clone) + async def clone( + self, *, name: str | None = None, reason: str | None = None + ) -> TextChannel: + return await self._clone_impl( + { + "topic": self.topic, + "nsfw": self.nsfw, + "rate_limit_per_user": self.slowmode_delay, + }, + name=name, + reason=reason, + ) + + async def delete_messages( + self, messages: Iterable[Snowflake], *, reason: str | None = None + ) -> None: + """|coro| + + Deletes a list of messages. This is similar to :meth:`Message.delete` + except it bulk deletes multiple messages. + + As a special case, if the number of messages is 0, then nothing + is done. If the number of messages is 1 then single message + delete is done. If it's more than two, then bulk delete is used. + + You cannot bulk delete more than 100 messages or messages that + are older than 14 days old. + + You must have the :attr:`~Permissions.manage_messages` permission to + use this. + + Parameters + ---------- + messages: Iterable[:class:`abc.Snowflake`] + An iterable of messages denoting which ones to bulk delete. + reason: Optional[:class:`str`] + The reason for deleting the messages. Shows up on the audit log. + + Raises + ------ + ClientException + The number of messages to delete was more than 100. + Forbidden + You do not have proper permissions to delete the messages. + NotFound + If single delete, then the message was already deleted. + HTTPException + Deleting the messages failed. + """ + if not isinstance(messages, (list, tuple)): + messages = list(messages) + + if len(messages) == 0: + return # do nothing + + if len(messages) == 1: + message_id: int = messages[0].id + await self._state.http.delete_message(self.id, message_id, reason=reason) + return + + if len(messages) > 100: + raise ClientException("Can only bulk delete messages up to 100 messages") + + message_ids: SnowflakeList = [m.id for m in messages] + await self._state.http.delete_messages(self.id, message_ids, reason=reason) + + async def purge( + self, + *, + limit: int | None = 100, + check: Callable[[Message], bool] = MISSING, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + around: SnowflakeTime | None = None, + oldest_first: bool | None = False, + bulk: bool = True, + reason: str | None = None, + ) -> list[Message]: + """|coro| + + Purges a list of messages that meet the criteria given by the predicate + ``check``. If a ``check`` is not provided then all messages are deleted + without discrimination. + + You must have the :attr:`~Permissions.manage_messages` permission to + delete messages even if they are your own. + The :attr:`~Permissions.read_message_history` permission is + also needed to retrieve message history. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of messages to search through. This is not the number + of messages that will be deleted, though it can be. + check: Callable[[:class:`Message`], :class:`bool`] + The function used to check if a message should be deleted. + It must take a :class:`Message` as its sole parameter. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``before`` in :meth:`history`. + after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``after`` in :meth:`history`. + around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``around`` in :meth:`history`. + oldest_first: Optional[:class:`bool`] + Same as ``oldest_first`` in :meth:`history`. + bulk: :class:`bool` + If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting + a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will + fall back to single delete if messages are older than two weeks. + reason: Optional[:class:`str`] + The reason for deleting the messages. Shows up on the audit log. + + Returns + ------- + List[:class:`.Message`] + The list of messages that were deleted. + + Raises + ------ + Forbidden + You do not have proper permissions to do the actions required. + HTTPException + Purging the messages failed. + + Examples + -------- + + Deleting bot's messages :: + + def is_me(m): + return m.author == client.user + + deleted = await channel.purge(limit=100, check=is_me) + await channel.send(f'Deleted {len(deleted)} message(s)') + """ + return await discord.abc._purge_messages_helper( + self, + limit=limit, + check=check, + before=before, + after=after, + around=around, + oldest_first=oldest_first, + bulk=bulk, + reason=reason, + ) + + async def webhooks(self) -> list[Webhook]: + """|coro| + + Gets the list of webhooks from this channel. + + Requires :attr:`~.Permissions.manage_webhooks` permissions. + + Returns + ------- + List[:class:`Webhook`] + The webhooks for this channel. + + Raises + ------ + Forbidden + You don't have permissions to get the webhooks. + """ + + from .webhook import Webhook + + data = await self._state.http.channel_webhooks(self.id) + return [Webhook.from_state(d, state=self._state) for d in data] + + async def create_webhook( + self, *, name: str, avatar: bytes | None = None, reason: str | None = None + ) -> Webhook: + """|coro| + + Creates a webhook for this channel. + + Requires :attr:`~.Permissions.manage_webhooks` permissions. + + .. versionchanged:: 1.1 + Added the ``reason`` keyword-only parameter. + + Parameters + ---------- + name: :class:`str` + The webhook's name. + avatar: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the webhook's default avatar. + This operates similarly to :meth:`~ClientUser.edit`. + reason: Optional[:class:`str`] + The reason for creating this webhook. Shows up in the audit logs. + + Returns + ------- + :class:`Webhook` + The created webhook. + + Raises + ------ + HTTPException + Creating the webhook failed. + Forbidden + You do not have permissions to create a webhook. + """ + + from .webhook import Webhook + + if avatar is not None: + avatar = utils._bytes_to_base64_data(avatar) # type: ignore + + data = await self._state.http.create_webhook( + self.id, name=str(name), avatar=avatar, reason=reason + ) + return Webhook.from_state(data, state=self._state) + + async def follow( + self, *, destination: TextChannel, reason: str | None = None + ) -> Webhook: + """ + Follows a channel using a webhook. + + Only news channels can be followed. + + .. note:: + + The webhook returned will not provide a token to do webhook + actions, as Discord does not provide it. + + .. versionadded:: 1.3 + + Parameters + ---------- + destination: :class:`TextChannel` + The channel you would like to follow from. + reason: Optional[:class:`str`] + The reason for following the channel. Shows up on the destination guild's audit log. + + .. versionadded:: 1.4 + + Returns + ------- + :class:`Webhook` + The created webhook. + + Raises + ------ + HTTPException + Following the channel failed. + Forbidden + You do not have the permissions to create a webhook. + """ + + if not self.is_news(): + raise ClientException("The channel must be a news channel.") + + if not isinstance(destination, TextChannel): + raise InvalidArgument( + f"Expected TextChannel received {destination.__class__.__name__}" + ) + + from .webhook import Webhook + + data = await self._state.http.follow_webhook( + self.id, webhook_channel_id=destination.id, reason=reason + ) + return Webhook._as_follower(data, channel=destination, user=self._state.user) + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 1.6 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message. + """ + + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + def get_thread(self, thread_id: int, /) -> Thread | None: + """Returns a thread with the given ID. + + .. versionadded:: 2.0 + + Parameters + ---------- + thread_id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`Thread`] + The returned thread or ``None`` if not found. + """ + return self.guild.get_thread(thread_id) + + def archived_threads( + self, + *, + private: bool = False, + joined: bool = False, + limit: int | None = 50, + before: Snowflake | datetime.datetime | None = None, + ) -> ArchivedThreadIterator: + """Returns an :class:`~discord.AsyncIterator` that iterates over all archived threads in the guild. + + You must have :attr:`~Permissions.read_message_history` to use this. If iterating over private threads + then :attr:`~Permissions.manage_threads` is also required. + + .. versionadded:: 2.0 + + Parameters + ---------- + limit: Optional[:class:`bool`] + The number of threads to retrieve. + If ``None``, retrieves every archived thread in the channel. Note, however, + that this would make it a slow operation. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Retrieve archived channels before the given date or ID. + private: :class:`bool` + Whether to retrieve private archived threads. + joined: :class:`bool` + Whether to retrieve private archived threads that you've joined. + You cannot set ``joined`` to ``True`` and ``private`` to ``False``. + + Yields + ------ + :class:`Thread` + The archived threads. + + Raises + ------ + Forbidden + You do not have permissions to get archived threads. + HTTPException + The request to get the archived threads failed. + """ + return ArchivedThreadIterator( + self.id, + self.guild, + limit=limit, + joined=joined, + private=private, + before=before, + ) + + +class TextChannel(discord.abc.Messageable, _TextChannel): + """Represents a Discord text channel. + + .. container:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + name: :class:`str` + The channel name. + guild: :class:`Guild` + The guild the channel belongs to. + id: :class:`int` + The channel ID. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it doesn't exist. + position: Optional[:class:`int`] + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. Can be ``None`` if the channel was received in an interaction. + last_message_id: Optional[:class:`int`] + The last message ID of the message sent to this channel. It may + *not* point to an existing or valid message. + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. A value of `0` denotes that it is disabled. + Bots and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages` bypass slowmode. + nsfw: :class:`bool` + If the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + + .. versionadded:: 2.0 + flags: :class:`ChannelFlags` + Extra features of the channel. + + .. versionadded:: 2.0 + default_thread_slowmode_delay: Optional[:class:`int`] + The initial slowmode delay to set on newly created threads in this channel. + + .. versionadded:: 2.3 + """ + + def __init__( + self, *, state: ConnectionState, guild: Guild, data: TextChannelPayload + ): + super().__init__(state=state, guild=guild, data=data) + + @property + def _repr_attrs(self) -> tuple[str, ...]: + return super()._repr_attrs + ("news",) + + def _update(self, guild: Guild, data: TextChannelPayload) -> None: + super()._update(guild, data) + + async def _get_channel(self) -> TextChannel: + return self + + def is_news(self) -> bool: + """Checks if the channel is a news/announcements channel.""" + return self._type == ChannelType.news.value + + @property + def news(self) -> bool: + """Equivalent to :meth:`is_news`.""" + return self.is_news() + + @overload + async def edit( + self, + *, + reason: str | None = ..., + name: str = ..., + topic: str = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: CategoryChannel | None = ..., + slowmode_delay: int = ..., + default_auto_archive_duration: ThreadArchiveDuration = ..., + default_thread_slowmode_delay: int = ..., + type: ChannelType = ..., + overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + ) -> TextChannel | None: ... + + @overload + async def edit(self) -> TextChannel | None: ... + + async def edit(self, *, reason=None, **options): + """|coro| + + Edits the channel. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + .. versionchanged:: 1.3 + The ``overwrites`` keyword-only parameter was added. + + .. versionchanged:: 1.4 + The ``type`` keyword-only parameter was added. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited channel is returned instead. + + Parameters + ---------- + name: :class:`str` + The new channel name. + topic: :class:`str` + The new channel's topic. + position: :class:`int` + The new channel's position. + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`CategoryChannel`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + type: :class:`ChannelType` + Change the type of this text channel. Currently, only conversion between + :attr:`ChannelType.text` and :attr:`ChannelType.news` is supported. This + is only available to guilds that contain ``NEWS`` in :attr:`Guild.features`. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] + The overwrites to apply to channel permissions. Useful for creating secret channels. + default_auto_archive_duration: :class:`int` + The new default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + default_thread_slowmode_delay: :class:`int` + The new default slowmode delay in seconds for threads created in this channel. + + .. versionadded:: 2.3 + + Returns + ------- + Optional[:class:`.TextChannel`] + The newly edited text channel. If the edit was only positional + then ``None`` is returned instead. + + Raises + ------ + InvalidArgument + If position is less than 0 or greater than the number of channels, or if + the permission overwrite information is not in proper form. + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + """ + payload = await self._edit(options, reason=reason) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def create_thread( + self, + *, + name: str, + message: Snowflake | None = None, + auto_archive_duration: ThreadArchiveDuration = MISSING, + type: ChannelType | None = None, + slowmode_delay: int | None = None, + invitable: bool | None = None, + reason: str | None = None, + ) -> Thread: + """|coro| + + Creates a thread in this text channel. + + To create a public thread, you must have :attr:`~discord.Permissions.create_public_threads`. + For a private thread, :attr:`~discord.Permissions.create_private_threads` is needed instead. + + .. versionadded:: 2.0 + + Parameters + ---------- + name: :class:`str` + The name of the thread. + message: Optional[:class:`abc.Snowflake`] + A snowflake representing the message to create the thread with. + If ``None`` is passed then a private thread is created. + Defaults to ``None``. + auto_archive_duration: :class:`int` + The duration in minutes before a thread is automatically archived for inactivity. + If not provided, the channel's default auto archive duration is used. + type: Optional[:class:`ChannelType`] + The type of thread to create. If a ``message`` is passed then this parameter + is ignored, as a thread created with a message is always a public thread. + By default, this creates a private thread if this is ``None``. + slowmode_delay: Optional[:class:`int`] + Specifies the slowmode rate limit for users in this thread, in seconds. + A value of ``0`` disables slowmode. The maximum value possible is ``21600``. + invitable: Optional[:class:`bool`] + Whether non-moderators can add other non-moderators to this thread. + Only available for private threads, where it defaults to True. + reason: :class:`str` + The reason for creating a new thread. Shows up on the audit log. + + Returns + ------- + :class:`Thread` + The created thread + + Raises + ------ + Forbidden + You do not have permissions to create a thread. + HTTPException + Starting the thread failed. + """ + + if type is None: + type = ChannelType.private_thread + + if message is None: + data = await self._state.http.start_thread_without_message( + self.id, + name=name, + auto_archive_duration=auto_archive_duration + or self.default_auto_archive_duration, + type=type.value, + rate_limit_per_user=slowmode_delay or 0, + invitable=invitable, + reason=reason, + ) + else: + data = await self._state.http.start_thread_with_message( + self.id, + message.id, + name=name, + auto_archive_duration=auto_archive_duration + or self.default_auto_archive_duration, + rate_limit_per_user=slowmode_delay or 0, + reason=reason, + ) + + return Thread(guild=self.guild, state=self._state, data=data) + + +class ForumChannel(_TextChannel): + """Represents a Discord forum channel. + + .. container:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + name: :class:`str` + The channel name. + guild: :class:`Guild` + The guild the channel belongs to. + id: :class:`int` + The channel ID. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it doesn't exist. + + .. note:: + + :attr:`guidelines` exists as an alternative to this attribute. + position: Optional[:class:`int`] + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. Can be ``None`` if the channel was received in an interaction. + last_message_id: Optional[:class:`int`] + The last message ID of the message sent to this channel. It may + *not* point to an existing or valid message. + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. A value of `0` denotes that it is disabled. + Bots and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages` bypass slowmode. + nsfw: :class:`bool` + If the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + + .. versionadded:: 2.0 + flags: :class:`ChannelFlags` + Extra features of the channel. + + .. versionadded:: 2.0 + available_tags: List[:class:`ForumTag`] + The set of tags that can be used in a forum channel. + + .. versionadded:: 2.3 + default_sort_order: Optional[:class:`SortOrder`] + The default sort order type used to order posts in this channel. + + .. versionadded:: 2.3 + default_thread_slowmode_delay: Optional[:class:`int`] + The initial slowmode delay to set on newly created threads in this channel. + + .. versionadded:: 2.3 + default_reaction_emoji: Optional[:class:`str` | :class:`discord.GuildEmoji`] + The default forum reaction emoji. + + .. versionadded:: 2.5 + """ + + def __init__( + self, *, state: ConnectionState, guild: Guild, data: ForumChannelPayload + ): + super().__init__(state=state, guild=guild, data=data) + + def _update(self, guild: Guild, data: ForumChannelPayload) -> None: + super()._update(guild, data) + self.available_tags: list[ForumTag] = [ + ForumTag.from_data(state=self._state, data=tag) + for tag in (data.get("available_tags") or []) + ] + self.default_sort_order: SortOrder | None = data.get("default_sort_order", None) + if self.default_sort_order is not None: + self.default_sort_order = try_enum(SortOrder, self.default_sort_order) + + self.default_reaction_emoji = None + + reaction_emoji_ctx: dict = data.get("default_reaction_emoji") + if reaction_emoji_ctx is not None: + emoji_name = reaction_emoji_ctx.get("emoji_name") + if emoji_name is not None: + self.default_reaction_emoji = reaction_emoji_ctx["emoji_name"] + else: + self.default_reaction_emoji = self._state.get_emoji( + utils._get_as_snowflake(reaction_emoji_ctx, "emoji_id") + ) + + @property + def guidelines(self) -> str | None: + """The channel's guidelines. An alias of :attr:`topic`.""" + return self.topic + + @property + def requires_tag(self) -> bool: + """Whether a tag is required to be specified when creating a thread in this forum or media channel. + + Tags are specified in :attr:`applied_tags`. + + .. versionadded:: 2.3 + """ + return self.flags.require_tag + + def get_tag(self, id: int, /) -> ForumTag | None: + """Returns the :class:`ForumTag` from this forum channel with the + given ID, if any. + + .. versionadded:: 2.3 + """ + return utils.get(self.available_tags, id=id) + + @overload + async def edit( + self, + *, + reason: str | None = ..., + name: str = ..., + topic: str = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: CategoryChannel | None = ..., + slowmode_delay: int = ..., + default_auto_archive_duration: ( + ThreadArchiveDuration | ThreadArchiveDurationEnum + ) = ..., + default_thread_slowmode_delay: int = ..., + default_sort_order: SortOrder = ..., + default_reaction_emoji: GuildEmoji | int | str | None = ..., + available_tags: list[ForumTag] = ..., + require_tag: bool = ..., + overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + ) -> ForumChannel | None: ... + + @overload + async def edit(self) -> ForumChannel | None: ... + + async def edit(self, *, reason=None, **options): + """|coro| + + Edits the channel. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + Parameters + ---------- + name: :class:`str` + The new channel name. + topic: :class:`str` + The new channel's topic. + position: :class:`int` + The new channel's position. + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`CategoryChannel`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] + The overwrites to apply to channel permissions. Useful for creating secret channels. + default_auto_archive_duration: :class:`int` + The new default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + :class:`ThreadArchiveDuration` can be used alternatively. + default_thread_slowmode_delay: :class:`int` + The new default slowmode delay in seconds for threads created in this channel. + + .. versionadded:: 2.3 + default_sort_order: Optional[:class:`SortOrder`] + The default sort order type to use to order posts in this channel. + + .. versionadded:: 2.3 + default_reaction_emoji: Optional[:class:`discord.GuildEmoji` | :class:`int` | :class:`str`] + The default reaction emoji. + Can be a unicode emoji or a custom emoji in the forms: + :class:`GuildEmoji`, snowflake ID, string representation (eg. ''). + + .. versionadded:: 2.5 + available_tags: List[:class:`ForumTag`] + The set of tags that can be used in this channel. Must be less than `20`. + + .. versionadded:: 2.3 + require_tag: :class:`bool` + Whether a tag should be required to be specified when creating a thread in this channel. + + .. versionadded:: 2.3 + + Returns + ------- + Optional[:class:`.ForumChannel`] + The newly edited forum channel. If the edit was only positional + then ``None`` is returned instead. + + Raises + ------ + InvalidArgument + If position is less than 0 or greater than the number of channels, or if + the permission overwrite information is not in proper form. + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + """ + if "require_tag" in options: + options["flags"] = ChannelFlags._from_value(self.flags.value) + options["flags"].require_tag = options.pop("require_tag") + + payload = await self._edit(options, reason=reason) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def create_thread( + self, + name: str, + content: str | None = None, + *, + embed: Embed | None = None, + embeds: list[Embed] | None = None, + file: File | None = None, + files: list[File] | None = None, + stickers: Sequence[GuildSticker | StickerItem] | None = None, + delete_message_after: float | None = None, + nonce: int | str | None = None, + allowed_mentions: AllowedMentions | None = None, + view: BaseView | None = None, + applied_tags: list[ForumTag] | None = None, + suppress: bool = False, + silent: bool = False, + auto_archive_duration: ThreadArchiveDuration = MISSING, + slowmode_delay: int = MISSING, + reason: str | None = None, + ) -> Thread: + """|coro| + + Creates a thread in this forum channel. + + To create a public thread, you must have :attr:`~discord.Permissions.create_public_threads`. + For a private thread, :attr:`~discord.Permissions.create_private_threads` is needed instead. + + .. versionadded:: 2.0 + + Parameters + ---------- + name: :class:`str` + The name of the thread. + content: :class:`str` + The content of the message to send. + embed: :class:`~discord.Embed` + The rich embed for the content. + embeds: List[:class:`~discord.Embed`] + A list of embeds to upload. Must be a maximum of 10. + file: :class:`~discord.File` + The file to upload. + files: List[:class:`~discord.File`] + A list of files to upload. Must be a maximum of 10. + stickers: Sequence[Union[:class:`~discord.GuildSticker`, :class:`~discord.StickerItem`]] + A list of stickers to upload. Must be a maximum of 3. + delete_message_after: :class:`int` + The time to wait before deleting the thread. + nonce: Union[:class:`str`, :class:`int`] + The nonce to use for sending this message. If the message was successfully sent, + then the message will have a nonce with this value. + allowed_mentions: :class:`~discord.AllowedMentions` + Controls the mentions being processed in this message. If this is + passed, then the object is merged with :attr:`~discord.Client.allowed_mentions`. + The merging behaviour only overrides attributes that have been explicitly passed + to the object, otherwise it uses the attributes set in :attr:`~discord.Client.allowed_mentions`. + If no object is passed at all then the defaults given by :attr:`~discord.Client.allowed_mentions` + are used instead. + view: :class:`discord.ui.BaseView` + A Discord UI View to add to the message. + applied_tags: List[:class:`discord.ForumTag`] + A list of tags to apply to the new thread. + auto_archive_duration: :class:`int` + The duration in minutes before a thread is automatically archived for inactivity. + If not provided, the channel's default auto archive duration is used. + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in the new thread. A value of `0` denotes that it is disabled. + Bots and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages` bypass slowmode. + If not provided, the forum channel's default slowmode is used. + reason: :class:`str` + The reason for creating a new thread. Shows up on the audit log. + + Returns + ------- + :class:`Thread` + The created thread + + Raises + ------ + Forbidden + You do not have permissions to create a thread. + HTTPException + Starting the thread failed. + """ + state = self._state + message_content = str(content) if content is not None else None + + if embed is not None and embeds is not None: + raise InvalidArgument( + "cannot pass both embed and embeds parameter to create_thread()" + ) + + if embed is not None: + embed = embed.to_dict() + + elif embeds is not None: + if len(embeds) > 10: + raise InvalidArgument( + "embeds parameter must be a list of up to 10 elements" + ) + embeds = [embed.to_dict() for embed in embeds] + + if stickers is not None: + stickers = [sticker.id for sticker in stickers] + + if allowed_mentions is None: + allowed_mentions = ( + state.allowed_mentions and state.allowed_mentions.to_dict() + ) + elif state.allowed_mentions is not None: + allowed_mentions = state.allowed_mentions.merge(allowed_mentions).to_dict() + else: + allowed_mentions = allowed_mentions.to_dict() + + flags = MessageFlags( + suppress_embeds=bool(suppress), + suppress_notifications=bool(silent), + ) + + if view: + if not hasattr(view, "__discord_ui_view__"): + raise InvalidArgument( + f"view parameter must be BaseView not {view.__class__!r}" + ) + + components = view.to_components() + if view.is_components_v2(): + if embeds or content: + raise TypeError( + "cannot send embeds or content with a view using v2 component logic" + ) + flags.is_components_v2 = True + else: + components = None + + if applied_tags is not None: + applied_tags = [str(tag.id) for tag in applied_tags] + + if file is not None and files is not None: + raise InvalidArgument("cannot pass both file and files parameter to send()") + + if files is not None: + if len(files) > 10: + raise InvalidArgument( + "files parameter must be a list of up to 10 elements" + ) + elif not all(isinstance(file, File) for file in files): + raise InvalidArgument("files parameter must be a list of File") + + if file is not None: + if not isinstance(file, File): + raise InvalidArgument("file parameter must be File") + files = [file] + + try: + data = await state.http.start_forum_thread( + self.id, + content=message_content, + name=name, + files=files, + embed=embed, + embeds=embeds, + nonce=nonce, + allowed_mentions=allowed_mentions, + stickers=stickers, + components=components, + auto_archive_duration=auto_archive_duration + or self.default_auto_archive_duration, + rate_limit_per_user=slowmode_delay or self.slowmode_delay, + applied_tags=applied_tags, + flags=flags.value, + reason=reason, + ) + finally: + if files is not None: + for f in files: + f.close() + + ret = Thread(guild=self.guild, state=self._state, data=data) + msg = ret.get_partial_message(int(data["last_message_id"])) + if view and view.is_dispatchable(): + state.store_view(view, msg.id) + + if delete_message_after is not None: + await msg.delete(delay=delete_message_after) + return ret + + +class MediaChannel(ForumChannel): + """Represents a Discord media channel. Subclass of :class:`ForumChannel`. + + .. container:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + .. versionadded:: 2.7 + + Attributes + ---------- + name: :class:`str` + The channel name. + guild: :class:`Guild` + The guild the channel belongs to. + id: :class:`int` + The channel ID. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it doesn't exist. + + .. note:: + + :attr:`guidelines` exists as an alternative to this attribute. + position: Optional[:class:`int`] + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. Can be ``None`` if the channel was received in an interaction. + last_message_id: Optional[:class:`int`] + The last message ID of the message sent to this channel. It may + *not* point to an existing or valid message. + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. A value of `0` denotes that it is disabled. + Bots and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages` bypass slowmode. + nsfw: :class:`bool` + If the channel is marked as "not safe for work". + + .. note:: + + To check if the channel or the guild of that channel are marked as NSFW, consider :meth:`is_nsfw` instead. + default_auto_archive_duration: :class:`int` + The default auto archive duration in minutes for threads created in this channel. + + flags: :class:`ChannelFlags` + Extra features of the channel. + + available_tags: List[:class:`ForumTag`] + The set of tags that can be used in a forum channel. + + default_sort_order: Optional[:class:`SortOrder`] + The default sort order type used to order posts in this channel. + + default_thread_slowmode_delay: Optional[:class:`int`] + The initial slowmode delay to set on newly created threads in this channel. + + default_reaction_emoji: Optional[:class:`str` | :class:`discord.GuildEmoji`] + The default forum reaction emoji. + """ + + @property + def media_download_options_hidden(self) -> bool: + """Whether media download options are hidden in this media channel. + + .. versionadded:: 2.7 + """ + return self.flags.hide_media_download_options + + @overload + async def edit( + self, + *, + reason: str | None = ..., + name: str = ..., + topic: str = ..., + position: int = ..., + nsfw: bool = ..., + sync_permissions: bool = ..., + category: CategoryChannel | None = ..., + slowmode_delay: int = ..., + default_auto_archive_duration: ThreadArchiveDuration = ..., + default_thread_slowmode_delay: int = ..., + default_sort_order: SortOrder = ..., + default_reaction_emoji: GuildEmoji | int | str | None = ..., + available_tags: list[ForumTag] = ..., + require_tag: bool = ..., + hide_media_download_options: bool = ..., + overwrites: Mapping[Role | Member | Snowflake, PermissionOverwrite] = ..., + ) -> ForumChannel | None: ... + + async def edit(self, *, reason=None, **options): + """|coro| + + Edits the channel. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + Parameters + ---------- + name: :class:`str` + The new channel name. + topic: :class:`str` + The new channel's topic. + position: :class:`int` + The new channel's position. + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`CategoryChannel`] + The new category for this channel. Can be ``None`` to remove the + category. + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] + The overwrites to apply to channel permissions. Useful for creating secret channels. + default_auto_archive_duration: :class:`int` + The new default auto archive duration in minutes for threads created in this channel. + Must be one of ``60``, ``1440``, ``4320``, or ``10080``. + default_thread_slowmode_delay: :class:`int` + The new default slowmode delay in seconds for threads created in this channel. + + default_sort_order: Optional[:class:`SortOrder`] + The default sort order type to use to order posts in this channel. + + default_reaction_emoji: Optional[:class:`discord.GuildEmoji` | :class:`int` | :class:`str`] + The default reaction emoji. + Can be a unicode emoji or a custom emoji in the forms: + :class:`GuildEmoji`, snowflake ID, string representation (e.g., ''). + + available_tags: List[:class:`ForumTag`] + The set of tags that can be used in this channel. Must be less than `20`. + + require_tag: :class:`bool` + Whether a tag should be required to be specified when creating a thread in this channel. + + hide_media_download_options: :class:`bool` + Whether media download options should be hidden in this media channel. + + Returns + ------- + Optional[:class:`.MediaChannel`] + The newly edited media channel. If the edit was only positional + then ``None`` is returned instead. + + Raises + ------ + InvalidArgument + If position is less than 0 or greater than the number of channels, or if + the permission overwrite information is not in proper form. + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + """ + + if "require_tag" in options or "hide_media_download_options" in options: + flags = ChannelFlags._from_value(self.flags.value) + flags.require_tag = options.pop("require_tag", flags.require_tag) + flags.hide_media_download_options = options.pop( + "hide_media_download_options", flags.hide_media_download_options + ) + options["flags"] = flags + + payload = await self._edit(options, reason=reason) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + +class VocalGuildChannel(discord.abc.Connectable, discord.abc.GuildChannel, Hashable): + __slots__ = ( + "name", + "id", + "guild", + "bitrate", + "user_limit", + "_state", + "position", + "slowmode_delay", + "_overwrites", + "category_id", + "rtc_region", + "video_quality_mode", + "last_message_id", + "flags", + "nsfw", + ) + + def __init__( + self, + *, + state: ConnectionState, + guild: Guild, + data: VoiceChannelPayload | StageChannelPayload, + ): + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self._update(guild, data) + + def _get_voice_client_key(self) -> tuple[int, str]: + return self.guild.id, "guild_id" + + def _get_voice_state_pair(self) -> tuple[int, int]: + return self.guild.id, self.id + + def _update( + self, guild: Guild, data: VoiceChannelPayload | StageChannelPayload + ) -> None: + # This data will always exist + self.guild: Guild = guild + self.name: str = data["name"] + self.category_id: int | None = utils._get_as_snowflake(data, "parent_id") + + # This data may be missing depending on how this object is being created/updated + if not data.pop("_invoke_flag", False): + rtc = data.get("rtc_region") + self.rtc_region: VoiceRegion | None = ( + try_enum(VoiceRegion, rtc) if rtc is not None else None + ) + self.video_quality_mode: VideoQualityMode = try_enum( + VideoQualityMode, data.get("video_quality_mode", 1) + ) + self.last_message_id: int | None = utils._get_as_snowflake( + data, "last_message_id" + ) + self.position: int = data.get("position") + self.slowmode_delay = data.get("rate_limit_per_user", 0) + self.bitrate: int = data.get("bitrate") + self.user_limit: int = data.get("user_limit") + self.flags: ChannelFlags = ChannelFlags._from_value(data.get("flags", 0)) + self.nsfw: bool = data.get("nsfw", False) + self._fill_overwrites(data) + + @property + def _sorting_bucket(self) -> int: + return ChannelType.voice.value + + @property + def members(self) -> list[Member]: + """Returns all members that are currently inside this voice channel.""" + ret = [] + for user_id, state in self.guild._voice_states.items(): + if state.channel and state.channel.id == self.id: + member = self.guild.get_member(user_id) + if member is not None: + ret.append(member) + return ret + + @property + def voice_states(self) -> dict[int, VoiceState]: + """Returns a mapping of member IDs who have voice states in this channel. + + .. versionadded:: 1.3 + + .. note:: + + This function is intentionally low level to replace :attr:`members` + when the member cache is unavailable. + + Returns + ------- + Mapping[:class:`int`, :class:`VoiceState`] + The mapping of member ID to a voice state. + """ + return { + key: value + for key, value in self.guild._voice_states.items() + if value.channel and value.channel.id == self.id + } + + @utils.copy_doc(discord.abc.GuildChannel.permissions_for) + def permissions_for(self, obj: Member | Role, /) -> Permissions: + base = super().permissions_for(obj) + + # Voice channels cannot be edited by people who can't connect to them. + # It also implicitly denies all other voice perms + if not base.connect: + denied = Permissions.voice() + denied.update(manage_channels=True, manage_roles=True) + base.value &= ~denied.value + return base + + +class VoiceChannel(discord.abc.Messageable, VocalGuildChannel): + """Represents a Discord guild voice channel. + + .. container:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + name: :class:`str` + The channel name. + guild: :class:`Guild` + The guild the channel belongs to. + id: :class:`int` + The channel ID. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + position: Optional[:class:`int`] + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. Can be ``None`` if the channel was received in an interaction. + bitrate: :class:`int` + The channel's preferred audio bitrate in bits per second. + user_limit: :class:`int` + The channel's limit for number of members that can be in a voice channel. + rtc_region: Optional[:class:`VoiceRegion`] + The region for the voice channel's voice communication. + A value of ``None`` indicates automatic voice region detection. + + .. versionadded:: 1.7 + video_quality_mode: :class:`VideoQualityMode` + The camera video quality for the voice channel's participants. + + .. versionadded:: 2.0 + last_message_id: Optional[:class:`int`] + The ID of the last message sent to this channel. It may not always point to an existing or valid message. + + .. versionadded:: 2.0 + slowmode_delay: :class:`int` + The number of seconds a member must wait between sending messages + in this channel. A value of `0` denotes that it is disabled. + Bots and users with :attr:`~Permissions.manage_channels` or + :attr:`~Permissions.manage_messages` bypass slowmode. + + .. versionadded:: 2.5 + status: Optional[:class:`str`] + The channel's status, if set. + + .. versionadded:: 2.5 + flags: :class:`ChannelFlags` + Extra features of the channel. + + .. versionadded:: 2.0 + + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + + .. versionadded:: 2.7 + """ + + def __init__( + self, + *, + state: ConnectionState, + guild: Guild, + data: VoiceChannelPayload, + ): + self.status: str | None = None + super().__init__(state=state, guild=guild, data=data) + + def _update(self, guild: Guild, data: VoiceChannelPayload): + super()._update(guild, data) + if data.get("status"): + self.status = data.get("status") + + def __repr__(self) -> str: + attrs = [ + ("id", self.id), + ("name", self.name), + ("status", self.status), + ("rtc_region", self.rtc_region), + ("position", self.position), + ("bitrate", self.bitrate), + ("video_quality_mode", self.video_quality_mode), + ("user_limit", self.user_limit), + ("category_id", self.category_id), + ] + joined = " ".join("%s=%r" % t for t in attrs) + return f"<{self.__class__.__name__} {joined}>" + + async def _get_channel(self): + return self + + def is_nsfw(self) -> bool: + """Checks if the channel is NSFW.""" + return self.nsfw + + @property + def last_message(self) -> Message | None: + """Fetches the last message from this channel in cache. + + The message might not be valid or point to an existing message. + + .. admonition:: Reliable Fetching + :class: helpful + + For a slightly more reliable method of fetching the + last message, consider using either :meth:`history` + or :meth:`fetch_message` with the :attr:`last_message_id` + attribute. + + Returns + ------- + Optional[:class:`Message`] + The last message in this channel or ``None`` if not found. + """ + return ( + self._state._get_message(self.last_message_id) + if self.last_message_id + else None + ) + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 1.6 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message. + """ + + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + async def delete_messages( + self, messages: Iterable[Snowflake], *, reason: str | None = None + ) -> None: + """|coro| + + Deletes a list of messages. This is similar to :meth:`Message.delete` + except it bulk deletes multiple messages. + + As a special case, if the number of messages is 0, then nothing + is done. If the number of messages is 1 then single message + delete is done. If it's more than two, then bulk delete is used. + + You cannot bulk delete more than 100 messages or messages that + are older than 14 days old. + + You must have the :attr:`~Permissions.manage_messages` permission to + use this. + + Parameters + ---------- + messages: Iterable[:class:`abc.Snowflake`] + An iterable of messages denoting which ones to bulk delete. + reason: Optional[:class:`str`] + The reason for deleting the messages. Shows up on the audit log. + + Raises + ------ + ClientException + The number of messages to delete was more than 100. + Forbidden + You do not have proper permissions to delete the messages. + NotFound + If single delete, then the message was already deleted. + HTTPException + Deleting the messages failed. + """ + if not isinstance(messages, (list, tuple)): + messages = list(messages) + + if len(messages) == 0: + return # do nothing + + if len(messages) == 1: + message_id: int = messages[0].id + await self._state.http.delete_message(self.id, message_id, reason=reason) + return + + if len(messages) > 100: + raise ClientException("Can only bulk delete messages up to 100 messages") + + message_ids: SnowflakeList = [m.id for m in messages] + await self._state.http.delete_messages(self.id, message_ids, reason=reason) + + async def purge( + self, + *, + limit: int | None = 100, + check: Callable[[Message], bool] = MISSING, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + around: SnowflakeTime | None = None, + oldest_first: bool | None = False, + bulk: bool = True, + reason: str | None = None, + ) -> list[Message]: + """|coro| + + Purges a list of messages that meet the criteria given by the predicate + ``check``. If a ``check`` is not provided then all messages are deleted + without discrimination. + + You must have the :attr:`~Permissions.manage_messages` permission to + delete messages even if they are your own. + The :attr:`~Permissions.read_message_history` permission is + also needed to retrieve message history. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of messages to search through. This is not the number + of messages that will be deleted, though it can be. + check: Callable[[:class:`Message`], :class:`bool`] + The function used to check if a message should be deleted. + It must take a :class:`Message` as its sole parameter. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``before`` in :meth:`history`. + after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``after`` in :meth:`history`. + around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``around`` in :meth:`history`. + oldest_first: Optional[:class:`bool`] + Same as ``oldest_first`` in :meth:`history`. + bulk: :class:`bool` + If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting + a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will + fall back to single delete if messages are older than two weeks. + reason: Optional[:class:`str`] + The reason for deleting the messages. Shows up on the audit log. + + Returns + ------- + List[:class:`.Message`] + The list of messages that were deleted. + + Raises + ------ + Forbidden + You do not have proper permissions to do the actions required. + HTTPException + Purging the messages failed. + + Examples + -------- + + Deleting bot's messages :: + + def is_me(m): + return m.author == client.user + + deleted = await channel.purge(limit=100, check=is_me) + await channel.send(f'Deleted {len(deleted)} message(s)') + """ + return await discord.abc._purge_messages_helper( + self, + limit=limit, + check=check, + before=before, + after=after, + around=around, + oldest_first=oldest_first, + bulk=bulk, + reason=reason, + ) + + async def webhooks(self) -> list[Webhook]: + """|coro| + + Gets the list of webhooks from this channel. + + Requires :attr:`~.Permissions.manage_webhooks` permissions. + + Returns + ------- + List[:class:`Webhook`] + The webhooks for this channel. + + Raises + ------ + Forbidden + You don't have permissions to get the webhooks. + """ + + from .webhook import Webhook + + data = await self._state.http.channel_webhooks(self.id) + return [Webhook.from_state(d, state=self._state) for d in data] + + async def create_webhook( + self, *, name: str, avatar: bytes | None = None, reason: str | None = None + ) -> Webhook: + """|coro| + + Creates a webhook for this channel. + + Requires :attr:`~.Permissions.manage_webhooks` permissions. + + .. versionchanged:: 1.1 + Added the ``reason`` keyword-only parameter. + + Parameters + ---------- + name: :class:`str` + The webhook's name. + avatar: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the webhook's default avatar. + This operates similarly to :meth:`~ClientUser.edit`. + reason: Optional[:class:`str`] + The reason for creating this webhook. Shows up in the audit logs. + + Returns + ------- + :class:`Webhook` + The created webhook. + + Raises + ------ + HTTPException + Creating the webhook failed. + Forbidden + You do not have permissions to create a webhook. + """ + + from .webhook import Webhook + + if avatar is not None: + avatar = utils._bytes_to_base64_data(avatar) # type: ignore + + data = await self._state.http.create_webhook( + self.id, name=str(name), avatar=avatar, reason=reason + ) + return Webhook.from_state(data, state=self._state) + + @property + def type(self) -> ChannelType: + """The channel's Discord type.""" + return ChannelType.voice + + @utils.copy_doc(discord.abc.GuildChannel.clone) + async def clone( + self, *, name: str | None = None, reason: str | None = None + ) -> VoiceChannel: + return await self._clone_impl( + {"bitrate": self.bitrate, "user_limit": self.user_limit}, + name=name, + reason=reason, + ) + + @overload + async def edit( + self, + *, + name: str = ..., + bitrate: int = ..., + user_limit: int = ..., + position: int = ..., + sync_permissions: int = ..., + category: CategoryChannel | None = ..., + overwrites: Mapping[Role | Member, PermissionOverwrite] = ..., + rtc_region: VoiceRegion | None = ..., + video_quality_mode: VideoQualityMode = ..., + slowmode_delay: int = ..., + nsfw: bool = ..., + reason: str | None = ..., + ) -> VoiceChannel | None: ... + + @overload + async def edit(self) -> VoiceChannel | None: ... + + async def edit(self, *, reason=None, **options): + """|coro| + + Edits the channel. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + .. versionchanged:: 1.3 + The ``overwrites`` keyword-only parameter was added. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited channel is returned instead. + + Parameters + ---------- + name: :class:`str` + The new channel's name. + bitrate: :class:`int` + The new channel's bitrate. + user_limit: :class:`int` + The new channel's user limit. + position: :class:`int` + The new channel's position. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`CategoryChannel`] + The new category for this channel. Can be ``None`` to remove the + category. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] + The overwrites to apply to channel permissions. Useful for creating secret channels. + rtc_region: Optional[:class:`VoiceRegion`] + The new region for the voice channel's voice communication. + A value of ``None`` indicates automatic voice region detection. + + .. versionadded:: 1.7 + video_quality_mode: :class:`VideoQualityMode` + The camera video quality for the voice channel's participants. + + .. versionadded:: 2.0 + + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + + .. versionadded:: 2.7 + + Returns + ------- + Optional[:class:`.VoiceChannel`] + The newly edited voice channel. If the edit was only positional + then ``None`` is returned instead. + + Raises + ------ + InvalidArgument + If the permission overwrite information is not in proper form. + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + """ + + payload = await self._edit(options, reason=reason) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + async def create_activity_invite( + self, activity: EmbeddedActivity | int, **kwargs + ) -> Invite: + """|coro| + + A shortcut method that creates an instant activity invite. + + You must have the :attr:`~discord.Permissions.start_embedded_activities` permission to + do this. + + Parameters + ---------- + activity: Union[:class:`discord.EmbeddedActivity`, :class:`int`] + The activity to create an invite for which can be an application id as well. + max_age: :class:`int` + How long the invite should last in seconds. If it's 0 then the invite + doesn't expire. Defaults to ``0``. + max_uses: :class:`int` + How many uses the invite could be used for. If it's 0 then there + are unlimited uses. Defaults to ``0``. + temporary: :class:`bool` + Denotes that the invite grants temporary membership + (i.e. they get kicked after they disconnect). Defaults to ``False``. + unique: :class:`bool` + Indicates if a unique invite URL should be created. Defaults to True. + If this is set to ``False`` then it will return a previously created + invite. + reason: Optional[:class:`str`] + The reason for creating this invite. Shows up on the audit log. + + Returns + ------- + :class:`~discord.Invite` + The invite that was created. + + Raises + ------ + TypeError + If the activity is not a valid activity or application id. + ~discord.HTTPException + Invite creation failed. + """ + + if isinstance(activity, EmbeddedActivity): + activity = activity.value + + elif not isinstance(activity, int): + raise TypeError("Invalid type provided for the activity.") + + return await self.create_invite( + target_type=InviteTarget.embedded_application, + target_application_id=activity, + **kwargs, + ) + + async def set_status( + self, status: str | None, *, reason: str | None = None + ) -> None: + """|coro| + + Sets the status of the voice channel. + + You must have the :attr:`~Permissions.set_voice_channel_status` permission to use this. + + Parameters + ---------- + status: Union[:class:`str`, None] + The new status. + reason: Optional[:class:`str`] + The reason for setting the status. Shows up on the audit log. + + Raises + ------ + Forbidden + You do not have proper permissions to set the status. + HTTPException + Setting the status failed. + """ + await self._state.http.set_voice_channel_status(self.id, status, reason=reason) + + async def send_soundboard_sound(self, sound: PartialSoundboardSound) -> None: + """|coro| + + Sends a soundboard sound to the voice channel. + + Parameters + ---------- + sound: :class:`PartialSoundboardSound` + The soundboard sound to send. + + Raises + ------ + Forbidden + You do not have proper permissions to send the soundboard sound. + HTTPException + Sending the soundboard sound failed. + """ + await self._state.http.send_soundboard_sound(self.id, sound) + + +class StageChannel(discord.abc.Messageable, VocalGuildChannel): + """Represents a Discord guild stage channel. + + .. versionadded:: 1.7 + + .. container:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns the channel's name. + + Attributes + ---------- + name: :class:`str` + The channel name. + guild: :class:`Guild` + The guild the channel belongs to. + id: :class:`int` + The channel ID. + topic: Optional[:class:`str`] + The channel's topic. ``None`` if it isn't set. + category_id: Optional[:class:`int`] + The category channel ID this channel belongs to, if applicable. + position: Optional[:class:`int`] + The position in the channel list. This is a number that starts at 0. e.g. the + top channel is position 0. Can be ``None`` if the channel was received in an interaction. + bitrate: :class:`int` + The channel's preferred audio bitrate in bits per second. + user_limit: :class:`int` + The channel's limit for number of members that can be in a stage channel. + rtc_region: Optional[:class:`VoiceRegion`] + The region for the stage channel's voice communication. + A value of ``None`` indicates automatic voice region detection. + video_quality_mode: :class:`VideoQualityMode` + The camera video quality for the stage channel's participants. + + .. versionadded:: 2.0 + flags: :class:`ChannelFlags` + Extra features of the channel. + + .. versionadded:: 2.0 + last_message_id: Optional[:class:`int`] + The ID of the last message sent to this channel. It may not always point to an existing or valid message. + .. versionadded:: 2.5 + + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + The maximum value possible is `21600`. + + nsfw: :class:`bool` + Whether the channel is marked as NSFW. + + .. versionadded:: 2.7 + """ + + __slots__ = ("topic",) + + def _update(self, guild: Guild, data: StageChannelPayload) -> None: + super()._update(guild, data) + self.topic = data.get("topic") + + def __repr__(self) -> str: + attrs = [ + ("id", self.id), + ("name", self.name), + ("topic", self.topic), + ("rtc_region", self.rtc_region), + ("position", self.position), + ("bitrate", self.bitrate), + ("video_quality_mode", self.video_quality_mode), + ("user_limit", self.user_limit), + ("category_id", self.category_id), + ] + joined = " ".join("%s=%r" % t for t in attrs) + return f"<{self.__class__.__name__} {joined}>" + + @property + def requesting_to_speak(self) -> list[Member]: + """A list of members who are requesting to speak in the stage channel.""" + return [ + member + for member in self.members + if member.voice and member.voice.requested_to_speak_at is not None + ] + + @property + def speakers(self) -> list[Member]: + """A list of members who have been permitted to speak in the stage channel. + + .. versionadded:: 2.0 + """ + return [ + member + for member in self.members + if member.voice + and not member.voice.suppress + and member.voice.requested_to_speak_at is None + ] + + @property + def listeners(self) -> list[Member]: + """A list of members who are listening in the stage channel. + + .. versionadded:: 2.0 + """ + return [ + member for member in self.members if member.voice and member.voice.suppress + ] + + async def _get_channel(self): + return self + + def is_nsfw(self) -> bool: + """Checks if the channel is NSFW.""" + return self.nsfw + + @property + def last_message(self) -> Message | None: + """Fetches the last message from this channel in cache. + + The message might not be valid or point to an existing message. + + .. admonition:: Reliable Fetching + :class: helpful + + For a slightly more reliable method of fetching the + last message, consider using either :meth:`history` + or :meth:`fetch_message` with the :attr:`last_message_id` + attribute. + + Returns + ------- + Optional[:class:`Message`] + The last message in this channel or ``None`` if not found. + """ + return ( + self._state._get_message(self.last_message_id) + if self.last_message_id + else None + ) + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 1.6 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message. + """ + + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + async def delete_messages( + self, messages: Iterable[Snowflake], *, reason: str | None = None + ) -> None: + """|coro| + + Deletes a list of messages. This is similar to :meth:`Message.delete` + except it bulk deletes multiple messages. + + As a special case, if the number of messages is 0, then nothing + is done. If the number of messages is 1 then single message + delete is done. If it's more than two, then bulk delete is used. + + You cannot bulk delete more than 100 messages or messages that + are older than 14 days old. + + You must have the :attr:`~Permissions.manage_messages` permission to + use this. + + Parameters + ---------- + messages: Iterable[:class:`abc.Snowflake`] + An iterable of messages denoting which ones to bulk delete. + reason: Optional[:class:`str`] + The reason for deleting the messages. Shows up on the audit log. + + Raises + ------ + ClientException + The number of messages to delete was more than 100. + Forbidden + You do not have proper permissions to delete the messages. + NotFound + If single delete, then the message was already deleted. + HTTPException + Deleting the messages failed. + """ + if not isinstance(messages, (list, tuple)): + messages = list(messages) + + if len(messages) == 0: + return # do nothing + + if len(messages) == 1: + message_id: int = messages[0].id + await self._state.http.delete_message(self.id, message_id, reason=reason) + return + + if len(messages) > 100: + raise ClientException("Can only bulk delete messages up to 100 messages") + + message_ids: SnowflakeList = [m.id for m in messages] + await self._state.http.delete_messages(self.id, message_ids, reason=reason) + + async def purge( + self, + *, + limit: int | None = 100, + check: Callable[[Message], bool] = MISSING, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + around: SnowflakeTime | None = None, + oldest_first: bool | None = False, + bulk: bool = True, + reason: str | None = None, + ) -> list[Message]: + """|coro| + + Purges a list of messages that meet the criteria given by the predicate + ``check``. If a ``check`` is not provided then all messages are deleted + without discrimination. + + You must have the :attr:`~Permissions.manage_messages` permission to + delete messages even if they are your own. + The :attr:`~Permissions.read_message_history` permission is + also needed to retrieve message history. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of messages to search through. This is not the number + of messages that will be deleted, though it can be. + check: Callable[[:class:`Message`], :class:`bool`] + The function used to check if a message should be deleted. + It must take a :class:`Message` as its sole parameter. + before: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``before`` in :meth:`history`. + after: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``after`` in :meth:`history`. + around: Optional[Union[:class:`abc.Snowflake`, :class:`datetime.datetime`]] + Same as ``around`` in :meth:`history`. + oldest_first: Optional[:class:`bool`] + Same as ``oldest_first`` in :meth:`history`. + bulk: :class:`bool` + If ``True``, use bulk delete. Setting this to ``False`` is useful for mass-deleting + a bot's own messages without :attr:`Permissions.manage_messages`. When ``True``, will + fall back to single delete if messages are older than two weeks. + reason: Optional[:class:`str`] + The reason for deleting the messages. Shows up on the audit log. + + Returns + ------- + List[:class:`.Message`] + The list of messages that were deleted. + + Raises + ------ + Forbidden + You do not have proper permissions to do the actions required. + HTTPException + Purging the messages failed. + + Examples + -------- + + Deleting bot's messages :: + + def is_me(m): + return m.author == client.user + + deleted = await channel.purge(limit=100, check=is_me) + await channel.send(f'Deleted {len(deleted)} message(s)') + """ + return await discord.abc._purge_messages_helper( + self, + limit=limit, + check=check, + before=before, + after=after, + around=around, + oldest_first=oldest_first, + bulk=bulk, + reason=reason, + ) + + async def webhooks(self) -> list[Webhook]: + """|coro| + + Gets the list of webhooks from this channel. + + Requires :attr:`~.Permissions.manage_webhooks` permissions. + + Returns + ------- + List[:class:`Webhook`] + The webhooks for this channel. + + Raises + ------ + Forbidden + You don't have permissions to get the webhooks. + """ + + from .webhook import Webhook + + data = await self._state.http.channel_webhooks(self.id) + return [Webhook.from_state(d, state=self._state) for d in data] + + async def create_webhook( + self, *, name: str, avatar: bytes | None = None, reason: str | None = None + ) -> Webhook: + """|coro| + + Creates a webhook for this channel. + + Requires :attr:`~.Permissions.manage_webhooks` permissions. + + .. versionchanged:: 1.1 + Added the ``reason`` keyword-only parameter. + + Parameters + ---------- + name: :class:`str` + The webhook's name. + avatar: Optional[:class:`bytes`] + A :term:`py:bytes-like object` representing the webhook's default avatar. + This operates similarly to :meth:`~ClientUser.edit`. + reason: Optional[:class:`str`] + The reason for creating this webhook. Shows up in the audit logs. + + Returns + ------- + :class:`Webhook` + The created webhook. + + Raises + ------ + HTTPException + Creating the webhook failed. + Forbidden + You do not have permissions to create a webhook. + """ + + from .webhook import Webhook + + if avatar is not None: + avatar = utils._bytes_to_base64_data(avatar) # type: ignore + + data = await self._state.http.create_webhook( + self.id, name=str(name), avatar=avatar, reason=reason + ) + return Webhook.from_state(data, state=self._state) + + @property + def moderators(self) -> list[Member]: + """A list of members who are moderating the stage channel. + + .. versionadded:: 2.0 + """ + required_permissions = Permissions.stage_moderator() + return [ + member + for member in self.members + if self.permissions_for(member) >= required_permissions + ] + + @property + def type(self) -> ChannelType: + """The channel's Discord type.""" + return ChannelType.stage_voice + + @utils.copy_doc(discord.abc.GuildChannel.clone) + async def clone( + self, *, name: str | None = None, reason: str | None = None + ) -> StageChannel: + return await self._clone_impl({}, name=name, reason=reason) + + @property + def instance(self) -> StageInstance | None: + """The running stage instance of the stage channel. + + .. versionadded:: 2.0 + """ + return utils.get(self.guild.stage_instances, channel_id=self.id) + + async def create_instance( + self, + *, + topic: str, + privacy_level: StagePrivacyLevel = MISSING, + reason: str | None = None, + send_notification: bool | None = False, + ) -> StageInstance: + """|coro| + + Create a stage instance. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + .. versionadded:: 2.0 + + Parameters + ---------- + topic: :class:`str` + The stage instance's topic. + privacy_level: :class:`StagePrivacyLevel` + The stage instance's privacy level. Defaults to :attr:`StagePrivacyLevel.guild_only`. + reason: :class:`str` + The reason the stage instance was created. Shows up on the audit log. + send_notification: :class:`bool` + Send a notification to everyone in the server that the stage instance has started. + Defaults to ``False``. Requires the ``mention_everyone`` permission. + + Returns + ------- + :class:`StageInstance` + The newly created stage instance. + + Raises + ------ + InvalidArgument + If the ``privacy_level`` parameter is not the proper type. + Forbidden + You do not have permissions to create a stage instance. + HTTPException + Creating a stage instance failed. + """ + + payload: dict[str, Any] = { + "channel_id": self.id, + "topic": topic, + "send_start_notification": send_notification, + } + + if privacy_level is not MISSING: + if not isinstance(privacy_level, StagePrivacyLevel): + raise InvalidArgument( + "privacy_level field must be of type PrivacyLevel" + ) + + payload["privacy_level"] = privacy_level.value + + data = await self._state.http.create_stage_instance(**payload, reason=reason) + return StageInstance(guild=self.guild, state=self._state, data=data) + + async def fetch_instance(self) -> StageInstance: + """|coro| + + Gets the running :class:`StageInstance`. + + .. versionadded:: 2.0 + + Returns + ------- + :class:`StageInstance` + The stage instance. + + Raises + ------ + NotFound + The stage instance or channel could not be found. + HTTPException + Getting the stage instance failed. + """ + data = await self._state.http.get_stage_instance(self.id) + return StageInstance(guild=self.guild, state=self._state, data=data) + + @overload + async def edit( + self, + *, + name: str = ..., + topic: str | None = ..., + position: int = ..., + sync_permissions: int = ..., + category: CategoryChannel | None = ..., + overwrites: Mapping[Role | Member, PermissionOverwrite] = ..., + rtc_region: VoiceRegion | None = ..., + video_quality_mode: VideoQualityMode = ..., + reason: str | None = ..., + ) -> StageChannel | None: ... + + @overload + async def edit(self) -> StageChannel | None: ... + + async def edit(self, *, reason=None, **options): + """|coro| + + Edits the channel. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + .. versionchanged:: 2.0 + The ``topic`` parameter must now be set via :attr:`create_instance`. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited channel is returned instead. + + Parameters + ---------- + name: :class:`str` + The new channel's name. + position: :class:`int` + The new channel's position. + sync_permissions: :class:`bool` + Whether to sync permissions with the channel's new or pre-existing + category. Defaults to ``False``. + category: Optional[:class:`CategoryChannel`] + The new category for this channel. Can be ``None`` to remove the + category. + reason: Optional[:class:`str`] + The reason for editing this channel. Shows up on the audit log. + overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] + The overwrites to apply to channel permissions. Useful for creating secret channels. + rtc_region: Optional[:class:`VoiceRegion`] + The new region for the stage channel's voice communication. + A value of ``None`` indicates automatic voice region detection. + video_quality_mode: :class:`VideoQualityMode` + The camera video quality for the stage channel's participants. + + .. versionadded:: 2.0 + + bitrate: :class:`int` + The channel's preferred audio bitrate in bits per second. + + user_limit: :class:`int` + The channel's limit for number of members that can be in a voice channel. + + slowmode_delay: :class:`int` + Specifies the slowmode rate limit for user in this channel, in seconds. + A value of `0` disables slowmode. The maximum value possible is `21600`. + + Returns + ------- + Optional[:class:`.StageChannel`] + The newly edited stage channel. If the edit was only positional + then ``None`` is returned instead. + + Raises + ------ + InvalidArgument + If the permission overwrite information is not in proper form. + Forbidden + You do not have permissions to edit the channel. + HTTPException + Editing the channel failed. + """ + + payload = await self._edit(options, reason=reason) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + +class CategoryChannel(discord.abc.GuildChannel, Hashable): + """Represents a Discord channel category. + + These are useful to group channels to logical compartments. + + .. container:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the category's hash. + + .. describe:: str(x) + + Returns the category's name. + + Attributes + ---------- + name: :class:`str` + The category name. + guild: :class:`Guild` + The guild the category belongs to. + id: :class:`int` + The category channel ID. + position: Optional[:class:`int`] + The position in the category list. This is a number that starts at 0. e.g. the + top category is position 0. Can be ``None`` if the channel was received in an interaction. + + flags: :class:`ChannelFlags` + Extra features of the channel. + + .. versionadded:: 2.0 + """ + + __slots__ = ( + "name", + "id", + "guild", + "_state", + "position", + "_overwrites", + "category_id", + "flags", + ) + + def __init__( + self, *, state: ConnectionState, guild: Guild, data: CategoryChannelPayload + ): + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self._update(guild, data) + + def __repr__(self) -> str: + return f"" + + def _update(self, guild: Guild, data: CategoryChannelPayload) -> None: + # This data will always exist + self.guild: Guild = guild + self.name: str = data["name"] + self.category_id: int | None = utils._get_as_snowflake(data, "parent_id") + + # This data may be missing depending on how this object is being created/updated + if not data.pop("_invoke_flag", False): + self.position: int = data.get("position") + self.flags: ChannelFlags = ChannelFlags._from_value(data.get("flags", 0)) + self._fill_overwrites(data) + + @property + def _sorting_bucket(self) -> int: + return ChannelType.category.value + + @property + def type(self) -> ChannelType: + """The channel's Discord type.""" + return ChannelType.category + + @utils.copy_doc(discord.abc.GuildChannel.clone) + async def clone( + self, *, name: str | None = None, reason: str | None = None + ) -> CategoryChannel: + return await self._clone_impl({}, name=name, reason=reason) + + @overload + async def edit( + self, + *, + name: str = ..., + position: int = ..., + overwrites: Mapping[Role | Member, PermissionOverwrite] = ..., + reason: str | None = ..., + ) -> CategoryChannel | None: ... + + @overload + async def edit(self) -> CategoryChannel | None: ... + + async def edit(self, *, reason=None, **options): + """|coro| + + Edits the channel. + + You must have the :attr:`~Permissions.manage_channels` permission to + use this. + + .. versionchanged:: 1.3 + The ``overwrites`` keyword-only parameter was added. + + .. versionchanged:: 2.0 + Edits are no longer in-place, the newly edited channel is returned instead. + + Parameters + ---------- + name: :class:`str` + The new category's name. + position: :class:`int` + The new category's position. + reason: Optional[:class:`str`] + The reason for editing this category. Shows up on the audit log. + overwrites: Dict[Union[:class:`Role`, :class:`Member`, :class:`~discord.abc.Snowflake`], :class:`PermissionOverwrite`] + The overwrites to apply to channel permissions. Useful for creating secret channels. + + Returns + ------- + Optional[:class:`.CategoryChannel`] + The newly edited category channel. If the edit was only positional + then ``None`` is returned instead. + + Raises + ------ + InvalidArgument + If position is less than 0 or greater than the number of categories. + Forbidden + You do not have permissions to edit the category. + HTTPException + Editing the category failed. + """ + + payload = await self._edit(options, reason=reason) + if payload is not None: + # the payload will always be the proper channel payload + return self.__class__(state=self._state, guild=self.guild, data=payload) # type: ignore + + @utils.copy_doc(discord.abc.GuildChannel.move) + async def move(self, **kwargs): + kwargs.pop("category", None) + await super().move(**kwargs) + + @property + def channels(self) -> list[GuildChannelType]: + """Returns the channels that are under this category. + + These are sorted by the official Discord UI, which places voice channels below the text channels. + """ + + def comparator(channel): + return not isinstance(channel, _TextChannel), (channel.position or -1) + + ret = [c for c in self.guild.channels if c.category_id == self.id] + ret.sort(key=comparator) + return ret + + @property + def text_channels(self) -> list[TextChannel]: + """Returns the text channels that are under this category.""" + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, TextChannel) + ] + ret.sort(key=lambda c: (c.position or -1, c.id)) + return ret + + @property + def voice_channels(self) -> list[VoiceChannel]: + """Returns the voice channels that are under this category.""" + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, VoiceChannel) + ] + ret.sort(key=lambda c: (c.position or -1, c.id)) + return ret + + @property + def stage_channels(self) -> list[StageChannel]: + """Returns the stage channels that are under this category. + + .. versionadded:: 1.7 + """ + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, StageChannel) + ] + ret.sort(key=lambda c: (c.position or -1, c.id)) + return ret + + @property + def forum_channels(self) -> list[ForumChannel]: + """Returns the forum channels that are under this category. + + .. versionadded:: 2.0 + """ + ret = [ + c + for c in self.guild.channels + if c.category_id == self.id and isinstance(c, ForumChannel) + ] + ret.sort(key=lambda c: (c.position or -1, c.id)) + return ret + + async def create_text_channel(self, name: str, **options: Any) -> TextChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_text_channel` to create a :class:`TextChannel` in the category. + + Returns + ------- + :class:`TextChannel` + The channel that was just created. + """ + return await self.guild.create_text_channel(name, category=self, **options) + + async def create_voice_channel(self, name: str, **options: Any) -> VoiceChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_voice_channel` to create a :class:`VoiceChannel` in the category. + + Returns + ------- + :class:`VoiceChannel` + The channel that was just created. + """ + return await self.guild.create_voice_channel(name, category=self, **options) + + async def create_stage_channel(self, name: str, **options: Any) -> StageChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_stage_channel` to create a :class:`StageChannel` in the category. + + .. versionadded:: 1.7 + + Returns + ------- + :class:`StageChannel` + The channel that was just created. + """ + return await self.guild.create_stage_channel(name, category=self, **options) + + async def create_forum_channel(self, name: str, **options: Any) -> ForumChannel: + """|coro| + + A shortcut method to :meth:`Guild.create_forum_channel` to create a :class:`ForumChannel` in the category. + + .. versionadded:: 2.0 + + Returns + ------- + :class:`ForumChannel` + The channel that was just created. + """ + return await self.guild.create_forum_channel(name, category=self, **options) + + @deprecated( + "CategoryChannel.is_nsfw is deprecated since version 2.7 and will be removed in version 3.0. NSFW categories are not available in the Discord API." + ) + def is_nsfw(self) -> bool: + return False + + # TODO: Remove in 3.0 + + @property + @deprecated( + "CategoryChannel.nsfw is deprecated since version 2.7 and will be removed in version 3.0. NSFW categories are not available in the Discord API." + ) + def nsfw(self) -> bool: + return False + + +DMC = TypeVar("DMC", bound="DMChannel") + + +class DMChannel(discord.abc.Messageable, Hashable): + """Represents a Discord direct message channel. + + .. container:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns a string representation of the channel + + Attributes + ---------- + recipient: Optional[:class:`User`] + The user you are participating with in the direct message channel. + If this channel is received through the gateway, the recipient information + may not be always available. + me: :class:`ClientUser` + The user presenting yourself. + id: :class:`int` + The direct message channel ID. + """ + + __slots__ = ("id", "recipient", "me", "_state") + + def __init__( + self, *, me: ClientUser, state: ConnectionState, data: DMChannelPayload + ): + self._state: ConnectionState = state + self.recipient: User | None = None + if r := data.get("recipients"): + self.recipient = state.store_user(r[0]) + self.me: ClientUser = me + self.id: int = int(data["id"]) + + async def _get_channel(self): + return self + + def __str__(self) -> str: + if self.recipient: + return f"Direct Message with {self.recipient}" + return "Direct Message with Unknown User" + + def __repr__(self) -> str: + return f"" + + @classmethod + def _from_message(cls: type[DMC], state: ConnectionState, channel_id: int) -> DMC: + self: DMC = cls.__new__(cls) + self._state = state + self.id = channel_id + self.recipient = None + # state.user won't be None here + self.me = state.user # type: ignore + return self + + @property + def type(self) -> ChannelType: + """The channel's Discord type.""" + return ChannelType.private + + @property + def jump_url(self) -> str: + """Returns a URL that allows the client to jump to the channel. + + .. versionadded:: 2.0 + """ + return f"https://discord.com/channels/@me/{self.id}" + + @property + def created_at(self) -> datetime.datetime: + """Returns the direct message channel's creation time in UTC.""" + return utils.snowflake_time(self.id) + + def permissions_for(self, obj: Any = None, /) -> Permissions: + """Handles permission resolution for a :class:`User`. + + This function is there for compatibility with other channel types. + + Actual direct messages do not really have the concept of permissions. + + This returns all the Text related permissions set to ``True`` except: + + - :attr:`~Permissions.send_tts_messages`: You cannot send TTS messages in a DM. + - :attr:`~Permissions.manage_messages`: You cannot delete others messages in a DM. + + Parameters + ---------- + obj: :class:`User` + The user to check permissions for. This parameter is ignored + but kept for compatibility with other ``permissions_for`` methods. + + Returns + ------- + :class:`Permissions` + The resolved permissions. + """ + + base = Permissions.text() + base.read_messages = True + base.send_tts_messages = False + base.manage_messages = False + return base + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + .. versionadded:: 1.6 + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message. + """ + + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + +class GroupChannel(discord.abc.Messageable, Hashable): + """Represents a Discord group channel. + + .. container:: operations + + .. describe:: x == y + + Checks if two channels are equal. + + .. describe:: x != y + + Checks if two channels are not equal. + + .. describe:: hash(x) + + Returns the channel's hash. + + .. describe:: str(x) + + Returns a string representation of the channel + + Attributes + ---------- + recipients: List[:class:`User`] + The users you are participating with in the group channel. + me: :class:`ClientUser` + The user presenting yourself. + id: :class:`int` + The group channel ID. + owner: Optional[:class:`User`] + The user that owns the group channel. + owner_id: :class:`int` + The owner ID that owns the group channel. + + .. versionadded:: 2.0 + name: Optional[:class:`str`] + The group channel's name if provided. + """ + + __slots__ = ( + "id", + "recipients", + "owner_id", + "owner", + "_icon", + "name", + "me", + "_state", + ) + + def __init__( + self, *, me: ClientUser, state: ConnectionState, data: GroupChannelPayload + ): + self._state: ConnectionState = state + self.id: int = int(data["id"]) + self.me: ClientUser = me + self._update_group(data) + + def _update_group(self, data: GroupChannelPayload) -> None: + self.owner_id: int | None = utils._get_as_snowflake(data, "owner_id") + self._icon: str | None = data.get("icon") + self.name: str | None = data.get("name") + self.recipients: list[User] = [ + self._state.store_user(u) for u in data.get("recipients", []) + ] + + self.owner: BaseUser | None + if self.owner_id == self.me.id: + self.owner = self.me + else: + self.owner = utils.find(lambda u: u.id == self.owner_id, self.recipients) + + async def _get_channel(self): + return self + + def __str__(self) -> str: + if self.name: + return self.name + + if len(self.recipients) == 0: + return "Unnamed" + + return ", ".join(map(lambda x: x.name, self.recipients)) + + def __repr__(self) -> str: + return f"" + + @property + def type(self) -> ChannelType: + """The channel's Discord type.""" + return ChannelType.group + + @property + def icon(self) -> Asset | None: + """Returns the channel's icon asset if available.""" + if self._icon is None: + return None + return Asset._from_icon(self._state, self.id, self._icon, path="channel") + + @property + def created_at(self) -> datetime.datetime: + """Returns the channel's creation time in UTC.""" + return utils.snowflake_time(self.id) + + @property + def jump_url(self) -> str: + """Returns a URL that allows the client to jump to the channel. + + .. versionadded:: 2.0 + """ + return f"https://discord.com/channels/@me/{self.id}" + + def permissions_for(self, obj: Snowflake, /) -> Permissions: + """Handles permission resolution for a :class:`User`. + + This function is there for compatibility with other channel types. + + Actual direct messages do not really have the concept of permissions. + + This returns all the Text related permissions set to ``True`` except: + + - :attr:`~Permissions.send_tts_messages`: You cannot send TTS messages in a DM. + - :attr:`~Permissions.manage_messages`: You cannot delete others messages in a DM. + + This also checks the kick_members permission if the user is the owner. + + Parameters + ---------- + obj: :class:`~discord.abc.Snowflake` + The user to check permissions for. + + Returns + ------- + :class:`Permissions` + The resolved permissions for the user. + """ + + base = Permissions.text() + base.read_messages = True + base.send_tts_messages = False + base.manage_messages = False + base.mention_everyone = True + + if obj.id == self.owner_id: + base.kick_members = True + + return base + + async def leave(self) -> None: + """|coro| + + Leave the group. + + If you are the only one in the group, this deletes it as well. + + Raises + ------ + HTTPException + Leaving the group failed. + """ + + await self._state.http.leave_group(self.id) + + +class PartialMessageable(discord.abc.Messageable, Hashable): + """Represents a partial messageable to aid with working messageable channels when + only a channel ID are present. + + The only way to construct this class is through :meth:`Client.get_partial_messageable`. + + Note that this class is trimmed down and has no rich attributes. + + .. versionadded:: 2.0 + + .. container:: operations + + .. describe:: x == y + + Checks if two partial messageables are equal. + + .. describe:: x != y + + Checks if two partial messageables are not equal. + + .. describe:: hash(x) + + Returns the partial messageable's hash. + + Attributes + ---------- + id: :class:`int` + The channel ID associated with this partial messageable. + type: Optional[:class:`ChannelType`] + The channel type associated with this partial messageable, if given. + """ + + def __init__( + self, state: ConnectionState, id: int, type: ChannelType | None = None + ): + self._state: ConnectionState = state + self._channel: Object = Object(id=id) + self.id: int = id + self.type: ChannelType | None = type + + async def _get_channel(self) -> Object: + return self._channel + + def get_partial_message(self, message_id: int, /) -> PartialMessage: + """Creates a :class:`PartialMessage` from the message ID. + + This is useful if you want to work with a message and only have its ID without + doing an unnecessary API call. + + Parameters + ---------- + message_id: :class:`int` + The message ID to create a partial message for. + + Returns + ------- + :class:`PartialMessage` + The partial message. + """ + + from .message import PartialMessage + + return PartialMessage(channel=self, id=message_id) + + def __repr__(self) -> str: + return f"" + + +class VoiceChannelEffectAnimation(NamedTuple): + """Represents an animation that can be sent to a voice channel. + + .. versionadded:: 2.7 + """ + + id: int + type: VoiceChannelEffectAnimationType + + +class VoiceChannelSoundEffect(PartialSoundboardSound): ... + + +class VoiceChannelEffectSendEvent: + """Represents the payload for an :func:`on_voice_channel_effect_send`. + + .. versionadded:: 2.7 + + Attributes + ---------- + animation_type: :class:`int` + The type of animation that is being sent. + animation_id: :class:`int` + The ID of the animation that is being sent. + sound: Optional[:class:`SoundboardSound`] + The sound that is being sent, could be ``None`` if the effect is not a sound effect. + guild: :class:`Guild` + The guild in which the sound is being sent. + user: :class:`Member` + The member that sent the sound. + channel: :class:`VoiceChannel` + The voice channel in which the sound is being sent. + data: :class:`dict` + The raw data sent by the gateway. + """ + + __slots__ = ( + "_state", + "animation_type", + "animation_id", + "sound", + "guild", + "user", + "channel", + "data", + "emoji", + ) + + def __init__( + self, + data: VoiceChannelEffectSend, + state: ConnectionState, + sound: SoundboardSound | PartialSoundboardSound | None = None, + ) -> None: + self._state = state + channel_id = int(data["channel_id"]) + user_id = int(data["user_id"]) + guild_id = int(data["guild_id"]) + self.animation_type: VoiceChannelEffectAnimationType = try_enum( + VoiceChannelEffectAnimationType, data["animation_type"] + ) + self.animation_id = int(data["animation_id"]) + self.sound = sound + self.guild = state._get_guild(guild_id) + self.user = self.guild.get_member(user_id) + self.channel = self.guild.get_channel(channel_id) + self.emoji = ( + PartialEmoji( + name=data["emoji"]["name"], + animated=data["emoji"]["animated"], + id=data["emoji"]["id"], + ) + if data.get("emoji", None) + else None + ) + self.data = data + + +def _guild_channel_factory(channel_type: int): + value = try_enum(ChannelType, channel_type) + if value is ChannelType.text: + return TextChannel, value + elif value is ChannelType.voice: + return VoiceChannel, value + elif value is ChannelType.category: + return CategoryChannel, value + elif value is ChannelType.news: + return TextChannel, value + elif value is ChannelType.stage_voice: + return StageChannel, value + elif value is ChannelType.directory: + return None, value # todo: Add DirectoryChannel when applicable + elif value is ChannelType.forum: + return ForumChannel, value + elif value is ChannelType.media: + return MediaChannel, value + else: + return None, value + + +def _channel_factory(channel_type: int): + cls, value = _guild_channel_factory(channel_type) + if value is ChannelType.private: + return DMChannel, value + elif value is ChannelType.group: + return GroupChannel, value + else: + return cls, value + + +def _threaded_channel_factory(channel_type: int): + cls, value = _channel_factory(channel_type) + if value in ( + ChannelType.private_thread, + ChannelType.public_thread, + ChannelType.news_thread, + ): + return Thread, value + return cls, value + + +def _threaded_guild_channel_factory(channel_type: int): + cls, value = _guild_channel_factory(channel_type) + if value in ( + ChannelType.private_thread, + ChannelType.public_thread, + ChannelType.news_thread, + ): + return Thread, value + return cls, value diff --git a/venv/lib/python3.11/site-packages/discord/client.py b/venv/lib/python3.11/site-packages/discord/client.py new file mode 100644 index 0000000..f227ccf --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/client.py @@ -0,0 +1,2387 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import asyncio +import logging +import signal +import sys +import traceback +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Coroutine, + Generator, + Sequence, + TypeVar, +) + +import aiohttp +from typing_extensions import Self, deprecated + +from . import utils +from .activity import ActivityTypes, BaseActivity, create_activity +from .appinfo import AppInfo, PartialAppInfo +from .application_role_connection import ApplicationRoleConnectionMetadata +from .backoff import ExponentialBackoff +from .channel import PartialMessageable, _threaded_channel_factory +from .emoji import AppEmoji, GuildEmoji +from .enums import ChannelType, Status +from .errors import * +from .flags import ApplicationFlags, Intents +from .gateway import * +from .guild import Guild +from .http import HTTPClient +from .invite import Invite +from .iterators import EntitlementIterator, GuildIterator +from .mentions import AllowedMentions +from .monetization import SKU +from .object import Object +from .soundboard import SoundboardSound +from .stage_instance import StageInstance +from .state import ConnectionState +from .sticker import GuildSticker, StandardSticker, StickerPack, _sticker_factory +from .template import Template +from .threads import Thread +from .ui.view import BaseView +from .user import ClientUser, User +from .utils import ( + _D, + _FETCHABLE, + MISSING, + _get_event_loop, + warn_if_voice_dependencies_missing, +) +from .webhook import Webhook +from .widget import Widget + +if TYPE_CHECKING: + from .abc import GuildChannel, PrivateChannel, Snowflake, SnowflakeTime + from .channel import ( + DMChannel, + ) + from .interactions import Interaction + from .member import Member + from .message import Message + from .poll import Poll + from .soundboard import SoundboardSound + from .threads import Thread + from .ui.item import ViewItem + from .voice import VoiceProtocol + +__all__ = ("Client",) + +Coro = TypeVar("Coro", bound=Callable[..., Coroutine[Any, Any, Any]]) + +_log = logging.getLogger(__name__) + + +def _cancel_tasks(loop: asyncio.AbstractEventLoop) -> None: + tasks = {t for t in asyncio.all_tasks(loop=loop) if not t.done()} + + if not tasks: + return + + _log.info("Cleaning up after %d tasks.", len(tasks)) + for task in tasks: + task.cancel() + + loop.run_until_complete(asyncio.gather(*tasks, return_exceptions=True)) + _log.info("All tasks finished cancelling.") + + for task in tasks: + if task.cancelled(): + continue + if task.exception() is not None: + loop.call_exception_handler( + { + "message": "Unhandled exception during Client.run shutdown.", + "exception": task.exception(), + "task": task, + } + ) + + +def _cleanup_loop(loop: asyncio.AbstractEventLoop) -> None: + try: + _cancel_tasks(loop) + loop.run_until_complete(loop.shutdown_asyncgens()) + finally: + _log.info("Closing the event loop.") + loop.close() + + +class Client: + r"""Represents a client connection that connects to Discord. + This class is used to interact with the Discord WebSocket and API. + + A number of options can be passed to the :class:`Client`. + + Parameters + ----------- + max_messages: Optional[:class:`int`] + The maximum number of messages to store in the internal message cache. + This defaults to ``1000``. Passing in ``None`` disables the message cache. + + .. versionchanged:: 1.3 + Allow disabling the message cache and change the default size to ``1000``. + loop: Optional[:class:`asyncio.AbstractEventLoop`] + The :class:`asyncio.AbstractEventLoop` to use for asynchronous operations. + Defaults to ``None``, in which case the default event loop is used via + :func:`asyncio.get_event_loop()` if it exists or one is created via :func:`asyncio.new_event_loop()`. + connector: Optional[:class:`aiohttp.BaseConnector`] + The connector to use for connection pooling. + proxy: Optional[:class:`str`] + Proxy URL. + proxy_auth: Optional[:class:`aiohttp.BasicAuth`] + An object that represents proxy HTTP Basic Authorization. + shard_id: Optional[:class:`int`] + Integer starting at ``0`` and less than :attr:`.shard_count`. + shard_count: Optional[:class:`int`] + The total number of shards. + application_id: :class:`int` + The client's application ID. + intents: :class:`Intents` + The intents that you want to enable for the session. This is a way of + disabling and enabling certain gateway events from triggering and being sent. + If not given, defaults to a regularly constructed :class:`Intents` class. + + .. versionadded:: 1.5 + member_cache_flags: :class:`MemberCacheFlags` + Allows for finer control over how the library caches members. + If not given, defaults to cache as much as possible with the + currently selected intents. + + .. versionadded:: 1.5 + chunk_guilds_at_startup: :class:`bool` + Indicates if :func:`.on_ready` should be delayed to chunk all guilds + at start-up if necessary. This operation is incredibly slow for large + amounts of guilds. The default is ``True`` if :attr:`Intents.members` + is ``True``. + + .. versionadded:: 1.5 + status: Optional[:class:`.Status`] + A status to start your presence with upon logging on to Discord. + activity: Optional[:class:`.BaseActivity`] + An activity to start your presence with upon logging on to Discord. + allowed_mentions: Optional[:class:`AllowedMentions`] + Control how the client handles mentions by default on every message sent. + + .. versionadded:: 1.4 + heartbeat_timeout: :class:`float` + The maximum numbers of seconds before timing out and restarting the + WebSocket in the case of not receiving a HEARTBEAT_ACK. Useful if + processing the initial packets take too long to the point of disconnecting + you. The default timeout is 60 seconds. + guild_ready_timeout: :class:`float` + The maximum number of seconds to wait for the GUILD_CREATE stream to end before + preparing the member cache and firing READY. The default timeout is 2 seconds. + + .. versionadded:: 1.4 + assume_unsync_clock: :class:`bool` + Whether to assume the system clock is unsynced. This applies to the ratelimit handling + code. If this is set to ``True``, the default, then the library uses the time to reset + a rate limit bucket given by Discord. If this is ``False`` then your system clock is + used to calculate how long to sleep for. If this is set to ``False`` it is recommended to + sync your system clock to Google's NTP server. + + .. versionadded:: 1.3 + enable_debug_events: :class:`bool` + Whether to enable events that are useful only for debugging gateway related information. + + Right now this involves :func:`on_socket_raw_receive` and :func:`on_socket_raw_send`. If + this is ``False`` then those events will not be dispatched (due to performance considerations). + To enable these events, this must be set to ``True``. Defaults to ``False``. + + .. versionadded:: 2.0 + cache_app_emojis: :class:`bool` + Whether to automatically fetch and cache the application's emojis on startup and when fetching. Defaults to ``False``. + + .. warning:: + + There are no events related to application emojis - if any are created/deleted on the + Developer Dashboard while the client is running, the cache will not be updated until you manually + run :func:`fetch_emojis`. + + .. versionadded:: 2.7 + cache_default_sounds: :class:`bool` + Whether to automatically fetch and cache the default soundboard sounds on startup. Defaults to ``True``. + + .. versionadded:: 2.8 + + Attributes + ----------- + ws + The WebSocket gateway the client is currently connected to. Could be ``None``. + loop: :class:`asyncio.AbstractEventLoop` + The event loop that the client uses for asynchronous operations. + """ + + def __init__( + self, + *, + loop: asyncio.AbstractEventLoop | None = None, + **options: Any, + ): + # self.ws is set in the connect method + self.ws: DiscordWebSocket = None # type: ignore + self.loop: asyncio.AbstractEventLoop = ( + _get_event_loop() if loop is None else loop + ) + self._listeners: dict[str, list[tuple[asyncio.Future, Callable[..., bool]]]] = ( + {} + ) + self.shard_id: int | None = options.get("shard_id") + self.shard_count: int | None = options.get("shard_count") + + connector: aiohttp.BaseConnector | None = options.pop("connector", None) + proxy: str | None = options.pop("proxy", None) + proxy_auth: aiohttp.BasicAuth | None = options.pop("proxy_auth", None) + unsync_clock: bool = options.pop("assume_unsync_clock", True) + self.http: HTTPClient = HTTPClient( + connector, + proxy=proxy, + proxy_auth=proxy_auth, + unsync_clock=unsync_clock, + loop=self.loop, + ) + + self._handlers: dict[str, Callable] = {"ready": self._handle_ready} + + self._hooks: dict[str, Callable] = { + "before_identify": self._call_before_identify_hook + } + + self._enable_debug_events: bool = options.pop("enable_debug_events", False) + self._connection: ConnectionState = self._get_state(**options) + self._connection.shard_count = self.shard_count + self._closed: bool = False + self._ready: asyncio.Event = asyncio.Event() + self._connection._get_websocket = self._get_websocket + self._connection._get_client = lambda: self + self._event_handlers: dict[str, list[Coro]] = {} + + warn_if_voice_dependencies_missing() + + # Used to hard-reference tasks so they don't get garbage collected (discarded with done_callbacks) + self._tasks = set() + + async def __aenter__(self) -> Client: + loop = asyncio.get_running_loop() + self.loop = loop + self.http.loop = loop + self._connection.loop = loop + + self._ready = asyncio.Event() + + return self + + async def __aexit__( + self, + exc_t: BaseException | None, + exc_v: BaseException | None, + exc_tb: TracebackType | None, + ) -> None: + if not self.is_closed(): + await self.close() + + # internals + + def _get_websocket( + self, guild_id: int | None = None, *, shard_id: int | None = None + ) -> DiscordWebSocket: + return self.ws + + def _get_state(self, **options: Any) -> ConnectionState: + return ConnectionState( + dispatch=self.dispatch, + handlers=self._handlers, + hooks=self._hooks, + http=self.http, + loop=self.loop, + **options, + ) + + def _handle_ready(self) -> None: + self._ready.set() + + @property + def latency(self) -> float: + """Measures latency between a HEARTBEAT and a HEARTBEAT_ACK in seconds. If no websocket + is present, this returns ``nan``, and if no heartbeat has been received yet, this returns ``float('inf')``. + + This could be referred to as the Discord WebSocket protocol latency. + """ + ws = self.ws + return float("nan") if not ws else ws.latency + + def is_ws_ratelimited(self) -> bool: + """Whether the WebSocket is currently rate limited. + + This can be useful to know when deciding whether you should query members + using HTTP or via the gateway. + + .. versionadded:: 1.6 + """ + if self.ws: + return self.ws.is_ratelimited() + return False + + @property + def user(self) -> ClientUser | None: + """Represents the connected client. ``None`` if not logged in.""" + return self._connection.user + + @property + def guilds(self) -> list[Guild]: + """The guilds that the connected client is a member of.""" + return self._connection.guilds + + @property + def emojis(self) -> list[GuildEmoji | AppEmoji]: + """The emojis that the connected client has. + + .. note:: + + This only includes the application's emojis if `cache_app_emojis` is ``True``. + """ + return self._connection.emojis + + @property + def guild_emojis(self) -> list[GuildEmoji]: + """The :class:`~discord.GuildEmoji` that the connected client has.""" + return [e for e in self.emojis if isinstance(e, GuildEmoji)] + + @property + def app_emojis(self) -> list[AppEmoji]: + """The :class:`~discord.AppEmoji` that the connected client has. + + .. note:: + + This is only available if `cache_app_emojis` is ``True``. + """ + return [e for e in self.emojis if isinstance(e, AppEmoji)] + + @property + def stickers(self) -> list[GuildSticker]: + """The stickers that the connected client has. + + .. versionadded:: 2.0 + """ + return self._connection.stickers + + @property + def polls(self) -> list[Poll]: + """The polls that the connected client has. + + .. versionadded:: 2.6 + """ + return self._connection.polls + + @property + def cached_messages(self) -> Sequence[Message]: + """Read-only list of messages the connected client has cached. + + .. versionadded:: 1.1 + """ + return utils.SequenceProxy(self._connection._messages or []) + + @property + def private_channels(self) -> list[PrivateChannel]: + """The private channels that the connected client is participating on. + + .. note:: + + This returns only up to 128 most recent private channels due to an internal working + on how Discord deals with private channels. + """ + return self._connection.private_channels + + @property + def voice_clients(self) -> list[VoiceProtocol[Self]]: + """Represents a list of voice connections. + + These are usually :class:`.VoiceClient` instances. + """ + return self._connection.voice_clients + + @property + def application_id(self) -> int | None: + """The client's application ID. + + If this is not passed via ``__init__`` then this is retrieved + through the gateway when an event contains the data. Usually + after :func:`~discord.on_connect` is called. + + .. versionadded:: 2.0 + """ + return self._connection.application_id + + @property + def application_flags(self) -> ApplicationFlags: + """The client's application flags. + + .. versionadded:: 2.0 + """ + return self._connection.application_flags # type: ignore + + def is_ready(self) -> bool: + """Specifies if the client's internal cache is ready for use.""" + return self._ready.is_set() + + async def _run_event( + self, + coro: Callable[..., Coroutine[Any, Any, Any]], + event_name: str, + *args: Any, + **kwargs: Any, + ) -> None: + try: + await coro(*args, **kwargs) + except asyncio.CancelledError: + pass + except Exception: + try: + await self.on_error(event_name, *args, **kwargs) + except asyncio.CancelledError: + pass + + def _schedule_event( + self, + coro: Callable[..., Coroutine[Any, Any, Any]], + event_name: str, + *args: Any, + **kwargs: Any, + ) -> asyncio.Task: + wrapped = self._run_event(coro, event_name, *args, **kwargs) + + # Schedule task and store in set to avoid task garbage collection + task = asyncio.create_task(wrapped, name=f"pycord: {event_name}") + self._tasks.add(task) + task.add_done_callback(self._tasks.discard) + return task + + def dispatch(self, event: str, *args: Any, **kwargs: Any) -> None: + _log.debug("Dispatching event %s", event) + method = f"on_{event}" + + listeners = self._listeners.get(event) + if listeners: + removed = [] + for i, (future, condition) in enumerate(listeners): + if future.cancelled(): + removed.append(i) + continue + + try: + result = condition(*args) + except Exception as exc: + future.set_exception(exc) + removed.append(i) + else: + if result: + if len(args) == 0: + future.set_result(None) + elif len(args) == 1: + future.set_result(args[0]) + else: + future.set_result(args) + removed.append(i) + + if len(removed) == len(listeners): + self._listeners.pop(event) + else: + for idx in reversed(removed): + del listeners[idx] + + # Schedule the main handler registered with @event + try: + coro = getattr(self, method) + except AttributeError: + pass + else: + self._schedule_event(coro, method, *args, **kwargs) + + # collect the once listeners as removing them from the list + # while iterating over it causes issues + once_listeners = [] + + # Schedule additional handlers registered with @listen + for coro in self._event_handlers.get(method, []): + self._schedule_event(coro, method, *args, **kwargs) + + try: + if coro._once: # added using @listen() + once_listeners.append(coro) + + except AttributeError: # added using @Cog.add_listener() + # https://github.com/Pycord-Development/pycord/pull/1989 + # Although methods are similar to functions, attributes can't be added to them. + # This means that we can't add the `_once` attribute in the `add_listener` method + # and can only be added using the `@listen` decorator. + + continue + + # remove the once listeners + for coro in once_listeners: + self._event_handlers[method].remove(coro) + + async def on_error(self, event_method: str, *args: Any, **kwargs: Any) -> None: + """|coro| + + The default error handler provided by the client. + + By default, this prints to :data:`sys.stderr` however it could be + overridden to have a different implementation. + Check :func:`~discord.on_error` for more details. + """ + print(f"Ignoring exception in {event_method}", file=sys.stderr) + traceback.print_exc() + + async def on_view_error( + self, error: Exception, item: ViewItem, interaction: Interaction + ) -> None: + """|coro| + + The default view error handler provided by the client. + + This only fires for a view if you did not define its :func:`~discord.ui.BaseView.on_error`. + + Parameters + ---------- + error: :class:`Exception` + The exception that was raised. + item: :class:`ViewItem` + The item that the user interacted with. + interaction: :class:`Interaction` + The interaction that was received. + """ + + print( + f"Ignoring exception in view {interaction.view} for item {item}:", + file=sys.stderr, + ) + traceback.print_exception( + error.__class__, error, error.__traceback__, file=sys.stderr + ) + + async def on_modal_error(self, error: Exception, interaction: Interaction) -> None: + """|coro| + + The default modal error handler provided by the client. + The default implementation prints the traceback to stderr. + + This only fires for a modal if you did not define its :func:`~discord.ui.BaseModal.on_error`. + + Parameters + ---------- + error: :class:`Exception` + The exception that was raised. + interaction: :class:`Interaction` + The interaction that was received. + """ + + print(f"Ignoring exception in modal {interaction.modal}:", file=sys.stderr) + traceback.print_exception( + error.__class__, error, error.__traceback__, file=sys.stderr + ) + + # hooks + + async def _call_before_identify_hook( + self, shard_id: int | None, *, initial: bool = False + ) -> None: + # This hook is an internal hook that actually calls the public one. + # It allows the library to have its own hook without stepping on the + # toes of those who need to override their own hook. + await self.before_identify_hook(shard_id, initial=initial) + + async def before_identify_hook( + self, shard_id: int | None, *, initial: bool = False + ) -> None: + """|coro| + + A hook that is called before IDENTIFYing a session. This is useful + if you wish to have more control over the synchronization of multiple + IDENTIFYing clients. + + The default implementation sleeps for 5 seconds. + + .. versionadded:: 1.4 + + Parameters + ---------- + shard_id: :class:`int` + The shard ID that requested being IDENTIFY'd + initial: :class:`bool` + Whether this IDENTIFY is the first initial IDENTIFY. + """ + + if not initial: + await asyncio.sleep(5.0) + + # login state management + + async def login(self, token: str) -> None: + """|coro| + + Logs in the client with the specified credentials. + + Parameters + ---------- + token: :class:`str` + The authentication token. Do not prefix this token with + anything as the library will do it for you. + + Raises + ------ + TypeError + The token was in invalid type. + :exc:`LoginFailure` + The wrong credentials are passed. + :exc:`HTTPException` + An unknown HTTP related error occurred, + usually when it isn't 200 or the known incorrect credentials + passing status code. + """ + if not isinstance(token, str): + raise TypeError( + f"token must be of type str, not {token.__class__.__name__}" + ) + + _log.info("logging in using static token") + + data = await self.http.static_login(token.strip()) + self._connection.user = ClientUser(state=self._connection, data=data) + + async def connect(self, *, reconnect: bool = True) -> None: + """|coro| + + Creates a WebSocket connection and lets the WebSocket listen + to messages from Discord. This is a loop that runs the entire + event system and miscellaneous aspects of the library. Control + is not resumed until the WebSocket connection is terminated. + + Parameters + ---------- + reconnect: :class:`bool` + If we should attempt reconnecting, either due to internet + failure or a specific failure on Discord's part. Certain + disconnects that lead to bad state will not be handled (such as + invalid sharding payloads or bad tokens). + + Raises + ------ + :exc:`GatewayNotFound` + The gateway to connect to Discord is not found. Usually if this + is thrown then there is a Discord API outage. + :exc:`ConnectionClosed` + The WebSocket connection has been terminated. + """ + + backoff = ExponentialBackoff() + ws_params = { + "initial": True, + "shard_id": self.shard_id, + } + while not self.is_closed(): + try: + coro = DiscordWebSocket.from_client(self, **ws_params) + self.ws = await asyncio.wait_for(coro, timeout=60.0) + ws_params["initial"] = False + while True: + await self.ws.poll_event() + except ReconnectWebSocket as e: + _log.info("Got a request to %s the websocket.", e.op) + self.dispatch("disconnect") + ws_params.update( + sequence=self.ws.sequence, + resume=e.resume, + session=self.ws.session_id, + ) + continue + except ( + OSError, + HTTPException, + GatewayNotFound, + ConnectionClosed, + aiohttp.ClientError, + asyncio.TimeoutError, + ) as exc: + self.dispatch("disconnect") + if not reconnect: + await self.close() + if isinstance(exc, ConnectionClosed) and exc.code == 1000: + # clean close, don't re-raise this + return + raise + + if self.is_closed(): + return + + # If we get connection reset by peer then try to RESUME + if isinstance(exc, OSError) and exc.errno in (54, 10054): + ws_params.update( + sequence=self.ws.sequence, + initial=False, + resume=True, + session=self.ws.session_id, + ) + continue + + # We should only get this when an unhandled close code happens, + # such as a clean disconnect (1000) or a bad state (bad token, no sharding, etc) + # sometimes, discord sends us 1000 for unknown reasons, so we should reconnect + # regardless and rely on is_closed instead + if isinstance(exc, ConnectionClosed): + if exc.code == 4014: + raise PrivilegedIntentsRequired(exc.shard_id) from None + if exc.code != 1000: + await self.close() + raise + + retry = backoff.delay() + _log.exception("Attempting a reconnect in %.2fs", retry) + await asyncio.sleep(retry) + # Always try to RESUME the connection + # If the connection is not RESUME-able then the gateway will invalidate the session. + # This is apparently what the official Discord client does. + if self.ws is None: + continue + ws_params.update( + sequence=self.ws.sequence, resume=True, session=self.ws.session_id + ) + + async def close(self) -> None: + """|coro| + + Closes the connection to Discord. + """ + if self._closed: + return + + await self.http.close() + self._closed = True + + for voice in self.voice_clients: + try: + await voice.disconnect(force=True) + except Exception: + # if an error happens during disconnects, disregard it. + pass + + if self.ws is not None and self.ws.open: + await self.ws.close(code=1000) + + self._ready.clear() + + def clear(self) -> None: + """Clears the internal state of the bot. + + After this, the bot can be considered "re-opened", i.e. :meth:`is_closed` + and :meth:`is_ready` both return ``False`` along with the bot's internal + cache cleared. + """ + self._closed = False + self._ready.clear() + self._connection.clear() + self.http.recreate() + + async def start(self, token: str, *, reconnect: bool = True) -> None: + """|coro| + + A shorthand coroutine for :meth:`login` + :meth:`connect`. + + Raises + ------ + TypeError + An unexpected keyword argument was received. + """ + await self.login(token) + await self.connect(reconnect=reconnect) + + def run(self, *args: Any, **kwargs: Any) -> None: + """A blocking call that abstracts away the event loop + initialisation from you. + + If you want more control over the event loop then this + function should not be used. Use :meth:`start` coroutine + or :meth:`connect` + :meth:`login`. + + Roughly Equivalent to: :: + + try: + loop.run_until_complete(start(*args, **kwargs)) + except KeyboardInterrupt: + loop.run_until_complete(close()) + # cancel all tasks lingering + finally: + loop.close() + + .. warning:: + + This function must be the last function to call due to the fact that it + is blocking. That means that registration of events or anything being + called after this function call will not execute until it returns. + """ + loop = self.loop + + try: + loop.add_signal_handler(signal.SIGINT, loop.stop) + loop.add_signal_handler(signal.SIGTERM, loop.stop) + except (NotImplementedError, RuntimeError): + pass + + async def runner(): + try: + await self.start(*args, **kwargs) + finally: + if not self.is_closed(): + await self.close() + + def stop_loop_on_completion(f): + loop.stop() + + future = asyncio.ensure_future(runner(), loop=loop) + future.add_done_callback(stop_loop_on_completion) + try: + loop.run_forever() + except KeyboardInterrupt: + _log.info("Received signal to terminate bot and event loop.") + finally: + future.remove_done_callback(stop_loop_on_completion) + _log.info("Cleaning up tasks.") + _cleanup_loop(loop) + + if not future.cancelled(): + try: + return future.result() + except KeyboardInterrupt: + # I am unsure why this gets raised here but suppress it anyway + return None + + # properties + + def is_closed(self) -> bool: + """Indicates if the WebSocket connection is closed.""" + return self._closed + + @property + def activity(self) -> ActivityTypes | None: + """The activity being used upon logging in. + + Returns + ------- + Optional[:class:`.BaseActivity`] + """ + return create_activity(self._connection._activity) + + @activity.setter + def activity(self, value: ActivityTypes | None) -> None: + if value is None: + self._connection._activity = None + elif isinstance(value, BaseActivity): + # ConnectionState._activity is typehinted as ActivityPayload, we're passing Dict[str, Any] + self._connection._activity = value.to_dict() # type: ignore + else: + raise TypeError("activity must derive from BaseActivity.") + + @property + def status(self) -> Status: + """The status being used upon logging on to Discord. + + .. versionadded: 2.0 + """ + if self._connection._status in {state.value for state in Status}: + return Status(self._connection._status) + return Status.online + + @status.setter + def status(self, value: Status) -> None: + if value is Status.offline: + self._connection._status = "invisible" + elif isinstance(value, Status): + self._connection._status = str(value) + else: + raise TypeError("status must derive from Status.") + + @property + def allowed_mentions(self) -> AllowedMentions | None: + """The allowed mention configuration. + + .. versionadded:: 1.4 + """ + return self._connection.allowed_mentions + + @allowed_mentions.setter + def allowed_mentions(self, value: AllowedMentions | None) -> None: + if value is None or isinstance(value, AllowedMentions): + self._connection.allowed_mentions = value + else: + raise TypeError( + f"allowed_mentions must be AllowedMentions not {value.__class__!r}" + ) + + @property + def intents(self) -> Intents: + """The intents configured for this connection. + + .. versionadded:: 1.5 + """ + return self._connection.intents + + # helpers/getters + + @property + def users(self) -> list[User]: + """Returns a list of all the users the bot can see.""" + return list(self._connection._users.values()) + + async def fetch_application(self, application_id: int, /) -> PartialAppInfo: + """|coro| + Retrieves a :class:`.PartialAppInfo` from an application ID. + + Parameters + ---------- + application_id: :class:`int` + The application ID to retrieve information from. + + Returns + ------- + :class:`.PartialAppInfo` + The application information. + + Raises + ------ + NotFound + An application with this ID does not exist. + HTTPException + Retrieving the application failed. + """ + data = await self.http.get_application(application_id) + return PartialAppInfo(state=self._connection, data=data) + + def get_channel(self, id: int, /) -> GuildChannel | Thread | PrivateChannel | None: + """Returns a channel or thread with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[Union[:class:`.abc.GuildChannel`, :class:`.Thread`, :class:`.abc.PrivateChannel`]] + The returned channel or ``None`` if not found. + """ + return self._connection.get_channel(id) + + def get_message(self, id: int, /) -> Message | None: + """Returns a message the given ID. + + This is useful if you have a message_id but don't want to do an API call + to access the message. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`.Message`] + The returned message or ``None`` if not found. + """ + return self._connection._get_message(id) + + def get_partial_messageable( + self, id: int, *, type: ChannelType | None = None + ) -> PartialMessageable: + """Returns a partial messageable with the given channel ID. + + This is useful if you have a channel_id but don't want to do an API call + to send messages to it. + + .. versionadded:: 2.0 + + Parameters + ---------- + id: :class:`int` + The channel ID to create a partial messageable for. + type: Optional[:class:`.ChannelType`] + The underlying channel type for the partial messageable. + + Returns + ------- + :class:`.PartialMessageable` + The partial messageable + """ + return PartialMessageable(state=self._connection, id=id, type=type) + + def get_stage_instance(self, id: int, /) -> StageInstance | None: + """Returns a stage instance with the given stage channel ID. + + .. versionadded:: 2.0 + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`.StageInstance`] + The stage instance or ``None`` if not found. + """ + from .channel import StageChannel + + channel = self._connection.get_channel(id) + + if isinstance(channel, StageChannel): + return channel.instance + + def get_guild(self, id: int, /) -> Guild | None: + """Returns a guild with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`.Guild`] + The guild or ``None`` if not found. + """ + return self._connection._get_guild(id) + + def get_user(self, id: int, /) -> User | None: + """Returns a user with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`~discord.User`] + The user or ``None`` if not found. + """ + return self._connection.get_user(id) + + def get_emoji(self, id: int, /) -> GuildEmoji | AppEmoji | None: + """Returns an emoji with the given ID. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`.GuildEmoji` | :class:`.AppEmoji`] + The custom emoji or ``None`` if not found. + """ + return self._connection.get_emoji(id) + + def get_sticker(self, id: int, /) -> GuildSticker | None: + """Returns a guild sticker with the given ID. + + .. versionadded:: 2.0 + + .. note:: + + To retrieve standard stickers, use :meth:`.fetch_sticker`. + or :meth:`.fetch_premium_sticker_packs`. + + Returns + ------- + Optional[:class:`.GuildSticker`] + The sticker or ``None`` if not found. + """ + return self._connection.get_sticker(id) + + def get_poll(self, id: int, /) -> Poll | None: + """Returns a poll attached to the given message ID. + + Parameters + ---------- + id: :class:`int` + The message ID of the poll to search for. + + Returns + ------- + Optional[:class:`.Poll`] + The poll or ``None`` if not found. + """ + return self._connection.get_poll(id) + + def get_all_channels(self) -> Generator[GuildChannel]: + """A generator that retrieves every :class:`.abc.GuildChannel` the client can 'access'. + + This is equivalent to: :: + + for guild in client.guilds: + for channel in guild.channels: + yield channel + + .. note:: + + Just because you receive a :class:`.abc.GuildChannel` does not mean that + you can communicate in said channel. :meth:`.abc.GuildChannel.permissions_for` should + be used for that. + + Yields + ------ + :class:`.abc.GuildChannel` + A channel the client can 'access'. + """ + + for guild in self.guilds: + yield from guild.channels + + def get_all_members(self) -> Generator[Member]: + """Returns a generator with every :class:`.Member` the client can see. + + This is equivalent to: :: + + for guild in client.guilds: + for member in guild.members: + yield member + + Yields + ------ + :class:`.Member` + A member the client can see. + """ + for guild in self.guilds: + yield from guild.members + + @deprecated( + "Client.get_or_fetch_user is deprecated since version 2.7 and will be removed in version 3.0, consider using Client.get_or_fetch(User, id) instead." + ) + async def get_or_fetch_user(self, id: int, /) -> User | None: # TODO: Remove in 3.0 + """|coro| + + Looks up a user in the user cache or fetches if not found. + + Parameters + ---------- + id: :class:`int` + The ID to search for. + + Returns + ------- + Optional[:class:`~discord.User`] + The user or ``None`` if not found. + """ + + return await self.get_or_fetch(object_type=User, object_id=id, default=None) + + async def get_or_fetch( + self: Client, + object_type: type[_FETCHABLE], + object_id: int | None, + default: _D = None, + ) -> _FETCHABLE | _D | None: + """ + Shortcut method to get data from an object either by returning the cached version, or if it does not exist, attempting to fetch it from the API. + + Parameters + ---------- + object_type: Type[:class:`VoiceChannel` | :class:`TextChannel` | :class:`ForumChannel` | :class:`StageChannel` | :class:`CategoryChannel` | :class:`Thread` | :class:`User` | :class:`Guild` | :class:`GuildEmoji` | :class:`AppEmoji`] + Type of object to fetch or get. + + object_id: :class:`int` | :data:`None` + ID of object to get. If :data:`None`, returns `default` if provided, else :data:`None`. + + default: Any | :data:`None` + A default to return instead of raising if fetch fails. + + Returns + ------- + :class:`VoiceChannel` | :class:`TextChannel` | :class:`ForumChannel` | :class:`StageChannel` | :class:`CategoryChannel` | :class:`Thread` | :class:`User` | :class:`Guild` | :class:`GuildEmoji` | :class:`AppEmoji` | :data:`None` + The object if found, or `default` if provided when not found. + + Raises + ------ + :exc:`TypeError` + Raised when required parameters are missing or invalid types are provided. + :exc:`InvalidArgument` + Raised when an unsupported or incompatible object type is used. + """ + try: + return await utils.get_or_fetch( + obj=self, + object_type=object_type, + object_id=object_id, + default=default, + ) + except (HTTPException, ValueError, InvalidData): + return default + + # listeners/waiters + + async def wait_until_ready(self) -> None: + """|coro| + + Waits until the client's internal cache is all ready. + """ + await self._ready.wait() + + def wait_for( + self, + event: str, + *, + check: Callable[..., bool] | None = None, + timeout: float | None = None, + ) -> Any: + """|coro| + + Waits for a WebSocket event to be dispatched. + + This could be used to wait for a user to reply to a message, + or to react to a message, or to edit a message in a self-contained + way. + + The ``timeout`` parameter is passed onto :func:`asyncio.wait_for`. By default, + it does not timeout. Note that this does propagate the + :exc:`asyncio.TimeoutError` for you in case of timeout and is provided for + ease of use. + + In case the event returns multiple arguments, a :class:`tuple` containing those + arguments is returned instead. Please check the + :ref:`documentation ` for a list of events and their + parameters. + + This function returns the **first event that meets the requirements**. + + Parameters + ---------- + event: :class:`str` + The event name, similar to the :ref:`event reference `, + but without the ``on_`` prefix, to wait for. + check: Optional[Callable[..., :class:`bool`]] + A predicate to check what to wait for. The arguments must meet the + parameters of the event being waited for. + timeout: Optional[:class:`float`] + The number of seconds to wait before timing out and raising + :exc:`asyncio.TimeoutError`. + + Returns + ------- + Any + Returns no arguments, a single argument, or a :class:`tuple` of multiple + arguments that mirrors the parameters passed in the + :ref:`event reference `. + + Raises + ------ + asyncio.TimeoutError + Raised if a timeout is provided and reached. + + Examples + -------- + + Waiting for a user reply: :: + + @client.event + async def on_message(message): + if message.content.startswith('$greet'): + channel = message.channel + await channel.send('Say hello!') + + def check(m): + return m.content == 'hello' and m.channel == channel + + msg = await client.wait_for('message', check=check) + await channel.send(f'Hello {msg.author}!') + + Waiting for a thumbs up reaction from the message author: :: + + @client.event + async def on_message(message): + if message.content.startswith('$thumb'): + channel = message.channel + await channel.send('Send me that \N{THUMBS UP SIGN} reaction, mate') + + def check(reaction, user): + return user == message.author and str(reaction.emoji) == '\N{THUMBS UP SIGN}' + + try: + reaction, user = await client.wait_for('reaction_add', timeout=60.0, check=check) + except asyncio.TimeoutError: + await channel.send('\N{THUMBS DOWN SIGN}') + else: + await channel.send('\N{THUMBS UP SIGN}') + """ + + future = self.loop.create_future() + if check is None: + + def _check(*args): + return True + + check = _check + + ev = event.lower() + try: + listeners = self._listeners[ev] + except KeyError: + listeners = [] + self._listeners[ev] = listeners + + listeners.append((future, check)) + return asyncio.wait_for(future, timeout) + + # event registration + def add_listener(self, func: Coro, name: str = MISSING) -> None: + """The non decorator alternative to :meth:`.listen`. + + Parameters + ---------- + func: :ref:`coroutine ` + The function to call. + name: :class:`str` + The name of the event to listen for. Defaults to ``func.__name__``. + + Raises + ------ + TypeError + The ``func`` parameter is not a coroutine function. + ValueError + The ``name`` (event name) does not start with ``on_``. + + Example + ------- + + .. code-block:: python3 + + async def on_ready(): pass + async def my_message(message): pass + + client.add_listener(on_ready) + client.add_listener(my_message, 'on_message') + """ + name = func.__name__ if name is MISSING else name + + if not name.startswith("on_"): + raise ValueError("The 'name' parameter must start with 'on_'") + + if not asyncio.iscoroutinefunction(func): + raise TypeError("Listeners must be coroutines") + + if name in self._event_handlers: + self._event_handlers[name].append(func) + else: + self._event_handlers[name] = [func] + + _log.debug( + "%s has successfully been registered as a handler for event %s", + func.__name__, + name, + ) + + def remove_listener(self, func: Coro, name: str = MISSING) -> None: + """Removes a listener from the pool of listeners. + + Parameters + ---------- + func + The function that was used as a listener to remove. + name: :class:`str` + The name of the event we want to remove. Defaults to + ``func.__name__``. + """ + + name = func.__name__ if name is MISSING else name + + if name in self._event_handlers: + try: + self._event_handlers[name].remove(func) + except ValueError: + pass + + def listen(self, name: str = MISSING, once: bool = False) -> Callable[[Coro], Coro]: + """A decorator that registers another function as an external + event listener. Basically this allows you to listen to multiple + events from different places e.g. such as :func:`.on_ready` + + The functions being listened to must be a :ref:`coroutine `. + + Raises + ------ + TypeError + The function being listened to is not a coroutine. + ValueError + The ``name`` (event name) does not start with ``on_``. + + Example + ------- + + .. code-block:: python3 + + @client.listen() + async def on_message(message): + print('one') + + # in some other file... + + @client.listen('on_message') + async def my_message(message): + print('two') + + # listen to the first event only + @client.listen('on_ready', once=True) + async def on_ready(): + print('ready!') + + Would print one and two in an unspecified order. + """ + + def decorator(func: Coro) -> Coro: + # Special case, where default should be overwritten + if name == "on_application_command_error": + return self.event(func) + + func._once = once + self.add_listener(func, name) + return func + + if asyncio.iscoroutinefunction(name): + coro = name + name = coro.__name__ + return decorator(coro) + + return decorator + + def event(self, coro: Coro) -> Coro: + """A decorator that registers an event to listen to. + + You can find more info about the events on the :ref:`documentation below `. + + The events must be a :ref:`coroutine `, if not, :exc:`TypeError` is raised. + + .. note:: + + This replaces any default handlers. + Developers are encouraged to use :py:meth:`~discord.Client.listen` for adding additional handlers + instead of :py:meth:`~discord.Client.event` unless default method replacement is intended. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + + Example + ------- + + .. code-block:: python3 + + @client.event + async def on_ready(): + print('Ready!') + """ + + if not asyncio.iscoroutinefunction(coro): + raise TypeError("event registered must be a coroutine function") + + setattr(self, coro.__name__, coro) + _log.debug("%s has successfully been registered as an event", coro.__name__) + return coro + + async def change_presence( + self, + *, + activity: BaseActivity | None = None, + status: Status | None = None, + ): + """|coro| + + Changes the client's presence. + + Parameters + ---------- + activity: Optional[:class:`.BaseActivity`] + The activity being done. ``None`` if no currently active activity is done. + status: Optional[:class:`.Status`] + Indicates what status to change to. If ``None``, then + :attr:`.Status.online` is used. + + Raises + ------ + :exc:`InvalidArgument` + If the ``activity`` parameter is not the proper type. + + Example + ------- + + .. code-block:: python3 + + game = discord.Game("with the API") + await client.change_presence(status=discord.Status.idle, activity=game) + + .. versionchanged:: 2.0 + Removed the ``afk`` keyword-only parameter. + """ + + if status is None: + status_str = "online" + status = Status.online + elif status is Status.offline: + status_str = "invisible" + status = Status.offline + else: + status_str = str(status) + + await self.ws.change_presence(activity=activity, status=status_str) + + for guild in self._connection.guilds: + me = guild.me + if me is None: + continue + + me.activities = (activity,) if activity is not None else () + me.status = status + + # Guild stuff + + def fetch_guilds( + self, + *, + limit: int | None = 100, + before: SnowflakeTime = None, + after: SnowflakeTime = None, + with_counts: bool = True, + ) -> GuildIterator: + """Retrieves an :class:`.AsyncIterator` that enables receiving your guilds. + + .. note:: + + Using this, you will only receive :attr:`.Guild.owner`, :attr:`.Guild.icon`, + :attr:`.Guild.id`, and :attr:`.Guild.name` per :class:`.Guild`. + + .. note:: + + This method is an API call. For general usage, consider :attr:`guilds` instead. + + Parameters + ---------- + limit: Optional[:class:`int`] + The number of guilds to retrieve. + If ``None``, it retrieves every guild you have access to. Note, however, + that this would make it a slow operation. + Defaults to ``100``. + before: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] + Retrieves guilds before this date or object. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + after: Union[:class:`.abc.Snowflake`, :class:`datetime.datetime`] + Retrieve guilds after this date or object. + If a datetime is provided, it is recommended to use a UTC aware datetime. + If the datetime is naive, it is assumed to be local time. + with_counts: :class:`bool` + Whether to include member count information in guilds. This fills the + :attr:`.Guild.approximate_member_count` and :attr:`.Guild.approximate_presence_count` + fields. + Defaults to ``True``. + + Yields + ------ + :class:`.Guild` + The guild with the guild data parsed. + + Raises + ------ + :exc:`HTTPException` + Getting the guilds failed. + + Examples + -------- + + Usage :: + + async for guild in client.fetch_guilds(limit=150): + print(guild.name) + + Flattening into a list :: + + guilds = await client.fetch_guilds(limit=150).flatten() + # guilds is now a list of Guild... + + All parameters are optional. + """ + return GuildIterator( + self, limit=limit, before=before, after=after, with_counts=with_counts + ) + + async def fetch_template(self, code: Template | str) -> Template: + """|coro| + + Gets a :class:`.Template` from a discord.new URL or code. + + Parameters + ---------- + code: Union[:class:`.Template`, :class:`str`] + The Discord Template Code or URL (must be a discord.new URL). + + Returns + ------- + :class:`.Template` + The template from the URL/code. + + Raises + ------ + :exc:`NotFound` + The template is invalid. + :exc:`HTTPException` + Getting the template failed. + """ + code = utils.resolve_template(code) + data = await self.http.get_template(code) + return Template(data=data, state=self._connection) # type: ignore + + async def fetch_guild(self, guild_id: int, /, *, with_counts=True) -> Guild: + """|coro| + + Retrieves a :class:`.Guild` from an ID. + + .. note:: + + Using this, you will **not** receive :attr:`.Guild.channels`, :attr:`.Guild.members`, + :attr:`.Member.activity` and :attr:`.Member.voice` per :class:`.Member`. + + .. note:: + + This method is an API call. For general usage, consider :meth:`get_guild` instead. + + Parameters + ---------- + guild_id: :class:`int` + The guild's ID to fetch from. + + with_counts: :class:`bool` + Whether to include count information in the guild. This fills the + :attr:`.Guild.approximate_member_count` and :attr:`.Guild.approximate_presence_count` + fields. + + .. versionadded:: 2.0 + + Returns + ------- + :class:`.Guild` + The guild from the ID. + + Raises + ------ + :exc:`Forbidden` + You do not have access to the guild. + :exc:`HTTPException` + Getting the guild failed. + """ + data = await self.http.get_guild(guild_id, with_counts=with_counts) + return Guild(data=data, state=self._connection) + + async def fetch_stage_instance(self, channel_id: int, /) -> StageInstance: + """|coro| + + Gets a :class:`.StageInstance` for a stage channel id. + + .. versionadded:: 2.0 + + Parameters + ---------- + channel_id: :class:`int` + The stage channel ID. + + Returns + ------- + :class:`.StageInstance` + The stage instance from the stage channel ID. + + Raises + ------ + :exc:`NotFound` + The stage instance or channel could not be found. + :exc:`HTTPException` + Getting the stage instance failed. + """ + data = await self.http.get_stage_instance(channel_id) + guild = self.get_guild(int(data["guild_id"])) + return StageInstance(guild=guild, state=self._connection, data=data) # type: ignore + + # Invite management + + async def fetch_invite( + self, + url: Invite | str, + *, + with_counts: bool = True, + with_expiration: bool = True, + event_id: int | None = None, + ) -> Invite: + """|coro| + + Gets an :class:`.Invite` from a discord.gg URL or ID. + + .. note:: + + If the invite is for a guild you have not joined, the guild and channel + attributes of the returned :class:`.Invite` will be :class:`.PartialInviteGuild` and + :class:`.PartialInviteChannel` respectively. + + Parameters + ---------- + url: Union[:class:`.Invite`, :class:`str`] + The Discord invite ID or URL (must be a discord.gg URL). + with_counts: :class:`bool` + Whether to include count information in the invite. This fills the + :attr:`.Invite.approximate_member_count` and :attr:`.Invite.approximate_presence_count` + fields. + with_expiration: :class:`bool` + Whether to include the expiration date of the invite. This fills the + :attr:`.Invite.expires_at` field. + + .. versionadded:: 2.0 + event_id: Optional[:class:`int`] + The ID of the scheduled event to be associated with the event. + + See :meth:`Invite.set_scheduled_event` for more + info on event invite linking. + + .. versionadded:: 2.0 + + Returns + ------- + :class:`.Invite` + The invite from the URL/ID. + + Raises + ------ + :exc:`NotFound` + The invite has expired or is invalid. + :exc:`HTTPException` + Getting the invite failed. + """ + + invite_id = utils.resolve_invite(url) + data = await self.http.get_invite( + invite_id, + with_counts=with_counts, + with_expiration=with_expiration, + guild_scheduled_event_id=event_id, + ) + return Invite.from_incomplete(state=self._connection, data=data) + + async def delete_invite(self, invite: Invite | str) -> None: + """|coro| + + Revokes an :class:`.Invite`, URL, or ID to an invite. + + You must have the :attr:`~.Permissions.manage_channels` permission in + the associated guild to do this. + + Parameters + ---------- + invite: Union[:class:`.Invite`, :class:`str`] + The invite to revoke. + + Raises + ------ + :exc:`Forbidden` + You do not have permissions to revoke invites. + :exc:`NotFound` + The invite is invalid or expired. + :exc:`HTTPException` + Revoking the invite failed. + """ + + invite_id = utils.resolve_invite(invite) + await self.http.delete_invite(invite_id) + + # Miscellaneous stuff + + async def fetch_widget(self, guild_id: int, /) -> Widget: + """|coro| + + Gets a :class:`.Widget` from a guild ID. + + .. note:: + + The guild must have the widget enabled to get this information. + + Parameters + ---------- + guild_id: :class:`int` + The ID of the guild. + + Returns + ------- + :class:`.Widget` + The guild's widget. + + Raises + ------ + :exc:`Forbidden` + The widget for this guild is disabled. + :exc:`HTTPException` + Retrieving the widget failed. + """ + data = await self.http.get_widget(guild_id) + + return Widget(state=self._connection, data=data) + + async def application_info(self) -> AppInfo: + """|coro| + + Retrieves the bot's application information. + + Returns + ------- + :class:`.AppInfo` + The bot's application information. + + Raises + ------ + :exc:`HTTPException` + Retrieving the information failed somehow. + """ + data = await self.http.application_info() + return AppInfo(self._connection, data) + + async def fetch_user(self, user_id: int, /) -> User: + """|coro| + + Retrieves a :class:`~discord.User` based on their ID. + You do not have to share any guilds with the user to get this information, + however many operations do require that you do. + + .. note:: + + This method is an API call. If you have :attr:`discord.Intents.members` and member cache enabled, + consider :meth:`get_user` instead. + + Parameters + ---------- + user_id: :class:`int` + The user's ID to fetch from. + + Returns + ------- + :class:`~discord.User` + The user you requested. + + Raises + ------ + :exc:`NotFound` + A user with this ID does not exist. + :exc:`HTTPException` + Fetching the user failed. + """ + data = await self.http.get_user(user_id) + return User(state=self._connection, data=data) + + async def fetch_channel( + self, channel_id: int, / + ) -> GuildChannel | PrivateChannel | Thread: + """|coro| + + Retrieves a :class:`.abc.GuildChannel`, :class:`.abc.PrivateChannel`, or :class:`.Thread` with the specified ID. + + .. note:: + + This method is an API call. For general usage, consider :meth:`get_channel` instead. + + .. versionadded:: 1.2 + + Returns + ------- + Union[:class:`.abc.GuildChannel`, :class:`.abc.PrivateChannel`, :class:`.Thread`] + The channel from the ID. + + Raises + ------ + :exc:`InvalidData` + An unknown channel type was received from Discord. + :exc:`HTTPException` + Retrieving the channel failed. + :exc:`NotFound` + Invalid Channel ID. + :exc:`Forbidden` + You do not have permission to fetch this channel. + """ + data = await self.http.get_channel(channel_id) + + factory, ch_type = _threaded_channel_factory(data["type"]) + if factory is None: + raise InvalidData( + "Unknown channel type {type} for channel ID {id}.".format_map(data) + ) + + if ch_type in (ChannelType.group, ChannelType.private): + # the factory will be a DMChannel or GroupChannel here + return factory(me=self.user, data=data, state=self._connection) + # the factory can't be a DMChannel or GroupChannel here + guild_id = int(data["guild_id"]) # type: ignore + guild = self.get_guild(guild_id) or Object(id=guild_id) + # GuildChannels expect a Guild, we may be passing an Object + return factory(guild=guild, state=self._connection, data=data) + + async def fetch_webhook(self, webhook_id: int, /) -> Webhook: + """|coro| + + Retrieves a :class:`.Webhook` with the specified ID. + + Returns + ------- + :class:`.Webhook` + The webhook you requested. + + Raises + ------ + :exc:`HTTPException` + Retrieving the webhook failed. + :exc:`NotFound` + Invalid webhook ID. + :exc:`Forbidden` + You do not have permission to fetch this webhook. + """ + data = await self.http.get_webhook(webhook_id) + return Webhook.from_state(data, state=self._connection) + + async def fetch_sticker(self, sticker_id: int, /) -> StandardSticker | GuildSticker: + """|coro| + + Retrieves a :class:`.Sticker` with the specified ID. + + .. versionadded:: 2.0 + + Returns + ------- + Union[:class:`.StandardSticker`, :class:`.GuildSticker`] + The sticker you requested. + + Raises + ------ + :exc:`HTTPException` + Retrieving the sticker failed. + :exc:`NotFound` + Invalid sticker ID. + """ + data = await self.http.get_sticker(sticker_id) + cls, _ = _sticker_factory(data["type"]) # type: ignore + return cls(state=self._connection, data=data) # type: ignore + + async def fetch_premium_sticker_packs(self) -> list[StickerPack]: + """|coro| + + Retrieves all available premium sticker packs. + + .. versionadded:: 2.0 + + Returns + ------- + List[:class:`.StickerPack`] + All available premium sticker packs. + + Raises + ------ + :exc:`HTTPException` + Retrieving the sticker packs failed. + """ + data = await self.http.list_premium_sticker_packs() + return [ + StickerPack(state=self._connection, data=pack) + for pack in data["sticker_packs"] + ] + + async def create_dm(self, user: Snowflake) -> DMChannel: + """|coro| + + Creates a :class:`.DMChannel` with this user. + + This should be rarely called, as this is done transparently for most + people. + + .. versionadded:: 2.0 + + Parameters + ---------- + user: :class:`~discord.abc.Snowflake` + The user to create a DM with. + + Returns + ------- + :class:`.DMChannel` + The channel that was created. + """ + state = self._connection + found = state._get_private_channel_by_user(user.id) + if found: + return found + + data = await state.http.start_private_message(user.id) + return state.add_dm_channel(data) + + def add_view(self, view: BaseView, *, message_id: int | None = None) -> None: + """Registers a :class:`~discord.ui.BaseView` for persistent listening. + + This method should be used for when a view is comprised of components + that last longer than the lifecycle of the program. + + .. versionadded:: 2.0 + + Parameters + ---------- + view: :class:`discord.ui.BaseView` + The view to register for dispatching. + message_id: Optional[:class:`int`] + The message ID that the view is attached to. This is currently used to + refresh the view's state during message update events. If not given + then message update events are not propagated for the view. + + Raises + ------ + TypeError + A view was not passed. + ValueError + The view is not persistent. A persistent view has no timeout + and all their components have an explicitly provided ``custom_id``. + """ + + if not isinstance(view, BaseView): + raise TypeError(f"expected an instance of BaseView not {view.__class__!r}") + + if not view.is_persistent(): + raise ValueError( + "View is not persistent. Items need to have a custom_id set and View" + " must have no timeout" + ) + + self._connection.store_view(view, message_id) + + @property + def persistent_views(self) -> Sequence[BaseView]: + """A sequence of persistent views added to the client. + + .. versionadded:: 2.0 + """ + return self._connection.persistent_views + + async def fetch_role_connection_metadata_records( + self, + ) -> list[ApplicationRoleConnectionMetadata]: + """|coro| + + Fetches the bot's role connection metadata records. + + .. versionadded:: 2.4 + + Returns + ------- + List[:class:`.ApplicationRoleConnectionMetadata`] + The bot's role connection metadata records. + """ + data = await self._connection.http.get_application_role_connection_metadata_records( + self.application_id + ) + return [ApplicationRoleConnectionMetadata.from_dict(r) for r in data] + + async def update_role_connection_metadata_records( + self, *role_connection_metadata + ) -> list[ApplicationRoleConnectionMetadata]: + """|coro| + + Updates the bot's role connection metadata records. + + .. versionadded:: 2.4 + + Parameters + ---------- + *role_connection_metadata: :class:`ApplicationRoleConnectionMetadata` + The new metadata records to send to Discord. + + Returns + ------- + List[:class:`.ApplicationRoleConnectionMetadata`] + The updated role connection metadata records. + """ + payload = [r.to_dict() for r in role_connection_metadata] + data = await self._connection.http.update_application_role_connection_metadata_records( + self.application_id, payload + ) + return [ApplicationRoleConnectionMetadata.from_dict(r) for r in data] + + async def fetch_skus(self) -> list[SKU]: + """|coro| + + Fetches the bot's SKUs. + + .. versionadded:: 2.5 + + Returns + ------- + List[:class:`.SKU`] + The bot's SKUs. + """ + data = await self._connection.http.list_skus(self.application_id) + return [SKU(state=self._connection, data=s) for s in data] + + def entitlements( + self, + user: Snowflake | None = None, + skus: list[Snowflake] | None = None, + before: SnowflakeTime | None = None, + after: SnowflakeTime | None = None, + limit: int | None = 100, + guild: Snowflake | None = None, + exclude_ended: bool = False, + ) -> EntitlementIterator: + """Returns an :class:`.AsyncIterator` that enables fetching the application's entitlements. + + .. versionadded:: 2.6 + + Parameters + ---------- + user: :class:`.abc.Snowflake` | None + Limit the fetched entitlements to entitlements owned by this user. + skus: list[:class:`.abc.Snowflake`] | None + Limit the fetched entitlements to entitlements that are for these SKUs. + before: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None + Retrieves guilds before this date or object. + If a datetime is provided, it is recommended to use a UTC-aware datetime. + If the datetime is naive, it is assumed to be local time. + after: :class:`.abc.Snowflake` | :class:`datetime.datetime` | None + Retrieve guilds after this date or object. + If a datetime is provided, it is recommended to use a UTC-aware datetime. + If the datetime is naive, it is assumed to be local time. + limit: Optional[:class:`int`] + The number of entitlements to retrieve. + If ``None``, retrieves every entitlement, which may be slow. + Defaults to ``100``. + guild: :class:`.abc.Snowflake` | None + Limit the fetched entitlements to entitlements owned by this guild. + exclude_ended: :class:`bool` + Whether to limit the fetched entitlements to those that have not ended. + Defaults to ``False``. + + Yields + ------ + :class:`.Entitlement` + The application's entitlements. + + Raises + ------ + :exc:`HTTPException` + Retrieving the entitlements failed. + + Examples + -------- + + Usage :: + + async for entitlement in client.entitlements(): + print(entitlement.user_id) + + Flattening into a list :: + + entitlements = await user.entitlements().flatten() + + All parameters are optional. + """ + return EntitlementIterator( + self._connection, + user_id=user.id if user else None, + sku_ids=[sku.id for sku in skus] if skus else None, + before=before, + after=after, + limit=limit, + guild_id=guild.id if guild else None, + exclude_ended=exclude_ended, + ) + + @property + def store_url(self) -> str: + """:class:`str`: The URL that leads to the application's store page for monetization. + + .. versionadded:: 2.6 + """ + return f"https://discord.com/application-directory/{self.application_id}/store" + + async def fetch_emojis(self) -> list[AppEmoji]: + r"""|coro| + + Retrieves all custom :class:`AppEmoji`\s from the application. + + Raises + --------- + HTTPException + An error occurred fetching the emojis. + + Returns + -------- + List[:class:`AppEmoji`] + The retrieved emojis. + """ + data = await self._connection.http.get_all_application_emojis( + self.application_id + ) + return [ + self._connection.maybe_store_app_emoji(self.application_id, d) + for d in data["items"] + ] + + async def fetch_emoji(self, emoji_id: int, /) -> AppEmoji: + """|coro| + + Retrieves a custom :class:`AppEmoji` from the application. + + Parameters + ---------- + emoji_id: :class:`int` + The emoji's ID. + + Returns + ------- + :class:`AppEmoji` + The retrieved emoji. + + Raises + ------ + NotFound + The emoji requested could not be found. + HTTPException + An error occurred fetching the emoji. + """ + data = await self._connection.http.get_application_emoji( + self.application_id, emoji_id + ) + return self._connection.maybe_store_app_emoji(self.application_id, data) + + async def create_emoji( + self, + *, + name: str, + image: bytes, + ) -> AppEmoji: + r"""|coro| + + Creates a custom :class:`AppEmoji` for the application. + + There is currently a limit of 2000 emojis per application. + + Parameters + ----------- + name: :class:`str` + The emoji name. Must be at least 2 characters. + image: :class:`bytes` + The :term:`py:bytes-like object` representing the image data to use. + Only JPG, PNG and GIF images are supported. + + Raises + ------- + HTTPException + An error occurred creating an emoji. + + Returns + -------- + :class:`AppEmoji` + The created emoji. + """ + + img = utils._bytes_to_base64_data(image) + data = await self._connection.http.create_application_emoji( + self.application_id, name, img + ) + return self._connection.maybe_store_app_emoji(self.application_id, data) + + async def delete_emoji(self, emoji: Snowflake) -> None: + """|coro| + + Deletes the custom :class:`AppEmoji` from the application. + + Parameters + ---------- + emoji: :class:`abc.Snowflake` + The emoji you are deleting. + + Raises + ------ + HTTPException + An error occurred deleting the emoji. + """ + + await self._connection.http.delete_application_emoji( + self.application_id, emoji.id + ) + if self._connection.cache_app_emojis and self._connection.get_emoji(emoji.id): + self._connection.remove_emoji(emoji) + + def get_sound(self, sound_id: int) -> SoundboardSound | None: + """Gets a :class:`.Sound` from the bot's sound cache. + + .. versionadded:: 2.7 + + Parameters + ---------- + sound_id: :class:`int` + The ID of the sound to get. + + Returns + ------- + Optional[:class:`.SoundboardSound`] + The sound with the given ID. + """ + return self._connection._get_sound(sound_id) + + @property + def sounds(self) -> list[SoundboardSound]: + """A list of all the sounds the bot can see. + + .. versionadded:: 2.7 + """ + return self._connection.sounds + + async def fetch_default_sounds(self) -> list[SoundboardSound]: + """|coro| + + Fetches the bot's default sounds. + + .. versionadded:: 2.7 + + Returns + ------- + List[:class:`.SoundboardSound`] + The bot's default sounds. + """ + data = await self._connection.http.get_default_sounds() + return [ + SoundboardSound(http=self.http, state=self._connection, data=s) + for s in data + ] diff --git a/venv/lib/python3.11/site-packages/discord/cog.py b/venv/lib/python3.11/site-packages/discord/cog.py new file mode 100644 index 0000000..5fbe6e6 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/cog.py @@ -0,0 +1,1168 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import importlib +import inspect +import os +import pathlib +import sys +import types +from collections.abc import Generator, Mapping +from typing import ( + TYPE_CHECKING, + Any, + Callable, + ClassVar, + TypeVar, + overload, +) + +from typing_extensions import TypeGuard + +import discord.utils + +from . import errors +from .commands import ( + ApplicationCommand, + ApplicationContext, + SlashCommandGroup, + _BaseCommand, +) + +if TYPE_CHECKING: + from .ext.bridge import BridgeCommand + + +__all__ = ( + "CogMeta", + "Cog", + "CogMixin", +) + +CogT = TypeVar("CogT", bound="Cog") +FuncT = TypeVar("FuncT", bound=Callable[..., Any]) + +MISSING: Any = discord.utils.MISSING + + +def _is_submodule(parent: str, child: str) -> bool: + return parent == child or child.startswith(f"{parent}.") + + +def _is_bridge_command(command: Any) -> TypeGuard[BridgeCommand]: + return getattr(command, "__bridge__", False) + + +def _name_filter(c: Any) -> str: + return ( + "app" + if isinstance(c, ApplicationCommand) + else ("bridge" if not _is_bridge_command(c) else "ext") + ) + + +def _validate_name_prefix(base_class: type, name: str) -> None: + if name.startswith(("cog_", "bot_")): + raise TypeError( + f"Commands or listeners must not start with cog_ or bot_ (in method {base_class}.{name})" + ) + + +def _process_attributes( + base: type, +) -> tuple[dict[str, Any], dict[str, Any]]: # pyright: ignore[reportExplicitAny] + commands: dict[str, _BaseCommand | BridgeCommand] = {} + listeners: dict[str, Callable[..., Any]] = {} + + for attr_name, attr_value in base.__dict__.items(): + if attr_name in commands: + del commands[attr_name] + if attr_name in listeners: + del listeners[attr_name] + + if getattr(attr_value, "parent", None) and isinstance( + attr_value, ApplicationCommand + ): + # Skip application commands if they are a part of a group + # Since they are already added when the group is added + continue + + is_static_method = isinstance(attr_value, staticmethod) + if is_static_method: + attr_value = attr_value.__func__ + + if inspect.iscoroutinefunction(attr_value) and getattr( + attr_value, "__cog_listener__", False + ): + _validate_name_prefix(base, attr_name) + listeners[attr_name] = attr_value + continue + + if isinstance(attr_value, _BaseCommand) or _is_bridge_command(attr_value): + if is_static_method: + raise TypeError( + f"Command in method {base}.{attr_name!r} must not be staticmethod." + ) + _validate_name_prefix(base, attr_name) + + if isinstance(attr_value, _BaseCommand): + commands[attr_name] = attr_value + + if _is_bridge_command(attr_value) and not attr_value.parent: + commands[f"ext_{attr_name}"] = attr_value.ext_variant + commands[f"app_{attr_name}"] = attr_value.slash_variant + commands[attr_name] = attr_value + for cmd in getattr(attr_value, "subcommands", []): + commands[f"ext_{cmd.ext_variant.qualified_name}"] = cmd.ext_variant + + return commands, listeners + + +def _update_command( + command: _BaseCommand | BridgeCommand, + guild_ids: list[int], + lookup_table: dict[str, _BaseCommand | BridgeCommand], + new_cls: type[Cog], +) -> None: + if isinstance(command, ApplicationCommand) and not command.guild_ids and guild_ids: + command.guild_ids = guild_ids + + if not isinstance(command, SlashCommandGroup) and not _is_bridge_command(command): + # ignore bridge commands + cmd: BridgeCommand | _BaseCommand | None = getattr( + new_cls, + command.callback.__name__, + None, # pyright: ignore[reportUnknownMemberType,reportUnknownArgumentType,reportAttributeAccessIssue] + ) + if _is_bridge_command(cmd): + setattr( + cmd, + f"{_name_filter(command).replace('app', 'slash')}_variant", + command, + ) + else: + setattr( + new_cls, + command.callback.__name__, + command, # pyright: ignore [reportAttributeAccessIssue, reportUnknownArgumentType, reportUnknownMemberType] + ) + + parent: ( + BridgeCommand | _BaseCommand | None + ) = ( # pyright: ignore [reportUnknownMemberType, reportUnknownVariableType] + command.parent # pyright: ignore [reportAttributeAccessIssue] + ) + if parent is not None: + # Get the latest parent reference + parent = lookup_table[f"{_name_filter(command)}_{parent.qualified_name}"] # type: ignore # pyright: ignore[reportUnknownMemberType] + + # Update the parent's reference to our self + parent.remove_command(command.name) # type: ignore # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType] + parent.add_command(command) # type: ignore # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType] + + +class CogMeta(type): + """A metaclass for defining a cog. + + Note that you should probably not use this directly. It is exposed + purely for documentation purposes along with making custom metaclasses to intermix + with other metaclasses such as the :class:`abc.ABCMeta` metaclass. + + For example, to create an abstract cog mixin class, the following would be done. + + .. code-block:: python3 + + import abc + + class CogABCMeta(discord.CogMeta, abc.ABCMeta): + pass + + class SomeMixin(metaclass=abc.ABCMeta): + pass + + class SomeCogMixin(SomeMixin, discord.Cog, metaclass=CogABCMeta): + pass + + .. note:: + + When passing an attribute of a metaclass that is documented below, note + that you must pass it as a keyword-only argument to the class creation + like the following example: + + .. code-block:: python3 + + class MyCog(discord.Cog, name='My Cog'): + pass + + Attributes + ---------- + name: :class:`str` + The cog name. By default, it is the name of the class with no modification. + description: :class:`str` + The cog description. By default, it is the cleaned docstring of the class. + + .. versionadded:: 1.6 + + command_attrs: :class:`dict` + A list of attributes to apply to every command inside this cog. The dictionary + is passed into the :class:`Command` options at ``__init__``. + If you specify attributes inside the command attribute in the class, it will + override the one specified inside this attribute. For example: + + .. code-block:: python3 + + class MyCog(discord.Cog, command_attrs=dict(hidden=True)): + @discord.slash_command() + async def foo(self, ctx): + pass # hidden -> True + + @discord.slash_command(hidden=False) + async def bar(self, ctx): + pass # hidden -> False + + guild_ids: Optional[List[:class:`int`]] + A shortcut to :attr:`.command_attrs`, what ``guild_ids`` should all application commands have + in the cog. You can override this by setting ``guild_ids`` per command. + + .. versionadded:: 2.0 + """ + + __cog_name__: str + __cog_settings__: dict[str, Any] + __cog_commands__: list[_BaseCommand | BridgeCommand] + __cog_listeners__: list[tuple[str, str]] + __cog_guild_ids__: list[int] + + def __new__(cls: type[CogMeta], *args: Any, **kwargs: Any) -> CogMeta: + name, bases, attrs = args + attrs["__cog_name__"] = kwargs.pop("name", name) + attrs["__cog_settings__"] = kwargs.pop("command_attrs", {}) + attrs["__cog_guild_ids__"] = kwargs.pop("guild_ids", []) + + description = kwargs.pop("description", None) + if description is None: + description = inspect.cleandoc(attrs.get("__doc__", "")) + attrs["__cog_description__"] = description + + commands: dict[str, _BaseCommand | BridgeCommand] = {} + listeners: dict[str, Callable[..., Any]] = {} + + new_cls = super().__new__(cls, name, bases, attrs, **kwargs) + + for base in reversed(new_cls.__mro__): + new_commands, new_listeners = _process_attributes(base) + commands.update(new_commands) + listeners.update(new_listeners) + + new_cls.__cog_commands__ = list(commands.values()) + + new_cls.__cog_listeners__ = [ + (listener_name, listener.__name__) + for listener in listeners.values() + for listener_name in listener.__cog_listener_names__ + ] + + cmd_attrs = new_cls.__cog_settings__ + + # Either update the command with the cog provided defaults or copy it. + # r.e type ignore, type-checker complains about overriding a ClassVar + new_cls.__cog_commands__ = list(tuple(c._update_copy(cmd_attrs) if not _is_bridge_command(c) else c for c in new_cls.__cog_commands__)) # type: ignore + + lookup = { + f"{_name_filter(cmd)}_{cmd.qualified_name}": cmd + for cmd in new_cls.__cog_commands__ + } + + # Update the Command instances dynamically as well + for command in new_cls.__cog_commands__: + _update_command(command, new_cls.__cog_guild_ids__, lookup, new_cls) + + return new_cls + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args) + + @classmethod + def qualified_name(cls) -> str: + return cls.__cog_name__ + + +def _cog_special_method(func: FuncT) -> FuncT: + func.__cog_special_method__ = None + return func + + +class Cog(metaclass=CogMeta): + """The base class that all cogs must inherit from. + + A cog is a collection of commands, listeners, and optional state to + help group commands together. More information on them can be found on + the :ref:`ext_commands_cogs` page. + + When inheriting from this class, the options shown in :class:`CogMeta` + are equally valid here. + """ + + __cog_name__: ClassVar[str] + __cog_settings__: ClassVar[dict[str, Any]] + __cog_commands__: ClassVar[list[ApplicationCommand]] + __cog_listeners__: ClassVar[list[tuple[str, str]]] + __cog_guild_ids__: ClassVar[list[int]] + + def __new__(cls: type[CogT], *args: Any, **kwargs: Any) -> CogT: + # For issue 426, we need to store a copy of the command objects + # since we modify them to inject `self` to them. + # To do this, we need to interfere with the Cog creation process. + return super().__new__(cls) + + def get_commands(self) -> list[ApplicationCommand]: + r""" + Returns + -------- + List[:class:`.ApplicationCommand`] + A :class:`list` of :class:`.ApplicationCommand`\s that are + defined inside this cog. + + .. note:: + + This does not include subcommands. + """ + return [ + c + for c in self.__cog_commands__ + if isinstance(c, ApplicationCommand) and c.parent is None + ] + + @property + def qualified_name(self) -> str: + """Returns the cog's specified name, not the class name.""" + return self.__cog_name__ + + @property + def description(self) -> str: + """Returns the cog's description, typically the cleaned docstring.""" + return self.__cog_description__ + + @description.setter + def description(self, description: str) -> None: + self.__cog_description__ = description + + def walk_commands(self) -> Generator[ApplicationCommand]: + """An iterator that recursively walks through this cog's commands and subcommands. + + Yields + ------ + Union[:class:`.Command`, :class:`.Group`] + A command or group from the cog. + """ + for command in self.__cog_commands__: + if isinstance(command, SlashCommandGroup): + yield from command.walk_commands() + + def get_listeners(self) -> list[tuple[str, Callable[..., Any]]]: + """Returns a :class:`list` of (name, function) listener pairs that are defined in this cog. + + Returns + ------- + List[Tuple[:class:`str`, :ref:`coroutine `]] + The listeners defined in this cog. + """ + return [ + (name, getattr(self, method_name)) + for name, method_name in self.__cog_listeners__ + ] + + @classmethod + def _get_overridden_method(cls, method: FuncT) -> FuncT | None: + """Return None if the method is not overridden. Otherwise, returns the overridden method.""" + return getattr( + getattr(method, "__func__", method), "__cog_special_method__", method + ) + + @classmethod + def listener( + cls, name: str = MISSING, once: bool = False + ) -> Callable[[FuncT], FuncT]: + """A decorator that marks a function as a listener. + + This is the cog equivalent of :meth:`.Bot.listen`. + + Parameters + ---------- + name: :class:`str` + The name of the event being listened to. If not provided, it + defaults to the function's name. + once: :class:`bool` + If this listener should only be called once after each cog load. + Defaults to false. + + Raises + ------ + TypeError + The function is not a coroutine function or a string was not passed as + the name. + """ + + if name is not MISSING and not isinstance(name, str): + raise TypeError( + "Cog.listener expected str but received" + f" {name.__class__.__name__!r} instead." + ) + + def decorator(func: FuncT) -> FuncT: + actual = func + if isinstance(actual, staticmethod): + actual = actual.__func__ + if not inspect.iscoroutinefunction(actual): + raise TypeError("Listener function must be a coroutine function.") + actual.__cog_listener__ = True + to_assign = name or actual.__name__ + actual._once = once + try: + actual.__cog_listener_names__.append(to_assign) + except AttributeError: + actual.__cog_listener_names__ = [to_assign] + # we have to return `func` instead of `actual` because + # we need the type to be `staticmethod` for the metaclass + # to pick it up but the metaclass unfurls the function and + # thus the assignments need to be on the actual function + return func + + return decorator + + def has_error_handler(self) -> bool: + """Checks whether the cog has an error handler. + + .. versionadded:: 1.7 + """ + return not hasattr(self.cog_command_error.__func__, "__cog_special_method__") + + @_cog_special_method + def cog_unload(self) -> None: + """A special method that is called when the cog gets removed. + + This function **cannot** be a coroutine. It must be a regular + function. + + Subclasses must replace this if they want special unloading behaviour. + """ + + @_cog_special_method + def bot_check_once(self, ctx: ApplicationContext) -> bool: + """A special method that registers as a :meth:`.Bot.check_once` + check. + + This function **can** be a coroutine and must take a sole parameter, + ``ctx``, to represent the :class:`.Context` or :class:`.ApplicationContext`. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context. + """ + return True + + @_cog_special_method + def bot_check(self, ctx: ApplicationContext) -> bool: + """A special method that registers as a :meth:`.Bot.check` + check. + + This function **can** be a coroutine and must take a sole parameter, + ``ctx``, to represent the :class:`.Context` or :class:`.ApplicationContext`. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context. + """ + return True + + @_cog_special_method + def cog_check(self, ctx: ApplicationContext) -> bool: + """A special method that registers as a :func:`~discord.ext.commands.check` + for every command and subcommand in this cog. + + This function **can** be a coroutine and must take a sole parameter, + ``ctx``, to represent the :class:`.Context` or :class:`.ApplicationContext`. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context. + """ + return True + + @_cog_special_method + async def cog_command_error( + self, ctx: ApplicationContext, error: Exception + ) -> None: + """A special method that is called whenever an error + is dispatched inside this cog. + + This is similar to :func:`.on_command_error` except only applying + to the commands inside this cog. + + This **must** be a coroutine. + + Parameters + ---------- + ctx: :class:`.ApplicationContext` + The invocation context where the error happened. + error: :class:`ApplicationCommandError` + The error that happened. + """ + + @_cog_special_method + async def cog_before_invoke(self, ctx: ApplicationContext) -> None: + """A special method that acts as a cog local pre-invoke hook. + + This is similar to :meth:`.ApplicationCommand.before_invoke`. + + This **must** be a coroutine. + + Parameters + ---------- + ctx: :class:`.ApplicationContext` + The invocation context. + """ + + @_cog_special_method + async def cog_after_invoke(self, ctx: ApplicationContext) -> None: + """A special method that acts as a cog local post-invoke hook. + + This is similar to :meth:`.ApplicationCommand.after_invoke`. + + This **must** be a coroutine. + + Parameters + ---------- + ctx: :class:`.ApplicationContext` + The invocation context. + """ + + def _inject(self: CogT, bot) -> CogT: + cls = self.__class__ + + # realistically, the only thing that can cause loading errors + # is essentially just the command loading, which raises if there are + # duplicates. When this condition is met, we want to undo all what + # we've added so far for some form of atomic loading. + + for index, command in enumerate(self.__cog_commands__): + if _is_bridge_command(command): + bot.bridge_commands.append(command) + continue + + command._set_cog(self) + + if isinstance(command, ApplicationCommand): + if isinstance(command, discord.SlashCommandGroup): + for x in command.subcommands: + if isinstance(x, discord.SlashCommandGroup): + for y in x.subcommands: + y.parent = x + x.parent = command + bot.add_application_command(command) + + elif command.parent is None: + try: + bot.add_command(command) + except Exception as e: + # undo our additions + for to_undo in self.__cog_commands__[:index]: + if to_undo.parent is None: + bot.remove_command(to_undo.name) + raise e + # check if we're overriding the default + if cls.bot_check is not Cog.bot_check: + bot.add_check(self.bot_check) + + if cls.bot_check_once is not Cog.bot_check_once: + bot.add_check(self.bot_check_once, call_once=True) + + # while Bot.add_listener can raise if it's not a coroutine, + # this precondition is already met by the listener decorator + # already, thus this should never raise. + # Outside of, memory errors and the like... + for name, method_name in self.__cog_listeners__: + bot.add_listener(getattr(self, method_name), name) + + return self + + def _eject(self, bot) -> None: + cls = self.__class__ + + try: + for command in self.__cog_commands__: + if _is_bridge_command(command): + bot.bridge_commands.remove(command) + continue + elif isinstance(command, ApplicationCommand): + bot.remove_application_command(command) + elif command.parent is None: + bot.remove_command(command.name) + + for _, method_name in self.__cog_listeners__: + bot.remove_listener(getattr(self, method_name)) + + if cls.bot_check is not Cog.bot_check: + bot.remove_check(self.bot_check) + + if cls.bot_check_once is not Cog.bot_check_once: + bot.remove_check(self.bot_check_once, call_once=True) + finally: + try: + self.cog_unload() + except Exception: + pass + + +class CogMixin: + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.__cogs: dict[str, Cog] = {} + self.__extensions: dict[str, types.ModuleType] = {} + + def add_cog(self, cog: Cog, *, override: bool = False) -> None: + """Adds a "cog" to the bot. + + A cog is a class that has its own event listeners and commands. + + .. versionchanged:: 2.0 + + :exc:`.ClientException` is raised when a cog with the same name + is already loaded. + + Parameters + ---------- + cog: :class:`.Cog` + The cog to register to the bot. + override: :class:`bool` + If a previously loaded cog with the same name should be ejected + instead of raising an error. + + .. versionadded:: 2.0 + + Raises + ------ + TypeError + The cog does not inherit from :class:`.Cog`. + ApplicationCommandError + An error happened during loading. + ClientException + A cog with the same name is already loaded. + """ + + if not isinstance(cog, Cog): + raise TypeError("cogs must derive from Cog") + + cog_name = cog.__cog_name__ + existing = self.__cogs.get(cog_name) + + if existing is not None: + if not override: + raise discord.ClientException(f"Cog named {cog_name!r} already loaded") + self.remove_cog(cog_name) + + cog = cog._inject(self) + self.__cogs[cog_name] = cog + + def get_cog(self, name: str) -> Cog | None: + """Gets the cog instance requested. + + If the cog is not found, ``None`` is returned instead. + + Parameters + ---------- + name: :class:`str` + The name of the cog you are requesting. + This is equivalent to the name passed via keyword + argument in class creation or the class name if unspecified. + + Returns + ------- + Optional[:class:`Cog`] + The cog that was requested. If not found, returns ``None``. + """ + return self.__cogs.get(name) + + def remove_cog(self, name: str) -> Cog | None: + """Removes a cog from the bot and returns it. + + All registered commands and event listeners that the + cog has registered will be removed as well. + + If no cog is found then this method has no effect. + + Parameters + ---------- + name: :class:`str` + The name of the cog to remove. + + Returns + ------- + Optional[:class:`.Cog`] + The cog that was removed. ``None`` if not found. + """ + + cog = self.__cogs.pop(name, None) + if cog is None: + return + + if hasattr(self, "_help_command"): + help_command = self._help_command + if help_command and help_command.cog is cog: + help_command.cog = None + + cog._eject(self) + + return cog + + @property + def cogs(self) -> Mapping[str, Cog]: + """A read-only mapping of cog name to cog.""" + return types.MappingProxyType(self.__cogs) + + # extensions + + def _remove_module_references(self, name: str) -> None: + # find all references to the module + # remove the cogs registered from the module + for cog_name, cog in self.__cogs.copy().items(): + if _is_submodule(name, cog.__module__): + self.remove_cog(cog_name) + + # remove all the commands from the module + if self._supports_prefixed_commands: + for cmd in self.prefixed_commands.copy().values(): + if cmd.module is not None and _is_submodule(name, cmd.module): + # if isinstance(cmd, GroupMixin): + # cmd.recursively_remove_all_commands() + self.remove_command(cmd.name) + for cmd in self._application_commands.copy().values(): + if cmd.module is not None and _is_submodule(name, cmd.module): + # if isinstance(cmd, GroupMixin): + # cmd.recursively_remove_all_commands() + self.remove_application_command(cmd) + + # remove all the listeners from the module + for event_list in self._event_handlers.copy().values(): + remove = [ + index + for index, event in enumerate(event_list) + if event.__module__ is not None + and _is_submodule(name, event.__module__) + ] + + for index in reversed(remove): + del event_list[index] + + def _call_module_finalizers(self, lib: types.ModuleType, key: str) -> None: + try: + func = getattr(lib, "teardown") + except AttributeError: + pass + else: + try: + func(self) + except Exception: + pass + finally: + self.__extensions.pop(key, None) + sys.modules.pop(key, None) + name = lib.__name__ + for module in list(sys.modules.keys()): + if _is_submodule(name, module): + del sys.modules[module] + + def _load_from_module_spec( + self, spec: importlib.machinery.ModuleSpec, key: str + ) -> None: + # precondition: key not in self.__extensions + lib = importlib.util.module_from_spec(spec) + sys.modules[key] = lib + try: + spec.loader.exec_module(lib) # type: ignore + except Exception as e: + del sys.modules[key] + raise errors.ExtensionFailed(key, e) from e + + try: + setup = getattr(lib, "setup") + except AttributeError: + del sys.modules[key] + raise errors.NoEntryPointError(key) + + try: + setup(self) + except Exception as e: + del sys.modules[key] + self._remove_module_references(lib.__name__) + self._call_module_finalizers(lib, key) + raise errors.ExtensionFailed(key, e) from e + else: + self.__extensions[key] = lib + + def _resolve_name(self, name: str, package: str | None) -> str: + try: + return importlib.util.resolve_name(name, package) + except ImportError: + raise errors.ExtensionNotFound(name) + + @overload + def load_extension( + self, + name: str, + *, + package: str | None = None, + recursive: bool = False, + ) -> list[str]: ... + + @overload + def load_extension( + self, + name: str, + *, + package: str | None = None, + recursive: bool = False, + store: bool = False, + ) -> dict[str, Exception | bool] | list[str] | None: ... + + def load_extension( + self, name, *, package=None, recursive=False, store=False + ) -> dict[str, Exception | bool] | list[str] | None: + """Loads an extension. + + An extension is a python module that contains commands, cogs, or + listeners. + + An extension must have a global function, ``setup`` defined as + the entry point on what to do when the extension is loaded. This entry + point must have a single argument, the ``bot``. + + The extension passed can either be the direct name of a file within + the current working directory or a folder that contains multiple extensions. + + Parameters + ---------- + name: :class:`str` + The extension or folder name to load. It must be dot separated + like regular Python imports if accessing a submodule. e.g. + ``foo.test`` if you want to import ``foo/test.py``. + package: Optional[:class:`str`] + The package name to resolve relative imports with. + This is required when loading an extension using a relative + path, e.g ``.foo.test``. + Defaults to ``None``. + + .. versionadded:: 1.7 + recursive: Optional[:class:`bool`] + If subdirectories under the given head directory should be + recursively loaded. + Defaults to ``False``. + + .. versionadded:: 2.0 + store: Optional[:class:`bool`] + If exceptions should be stored or raised. If set to ``True``, + all exceptions encountered will be stored in a returned dictionary + as a load status. If set to ``False``, if any exceptions are + encountered they will be raised and the bot will be closed. + If no exceptions are encountered, a list of loaded + extension names will be returned. + Defaults to ``False``. + + .. versionadded:: 2.0 + + Returns + ------- + Optional[Union[Dict[:class:`str`, Union[:exc:`errors.ExtensionError`, :class:`bool`]], List[:class:`str`]]] + If the store parameter is set to ``True``, a dictionary will be returned that + contains keys to represent the loaded extension names. The values bound to + each key can either be an exception that occurred when loading that extension + or a ``True`` boolean representing a successful load. If the store parameter + is set to ``False``, either a list containing a list of loaded extensions or + nothing due to an encountered exception. + + Raises + ------ + ExtensionNotFound + The extension could not be imported. + This is also raised if the name of the extension could not + be resolved using the provided ``package`` parameter. + ExtensionAlreadyLoaded + The extension is already loaded. + NoEntryPointError + The extension does not have a setup function. + ExtensionFailed + The extension or its setup function had an execution error. + """ + + name = self._resolve_name(name, package) + + if name in self.__extensions: + exc = errors.ExtensionAlreadyLoaded(name) + final_out = {name: exc} if store else exc + # This indicates that there is neither an extension nor folder here + elif (spec := importlib.util.find_spec(name)) is None: + exc = errors.ExtensionNotFound(name) + final_out = {name: exc} if store else exc + # This indicates we've found an extension file to load, and we need to store any exceptions + elif spec.has_location and store: + try: + self._load_from_module_spec(spec, name) + except Exception as exc: + final_out = {name: exc} + else: + final_out = {name: True} + # This indicates we've found an extension file to load, and any encountered exceptions can be raised + elif spec.has_location: + self._load_from_module_spec(spec, name) + final_out = [name] + # This indicates we've been given a folder because the ModuleSpec exists but is not a file + else: + # Split the directory path and join it to get an os-native Path object + path = pathlib.Path(os.path.join(*name.split("."))) + glob = path.rglob if recursive else path.glob + final_out = {} if store else [] + + # Glob all files with a pattern to gather all .py files that don't start with _ + for ext_file in glob("[!_]*.py"): + # Gets all parts leading to the directory minus the file name + parts = list(ext_file.parts[:-1]) + # Gets the file name without the extension + parts.append(ext_file.stem) + loaded = self.load_extension( + ".".join(parts), package=package, recursive=recursive, store=store + ) + final_out.update(loaded) if store else final_out.extend(loaded) + + if isinstance(final_out, Exception): + raise final_out + else: + return final_out + + @overload + def load_extensions( + self, + *names: str, + package: str | None = None, + recursive: bool = False, + ) -> list[str]: ... + + @overload + def load_extensions( + self, + *names: str, + package: str | None = None, + recursive: bool = False, + store: bool = False, + ) -> dict[str, Exception | bool] | list[str] | None: ... + + def load_extensions( + self, *names, package=None, recursive=False, store=False + ) -> dict[str, Exception | bool] | list[str] | None: + """Loads multiple extensions at once. + + This method simplifies the process of loading multiple + extensions by handling the looping of ``load_extension``. + + Parameters + ---------- + names: :class:`str` + The extension or folder names to load. It must be dot separated + like regular Python imports if accessing a submodule. e.g. + ``foo.test`` if you want to import ``foo/test.py``. + package: Optional[:class:`str`] + The package name to resolve relative imports with. + This is required when loading an extension using a relative + path, e.g ``.foo.test``. + Defaults to ``None``. + + .. versionadded:: 1.7 + recursive: Optional[:class:`bool`] + If subdirectories under the given head directory should be + recursively loaded. + Defaults to ``False``. + + .. versionadded:: 2.0 + store: Optional[:class:`bool`] + If exceptions should be stored or raised. If set to ``True``, + all exceptions encountered will be stored in a returned dictionary + as a load status. If set to ``False``, if any exceptions are + encountered they will be raised and the bot will be closed. + If no exceptions are encountered, a list of loaded + extension names will be returned. + Defaults to ``False``. + + .. versionadded:: 2.0 + + Returns + ------- + Optional[Union[Dict[:class:`str`, Union[:exc:`errors.ExtensionError`, :class:`bool`]], List[:class:`str`]]] + If the store parameter is set to ``True``, a dictionary will be returned that + contains keys to represent the loaded extension names. The values bound to + each key can either be an exception that occurred when loading that extension + or a ``True`` boolean representing a successful load. If the store parameter + is set to ``False``, either a list containing names of loaded extensions or + nothing due to an encountered exception. + + Raises + ------ + ExtensionNotFound + A given extension could not be imported. + This is also raised if the name of the extension could not + be resolved using the provided ``package`` parameter. + ExtensionAlreadyLoaded + A given extension is already loaded. + NoEntryPointError + A given extension does not have a setup function. + ExtensionFailed + A given extension or its setup function had an execution error. + """ + + loaded_extensions = {} if store else [] + + for ext_path in names: + loaded = self.load_extension( + ext_path, package=package, recursive=recursive, store=store + ) + ( + loaded_extensions.update(loaded) + if store + else loaded_extensions.extend(loaded) + ) + + return loaded_extensions + + def unload_extension(self, name: str, *, package: str | None = None) -> None: + """Unloads an extension. + + When the extension is unloaded, all commands, listeners, and cogs are + removed from the bot and the module is un-imported. + + The extension can provide an optional global function, ``teardown``, + to do miscellaneous clean-up if necessary. This function takes a single + parameter, the ``bot``, similar to ``setup`` from + :meth:`~.Bot.load_extension`. + + Parameters + ---------- + name: :class:`str` + The extension name to unload. It must be dot separated like + regular Python imports if accessing a submodule. e.g. + ``foo.test`` if you want to import ``foo/test.py``. + package: Optional[:class:`str`] + The package name to resolve relative imports with. + This is required when unloading an extension using a relative path, e.g ``.foo.test``. + Defaults to ``None``. + + .. versionadded:: 1.7 + + Raises + ------ + ExtensionNotFound + The name of the extension could not + be resolved using the provided ``package`` parameter. + ExtensionNotLoaded + The extension was not loaded. + """ + + name = self._resolve_name(name, package) + lib = self.__extensions.get(name) + if lib is None: + raise errors.ExtensionNotLoaded(name) + + self._remove_module_references(lib.__name__) + self._call_module_finalizers(lib, name) + + def reload_extension(self, name: str, *, package: str | None = None) -> None: + """Atomically reloads an extension. + + This replaces the extension with the same extension, only refreshed. This is + equivalent to a :meth:`unload_extension` followed by a :meth:`load_extension` + except done in an atomic way. That is, if an operation fails mid-reload then + the bot will roll back to the prior working state. + + Parameters + ---------- + name: :class:`str` + The extension name to reload. It must be dot separated like + regular Python imports if accessing a submodule. e.g. + ``foo.test`` if you want to import ``foo/test.py``. + package: Optional[:class:`str`] + The package name to resolve relative imports with. + This is required when reloading an extension using a relative path, e.g ``.foo.test``. + Defaults to ``None``. + + .. versionadded:: 1.7 + + Raises + ------ + ExtensionNotLoaded + The extension was not loaded. + ExtensionNotFound + The extension could not be imported. + This is also raised if the name of the extension could not + be resolved using the provided ``package`` parameter. + NoEntryPointError + The extension does not have a setup function. + ExtensionFailed + The extension setup function had an execution error. + """ + + name = self._resolve_name(name, package) + lib = self.__extensions.get(name) + if lib is None: + raise errors.ExtensionNotLoaded(name) + + # get the previous module states from sys modules + modules = { + name: module + for name, module in sys.modules.items() + if _is_submodule(lib.__name__, name) + } + + try: + # Unload and then load the module... + self._remove_module_references(lib.__name__) + self._call_module_finalizers(lib, name) + self.load_extension(name) + except Exception: + # if the load failed, the remnants should have been + # cleaned from the load_extension function call + # so let's load it from our old compiled library. + lib.setup(self) # type: ignore + self.__extensions[name] = lib + + # revert sys.modules back to normal and raise back to caller + sys.modules.update(modules) + raise + + @property + def extensions(self) -> Mapping[str, types.ModuleType]: + """A read-only mapping of extension name to extension.""" + return types.MappingProxyType(self.__extensions) diff --git a/venv/lib/python3.11/site-packages/discord/collectibles.py b/venv/lib/python3.11/site-packages/discord/collectibles.py new file mode 100644 index 0000000..6c18d1a --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/collectibles.py @@ -0,0 +1,136 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +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. +""" + +from functools import cached_property +from typing import TYPE_CHECKING + +from .asset import Asset +from .types.collectibles import Collectibles as CollectiblesPayload +from .types.collectibles import Nameplate as NameplatePayload + +if TYPE_CHECKING: + from .state import ConnectionState + +__all__ = ( + "Collectibles", + "Nameplate", +) + + +class Collectibles: + """ + Represents a user or member's equipped collectibles. + + .. versionadded:: 2.8 + + .. container:: operations + + .. describe:: x == y + + Checks if two sets of collectibles are equal. + + .. describe:: x != y + + Checks if two sets of collectibles are not equal. + + Attributes + ---------- + nameplate: :class:`Nameplate` + The user's nameplate. + """ + + def __init__(self, data: CollectiblesPayload, state: "ConnectionState") -> None: + if nameplate_data := data.get("nameplate"): + self.nameplate = Nameplate(data=nameplate_data, state=state) + else: + self.nameplate = None + self._state = state + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + return isinstance(other, Collectibles) and self.nameplate == other.nameplate + + +class Nameplate: + """ + Represents a Discord Nameplate. + + .. versionadded:: 2.7 + .. versionchanged:: 2.8 + Nameplates are now comparable. + + .. container:: operations + + .. describe:: x == y + + Checks if two nameplates are equal. + + .. describe:: x != y + + Checks if two nameplates are not equal. + + Attributes + ---------- + sku_id: :class:`int` + The SKU ID of the nameplate. + palette: :class:`str` + The color palette of the nameplate. + """ + + def __init__(self, data: NameplatePayload, state: "ConnectionState") -> None: + self.sku_id: int = data["sku_id"] + self.palette: str = data["palette"] + self._label: str = data["label"] + self._asset: str = data["asset"] + self._state: "ConnectionState" = state + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: object) -> bool: + return ( + isinstance(other, Nameplate) + and self.sku_id == other.sku_id + and self.palette == other.palette + ) + + @cached_property + def static_asset(self) -> Asset: + """ + The static :class:`Asset` of this nameplate. + + .. versionadded:: 2.7 + """ + return Asset._from_collectible(self._state, self._asset) + + @cached_property + def animated_asset(self) -> Asset: + """ + The animated :class:`Asset` of this nameplate. + + .. versionadded:: 2.7 + """ + return Asset._from_collectible(self._state, self._asset, animated=True) diff --git a/venv/lib/python3.11/site-packages/discord/colour.py b/venv/lib/python3.11/site-packages/discord/colour.py new file mode 100644 index 0000000..420ab90 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/colour.py @@ -0,0 +1,411 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import colorsys +import random +from typing import Any + +from typing_extensions import Self, deprecated, override + +__all__ = ( + "Colour", + "Color", +) + + +class Colour: + """Represents a Colour. This class is similar + to a (red, green, blue) :class:`tuple`. + + There is an alias for this called Color. + + .. container:: operations + + .. describe:: x == y + + Checks if two colours are equal. + + .. describe:: x != y + + Checks if two colours are not equal. + + .. describe:: hash(x) + + Return the colour's hash. + + .. describe:: str(x) + + Returns the hex format for the colour. + + .. describe:: int(x) + + Returns the raw colour value. + + Attributes + ---------- + value: :class:`int` + The raw integer colour value. + """ + + __slots__ = ("value",) + + def __init__(self, value: int): + if not isinstance(value, int): + raise TypeError( + f"Expected int parameter, received {value.__class__.__name__} instead." + ) + + self.value: int = value + + def _get_byte(self, byte: int) -> int: + return (self.value >> (8 * byte)) & 0xFF + + @override + def __eq__(self, other: Any) -> bool: + return isinstance(other, Colour) and self.value == other.value + + @override + def __str__(self) -> str: + return f"#{self.value:0>6x}" + + def __int__(self) -> int: + return self.value + + @override + def __repr__(self) -> str: + return f"" + + @override + def __hash__(self) -> int: + return hash(self.value) + + @property + def r(self) -> int: + """Returns the red component of the colour.""" + return self._get_byte(2) + + @property + def g(self) -> int: + """Returns the green component of the colour.""" + return self._get_byte(1) + + @property + def b(self) -> int: + """Returns the blue component of the colour.""" + return self._get_byte(0) + + def to_rgb(self) -> tuple[int, int, int]: + """Returns an (r, g, b) tuple representing the colour.""" + return self.r, self.g, self.b + + @classmethod + def resolve_value(cls, value: int | Colour | None) -> Self: + if value is None or isinstance(value, Colour): + return value + elif isinstance(value, int): + return cls(value=value) + else: + raise TypeError( + "Expected discord.Colour, int, or None but received" + f" {value.__class__.__name__} instead." + ) + + @classmethod + def from_rgb(cls, r: int, g: int, b: int) -> Self: + """Constructs a :class:`Colour` from an RGB tuple.""" + return cls((r << 16) + (g << 8) + b) + + @classmethod + def from_hsv(cls, h: float, s: float, v: float) -> Self: + """Constructs a :class:`Colour` from an HSV tuple.""" + rgb = colorsys.hsv_to_rgb(h, s, v) + return cls.from_rgb(*(int(x * 255) for x in rgb)) + + @classmethod + def default(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0``.""" + return cls(0) + + @classmethod + def random( + cls, + *, + seed: int | str | float | bytes | bytearray | None = None, + ) -> Self: + """A factory method that returns a :class:`Colour` with a random hue. + + .. note:: + + The random algorithm works by choosing a colour with a random hue but + with maxed out saturation and value. + + .. versionadded:: 1.6 + + Parameters + ---------- + seed: Optional[Union[:class:`int`, :class:`str`, :class:`float`, :class:`bytes`, :class:`bytearray`]] + The seed to initialize the RNG with. If ``None`` is passed the default RNG is used. + + .. versionadded:: 1.7 + """ + rand = random if seed is None else random.Random(seed) + return cls.from_hsv(rand.random(), 1, 1) + + @classmethod + def teal(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x1abc9c``.""" + return cls(0x1ABC9C) + + @classmethod + def dark_teal(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x11806a``.""" + return cls(0x11806A) + + @classmethod + def brand_green(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x57F287``. + + .. versionadded:: 2.0 + """ + return cls(0x57F287) + + @classmethod + def green(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x2ecc71``.""" + return cls(0x2ECC71) + + @classmethod + def dark_green(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x1f8b4c``.""" + return cls(0x1F8B4C) + + @classmethod + def blue(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x3498db``.""" + return cls(0x3498DB) + + @classmethod + def dark_blue(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x206694``.""" + return cls(0x206694) + + @classmethod + def purple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x9b59b6``.""" + return cls(0x9B59B6) + + @classmethod + def dark_purple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x71368a``.""" + return cls(0x71368A) + + @classmethod + def magenta(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xe91e63``.""" + return cls(0xE91E63) + + @classmethod + def dark_magenta(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xad1457``.""" + return cls(0xAD1457) + + @classmethod + def gold(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xf1c40f``.""" + return cls(0xF1C40F) + + @classmethod + def dark_gold(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xc27c0e``.""" + return cls(0xC27C0E) + + @classmethod + def orange(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xe67e22``.""" + return cls(0xE67E22) + + @classmethod + def dark_orange(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xa84300``.""" + return cls(0xA84300) + + @classmethod + def brand_red(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xED4245``. + + .. versionadded:: 2.0 + """ + return cls(0xED4245) + + @classmethod + def red(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xe74c3c``.""" + return cls(0xE74C3C) + + @classmethod + def dark_red(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x992d22``.""" + return cls(0x992D22) + + @classmethod + def lighter_grey(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x95a5a6``.""" + return cls(0x95A5A6) + + lighter_gray = lighter_grey + + @classmethod + def dark_grey(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x607d8b``.""" + return cls(0x607D8B) + + dark_gray = dark_grey + + @classmethod + def light_grey(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x979c9f``.""" + return cls(0x979C9F) + + light_gray = light_grey + + @classmethod + def darker_grey(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x546e7a``.""" + return cls(0x546E7A) + + darker_gray = darker_grey + + @classmethod + def og_blurple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x7289da``.""" + return cls(0x7289DA) + + @classmethod + def blurple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x5865F2``.""" + return cls(0x5865F2) + + @classmethod + def greyple(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x99aab5``.""" + return cls(0x99AAB5) + + @classmethod + def light_theme(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xfbfbfb``. + This will appear transparent on Discord's light theme. + + .. versionadded:: 2.8 + """ + return cls(0xFBFBFB) + + @classmethod + def ash_theme(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x323339``. + This will appear transparent on Discord's ash theme. + + .. versionadded:: 2.8 + """ + return cls(0x323339) + + @classmethod + def dark_theme(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x1a1a1e``. + This will appear transparent on Discord's dark theme. + + .. versionadded:: 1.5 + + .. versionchanged:: 2.8 + Updated to match Discord's new theme colour. + """ + return cls(0x1A1A1E) + + @classmethod + def onyx_theme(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0x070709``. + This will appear transparent on Discord's onyx theme. + + .. versionadded:: 2.8 + """ + return cls(0x070709) + + @classmethod + def fuchsia(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xEB459E``. + + .. versionadded:: 2.0 + """ + return cls(0xEB459E) + + @classmethod + def yellow(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xFEE75C``. + + .. versionadded:: 2.0 + """ + return cls(0xFEE75C) + + @classmethod + def nitro_pink(cls) -> Self: + """A factory method that returns a :class:`Colour` with a value of ``0xf47fff``. + + .. versionadded:: 2.0 + """ + return cls(0xF47FFF) + + @classmethod + @deprecated( + "Colour.embed_background is deprecated since version 2.8 and will be removed in version 3.0. This is not relevant anymore since Discord provides the custom themes feature." + ) + def embed_background(cls, theme: str = "dark") -> Self: + """A factory method that returns a :class:`Colour` corresponding to the + embed colours on discord clients, with a value of: + + - ``0x2B2D31`` (dark) + - ``0xEEEFF1`` (light) + - ``0x000000`` (amoled). + + .. versionadded:: 2.0 + + Parameters + ---------- + theme: :class:`str` + The theme colour to apply, must be one of "dark", "light", or "amoled". + """ + themes_cls = { + "dark": 0x2B2D31, + "light": 0xEEEFF1, + "amoled": 0x000000, + } + + if theme not in themes_cls: + raise TypeError('Theme must be "dark", "light", or "amoled".') + + return cls(themes_cls[theme]) + + +Color = Colour diff --git a/venv/lib/python3.11/site-packages/discord/commands/__init__.py b/venv/lib/python3.11/site-packages/discord/commands/__init__.py new file mode 100644 index 0000000..1813faf --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/commands/__init__.py @@ -0,0 +1,29 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from .context import * +from .core import * +from .options import * +from .permissions import * diff --git a/venv/lib/python3.11/site-packages/discord/commands/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..cb04adb Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/commands/__pycache__/context.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/context.cpython-311.pyc new file mode 100644 index 0000000..be2d888 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/context.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/commands/__pycache__/core.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/core.cpython-311.pyc new file mode 100644 index 0000000..b52d8b6 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/core.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/commands/__pycache__/options.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/options.cpython-311.pyc new file mode 100644 index 0000000..730f4a1 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/options.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/commands/__pycache__/permissions.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/permissions.cpython-311.pyc new file mode 100644 index 0000000..144d937 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/commands/__pycache__/permissions.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/commands/context.py b/venv/lib/python3.11/site-packages/discord/commands/context.py new file mode 100644 index 0000000..e97f19f --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/commands/context.py @@ -0,0 +1,476 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, TypeVar, overload + +import discord.abc +from discord.interactions import Interaction, InteractionMessage, InteractionResponse +from discord.webhook.async_ import Webhook + +if TYPE_CHECKING: + from typing import Awaitable, Callable + + from typing_extensions import ParamSpec + + import discord + + from .. import AllowedMentions, Bot + from ..client import ClientUser + from ..cog import Cog + from ..embeds import Embed + from ..file import File + from ..guild import Guild + from ..interactions import InteractionChannel + from ..member import Member + from ..message import Message + from ..permissions import Permissions + from ..poll import Poll + from ..state import ConnectionState + from ..ui import BaseView + from ..user import User + from ..voice import VoiceClient + from ..webhook import WebhookMessage + from .core import ApplicationCommand, Option + +T = TypeVar("T") +CogT = TypeVar("CogT", bound="Cog") + +if TYPE_CHECKING: + P = ParamSpec("P") +else: + P = TypeVar("P") + +__all__ = ("ApplicationContext", "AutocompleteContext") + + +class ApplicationContext(discord.abc.Messageable): + """Represents a Discord application command interaction context. + + This class is not created manually and is instead passed to application + commands as the first parameter. + + .. versionadded:: 2.0 + + Attributes + ---------- + bot: :class:`.Bot` + The bot that the command belongs to. + interaction: :class:`.Interaction` + The interaction object that invoked the command. + """ + + def __init__(self, bot: Bot, interaction: Interaction): + self.bot = bot + self.interaction = interaction + + # below attributes will be set after initialization + self.focused: Option = None # type: ignore + self.value: str = None # type: ignore + self.options: dict = None # type: ignore + + self._state: ConnectionState = self.interaction._state + + async def _get_channel(self) -> InteractionChannel | None: + return self.interaction.channel + + async def invoke( + self, + command: ApplicationCommand[CogT, P, T], + /, + *args: P.args, + **kwargs: P.kwargs, + ) -> T: + r"""|coro| + + Calls a command with the arguments given. + This is useful if you want to just call the callback that a + :class:`.ApplicationCommand` holds internally. + + .. note:: + + This does not handle converters, checks, cooldowns, pre-invoke, + or after-invoke hooks in any matter. It calls the internal callback + directly as-if it was a regular function. + You must take care in passing the proper arguments when + using this function. + + Parameters + ----------- + command: :class:`.ApplicationCommand` + The command that is going to be called. + \*args + The arguments to use. + \*\*kwargs + The keyword arguments to use. + + Raises + ------- + TypeError + The command argument to invoke is missing. + """ + return await command(self, *args, **kwargs) + + @property + def command(self) -> ApplicationCommand | None: + """The command that this context belongs to.""" + return self.interaction.command + + @command.setter + def command(self, value: ApplicationCommand | None) -> None: + self.interaction.command = value + + @property + def channel(self) -> InteractionChannel | None: + """Union[:class:`abc.GuildChannel`, :class:`PartialMessageable`, :class:`Thread`]: + Returns the channel associated with this context's command. Shorthand for :attr:`.Interaction.channel`. + """ + return self.interaction.channel + + @property + def channel_id(self) -> int | None: + """Returns the ID of the channel associated with this context's command. + Shorthand for :attr:`.Interaction.channel_id`. + """ + return self.interaction.channel_id + + @property + def guild(self) -> Guild | None: + """Returns the guild associated with this context's command. + Shorthand for :attr:`.Interaction.guild`. + """ + return self.interaction.guild + + @property + def guild_id(self) -> int | None: + """Returns the ID of the guild associated with this context's command. + Shorthand for :attr:`.Interaction.guild_id`. + """ + return self.interaction.guild_id + + @property + def locale(self) -> str | None: + """Returns the locale of the guild associated with this context's command. + Shorthand for :attr:`.Interaction.locale`. + """ + return self.interaction.locale + + @property + def guild_locale(self) -> str | None: + """Returns the locale of the guild associated with this context's command. + Shorthand for :attr:`.Interaction.guild_locale`. + """ + return self.interaction.guild_locale + + @property + def app_permissions(self) -> Permissions: + return self.interaction.app_permissions + + @property + def me(self) -> Member | ClientUser | None: + """Union[:class:`.Member`, :class:`.ClientUser`]: + Similar to :attr:`.Guild.me` except it may return the :class:`.ClientUser` in private message + message contexts, or when :meth:`Intents.guilds` is absent. + """ + return ( + self.interaction.guild.me + if self.interaction.guild is not None + else self.bot.user + ) + + @property + def message(self) -> Message | None: + """Returns the message sent with this context's command. + Shorthand for :attr:`.Interaction.message`, if applicable. + """ + return self.interaction.message + + @property + def user(self) -> Member | User: + """Returns the user that sent this context's command. + Shorthand for :attr:`.Interaction.user`. + """ + return self.interaction.user # type: ignore # command user will never be None + + author: Member | User = user + + @property + def voice_client(self) -> VoiceClient | None: + """Returns the voice client associated with this context's command. + Shorthand for :attr:`Interaction.guild.voice_client<~discord.Guild.voice_client>`, if applicable. + """ + if self.interaction.guild is None: + return None + + return self.interaction.guild.voice_client + + @property + def response(self) -> InteractionResponse: + """Returns the response object associated with this context's command. + Shorthand for :attr:`.Interaction.response`. + """ + return self.interaction.response + + @property + def selected_options(self) -> list[dict[str, Any]] | None: + """The options and values that were selected by the user when sending the command. + + Returns + ------- + Optional[List[Dict[:class:`str`, Any]]] + A dictionary containing the options and values that were selected by the user when the command + was processed, if applicable. Returns ``None`` if the command has not yet been invoked, + or if there are no options defined for that command. + """ + return self.interaction.data.get("options", None) + + @property + def unselected_options(self) -> list[Option] | None: + """The options that were not provided by the user when sending the command. + + Returns + ------- + Optional[List[:class:`.Option`]] + A list of Option objects (if any) that were not selected by the user when the command was processed. + Returns ``None`` if there are no options defined for that command. + """ + if self.command.options is not None: # type: ignore + if self.selected_options: + return [ + option + for option in self.command.options # type: ignore + if option.to_dict()["name"] + not in [opt["name"] for opt in self.selected_options] + ] + else: + return self.command.options # type: ignore + return None + + @property + def attachment_size_limit(self) -> int: + """Returns the attachment size limit associated with this context's interaction. + Shorthand for :attr:`.Interaction.attachment_size_limit`. + """ + return self.interaction.attachment_size_limit + + @property + @discord.utils.copy_doc(InteractionResponse.send_modal) + def send_modal(self) -> Callable[..., Awaitable[Interaction]]: + return self.interaction.response.send_modal + + @overload + async def respond( + self, + content: Any | None = None, + embed: Embed | None = None, + view: BaseView | None = None, + tts: bool = False, + ephemeral: bool = False, + allowed_mentions: AllowedMentions | None = None, + file: File | None = None, + files: list[File] | None = None, + poll: Poll | None = None, + delete_after: float | None = None, + silent: bool = False, + suppress_embeds: bool = False, + ) -> Interaction | WebhookMessage: ... + + @overload + async def respond( + self, + content: Any | None = None, + embeds: list[Embed] | None = None, + view: BaseView | None = None, + tts: bool = False, + ephemeral: bool = False, + allowed_mentions: AllowedMentions | None = None, + file: File | None = None, + files: list[File] | None = None, + poll: Poll | None = None, + delete_after: float | None = None, + silent: bool = False, + suppress_embeds: bool = False, + ) -> Interaction | WebhookMessage: ... + + @discord.utils.copy_doc(Interaction.respond) + async def respond(self, *args, **kwargs) -> Interaction | WebhookMessage: + return await self.interaction.respond(*args, **kwargs) + + @property + @discord.utils.copy_doc(InteractionResponse.send_message) + def send_response(self) -> Callable[..., Awaitable[Interaction]]: + if not self.interaction.response.is_done(): + return self.interaction.response.send_message + else: + raise RuntimeError( + "Interaction was already issued a response. Try using" + f" {type(self).__name__}.send_followup() instead." + ) + + @property + @discord.utils.copy_doc(Webhook.send) + def send_followup(self) -> Callable[..., Awaitable[WebhookMessage]]: + if self.interaction.response.is_done(): + return self.followup.send + else: + raise RuntimeError( + "Interaction was not yet issued a response. Try using" + f" {type(self).__name__}.respond() first." + ) + + @property + @discord.utils.copy_doc(InteractionResponse.defer) + def defer(self) -> Callable[..., Awaitable[None]]: + return self.interaction.response.defer + + @property + def followup(self) -> Webhook: + """Returns the followup webhook for followup interactions.""" + return self.interaction.followup + + async def delete(self, *, delay: float | None = None) -> None: + """|coro| + + Deletes the original interaction response message. + + This is a higher level interface to :meth:`Interaction.delete_original_response`. + + Parameters + ---------- + delay: Optional[:class:`float`] + If provided, the number of seconds to wait before deleting the message. + + Raises + ------ + HTTPException + Deleting the message failed. + Forbidden + You do not have proper permissions to delete the message. + """ + if not self.interaction.response.is_done(): + await self.defer() + + return await self.interaction.delete_original_response(delay=delay) + + @property + @discord.utils.copy_doc(Interaction.edit_original_response) + def edit(self) -> Callable[..., Awaitable[InteractionMessage]]: + return self.interaction.edit_original_response + + @property + def cog(self) -> Cog | None: + """Returns the cog associated with this context's command. + ``None`` if it does not exist. + """ + if self.command is None: + return None + + return self.command.cog + + def is_guild_authorised(self) -> bool: + """:class:`bool`: Checks if the invoked command is guild-installed. + This is a shortcut for :meth:`Interaction.is_guild_authorised`. + + There is an alias for this called :meth:`.is_guild_authorized`. + + .. versionadded:: 2.7 + """ + return self.interaction.is_guild_authorised() + + def is_user_authorised(self) -> bool: + """:class:`bool`: Checks if the invoked command is user-installed. + This is a shortcut for :meth:`Interaction.is_user_authorised`. + + There is an alias for this called :meth:`.is_user_authorized`. + + .. versionadded:: 2.7 + """ + return self.interaction.is_user_authorised() + + def is_guild_authorized(self) -> bool: + """:class:`bool`: An alias for :meth:`.is_guild_authorised`. + + .. versionadded:: 2.7 + """ + return self.is_guild_authorised() + + def is_user_authorized(self) -> bool: + """:class:`bool`: An alias for :meth:`.is_user_authorised`. + + .. versionadded:: 2.7 + """ + return self.is_user_authorised() + + +class AutocompleteContext: + """Represents context for a slash command's option autocomplete. + + This class is not created manually and is instead passed to an :class:`.Option`'s autocomplete callback. + + .. versionadded:: 2.0 + + Attributes + ---------- + bot: :class:`.Bot` + The bot that the command belongs to. + interaction: :class:`.Interaction` + The interaction object that invoked the autocomplete. + focused: :class:`.Option` + The option the user is currently typing. + value: :class:`.str` + The content of the focused option. + options: Dict[:class:`str`, Any] + A name to value mapping of the options that the user has selected before this option. + """ + + __slots__ = ("bot", "interaction", "focused", "value", "options") + + def __init__(self, bot: Bot, interaction: Interaction): + self.bot = bot + self.interaction = interaction + + self.focused: Option = None # type: ignore + self.value: str = None # type: ignore + self.options: dict = None # type: ignore + + @property + def cog(self) -> Cog | None: + """Returns the cog associated with this context's command. + ``None`` if it does not exist. + """ + if self.command is None: + return None + + return self.command.cog + + @property + def command(self) -> ApplicationCommand | None: + """The command that this context belongs to.""" + return self.interaction.command + + @command.setter + def command(self, value: ApplicationCommand | None) -> None: + self.interaction.command = value diff --git a/venv/lib/python3.11/site-packages/discord/commands/core.py b/venv/lib/python3.11/site-packages/discord/commands/core.py new file mode 100644 index 0000000..d2f8f52 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/commands/core.py @@ -0,0 +1,2303 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import asyncio +import datetime +import functools +import inspect +import re +import sys +import types +from collections import OrderedDict +from enum import Enum +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Coroutine, + Generator, + Generic, + TypeVar, + Union, +) + +from ..channel import PartialMessageable, _threaded_guild_channel_factory +from ..enums import Enum as DiscordEnum +from ..enums import ( + IntegrationType, + InteractionContextType, + SlashCommandOptionType, +) +from ..errors import ( + ApplicationCommandError, + ApplicationCommandInvokeError, + CheckFailure, + ClientException, + InvalidArgument, + ValidationError, +) +from ..message import Attachment, Message +from ..object import Object +from ..role import Role +from ..threads import Thread +from ..user import User +from ..utils import MISSING, async_all, find, maybe_coroutine, utcnow, warn_deprecated +from .context import ApplicationContext, AutocompleteContext +from .options import Option, OptionChoice + +if sys.version_info >= (3, 11): + from typing import Annotated, Literal, get_args, get_origin +else: + from typing_extensions import Annotated, Literal, get_args, get_origin + +__all__ = ( + "_BaseCommand", + "ApplicationCommand", + "SlashCommand", + "slash_command", + "application_command", + "user_command", + "message_command", + "command", + "SlashCommandGroup", + "ContextMenuCommand", + "UserCommand", + "MessageCommand", +) + +if TYPE_CHECKING: + from typing_extensions import Concatenate, Never, ParamSpec + + from .. import Permissions + from ..bot import C + from ..cog import Cog + from ..ext.commands.cooldowns import Cooldown, CooldownMapping, MaxConcurrency + +T = TypeVar("T") +CogT = TypeVar("CogT", bound="Cog") +Coro = TypeVar("Coro", bound=Callable[..., Coroutine[Any, Any, Any]]) + +if TYPE_CHECKING: + P = ParamSpec("P") +else: + P = TypeVar("P") + + +def wrap_callback(coro): + from ..ext.commands.errors import CommandError + + @functools.wraps(coro) + async def wrapped(*args, **kwargs): + try: + ret = await coro(*args, **kwargs) + except ApplicationCommandError: + raise + except CommandError: + raise + except asyncio.CancelledError: + return + except Exception as exc: + raise ApplicationCommandInvokeError(exc) from exc + return ret + + return wrapped + + +def hooked_wrapped_callback(command, ctx, coro): + from ..ext.commands.errors import CommandError + + @functools.wraps(coro) + async def wrapped(arg): + try: + ret = await coro(arg) + except ApplicationCommandError: + raise + except CommandError: + raise + except asyncio.CancelledError: + return + except Exception as exc: + raise ApplicationCommandInvokeError(exc) from exc + finally: + if ( + hasattr(command, "_max_concurrency") + and command._max_concurrency is not None + ): + await command._max_concurrency.release(ctx) + await command.call_after_hooks(ctx) + return ret + + return wrapped + + +def unwrap_function(function: Callable[..., Any]) -> Callable[..., Any]: + partial = functools.partial + while True: + if hasattr(function, "__wrapped__"): + function = function.__wrapped__ + elif isinstance(function, partial): + function = function.func + else: + return function + + +def _validate_names(obj): + validate_chat_input_name(obj.name) + if obj.name_localizations: + for locale, string in obj.name_localizations.items(): + validate_chat_input_name(string, locale=locale) + + +def _validate_descriptions(obj): + validate_chat_input_description(obj.description) + if obj.description_localizations: + for locale, string in obj.description_localizations.items(): + validate_chat_input_description(string, locale=locale) + + +class _BaseCommand: + __slots__ = () + + +class ApplicationCommand(_BaseCommand, Generic[CogT, P, T]): + __original_kwargs__: dict[str, Any] + cog = None + + def __init__(self, func: Callable, **kwargs) -> None: + from ..ext.commands.cooldowns import BucketType, CooldownMapping + + cooldown = getattr(func, "__commands_cooldown__", kwargs.get("cooldown")) + + if cooldown is None: + buckets = CooldownMapping(cooldown, BucketType.default) + elif isinstance(cooldown, CooldownMapping): + buckets = cooldown + else: + raise TypeError( + "Cooldown must be a an instance of CooldownMapping or None." + ) + + self._buckets: CooldownMapping = buckets + + max_concurrency = getattr( + func, "__commands_max_concurrency__", kwargs.get("max_concurrency") + ) + + self._max_concurrency: MaxConcurrency | None = max_concurrency + + self._callback = None + self.module = None + + self.name: str = kwargs.get("name", func.__name__) + + try: + checks = func.__commands_checks__ + checks.reverse() + except AttributeError: + checks = kwargs.get("checks", []) + + self.checks = checks + self.id: int | None = kwargs.get("id") + self.guild_ids: list[int] | None = kwargs.get("guild_ids", None) + self.parent = kwargs.get("parent") + + # Permissions + self.default_member_permissions: Permissions | None = getattr( + func, + "__default_member_permissions__", + kwargs.get("default_member_permissions", None), + ) + self.nsfw: bool | None = getattr(func, "__nsfw__", kwargs.get("nsfw", False)) + + integration_types = getattr( + func, "__integration_types__", kwargs.get("integration_types", None) + ) + contexts = getattr(func, "__contexts__", kwargs.get("contexts", None)) + guild_only = getattr(func, "__guild_only__", kwargs.get("guild_only", MISSING)) + if guild_only is not MISSING: + warn_deprecated( + "guild_only", + "contexts", + "2.6", + reference="https://docs.discord.com/developers/change-log#user-installable-apps-preview", + ) + if contexts and guild_only: + raise InvalidArgument( + "cannot pass both 'contexts' and 'guild_only' to ApplicationCommand" + ) + if self.guild_ids and ( + (contexts is not None) or guild_only or integration_types + ): + raise InvalidArgument( + "the 'contexts' and 'integration_types' parameters are not available for guild commands" + ) + + if guild_only: + contexts = {InteractionContextType.guild} + self.contexts: set[InteractionContextType] | None = contexts + self.integration_types: set[IntegrationType] | None = integration_types + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other) -> bool: + return ( + isinstance(other, self.__class__) + and self.qualified_name == other.qualified_name + and self.guild_ids == other.guild_ids + ) + + async def __call__(self, ctx, *args, **kwargs): + """|coro| + Calls the command's callback. + + This method bypasses all checks that a command has and does not + convert the arguments beforehand, so take care to pass the correct + arguments in. + """ + if self.cog is not None: + return await self.callback(self.cog, ctx, *args, **kwargs) + return await self.callback(ctx, *args, **kwargs) + + @property + def callback( + self, + ) -> ( + Callable[Concatenate[CogT, ApplicationContext, P], Coro[T]] + | Callable[Concatenate[ApplicationContext, P], Coro[T]] + ): + return self._callback + + @callback.setter + def callback( + self, + function: ( + Callable[Concatenate[CogT, ApplicationContext, P], Coro[T]] + | Callable[Concatenate[ApplicationContext, P], Coro[T]] + ), + ) -> None: + self._callback = function + unwrap = unwrap_function(function) + self.module = unwrap.__module__ + + @property + def guild_only(self) -> bool: + warn_deprecated( + "guild_only", + "contexts", + "2.6", + reference="https://docs.discord.com/developers/change-log#user-installable-apps-preview", + ) + return InteractionContextType.guild in self.contexts and len(self.contexts) == 1 + + @guild_only.setter + def guild_only(self, value: bool) -> None: + warn_deprecated( + "guild_only", + "contexts", + "2.6", + reference="https://docs.discord.com/developers/change-log#user-installable-apps-preview", + ) + if value: + self.contexts = {InteractionContextType.guild} + else: + self.contexts = { + InteractionContextType.guild, + InteractionContextType.bot_dm, + InteractionContextType.private_channel, + } + + def _prepare_cooldowns(self, ctx: ApplicationContext): + if self._buckets.valid: + current = datetime.datetime.now().timestamp() + bucket = self._buckets.get_bucket(ctx, current) # type: ignore # ctx instead of non-existent message + + if bucket is not None: + retry_after = bucket.update_rate_limit(current) + + if retry_after: + from ..ext.commands.errors import CommandOnCooldown + + raise CommandOnCooldown(bucket, retry_after, self._buckets.type) # type: ignore + + async def prepare(self, ctx: ApplicationContext) -> None: + # This should be same across all 3 types + ctx.command = self + + if not await self.can_run(ctx): + raise CheckFailure( + f"The check functions for the command {self.name} failed" + ) + + if self._max_concurrency is not None: + # For this application, context can be duck-typed as a Message + await self._max_concurrency.acquire(ctx) # type: ignore # ctx instead of non-existent message + + try: + self._prepare_cooldowns(ctx) + await self.call_before_hooks(ctx) + except: + if self._max_concurrency is not None: + await self._max_concurrency.release(ctx) # type: ignore # ctx instead of non-existent message + raise + + def is_on_cooldown(self, ctx: ApplicationContext) -> bool: + """Checks whether the command is currently on cooldown. + + .. note:: + + This uses the current time instead of the interaction time. + + Parameters + ---------- + ctx: :class:`.ApplicationContext` + The invocation context to use when checking the command's cooldown status. + + Returns + ------- + :class:`bool` + A boolean indicating if the command is on cooldown. + """ + if not self._buckets.valid: + return False + + bucket = self._buckets.get_bucket(ctx) # type: ignore + current = utcnow().timestamp() + return bucket.get_tokens(current) == 0 + + def reset_cooldown(self, ctx: ApplicationContext) -> None: + """Resets the cooldown on this command. + + Parameters + ---------- + ctx: :class:`.ApplicationContext` + The invocation context to reset the cooldown under. + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(ctx) # type: ignore # ctx instead of non-existent message + bucket.reset() + + def get_cooldown_retry_after(self, ctx: ApplicationContext) -> float: + """Retrieves the amount of seconds before this command can be tried again. + + .. note:: + + This uses the current time instead of the interaction time. + + Parameters + ---------- + ctx: :class:`.ApplicationContext` + The invocation context to retrieve the cooldown from. + + Returns + ------- + :class:`float` + The amount of time left on this command's cooldown in seconds. + If this is ``0.0`` then the command isn't on cooldown. + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(ctx) # type: ignore + current = utcnow().timestamp() + return bucket.get_retry_after(current) + + return 0.0 + + async def invoke(self, ctx: ApplicationContext) -> None: + await self.prepare(ctx) + + injected = hooked_wrapped_callback(self, ctx, self._invoke) + await injected(ctx) + + async def can_run(self, ctx: ApplicationContext) -> bool: + if not await ctx.bot.can_run(ctx): + raise CheckFailure( + f"The global check functions for command {self.name} failed." + ) + + predicates = self.checks + if self.parent is not None: + # parent checks should be run first + predicates = self.parent.checks + predicates + + cog = self.cog + if cog is not None: + local_check = cog._get_overridden_method(cog.cog_check) + if local_check is not None: + ret = await maybe_coroutine(local_check, ctx) + if not ret: + return False + + if not predicates: + # since we have no checks, then we just return True. + return True + + return await async_all(predicate(ctx) for predicate in predicates) # type: ignore + + async def dispatch_error(self, ctx: ApplicationContext, error: Exception) -> None: + ctx.command_failed = True + cog = self.cog + try: + coro = self.on_error + except AttributeError: + pass + else: + injected = wrap_callback(coro) + if cog is not None: + await injected(cog, ctx, error) + else: + await injected(ctx, error) + + try: + if cog is not None: + local = cog.__class__._get_overridden_method(cog.cog_command_error) + if local is not None: + wrapped = wrap_callback(local) + await wrapped(ctx, error) + finally: + ctx.bot.dispatch("application_command_error", ctx, error) + + def _get_signature_parameters(self): + return OrderedDict(inspect.signature(self.callback).parameters) + + def error(self, coro): + """A decorator that registers a coroutine as a local error handler. + + A local error handler is an :func:`.on_command_error` event limited to + a single command. However, the :func:`.on_command_error` is still + invoked afterwards as the catch-all. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the local error handler. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + + if not asyncio.iscoroutinefunction(coro): + raise TypeError("The error handler must be a coroutine.") + + self.on_error = coro + return coro + + def has_error_handler(self) -> bool: + """Checks whether the command has an error handler registered.""" + return hasattr(self, "on_error") + + def before_invoke(self, coro): + """A decorator that registers a coroutine as a pre-invoke hook. + A pre-invoke hook is called directly before the command is + called. This makes it a useful function to set up database + connections or any type of set up required. + + This pre-invoke hook takes a sole parameter, an :class:`.ApplicationContext`. + See :meth:`.Bot.before_invoke` for more info. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the pre-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not asyncio.iscoroutinefunction(coro): + raise TypeError("The pre-invoke hook must be a coroutine.") + + self._before_invoke = coro + return coro + + def after_invoke(self, coro): + """A decorator that registers a coroutine as a post-invoke hook. + A post-invoke hook is called directly after the command is + called. This makes it a useful function to clean-up database + connections or any type of clean up required. + + This post-invoke hook takes a sole parameter, an :class:`.ApplicationContext`. + See :meth:`.Bot.after_invoke` for more info. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the post-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not asyncio.iscoroutinefunction(coro): + raise TypeError("The post-invoke hook must be a coroutine.") + + self._after_invoke = coro + return coro + + async def call_before_hooks(self, ctx: ApplicationContext) -> None: + # now that we're done preparing we can call the pre-command hooks + # first, call the command local hook: + cog = self.cog + if self._before_invoke is not None: + # should be cog if @commands.before_invoke is used + instance = getattr(self._before_invoke, "__self__", cog) + # __self__ only exists for methods, not functions + # however, if @command.before_invoke is used, it will be a function + if instance: + await self._before_invoke(instance, ctx) # type: ignore + else: + await self._before_invoke(ctx) # type: ignore + + # call the cog local hook if applicable: + if cog is not None: + hook = cog.__class__._get_overridden_method(cog.cog_before_invoke) + if hook is not None: + await hook(ctx) + + # call the bot global hook if necessary + hook = ctx.bot._before_invoke + if hook is not None: + await hook(ctx) + + async def call_after_hooks(self, ctx: ApplicationContext) -> None: + cog = self.cog + if self._after_invoke is not None: + instance = getattr(self._after_invoke, "__self__", cog) + if instance: + await self._after_invoke(instance, ctx) # type: ignore + else: + await self._after_invoke(ctx) # type: ignore + + # call the cog local hook if applicable: + if cog is not None: + hook = cog.__class__._get_overridden_method(cog.cog_after_invoke) + if hook is not None: + await hook(ctx) + + hook = ctx.bot._after_invoke + if hook is not None: + await hook(ctx) + + @property + def cooldown(self): + return self._buckets._cooldown + + @property + def full_parent_name(self) -> str: + """Retrieves the fully qualified parent command name. + + This the base command name required to execute it. For example, + in ``/one two three`` the parent name would be ``one two``. + """ + entries = [] + command = self + while command.parent is not None and hasattr(command.parent, "name"): + command = command.parent + entries.append(command.name) + + return " ".join(reversed(entries)) + + @property + def qualified_name(self) -> str: + """Retrieves the fully qualified command name. + + This is the full parent name with the command name as well. + For example, in ``/one two three`` the qualified name would be + ``one two three``. + """ + + parent = self.full_parent_name + + if parent: + return f"{parent} {self.name}" + else: + return self.name + + @property + def qualified_id(self) -> int: + """Retrieves the fully qualified command ID. + + This is the root parent ID. For example, in ``/one two three`` + the qualified ID would return ``one.id``. + """ + if self.id is None: + return self.parent.qualified_id + return self.id + + def to_dict(self) -> dict[str, Any]: + raise NotImplementedError + + def __str__(self) -> str: + return self.qualified_name + + def _set_cog(self, cog): + self.cog = cog + + +class SlashCommand(ApplicationCommand): + r"""A class that implements the protocol for a slash command. + + These are not created manually, instead they are created via the + decorator or functional interface. + + .. versionadded:: 2.0 + + Attributes + ----------- + name: :class:`str` + The name of the command. + callback: :ref:`coroutine ` + The coroutine that is executed when the command is called. + description: Optional[:class:`str`] + The description for the command. + guild_ids: Optional[List[:class:`int`]] + The ids of the guilds where this command will be registered. + options: List[:class:`Option`] + The parameters for this command. + parent: Optional[:class:`SlashCommandGroup`] + The parent group that this command belongs to. ``None`` if there + isn't one. + mention: :class:`str` + Returns a string that allows you to mention the slash command. + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + + .. deprecated:: 2.6 + Use the :attr:`contexts` parameter instead. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. + default_member_permissions: :class:`~discord.Permissions` + The default permissions a member needs to be able to run the command. + cog: Optional[:class:`Cog`] + The cog that this command belongs to. ``None`` if there isn't one. + checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] + A list of predicates that verifies if the command could be executed + with the given :class:`.ApplicationContext` as the sole parameter. If an exception + is necessary to be thrown to signal failure, then one inherited from + :exc:`.ApplicationCommandError` should be used. Note that if the checks fail then + :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` + event. + cooldown: Optional[:class:`~discord.ext.commands.Cooldown`] + The cooldown applied when the command is invoked. ``None`` if the command + doesn't have a cooldown. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this command. The values of this should be ``"locale": "name"``. See + `here `_ for a list of valid locales. + description_localizations: Dict[:class:`str`, :class:`str`] + The description localizations for this command. The values of this should be ``"locale": "description"``. + See `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The type of installation this command should be available to. For instance, if set to + :attr:`IntegrationType.user_install`, the command will only be available to users with + the application installed on their account. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The location where this command can be used. Cannot be set if this is a guild command. + """ + + type = 1 + + parent: SlashCommandGroup | None + + def __new__(cls, *args, **kwargs) -> SlashCommand: + self = super().__new__(cls) + + self.__original_kwargs__ = kwargs.copy() + return self + + def __init__(self, func: Callable, *args, **kwargs) -> None: + super().__init__(func, **kwargs) + if not asyncio.iscoroutinefunction(func): + raise TypeError("Callback must be a coroutine.") + self.callback = func + + self.name_localizations: dict[str, str] = kwargs.get( + "name_localizations", MISSING + ) + _validate_names(self) + + description = kwargs.get("description") or ( + inspect.cleandoc(func.__doc__).splitlines()[0] + if func.__doc__ is not None + else "No description provided" + ) + + self.description: str = description + self.description_localizations: dict[str, str] = kwargs.get( + "description_localizations", MISSING + ) + _validate_descriptions(self) + + self.attached_to_group: bool = False + + self._options_kwargs = kwargs.get("options", []) + self.options: list[Option] = [] + self._validate_parameters() + + try: + checks = func.__commands_checks__ + checks.reverse() + except AttributeError: + checks = kwargs.get("checks", []) + + self.checks = checks + + self._before_invoke = None + self._after_invoke = None + + def _validate_parameters(self): + params = self._get_signature_parameters() + if kwop := self._options_kwargs: + self.options = self._match_option_param_names(params, kwop) + else: + self.options = self._parse_options(params) + + def _check_required_params(self, params): + params = iter(params.items()) + required_params = ( + ["self", "context"] if self.attached_to_group or self.cog else ["context"] + ) + for p in required_params: + try: + next(params) + except StopIteration: + raise ClientException( + f'Callback for {self.name} command is missing "{p}" parameter.' + ) + + return params + + def _parse_options(self, params, *, check_params: bool = True) -> list[Option]: + if check_params: + params = self._check_required_params(params) + else: + params = iter(params.items()) + + final_options = [] + for p_name, p_obj in params: + option = p_obj.annotation + if option == inspect.Parameter.empty: + option = str + + option = Option._strip_none_type(option) + if self._is_typing_literal(option): + literal_values = get_args(option) + if not all(isinstance(v, (str, int, float)) for v in literal_values): + raise TypeError( + "Literal values for choices must be str, int, or float." + ) + + value_type = type(literal_values[0]) + if not all(isinstance(v, value_type) for v in literal_values): + raise TypeError( + "All Literal values for choices must be of the same type." + ) + + option = Option( + value_type, + choices=[ + OptionChoice(name=str(v), value=v) for v in literal_values + ], + ) + + if self._is_typing_annotated(option): + type_hint = get_args(option)[0] + metadata = option.__metadata__ + # If multiple Options in metadata, the first will be used. + option_gen = (elem for elem in metadata if isinstance(elem, Option)) + option = next(option_gen, Option()) + # Handle Optional + if self._is_typing_optional(type_hint): + option.input_type = SlashCommandOptionType.from_datatype( + get_args(type_hint)[0] + ) + option.default = None + else: + option.input_type = SlashCommandOptionType.from_datatype(type_hint) + + if self._is_typing_union(option): + if self._is_typing_optional(option): + option = Option(option.__args__[0], default=None) + else: + option = Option(option.__args__) + + if not isinstance(option, Option): + if isinstance(p_obj.default, Option): + if p_obj.default.input_type is None: + p_obj.default.input_type = SlashCommandOptionType.from_datatype( + option + ) + option = p_obj.default + else: + option = Option(option) + + if option.default is None and not p_obj.default == inspect.Parameter.empty: + if isinstance(p_obj.default, Option): + pass + elif isinstance(p_obj.default, type) and issubclass( + p_obj.default, (DiscordEnum, Enum) + ): + option = Option(p_obj.default) + else: + option.default = p_obj.default + option.required = False + if option.name is None: + option.name = p_name + if option.name != p_name or option._parameter_name is None: + option._parameter_name = p_name + + _validate_names(option) + _validate_descriptions(option) + + final_options.append(option) + + return final_options + + def _match_option_param_names(self, params, options): + options = list(options) + params = self._check_required_params(params) + + check_annotations: list[Callable[[Option, type], bool]] = [ + lambda o, a: ( + o.input_type == SlashCommandOptionType.string + and o.converter is not None + ), # pass on converters + lambda o, a: isinstance( + o.input_type, SlashCommandOptionType + ), # pass on slash cmd option type enums + lambda o, a: isinstance(o._raw_type, tuple) and a == Union[o._raw_type], # type: ignore # union types + lambda o, a: ( + self._is_typing_optional(a) + and not o.required + and o._raw_type in a.__args__ + ), # optional + lambda o, a: ( + isinstance(a, type) and issubclass(a, o._raw_type) + ), # 'normal' types + ] + for o in options: + _validate_names(o) + _validate_descriptions(o) + try: + p_name, p_obj = next(params) + except StopIteration: # not enough params for all the options + raise ClientException("Too many arguments passed to the options kwarg.") + p_obj = p_obj.annotation + + if not any(check(o, p_obj) for check in check_annotations): + raise TypeError( + f"Parameter {p_name} does not match input type of {o.name}." + ) + o._parameter_name = p_name + + left_out_params = OrderedDict() + for k, v in params: + left_out_params[k] = v + options.extend(self._parse_options(left_out_params, check_params=False)) + + return options + + def _is_typing_union(self, annotation): + return getattr(annotation, "__origin__", None) is Union or type( + annotation + ) is getattr( + types, "UnionType", Union + ) # type: ignore + + def _is_typing_optional(self, annotation): + return self._is_typing_union(annotation) and type(None) in annotation.__args__ # type: ignore + + def _is_typing_literal(self, annotation): + return get_origin(annotation) is Literal + + def _is_typing_annotated(self, annotation): + return get_origin(annotation) is Annotated + + @property + def cog(self): + return getattr(self, "_cog", None) + + @cog.setter + def cog(self, value): + old_cog = self.cog + self._cog = value + + if ( + old_cog is None + and value is not None + or value is None + and old_cog is not None + ): + self._validate_parameters() + + @property + def is_subcommand(self) -> bool: + return self.parent is not None + + @property + def mention(self) -> str: + return f"" + + def to_dict(self) -> dict: + as_dict = { + "name": self.name, + "description": self.description, + "options": [o.to_dict() for o in self.options], + } + if self.name_localizations is not MISSING: + as_dict["name_localizations"] = self.name_localizations + if self.description_localizations is not MISSING: + as_dict["description_localizations"] = self.description_localizations + if self.is_subcommand: + as_dict["type"] = SlashCommandOptionType.sub_command.value + + if self.nsfw is not None: + as_dict["nsfw"] = self.nsfw + + if self.default_member_permissions is not None: + as_dict["default_member_permissions"] = ( + self.default_member_permissions.value + ) + + if not self.guild_ids and not self.is_subcommand: + as_dict["integration_types"] = [it.value for it in self.integration_types] + as_dict["contexts"] = [ctx.value for ctx in self.contexts] + + return as_dict + + async def _invoke(self, ctx: ApplicationContext) -> None: + # TODO: Parse the args better + kwargs = {} + for arg in ctx.interaction.data.get("options", []): + op = find(lambda x: x.name == arg["name"], self.options) + if op is None: + continue + arg = arg["value"] + + # Checks if input_type is user, role or channel + if op.input_type in ( + SlashCommandOptionType.user, + SlashCommandOptionType.role, + SlashCommandOptionType.channel, + SlashCommandOptionType.attachment, + SlashCommandOptionType.mentionable, + ): + resolved = ctx.interaction.data.get("resolved", {}) + if ( + op.input_type + in (SlashCommandOptionType.user, SlashCommandOptionType.mentionable) + and (_data := resolved.get("members", {}).get(arg)) is not None + ): + # The option type is a user, we resolved a member from the snowflake and assigned it to _data + if (_user_data := resolved.get("users", {}).get(arg)) is not None: + # We resolved the user from the user id + _data["user"] = _user_data + cache_flag = ctx.interaction._state.member_cache_flags.interaction + arg = ctx.guild._get_and_update_member(_data, int(arg), cache_flag) + elif op.input_type is SlashCommandOptionType.mentionable: + if (_data := resolved.get("users", {}).get(arg)) is not None: + arg = User(state=ctx.interaction._state, data=_data) + elif (_data := resolved.get("roles", {}).get(arg)) is not None: + arg = Role( + state=ctx.interaction._state, data=_data, guild=ctx.guild + ) + else: + arg = Object(id=int(arg)) + elif ( + _data := resolved.get(f"{op.input_type.name}s", {}).get(arg) + ) is not None: + if op.input_type is SlashCommandOptionType.channel and ( + int(arg) in ctx.guild._channels + or int(arg) in ctx.guild._threads + ): + arg = ctx.guild.get_channel_or_thread(int(arg)) + _data["_invoke_flag"] = True + ( + arg._update(_data) + if isinstance(arg, Thread) + else arg._update(ctx.guild, _data) + ) + else: + obj_type = None + kw = {} + if op.input_type is SlashCommandOptionType.user: + obj_type = User + elif op.input_type is SlashCommandOptionType.role: + obj_type = Role + kw["guild"] = ctx.guild + elif op.input_type is SlashCommandOptionType.channel: + # NOTE: + # This is a fallback in case the channel/thread is not found in the + # guild's channels/threads. For channels, if this fallback occurs, at the very minimum, + # permissions will be incorrect due to a lack of permission_overwrite data. + # For threads, if this fallback occurs, info like thread owner id, message count, + # flags, and more will be missing due to a lack of data sent by Discord. + obj_type = _threaded_guild_channel_factory(_data["type"])[0] + kw["guild"] = ctx.guild + elif op.input_type is SlashCommandOptionType.attachment: + obj_type = Attachment + arg = obj_type(state=ctx.interaction._state, data=_data, **kw) + else: + # We couldn't resolve the object, so we just return an empty object + arg = Object(id=int(arg)) + + elif ( + op.input_type == SlashCommandOptionType.string + and (converter := op.converter) is not None + ): + from discord.ext.commands import Converter + + if isinstance(converter, Converter): + if isinstance(converter, type): + arg = await converter().convert(ctx, arg) + else: + arg = await converter.convert(ctx, arg) + + elif op._raw_type in ( + SlashCommandOptionType.integer, + SlashCommandOptionType.number, + SlashCommandOptionType.string, + SlashCommandOptionType.boolean, + ): + pass + + elif issubclass(op._raw_type, Enum): + if isinstance(arg, str) and arg.isdigit(): + try: + arg = op._raw_type(int(arg)) + except ValueError: + arg = op._raw_type(arg) + elif choice := find(lambda c: c.value == arg, op.choices): + arg = getattr(op._raw_type, choice.name) + + kwargs[op._parameter_name] = arg + + for o in self.options: + if o._parameter_name not in kwargs: + kwargs[o._parameter_name] = o.default + + if self.cog is not None: + await self.callback(self.cog, ctx, **kwargs) + elif self.parent is not None and self.attached_to_group is True: + await self.callback(self.parent, ctx, **kwargs) + else: + await self.callback(ctx, **kwargs) + + async def invoke_autocomplete_callback(self, ctx: AutocompleteContext): + values = {i.name: i.default for i in self.options} + + for op in ctx.interaction.data.get("options", []): + if op.get("focused", False): + option = find(lambda o: o.name == op["name"], self.options) + values.update( + {i["name"]: i["value"] for i in ctx.interaction.data["options"]} + ) + ctx.command = self + ctx.focused = option + ctx.value = op.get("value") + ctx.options = values + + if option._autocomplete_is_instance_method: + instance = getattr(option.autocomplete, "__self__", ctx.cog) + result = option.autocomplete(instance, ctx) + else: + result = option.autocomplete(ctx) + + if inspect.isawaitable(result): + result = await result + + choices = [ + o if isinstance(o, OptionChoice) else OptionChoice(o) + for o in result + ][:25] + return await ctx.interaction.response.send_autocomplete_result( + choices=choices + ) + + def copy(self): + """Creates a copy of this command. + + Returns + ------- + :class:`SlashCommand` + A new instance of this command. + """ + ret = self.__class__(self.callback, **self.__original_kwargs__) + return self._ensure_assignment_on_copy(ret) + + def _ensure_assignment_on_copy(self, other): + other._before_invoke = self._before_invoke + other._after_invoke = self._after_invoke + if self.checks != other.checks: + other.checks = self.checks.copy() + # if self._buckets.valid and not other._buckets.valid: + # other._buckets = self._buckets.copy() + # if self._max_concurrency != other._max_concurrency: + # # _max_concurrency won't be None at this point + # other._max_concurrency = self._max_concurrency.copy() # type: ignore + + try: + other.on_error = self.on_error + except AttributeError: + pass + return other + + def _update_copy(self, kwargs: dict[str, Any]): + if kwargs: + kw = kwargs.copy() + kw.update(self.__original_kwargs__) + copy = self.__class__(self.callback, **kw) + return self._ensure_assignment_on_copy(copy) + else: + return self.copy() + + +class SlashCommandGroup(ApplicationCommand): + r"""A class that implements the protocol for a slash command group. + + These can be created manually, but they should be created via the + decorator or functional interface. + + Attributes + ----------- + name: :class:`str` + The name of the command. + description: Optional[:class:`str`] + The description for the command. + guild_ids: Optional[List[:class:`int`]] + The ids of the guilds where this command will be registered. + parent: Optional[:class:`SlashCommandGroup`] + The parent group that this group belongs to. ``None`` if there + isn't one. + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + + .. deprecated:: 2.6 + Use the :attr:`contexts` parameter instead. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. + default_member_permissions: :class:`~discord.Permissions` + The default permissions a member needs to be able to run the command. + checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] + A list of predicates that verifies if the command could be executed + with the given :class:`.ApplicationContext` as the sole parameter. If an exception + is necessary to be thrown to signal failure, then one inherited from + :exc:`.ApplicationCommandError` should be used. Note that if the checks fail then + :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` + event. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this command. The values of this should be ``"locale": "name"``. See + `here `_ for a list of valid locales. + description_localizations: Dict[:class:`str`, :class:`str`] + The description localizations for this command. The values of this should be ``"locale": "description"``. + See `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The type of installation this command should be available to. For instance, if set to + :attr:`IntegrationType.user_install`, the command will only be available to users with + the application installed on their account. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The location where this command can be used. Unapplicable for guild commands. + """ + + __initial_commands__: list[SlashCommand | SlashCommandGroup] + type = 1 + + def __new__(cls, *args, **kwargs) -> SlashCommandGroup: + self = super().__new__(cls) + self.__original_kwargs__ = kwargs.copy() + + self.__initial_commands__ = [] + for i, c in cls.__dict__.items(): + if isinstance(c, type) and SlashCommandGroup in c.__bases__: + c = c( + c.__name__, + ( + inspect.cleandoc(cls.__doc__).splitlines()[0] + if cls.__doc__ is not None + else "No description provided" + ), + ) + if isinstance(c, (SlashCommand, SlashCommandGroup)): + c.parent = self + c.attached_to_group = True + self.__initial_commands__.append(c) + + return self + + def __init__( + self, + name: str, + description: str | None = None, + guild_ids: list[int] | None = None, + parent: SlashCommandGroup | None = None, + cooldown: CooldownMapping | None = None, + max_concurrency: MaxConcurrency | None = None, + **kwargs, + ) -> None: + self.name = str(name) + self.description = description or "No description provided" + validate_chat_input_name(self.name) + validate_chat_input_description(self.description) + self.input_type = SlashCommandOptionType.sub_command_group + self.subcommands: list[SlashCommand | SlashCommandGroup] = ( + self.__initial_commands__ + ) + self.guild_ids = guild_ids + self.parent = parent + self.attached_to_group: bool = False + self.checks = kwargs.get("checks", []) + + self._before_invoke = None + self._after_invoke = None + self.cog = None + self.id = None + + # Permissions + self.default_member_permissions: Permissions | None = kwargs.get( + "default_member_permissions", None + ) + self.nsfw: bool | None = kwargs.get("nsfw", False) + + integration_types = kwargs.get("integration_types", None) + contexts = kwargs.get("contexts", None) + guild_only = kwargs.get("guild_only", MISSING) + if guild_only is not MISSING: + warn_deprecated("guild_only", "contexts", "2.6") + if contexts and guild_only: + raise InvalidArgument( + "cannot pass both 'contexts' and 'guild_only' to ApplicationCommand" + ) + if self.guild_ids and ( + (contexts is not None) or guild_only or integration_types + ): + raise InvalidArgument( + "the 'contexts' and 'integration_types' parameters are not available for guild commands" + ) + + # These are set to None and their defaults are then set when added to the bot + self.contexts: set[InteractionContextType] | None = contexts + if guild_only: + self.guild_only: bool | None = guild_only + self.integration_types: set[IntegrationType] | None = integration_types + + self.name_localizations: dict[str, str] = kwargs.get( + "name_localizations", MISSING + ) + self.description_localizations: dict[str, str] = kwargs.get( + "description_localizations", MISSING + ) + + # similar to ApplicationCommand + from ..ext.commands.cooldowns import BucketType, CooldownMapping, MaxConcurrency + + # no need to getattr, since slash cmds groups cant be created using a decorator + + if cooldown is None: + buckets = CooldownMapping(cooldown, BucketType.default) + elif isinstance(cooldown, CooldownMapping): + buckets = cooldown + else: + raise TypeError( + "Cooldown must be a an instance of CooldownMapping or None." + ) + + self._buckets: CooldownMapping = buckets + + # no need to getattr, since slash cmds groups cant be created using a decorator + + if max_concurrency is not None and not isinstance( + max_concurrency, MaxConcurrency + ): + raise TypeError( + "max_concurrency must be an instance of MaxConcurrency or None" + ) + + self._max_concurrency: MaxConcurrency | None = max_concurrency + + @property + def module(self) -> str | None: + return self.__module__ + + @property + def guild_only(self) -> bool: + warn_deprecated("guild_only", "contexts", "2.6") + return InteractionContextType.guild in self.contexts and len(self.contexts) == 1 + + @guild_only.setter + def guild_only(self, value: bool) -> None: + warn_deprecated("guild_only", "contexts", "2.6") + if value: + self.contexts = {InteractionContextType.guild} + else: + self.contexts = { + InteractionContextType.guild, + InteractionContextType.bot_dm, + InteractionContextType.private_channel, + } + + def to_dict(self) -> dict: + as_dict = { + "name": self.name, + "description": self.description, + "options": [c.to_dict() for c in self.subcommands], + } + if self.name_localizations is not MISSING: + as_dict["name_localizations"] = self.name_localizations + if self.description_localizations is not MISSING: + as_dict["description_localizations"] = self.description_localizations + + if self.parent is not None: + as_dict["type"] = self.input_type.value + + if self.nsfw is not None: + as_dict["nsfw"] = self.nsfw + + if self.default_member_permissions is not None: + as_dict["default_member_permissions"] = ( + self.default_member_permissions.value + ) + + if not self.guild_ids and self.parent is None: + as_dict["integration_types"] = [it.value for it in self.integration_types] + as_dict["contexts"] = [ctx.value for ctx in self.contexts] + + return as_dict + + def add_command(self, command: SlashCommand | SlashCommandGroup) -> None: + if command.cog is None and self.cog is not None: + command.cog = self.cog + + self.subcommands.append(command) + + def command( + self, cls: type[T] = SlashCommand, **kwargs + ) -> Callable[[Callable], SlashCommand]: + """A shortcut decorator for adding a subcommand to this slash command group. + + Returns + ------- + Callable[..., :class:`SlashCommand`] + A decorator that converts the provided function into a :class:`.SlashCommand`, + adds it to this group, then returns it. + """ + + def wrap(func) -> T: + command = cls(func, parent=self, **kwargs) + self.add_command(command) + return command + + return wrap + + def create_subgroup( + self, + name: str, + description: str | None = None, + guild_ids: list[int] | None = None, + **kwargs, + ) -> SlashCommandGroup: + """ + Creates a new subgroup under this slash command group. + + Parameters + ---------- + name: :class:`str` + The name of the group to create. + description: Optional[:class:`str`] + The description of the group to create. + guild_ids: Optional[List[:class:`int`]] + A list of the IDs of each guild this group should be added to, making it a guild command. + This will be a global command if ``None`` is passed. + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. + default_member_permissions: :class:`~discord.Permissions` + The default permissions a member needs to be able to run the command. + checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] + A list of predicates that verifies if the command could be executed + with the given :class:`.ApplicationContext` as the sole parameter. If an exception + is necessary to be thrown to signal failure, then one inherited from + :exc:`.ApplicationCommandError` should be used. Note that if the checks fail then + :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` + event. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this command. The values of this should be ``"locale": "name"``. See + `here `_ for a list of valid locales. + description_localizations: Dict[:class:`str`, :class:`str`] + The description localizations for this command. The values of this should be ``"locale": "description"``. + See `here `_ for a list of valid locales. + + Returns + ------- + SlashCommandGroup + The slash command group that was created. + """ + + if self.parent is not None: + raise Exception("A subcommand group cannot be added to a subcommand group") + + sub_command_group = SlashCommandGroup( + name, description, guild_ids, parent=self, **kwargs + ) + self.subcommands.append(sub_command_group) + return sub_command_group + + def subgroup( + self, + name: str | None = None, + description: str | None = None, + guild_ids: list[int] | None = None, + ) -> Callable[[type[SlashCommandGroup]], SlashCommandGroup]: + """A shortcut decorator that initializes the provided subclass of :class:`.SlashCommandGroup` + as a subgroup. + + .. versionadded:: 2.0 + + Parameters + ---------- + name: Optional[:class:`str`] + The name of the group to create. This will resolve to the name of the decorated class if ``None`` is passed. + description: Optional[:class:`str`] + The description of the group to create. + guild_ids: Optional[List[:class:`int`]] + A list of the IDs of each guild this group should be added to, making it a guild command. + This will be a global command if ``None`` is passed. + + Returns + ------- + Callable[[Type[SlashCommandGroup]], SlashCommandGroup] + The slash command group that was created. + """ + + def inner(cls: type[SlashCommandGroup]) -> SlashCommandGroup: + group = cls( + name or cls.__name__, + description + or ( + inspect.cleandoc(cls.__doc__).splitlines()[0] + if cls.__doc__ is not None + else "No description provided" + ), + guild_ids=guild_ids, + parent=self, + ) + self.add_command(group) + return group + + return inner + + async def _invoke(self, ctx: ApplicationContext) -> None: + option = ctx.interaction.data["options"][0] + resolved = ctx.interaction.data.get("resolved", None) + command = find(lambda x: x.name == option["name"], self.subcommands) + option["resolved"] = resolved + ctx.interaction.data = option + await command.invoke(ctx) + + async def invoke_autocomplete_callback(self, ctx: AutocompleteContext) -> None: + option = ctx.interaction.data["options"][0] + command = find(lambda x: x.name == option["name"], self.subcommands) + ctx.interaction.data = option + await command.invoke_autocomplete_callback(ctx) + + async def call_before_hooks(self, ctx: ApplicationContext) -> None: + # only call local hooks + cog = self.cog + if self._before_invoke is not None: + # should be cog if @commands.before_invoke is used + instance = getattr(self._before_invoke, "__self__", cog) + # __self__ only exists for methods, not functions + # however, if @command.before_invoke is used, it will be a function + if instance: + await self._before_invoke(instance, ctx) # type: ignore + else: + await self._before_invoke(ctx) # type: ignore + + async def call_after_hooks(self, ctx: ApplicationContext) -> None: + cog = self.cog + if self._after_invoke is not None: + instance = getattr(self._after_invoke, "__self__", cog) + if instance: + await self._after_invoke(instance, ctx) # type: ignore + else: + await self._after_invoke(ctx) # type: ignore + + def walk_commands(self) -> Generator[SlashCommand | SlashCommandGroup]: + """An iterator that recursively walks through all slash commands and groups in this group. + + Yields + ------ + :class:`.SlashCommand` | :class:`.SlashCommandGroup` + A nested slash command or slash command group from the group. + """ + for command in self.subcommands: + if isinstance(command, SlashCommandGroup): + yield from command.walk_commands() + yield command + + def copy(self): + """Creates a copy of this command group. + + Returns + ------- + :class:`SlashCommandGroup` + A new instance of this command group. + """ + ret = self.__class__( + name=self.name, + description=self.description, + **{ + param: value + for param, value in self.__original_kwargs__.items() + if param not in ("name", "description") + }, + ) + return self._ensure_assignment_on_copy(ret) + + def _ensure_assignment_on_copy(self, other): + other.parent = self.parent + + other._before_invoke = self._before_invoke + other._after_invoke = self._after_invoke + + if self.subcommands != other.subcommands: + other.subcommands = self.subcommands.copy() + + if self.checks != other.checks: + other.checks = self.checks.copy() + + return other + + def _update_copy(self, kwargs: dict[str, Any]): + if kwargs: + kw = kwargs.copy() + kw.update(self.__original_kwargs__) + copy = self.__class__(**kw) + return self._ensure_assignment_on_copy(copy) + else: + return self.copy() + + def _set_cog(self, cog): + super()._set_cog(cog) + for subcommand in self.subcommands: + subcommand._set_cog(cog) + + +class ContextMenuCommand(ApplicationCommand): + r"""A class that implements the protocol for context menu commands. + + These are not created manually, instead they are created via the + decorator or functional interface. + + .. versionadded:: 2.0 + + Attributes + ----------- + name: :class:`str` + The name of the command. + callback: :ref:`coroutine ` + The coroutine that is executed when the command is called. + guild_ids: Optional[List[:class:`int`]] + The ids of the guilds where this command will be registered. + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + + .. deprecated:: 2.6 + Use the ``contexts`` parameter instead. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. + default_member_permissions: :class:`~discord.Permissions` + The default permissions a member needs to be able to run the command. + cog: Optional[:class:`Cog`] + The cog that this command belongs to. ``None`` if there isn't one. + checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] + A list of predicates that verifies if the command could be executed + with the given :class:`.ApplicationContext` as the sole parameter. If an exception + is necessary to be thrown to signal failure, then one inherited from + :exc:`.ApplicationCommandError` should be used. Note that if the checks fail then + :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` + event. + cooldown: Optional[:class:`~discord.ext.commands.Cooldown`] + The cooldown applied when the command is invoked. ``None`` if the command + doesn't have a cooldown. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this command. The values of this should be ``"locale": "name"``. See + `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The installation contexts where this command is available. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The interaction contexts where this command is available. Unapplicable for guild commands. + """ + + def __new__(cls, *args, **kwargs) -> ContextMenuCommand: + self = super().__new__(cls) + + self.__original_kwargs__ = kwargs.copy() + return self + + def __init__(self, func: Callable, *args, **kwargs) -> None: + super().__init__(func, **kwargs) + if not asyncio.iscoroutinefunction(func): + raise TypeError("Callback must be a coroutine.") + self.callback = func + + self.name_localizations: dict[str, str] = kwargs.get( + "name_localizations", MISSING + ) + + # Discord API doesn't support setting descriptions for context menu commands, so it must be empty + self.description = "" + if not isinstance(self.name, str): + raise TypeError("Name of a command must be a string.") + + self.cog = None + self.id = None + + self._before_invoke = None + self._after_invoke = None + + self.validate_parameters() + + # Context Menu commands can't have parents + self.parent = None + + def validate_parameters(self): + params = self._get_signature_parameters() + if list(params.items())[0][0] == "self": + temp = list(params.items()) + temp.pop(0) + params = dict(temp) + params = iter(params) + + # next we have the 'ctx' as the next parameter + try: + next(params) + except StopIteration: + raise ClientException( + f'Callback for {self.name} command is missing "ctx" parameter.' + ) + + # next we have the 'user/message' as the next parameter + try: + next(params) + except StopIteration: + cmd = "user" if type(self) == UserCommand else "message" + raise ClientException( + f'Callback for {self.name} command is missing "{cmd}" parameter.' + ) + + # next there should be no more parameters + try: + next(params) + raise ClientException( + f"Callback for {self.name} command has too many parameters." + ) + except StopIteration: + pass + + @property + def qualified_name(self): + return self.name + + def to_dict(self) -> dict[str, str | int]: + as_dict = { + "name": self.name, + "description": self.description, + "type": self.type, + } + + if not self.guild_ids: + as_dict["integration_types"] = [it.value for it in self.integration_types] + as_dict["contexts"] = [ctx.value for ctx in self.contexts] + + if self.nsfw is not None: + as_dict["nsfw"] = self.nsfw + + if self.default_member_permissions is not None: + as_dict["default_member_permissions"] = ( + self.default_member_permissions.value + ) + + if self.name_localizations: + as_dict["name_localizations"] = self.name_localizations + + return as_dict + + +class UserCommand(ContextMenuCommand): + r"""A class that implements the protocol for user context menu commands. + + These are not created manually, instead they are created via the + decorator or functional interface. + + Attributes + ----------- + name: :class:`str` + The name of the command. + callback: :ref:`coroutine ` + The coroutine that is executed when the command is called. + guild_ids: Optional[List[:class:`int`]] + The ids of the guilds where this command will be registered. + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + + .. deprecated:: 2.6 + Use the ``contexts`` parameter instead. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. + default_member_permissions: :class:`~discord.Permissions` + The default permissions a member needs to be able to run the command. + cog: Optional[:class:`Cog`] + The cog that this command belongs to. ``None`` if there isn't one. + checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] + A list of predicates that verifies if the command could be executed + with the given :class:`.ApplicationContext` as the sole parameter. If an exception + is necessary to be thrown to signal failure, then one inherited from + :exc:`.ApplicationCommandError` should be used. Note that if the checks fail then + :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` + event. + cooldown: Optional[:class:`~discord.ext.commands.Cooldown`] + The cooldown applied when the command is invoked. ``None`` if the command + doesn't have a cooldown. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this command. The values of this should be ``"locale": "name"``. See + `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The installation contexts where this command is available. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The interaction contexts where this command is available. Unapplicable for guild commands. + """ + + type = 2 + + def __new__(cls, *args, **kwargs) -> UserCommand: + self = super().__new__(cls) + + self.__original_kwargs__ = kwargs.copy() + return self + + async def _invoke(self, ctx: ApplicationContext) -> None: + if "members" not in ctx.interaction.data["resolved"]: + _data = ctx.interaction.data["resolved"]["users"] + for i, v in _data.items(): + v["id"] = int(i) + user = v + target = User(state=ctx.interaction._state, data=user) + else: + _data = ctx.interaction.data["resolved"]["members"] + for i, v in _data.items(): + v["id"] = int(i) + member = v + _data = ctx.interaction.data["resolved"]["users"] + for i, v in _data.items(): + v["id"] = int(i) + user = v + member["user"] = user + cache_flag = ctx.interaction._state.member_cache_flags.interaction + target = ctx.guild._get_and_update_member(member, user["id"], cache_flag) + if self.cog is not None: + await self.callback(self.cog, ctx, target) + else: + await self.callback(ctx, target) + + def copy(self): + """Creates a copy of this command. + + Returns + ------- + :class:`UserCommand` + A new instance of this command. + """ + ret = self.__class__(self.callback, **self.__original_kwargs__) + return self._ensure_assignment_on_copy(ret) + + def _ensure_assignment_on_copy(self, other): + other._before_invoke = self._before_invoke + other._after_invoke = self._after_invoke + if self.checks != other.checks: + other.checks = self.checks.copy() + # if self._buckets.valid and not other._buckets.valid: + # other._buckets = self._buckets.copy() + # if self._max_concurrency != other._max_concurrency: + # # _max_concurrency won't be None at this point + # other._max_concurrency = self._max_concurrency.copy() # type: ignore + + try: + other.on_error = self.on_error + except AttributeError: + pass + return other + + def _update_copy(self, kwargs: dict[str, Any]): + if kwargs: + kw = kwargs.copy() + kw.update(self.__original_kwargs__) + copy = self.__class__(self.callback, **kw) + return self._ensure_assignment_on_copy(copy) + else: + return self.copy() + + +class MessageCommand(ContextMenuCommand): + r"""A class that implements the protocol for message context menu commands. + + These are not created manually, instead they are created via the + decorator or functional interface. + + Attributes + ----------- + name: :class:`str` + The name of the command. + callback: :ref:`coroutine ` + The coroutine that is executed when the command is called. + guild_ids: Optional[List[:class:`int`]] + The ids of the guilds where this command will be registered. + guild_only: :class:`bool` + Whether the command should only be usable inside a guild. + + .. deprecated:: 2.6 + Use the ``contexts`` parameter instead. + nsfw: :class:`bool` + Whether the command should be restricted to 18+ channels and users. + Apps intending to be listed in the App Directory cannot have NSFW commands. + default_member_permissions: :class:`~discord.Permissions` + The default permissions a member needs to be able to run the command. + cog: Optional[:class:`Cog`] + The cog that this command belongs to. ``None`` if there isn't one. + checks: List[Callable[[:class:`.ApplicationContext`], :class:`bool`]] + A list of predicates that verifies if the command could be executed + with the given :class:`.ApplicationContext` as the sole parameter. If an exception + is necessary to be thrown to signal failure, then one inherited from + :exc:`.ApplicationCommandError` should be used. Note that if the checks fail then + :exc:`.CheckFailure` exception is raised to the :func:`.on_application_command_error` + event. + cooldown: Optional[:class:`~discord.ext.commands.Cooldown`] + The cooldown applied when the command is invoked. ``None`` if the command + doesn't have a cooldown. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this command. The values of this should be ``"locale": "name"``. See + `here `_ for a list of valid locales. + integration_types: Set[:class:`IntegrationType`] + The installation contexts where this command is available. Unapplicable for guild commands. + contexts: Set[:class:`InteractionContextType`] + The interaction contexts where this command is available. Unapplicable for guild commands. + """ + + type = 3 + + def __new__(cls, *args, **kwargs) -> MessageCommand: + self = super().__new__(cls) + + self.__original_kwargs__ = kwargs.copy() + return self + + async def _invoke(self, ctx: ApplicationContext): + _data = ctx.interaction.data["resolved"]["messages"] + for i, v in _data.items(): + v["id"] = int(i) + message = v + channel = ctx.interaction.channel + if channel.id != int(message["channel_id"]): + # we got weird stuff going on, make up a channel + channel = PartialMessageable( + state=ctx.interaction._state, id=int(message["channel_id"]) + ) + + target = Message(state=ctx.interaction._state, channel=channel, data=message) + + if self.cog is not None: + await self.callback(self.cog, ctx, target) + else: + await self.callback(ctx, target) + + def copy(self): + """Creates a copy of this command. + + Returns + ------- + :class:`MessageCommand` + A new instance of this command. + """ + ret = self.__class__(self.callback, **self.__original_kwargs__) + return self._ensure_assignment_on_copy(ret) + + def _ensure_assignment_on_copy(self, other): + other._before_invoke = self._before_invoke + other._after_invoke = self._after_invoke + if self.checks != other.checks: + other.checks = self.checks.copy() + # if self._buckets.valid and not other._buckets.valid: + # other._buckets = self._buckets.copy() + # if self._max_concurrency != other._max_concurrency: + # # _max_concurrency won't be None at this point + # other._max_concurrency = self._max_concurrency.copy() # type: ignore + + try: + other.on_error = self.on_error + except AttributeError: + pass + return other + + def _update_copy(self, kwargs: dict[str, Any]): + if kwargs: + kw = kwargs.copy() + kw.update(self.__original_kwargs__) + copy = self.__class__(self.callback, **kw) + return self._ensure_assignment_on_copy(copy) + else: + return self.copy() + + +def slash_command( + *, + checks: list[Callable[[ApplicationContext], bool]] | None = MISSING, + cog: Cog | None = MISSING, + contexts: set[InteractionContextType] | None = MISSING, + cooldown: Cooldown | None = MISSING, + default_member_permissions: Permissions | None = MISSING, + description: str | None = MISSING, + description_localizations: dict[str, str] | None = MISSING, + guild_ids: list[int] | None = MISSING, + guild_only: bool | None = MISSING, + integration_types: set[IntegrationType] | None = MISSING, + name: str | None = MISSING, + name_localizations: dict[str, str] | None = MISSING, + nsfw: bool | None = MISSING, + options: list[Option] | None = MISSING, + parent: SlashCommandGroup | None = MISSING, + **kwargs: Never, +) -> Callable[..., SlashCommand]: + """Decorator for slash commands that invokes :func:`application_command`. + + .. versionadded:: 2.0 + + Returns + ------- + Callable[..., :class:`.SlashCommand`] + A decorator that converts the provided method into a :class:`.SlashCommand`. + """ + return application_command( + cls=SlashCommand, + checks=checks, + cog=cog, + contexts=contexts, + cooldown=cooldown, + default_member_permissions=default_member_permissions, + description=description, + description_localizations=description_localizations, + guild_ids=guild_ids, + guild_only=guild_only, + integration_types=integration_types, + name=name, + name_localizations=name_localizations, + nsfw=nsfw, + options=options, + parent=parent, + **kwargs, + ) + + +def user_command( + *, + checks: list[Callable[[ApplicationContext], bool]] | None = MISSING, + cog: Cog | None = MISSING, + contexts: set[InteractionContextType] | None = MISSING, + cooldown: Cooldown | None = MISSING, + default_member_permissions: Permissions | None = MISSING, + guild_ids: list[int] | None = MISSING, + guild_only: bool | None = MISSING, + integration_types: set[IntegrationType] | None = MISSING, + name: str | None = MISSING, + name_localizations: dict[str, str] | None = MISSING, + nsfw: bool | None = MISSING, + **kwargs: Never, +) -> Callable[..., UserCommand]: + """Decorator for user commands that invokes :func:`application_command`. + + .. versionadded:: 2.0 + + Returns + ------- + Callable[..., :class:`.UserCommand`] + A decorator that converts the provided method into a :class:`.UserCommand`. + """ + return application_command( + cls=UserCommand, + checks=checks, + cog=cog, + contexts=contexts, + cooldown=cooldown, + default_member_permissions=default_member_permissions, + guild_ids=guild_ids, + guild_only=guild_only, + integration_types=integration_types, + name=name, + name_localizations=name_localizations, + nsfw=nsfw, + **kwargs, + ) + + +def message_command( + *, + checks: list[Callable[[ApplicationContext], bool]] | None = MISSING, + cog: Cog | None = MISSING, + contexts: set[InteractionContextType] | None = MISSING, + cooldown: Cooldown | None = MISSING, + default_member_permissions: Permissions | None = MISSING, + guild_ids: list[int] | None = MISSING, + guild_only: bool | None = MISSING, + integration_types: set[IntegrationType] | None = MISSING, + name: str | None = MISSING, + name_localizations: dict[str, str] | None = MISSING, + nsfw: bool | None = MISSING, + **kwargs: Never, +) -> Callable[..., MessageCommand]: + """Decorator for message commands that invokes :func:`application_command`. + + .. versionadded:: 2.0 + + Returns + ------- + Callable[..., :class:`.MessageCommand`] + A decorator that converts the provided method into a :class:`.MessageCommand`. + """ + return application_command( + cls=MessageCommand, + checks=checks, + cog=cog, + contexts=contexts, + cooldown=cooldown, + default_member_permissions=default_member_permissions, + guild_ids=guild_ids, + guild_only=guild_only, + integration_types=integration_types, + name=name, + name_localizations=name_localizations, + nsfw=nsfw, + **kwargs, + ) + + +def application_command( + *, + cls: type[C] = SlashCommand, + checks: list[Callable[[ApplicationContext], bool]] | None = MISSING, + cog: Cog | None = MISSING, + contexts: set[InteractionContextType] | None = MISSING, + cooldown: Cooldown | None = MISSING, + default_member_permissions: Permissions | None = MISSING, + description: str | None = MISSING, + description_localizations: dict[str, str] | None = MISSING, + guild_ids: list[int] | None = MISSING, + guild_only: bool | None = MISSING, + integration_types: set[IntegrationType] | None = MISSING, + name: str | None = MISSING, + name_localizations: dict[str, str] | None = MISSING, + nsfw: bool | None = MISSING, + options: list[Option] | None = MISSING, + parent: SlashCommandGroup | None = MISSING, + **kwargs: Any, +) -> Callable[..., C]: + """A decorator that transforms a function into an :class:`.ApplicationCommand`. More specifically, + usually one of :class:`.SlashCommand`, :class:`.UserCommand`, or :class:`.MessageCommand`. The exact class + depends on the ``cls`` parameter. + By default, the ``description`` attribute is received automatically from the + docstring of the function and is cleaned up with the use of + ``inspect.cleandoc``. If the docstring is ``bytes``, then it is decoded + into :class:`str` using utf-8 encoding. + The ``name`` attribute also defaults to the function name unchanged. + + .. versionadded:: 2.0 + + Parameters + ---------- + cls: :class:`.ApplicationCommand` + The class to construct with. By default, this is :class:`.SlashCommand`. + You usually do not change this. + attrs + Keyword arguments to pass into the construction of the class denoted + by ``cls``. + + Returns + ------- + Callable[..., :class:`.ApplicationCommand`] + A decorator that converts the provided method into an :class:`.ApplicationCommand`, or subclass of it. + + Raises + ------ + TypeError + If the function is not a coroutine or is already a command. + """ + params = { + "checks": checks, + "cog": cog, + "contexts": contexts, + "cooldown": cooldown, + "default_member_permissions": default_member_permissions, + "description": description, + "description_localizations": description_localizations, + "guild_ids": guild_ids, + "guild_only": guild_only, + "integration_types": integration_types, + "name": name, + "name_localizations": name_localizations, + "nsfw": nsfw, + "options": options, + "parent": parent, + **kwargs, + } + kwargs = {k: v for k, v in params.items() if v is not MISSING} + + def decorator(func: Callable) -> cls: + if isinstance(func, ApplicationCommand): + func = func.callback + elif not callable(func): + raise TypeError( + "func needs to be a callable or a subclass of ApplicationCommand." + ) + return cls(func, **kwargs) + + return decorator + + +def command(**kwargs): + """An alias for :meth:`application_command`. + + .. note:: + This decorator is overridden by :func:`ext.commands.command`. + + .. versionadded:: 2.0 + + Returns + ------- + Callable[..., :class:`.ApplicationCommand`] + A decorator that converts the provided method into an :class:`.ApplicationCommand`. + """ + return application_command(**kwargs) + + +docs = "https://docs.discord.com/developers" +valid_locales = [ + "id", + "da", + "de", + "en-GB", + "en-US", + "es-ES", + "es-419", + "fr", + "hr", + "it", + "lt", + "hu", + "nl", + "no", + "pl", + "pt-BR", + "ro", + "fi", + "sv-SE", + "vi", + "tr", + "cs", + "el", + "bg", + "ru", + "uk", + "hi", + "th", + "zh-CN", + "ja", + "zh-TW", + "ko", +] + + +# Validation +def validate_chat_input_name(name: Any, locale: str | None = None): + # Must meet the regex ^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$ + if locale is not None and locale not in valid_locales: + raise ValidationError( + f"Locale '{locale}' is not a valid locale, see {docs}/reference#locales for" + " list of supported locales." + ) + error = None + if not isinstance(name, str): + error = TypeError( + f'Command names and options must be of type str. Received "{name}"' + ) + elif not re.match(r"^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$", name): + error = ValidationError( + r"Command names and options must follow the regex" + r" \"^[-_\w\d\u0901-\u097D\u0E00-\u0E7F]{1,32}$\". " + "For more information, see" + f" {docs}/interactions/application-commands#application-command-object-" + f'application-command-naming. Received "{name}"' + ) + elif ( + name.lower() != name + ): # Can't use islower() as it fails if none of the chars can be lowered. See #512. + error = ValidationError( + f'Command names and options must be lowercase. Received "{name}"' + ) + + if error: + if locale: + error.args = (f"{error.args[0]} in locale {locale}",) + raise error + + +def validate_chat_input_description(description: Any, locale: str | None = None): + if locale is not None and locale not in valid_locales: + raise ValidationError( + f"Locale '{locale}' is not a valid locale, see {docs}/reference#locales for" + " list of supported locales." + ) + error = None + if not isinstance(description, str): + error = TypeError( + "Command and option description must be of type str. Received" + f' "{description}"' + ) + elif not 1 <= len(description) <= 100: + error = ValidationError( + "Command and option description must be 1-100 characters long. Received" + f' "{description}"' + ) + + if error: + if locale: + error.args = (f"{error.args[0]} in locale {locale}",) + raise error diff --git a/venv/lib/python3.11/site-packages/discord/commands/options.py b/venv/lib/python3.11/site-packages/discord/commands/options.py new file mode 100644 index 0000000..6567d0c --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/commands/options.py @@ -0,0 +1,557 @@ +""" +The MIT License (MIT) + +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import inspect +import logging +import sys +import types +from collections.abc import Awaitable, Callable, Iterable +from enum import Enum +from typing import ( + TYPE_CHECKING, + Any, + Literal, + Optional, + Type, + TypeVar, + Union, + get_args, +) + +if sys.version_info >= (3, 12): + from typing import TypeAliasType +else: + from typing_extensions import TypeAliasType + +from ..abc import GuildChannel, Mentionable +from ..channel import ( + CategoryChannel, + DMChannel, + ForumChannel, + MediaChannel, + StageChannel, + TextChannel, + Thread, + VoiceChannel, +) +from ..commands import ApplicationContext, AutocompleteContext +from ..enums import ChannelType +from ..enums import Enum as DiscordEnum +from ..enums import SlashCommandOptionType +from ..utils import MISSING, basic_autocomplete + +if TYPE_CHECKING: + from ..cog import Cog + from ..ext.commands import Converter + from ..member import Member + from ..message import Attachment + from ..role import Role + from ..user import User + + InputType = Union[ + Type[str], + Type[bool], + Type[int], + Type[float], + Type[GuildChannel], + Type[Thread], + Type[Member], + Type[User], + Type[Attachment], + Type[Role], + Type[Mentionable], + SlashCommandOptionType, + Converter, + Type[Converter], + Type[Enum], + Type[DiscordEnum], + ] + + AutocompleteReturnType = Union[ + Iterable["OptionChoice"], Iterable[str], Iterable[int], Iterable[float] + ] + T = TypeVar("T", bound=AutocompleteReturnType) + MaybeAwaitable = Union[T, Awaitable[T]] + AutocompleteFunction = Union[ + Callable[[AutocompleteContext], MaybeAwaitable[AutocompleteReturnType]], + Callable[[Cog, AutocompleteContext], MaybeAwaitable[AutocompleteReturnType]], + Callable[ + [AutocompleteContext, Any], # pyright: ignore [reportExplicitAny] + MaybeAwaitable[AutocompleteReturnType], + ], + Callable[ + [Cog, AutocompleteContext, Any], # pyright: ignore [reportExplicitAny] + MaybeAwaitable[AutocompleteReturnType], + ], + ] + + +__all__ = ( + "ThreadOption", + "Option", + "OptionChoice", + "option", +) + +CHANNEL_TYPE_MAP = { + TextChannel: ChannelType.text, + VoiceChannel: ChannelType.voice, + StageChannel: ChannelType.stage_voice, + CategoryChannel: ChannelType.category, + Thread: ChannelType.public_thread, + ForumChannel: ChannelType.forum, + MediaChannel: ChannelType.media, + DMChannel: ChannelType.private, +} + +_log = logging.getLogger(__name__) + + +class ThreadOption: + """Represents a class that can be passed as the ``input_type`` for an :class:`Option` class. + + .. versionadded:: 2.0 + + Parameters + ---------- + thread_type: Literal["public", "private", "news"] + The thread type to expect for this options input. + """ + + def __init__(self, thread_type: Literal["public", "private", "news"]): + type_map = { + "public": ChannelType.public_thread, + "private": ChannelType.private_thread, + "news": ChannelType.news_thread, + } + self._type = type_map[thread_type] + + +class Option: + """Represents a selectable option for a slash command. + + Attributes + ---------- + input_type: Union[Type[:class:`str`], Type[:class:`bool`], Type[:class:`int`], Type[:class:`float`], Type[:class:`.abc.GuildChannel`], Type[:class:`Thread`], Type[:class:`Member`], Type[:class:`User`], Type[:class:`Attachment`], Type[:class:`Role`], Type[:class:`.abc.Mentionable`], :class:`SlashCommandOptionType`, Type[:class:`.ext.commands.Converter`], Type[:class:`enums.Enum`], Type[:class:`Enum`]] + The type of input that is expected for this option. This can be a :class:`SlashCommandOptionType`, + an associated class, a channel type, a :class:`Converter`, a converter class or an :class:`enum.Enum`. + If a :class:`enum.Enum` is used and it has up to 25 values, :attr:`choices` will be automatically filled. If the :class:`enum.Enum` has more than 25 values, :attr:`autocomplete` will be implemented with :func:`discord.utils.basic_autocomplete` instead. + name: :class:`str` + The name of this option visible in the UI. + Inherits from the variable name if not provided as a parameter. + description: Optional[:class:`str`] + The description of this option. + Must be 100 characters or fewer. If :attr:`input_type` is a :class:`enum.Enum` and :attr:`description` is not specified, :attr:`input_type`'s docstring will be used. + choices: Optional[List[Union[:class:`Any`, :class:`OptionChoice`]]] + The list of available choices for this option. + Can be a list of values or :class:`OptionChoice` objects (which represent a name:value pair). + If provided, the input from the user must match one of the choices in the list. + required: Optional[:class:`bool`] + Whether this option is required. + default: Optional[:class:`Any`] + The default value for this option. If provided, ``required`` will be considered ``False``. + min_value: Optional[:class:`int`] + The minimum value that can be entered. + Only applies to Options with an :attr:`.input_type` of :class:`int` or :class:`float`. + max_value: Optional[:class:`int`] + The maximum value that can be entered. + Only applies to Options with an :attr:`.input_type` of :class:`int` or :class:`float`. + min_length: Optional[:class:`int`] + The minimum length of the string that can be entered. Must be between 0 and 6000 (inclusive). + Only applies to Options with an :attr:`input_type` of :class:`str`. + max_length: Optional[:class:`int`] + The maximum length of the string that can be entered. Must be between 1 and 6000 (inclusive). + Only applies to Options with an :attr:`input_type` of :class:`str`. + channel_types: list[:class:`discord.ChannelType`] | None + A list of channel types that can be selected in this option. + Only applies to Options with an :attr:`input_type` of :class:`discord.SlashCommandOptionType.channel`. + If this argument is used, :attr:`input_type` will be ignored. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this option. The values of this should be ``"locale": "name"``. + See `here `_ for a list of valid locales. + description_localizations: Dict[:class:`str`, :class:`str`] + The description localizations for this option. The values of this should be ``"locale": "description"``. + See `here `_ for a list of valid locales. + + Examples + -------- + Basic usage: :: + + @bot.slash_command(guild_ids=[...]) + async def hello( + ctx: discord.ApplicationContext, + name: Option(str, "Enter your name"), + age: Option(int, "Enter your age", min_value=1, max_value=99, default=18) + # passing the default value makes an argument optional + # you also can create optional argument using: + # age: Option(int, "Enter your age") = 18 + ): + await ctx.respond(f"Hello! Your name is {name} and you are {age} years old.") + + .. versionadded:: 2.0 + """ + + input_type: SlashCommandOptionType + converter: Converter | type[Converter] | None = None + + def __init__( + self, input_type: InputType = str, /, description: str | None = None, **kwargs + ) -> None: + self.name: str | None = kwargs.pop("name", None) + if self.name is not None: + self.name = str(self.name) + self._parameter_name = self.name # default + input_type = self._parse_type_alias(input_type) + input_type = self._strip_none_type(input_type) + self._raw_type: InputType | tuple = input_type + + enum_choices = [] + input_type_is_class = isinstance(input_type, type) + if input_type_is_class and issubclass(input_type, (Enum, DiscordEnum)): + if description is None and input_type.__doc__ is not None: + description = inspect.cleandoc(input_type.__doc__) + if description and len(description) > 100: + description = description[:97] + "..." + _log.warning( + "Option %s's description was truncated due to Enum %s's docstring exceeding 100 characters.", + self.name, + input_type, + ) + enum_choices = [OptionChoice(e.name, e.value) for e in input_type] + value_class = enum_choices[0].value.__class__ + if value_class in SlashCommandOptionType.__members__ and all( + isinstance(elem.value, value_class) for elem in enum_choices + ): + input_type = SlashCommandOptionType.from_datatype( + enum_choices[0].value.__class__ + ) + else: + enum_choices = [OptionChoice(e.name, str(e.value)) for e in input_type] + input_type = SlashCommandOptionType.string + + self.description = description or "No description provided" + self.channel_types: list[ChannelType] = kwargs.pop("channel_types", []) + + if self.channel_types: + self.input_type = SlashCommandOptionType.channel + elif isinstance(input_type, SlashCommandOptionType): + self.input_type = input_type + else: + from ..ext.commands import Converter + + if isinstance(input_type, tuple) and any( + issubclass(op, ApplicationContext) for op in input_type + ): + input_type = next( + op for op in input_type if issubclass(op, ApplicationContext) + ) + + if ( + isinstance(input_type, Converter) + or input_type_is_class + and issubclass(input_type, Converter) + ): + self.converter = input_type + self._raw_type = str + self.input_type = SlashCommandOptionType.string + else: + try: + self.input_type = SlashCommandOptionType.from_datatype(input_type) + except TypeError as exc: + from ..ext.commands.converter import CONVERTER_MAPPING + + if input_type not in CONVERTER_MAPPING: + raise exc + self.converter = CONVERTER_MAPPING[input_type] + self._raw_type = str + self.input_type = SlashCommandOptionType.string + else: + if self.input_type == SlashCommandOptionType.channel: + if not isinstance(self._raw_type, tuple): + if hasattr(input_type, "__args__"): + self._raw_type = input_type.__args__ # type: ignore # Union.__args__ + else: + self._raw_type = (input_type,) + if not self.channel_types: + self.channel_types = [ + CHANNEL_TYPE_MAP[t] + for t in self._raw_type + if t is not GuildChannel + ] + self.required: bool = ( + kwargs.pop("required", True) if "default" not in kwargs else False + ) + self.default = kwargs.pop("default", None) + + self._autocomplete: AutocompleteFunction | None = None + self._autocomplete_is_instance_method: bool = False + self.autocomplete = kwargs.pop("autocomplete", None) + if len(enum_choices) > 25: + self.choices: list[OptionChoice] = [] + for e in enum_choices: + e.value = str(e.value) + self.autocomplete = basic_autocomplete(enum_choices) + self.input_type = SlashCommandOptionType.string + else: + self.choices: list[OptionChoice] = enum_choices or [ + o if isinstance(o, OptionChoice) else OptionChoice(o) + for o in kwargs.pop("choices", []) + ] + + if self.input_type == SlashCommandOptionType.integer: + minmax_types = (int, type(None)) + minmax_typehint = Optional[int] + elif self.input_type == SlashCommandOptionType.number: + minmax_types = (int, float, type(None)) + minmax_typehint = Optional[Union[int, float]] + else: + minmax_types = (type(None),) + minmax_typehint = type(None) + + if self.input_type == SlashCommandOptionType.string: + minmax_length_types = (int, type(None)) + minmax_length_typehint = Optional[int] + else: + minmax_length_types = (type(None),) + minmax_length_typehint = type(None) + + self.min_value: int | float | None = kwargs.pop("min_value", None) + self.max_value: int | float | None = kwargs.pop("max_value", None) + self.min_length: int | None = kwargs.pop("min_length", None) + self.max_length: int | None = kwargs.pop("max_length", None) + + if ( + self.input_type != SlashCommandOptionType.integer + and self.input_type != SlashCommandOptionType.number + and (self.min_value or self.max_value) + ): + raise AttributeError( + "Option does not take min_value or max_value if not of type " + "SlashCommandOptionType.integer or SlashCommandOptionType.number" + ) + if self.input_type != SlashCommandOptionType.string and ( + self.min_length or self.max_length + ): + raise AttributeError( + "Option does not take min_length or max_length if not of type str" + ) + + if self.min_value is not None and not isinstance(self.min_value, minmax_types): + raise TypeError( + f"Expected {minmax_typehint} for min_value, got" + f' "{type(self.min_value).__name__}"' + ) + if self.max_value is not None and not isinstance(self.max_value, minmax_types): + raise TypeError( + f"Expected {minmax_typehint} for max_value, got" + f' "{type(self.max_value).__name__}"' + ) + + if self.min_length is not None: + if not isinstance(self.min_length, minmax_length_types): + raise TypeError( + f"Expected {minmax_length_typehint} for min_length," + f' got "{type(self.min_length).__name__}"' + ) + if self.min_length < 0 or self.min_length > 6000: + raise AttributeError( + "min_length must be between 0 and 6000 (inclusive)" + ) + if self.max_length is not None: + if not isinstance(self.max_length, minmax_length_types): + raise TypeError( + f"Expected {minmax_length_typehint} for max_length," + f' got "{type(self.max_length).__name__}"' + ) + if self.max_length < 1 or self.max_length > 6000: + raise AttributeError("max_length must between 1 and 6000 (inclusive)") + + self.name_localizations = kwargs.pop("name_localizations", MISSING) + self.description_localizations = kwargs.pop( + "description_localizations", MISSING + ) + + if input_type is None: + raise TypeError("input_type cannot be NoneType.") + + @staticmethod + def _parse_type_alias(input_type: InputType) -> InputType: + if isinstance(input_type, TypeAliasType): + return input_type.__value__ + return input_type + + @staticmethod + def _strip_none_type(input_type): + if isinstance(input_type, SlashCommandOptionType): + return input_type + + if input_type is type(None): + raise TypeError("Option type cannot be only NoneType") + + args = () + if isinstance(input_type, types.UnionType): + args = get_args(input_type) + elif getattr(input_type, "__origin__", None) is Union: + args = get_args(input_type) + elif isinstance(input_type, tuple): + args = input_type + + if args: + filtered = tuple(t for t in args if t is not type(None)) + if not filtered: + raise TypeError("Option type cannot be only NoneType") + if len(filtered) == 1: + return filtered[0] + + return filtered + + return input_type + + def to_dict(self) -> dict: + as_dict = { + "name": self.name, + "description": self.description, + "type": self.input_type.value, + "required": self.required, + "choices": [c.to_dict() for c in self.choices], + "autocomplete": bool(self.autocomplete), + } + if self.name_localizations is not MISSING: + as_dict["name_localizations"] = self.name_localizations + if self.description_localizations is not MISSING: + as_dict["description_localizations"] = self.description_localizations + if self.channel_types: + as_dict["channel_types"] = [t.value for t in self.channel_types] + if self.min_value is not None: + as_dict["min_value"] = self.min_value + if self.max_value is not None: + as_dict["max_value"] = self.max_value + if self.min_length is not None: + as_dict["min_length"] = self.min_length + if self.max_length is not None: + as_dict["max_length"] = self.max_length + + return as_dict + + def __repr__(self): + return f"" + + @property + def autocomplete(self) -> AutocompleteFunction | None: + """ + The autocomplete handler for the option. Accepts a callable (sync or async) + that takes a single required argument of :class:`AutocompleteContext` or two arguments + of :class:`discord.Cog` (being the command's cog) and :class:`AutocompleteContext`. + The callable must return an iterable of :class:`str` or :class:`OptionChoice`. + Alternatively, :func:`discord.utils.basic_autocomplete` may be used in place of the callable. + + Returns + ------- + Optional[AutocompleteFunction] + + .. versionchanged:: 2.7 + + .. note:: + Does not validate the input value against the autocomplete results. + """ + return self._autocomplete + + @autocomplete.setter + def autocomplete(self, value: AutocompleteFunction | None) -> None: + self._autocomplete = value + # this is done here so it does not have to be computed every time the autocomplete is invoked + if self._autocomplete is not None: + self._autocomplete_is_instance_method = ( + sum( + 1 + for param in inspect.signature( + self._autocomplete + ).parameters.values() + if param.default == param.empty + and param.kind not in (param.VAR_POSITIONAL, param.VAR_KEYWORD) + ) + == 2 + ) + + +class OptionChoice: + """ + Represents a name:value pairing for a selected :class:`.Option`. + + .. versionadded:: 2.0 + + Attributes + ---------- + name: :class:`str` + The name of the choice. Shown in the UI when selecting an option. + value: Optional[Union[:class:`str`, :class:`int`, :class:`float`]] + The value of the choice. If not provided, will use the value of ``name``. + name_localizations: Dict[:class:`str`, :class:`str`] + The name localizations for this choice. The values of this should be ``"locale": "name"``. + See `here `_ for a list of valid locales. + """ + + def __init__( + self, + name: str, + value: str | int | float | None = None, + name_localizations: dict[str, str] = MISSING, + ): + self.name = str(name) + self.value = value if value is not None else name + self.name_localizations = name_localizations + + def to_dict(self) -> dict[str, str | int | float]: + as_dict = {"name": self.name, "value": self.value} + if self.name_localizations is not MISSING: + as_dict["name_localizations"] = self.name_localizations + + return as_dict + + +def option(name, input_type=None, **kwargs): + """A decorator that can be used instead of typehinting :class:`.Option`. + + .. versionadded:: 2.0 + + Attributes + ---------- + parameter_name: :class:`str` + The name of the target function parameter this option is mapped to. + This allows you to have a separate UI ``name`` and parameter name. + """ + + def decorator(func): + resolved_name = kwargs.pop("parameter_name", None) or name + itype = ( + kwargs.pop("type", None) + or input_type + or func.__annotations__.get(resolved_name, str) + ) + func.__annotations__[resolved_name] = Option(itype, name=name, **kwargs) + return func + + return decorator diff --git a/venv/lib/python3.11/site-packages/discord/commands/permissions.py b/venv/lib/python3.11/site-packages/discord/commands/permissions.py new file mode 100644 index 0000000..daf633b --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/commands/permissions.py @@ -0,0 +1,139 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from typing import Callable + +from ..enums import InteractionContextType +from ..permissions import Permissions +from .core import ApplicationCommand + +__all__ = ("default_permissions", "guild_only", "is_nsfw") + + +def default_permissions(**perms: bool) -> Callable: + """A decorator that limits the usage of an application command to members with certain + permissions. + + The permissions passed in must be exactly like the properties shown under + :class:`.discord.Permissions`. + + .. note:: + These permissions can be updated by server administrators per-guild. As such, these are only "defaults", as the + name suggests. If you want to make sure that a user **always** has the specified permissions regardless, you + should use an internal check such as :func:`~.ext.commands.has_permissions`. + + Parameters + ---------- + **perms: Dict[:class:`str`, :class:`bool`] + An argument list of permissions to check for. + + Example + ------- + + .. code-block:: python3 + + from discord import default_permissions + + @bot.slash_command() + @default_permissions(manage_messages=True) + async def test(ctx): + await ctx.respond('You can manage messages.') + """ + + invalid = set(perms) - set(Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + + def inner(command: Callable): + if isinstance(command, ApplicationCommand): + if command.parent is not None: + raise RuntimeError( + "Permission restrictions can only be set on top-level commands" + ) + command.default_member_permissions = Permissions(**perms) + else: + command.__default_member_permissions__ = Permissions(**perms) + return command + + return inner + + +def guild_only() -> Callable: + """A decorator that limits the usage of an application command to guild contexts. + The command won't be able to be used in private message channels. + + Example + ------- + + .. code-block:: python3 + + from discord import guild_only + + @bot.slash_command() + @guild_only() + async def test(ctx): + await ctx.respond("You're in a guild.") + """ + + def inner(command: Callable): + if isinstance(command, ApplicationCommand): + command.contexts = {InteractionContextType.guild} + else: + command.__contexts__ = {InteractionContextType.guild} + + return command + + return inner + + +def is_nsfw() -> Callable: + """A decorator that limits the usage of an application command to 18+ channels and users. + In guilds, the command will only be able to be used in channels marked as NSFW. + In DMs, users must have opted into age-restricted commands via privacy settings. + + Note that apps intending to be listed in the App Directory cannot have NSFW commands. + + Example + ------- + + .. code-block:: python3 + + from discord import is_nsfw + + @bot.slash_command() + @is_nsfw() + async def test(ctx): + await ctx.respond("This command is age restricted.") + """ + + def inner(command: Callable): + if isinstance(command, ApplicationCommand): + command.nsfw = True + else: + command.__nsfw__ = True + + return command + + return inner diff --git a/venv/lib/python3.11/site-packages/discord/components.py b/venv/lib/python3.11/site-packages/discord/components.py new file mode 100644 index 0000000..d6e1eb8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/components.py @@ -0,0 +1,1824 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, ClassVar, Iterator, TypeVar, overload + +from .asset import AssetMixin +from .colour import Colour +from .enums import ( + ButtonStyle, + ChannelType, + ComponentType, + InputTextStyle, + SelectDefaultValueType, + SeparatorSpacingSize, + try_enum, +) +from .flags import AttachmentFlags +from .partial_emoji import PartialEmoji, _EmojiTag +from .utils import MISSING, find, get_slots + +if TYPE_CHECKING: + from . import abc + from .emoji import AppEmoji, GuildEmoji + from .types.components import ActionRow as ActionRowPayload + from .types.components import ButtonComponent as ButtonComponentPayload + from .types.components import CheckboxComponent as CheckboxComponentPayload + from .types.components import ( + CheckboxGroupComponent as CheckboxGroupComponentPayload, + ) + from .types.components import CheckboxGroupOption as CheckboxGroupOptionPayload + from .types.components import Component as ComponentPayload + from .types.components import ContainerComponent as ContainerComponentPayload + from .types.components import FileComponent as FileComponentPayload + from .types.components import FileUploadComponent as FileUploadComponentPayload + from .types.components import InputText as InputTextComponentPayload + from .types.components import LabelComponent as LabelComponentPayload + from .types.components import MediaGalleryComponent as MediaGalleryComponentPayload + from .types.components import MediaGalleryItem as MediaGalleryItemPayload + from .types.components import RadioGroupComponent as RadioGroupComponentPayload + from .types.components import RadioGroupOption as RadioGroupOptionPayload + from .types.components import SectionComponent as SectionComponentPayload + from .types.components import SelectDefaultValue as SelectDefaultValuePayload + from .types.components import SelectMenu as SelectMenuPayload + from .types.components import SelectOption as SelectOptionPayload + from .types.components import SeparatorComponent as SeparatorComponentPayload + from .types.components import TextDisplayComponent as TextDisplayComponentPayload + from .types.components import ThumbnailComponent as ThumbnailComponentPayload + from .types.components import UnfurledMediaItem as UnfurledMediaItemPayload + +__all__ = ( + "Component", + "ActionRow", + "Button", + "SelectMenu", + "SelectOption", + "InputText", + "Section", + "TextDisplay", + "Thumbnail", + "MediaGallery", + "MediaGalleryItem", + "UnfurledMediaItem", + "FileComponent", + "Separator", + "Container", + "Label", + "SelectDefaultValue", + "FileUpload", + "RadioGroup", + "RadioGroupOption", + "CheckboxGroup", + "CheckboxGroupOption", + "Checkbox", +) + +C = TypeVar("C", bound="Component") + + +class Component: + """Represents a Discord Bot UI Kit Component. + + The components supported by Discord in messages are as follows: + + - :class:`ActionRow` + - :class:`Button` + - :class:`SelectMenu` + - :class:`Section` + - :class:`TextDisplay` + - :class:`Thumbnail` + - :class:`MediaGallery` + - :class:`FileComponent` + - :class:`Separator` + - :class:`Container` + + This class is abstract and cannot be instantiated. + + .. versionadded:: 2.0 + + Attributes + ---------- + type: :class:`ComponentType` + The type of component. + id: :class:`int` + The component's ID. If not provided by the user, it is set sequentially by Discord. + The ID `0` is treated as if no ID was provided. + """ + + __slots__: tuple[str, ...] = ("type", "id") + + __repr_info__: ClassVar[tuple[str, ...]] + type: ComponentType + versions: tuple[int, ...] + + def __repr__(self) -> str: + attrs = " ".join(f"{key}={getattr(self, key)!r}" for key in self.__repr_info__) + return f"<{self.__class__.__name__} {attrs}>" + + @classmethod + def _raw_construct(cls: type[C], **kwargs) -> C: + self: C = cls.__new__(cls) + for slot in get_slots(cls): + try: + value = kwargs[slot] + except KeyError: + setattr(self, slot, None) + else: + setattr(self, slot, value) + return self + + def to_dict(self) -> dict[str, Any]: + raise NotImplementedError + + def is_v2(self) -> bool: + """Whether this component was introduced in Components V2.""" + return self.versions and 1 not in self.versions + + +class ActionRow(Component): + """Represents a Discord Bot UI Kit Action Row. + + This is a component that holds up to 5 children components in a row. + + This inherits from :class:`Component`. + + .. versionadded:: 2.0 + + Attributes + ---------- + type: :class:`ComponentType` + The type of component. + children: List[:class:`Component`] + The children components that this holds, if any. + """ + + __slots__: tuple[str, ...] = ("children",) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (1, 2) + + def __init__(self, data: ComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.children: list[Component] = [ + _component_factory(d) for d in data.get("components", []) + ] + + @property + def width(self): + """Returns the sum of the item's widths.""" + t = 0 + for item in self.children: + t += 1 if item.type is ComponentType.button else 5 + return t + + def to_dict(self) -> ActionRowPayload: + return { + "type": int(self.type), + "id": self.id, + "components": [child.to_dict() for child in self.children], + } # type: ignore + + def walk_components(self) -> Iterator[Component]: + yield from self.children + + @property + def components(self) -> list[Component]: + return self.children + + def get_component(self, id: str | int) -> Component | None: + """Get a component from this action row. Roughly equivalent to `utils.get(row.children, ...)`. + If an ``int`` is provided, the component will be retrieved by ``id``, otherwise by ``custom_id``. + + Parameters + ---------- + id: Union[:class:`str`, :class:`int`] + The custom_id or id of the component to get. + + Returns + ------- + Optional[:class:`Component`] + The component with the matching ``id`` or ``custom_id`` if it exists. + """ + if not id: + return None + attr = "id" if isinstance(id, int) else "custom_id" + return find(lambda i: getattr(i, attr, None) == id, self.children) + + @classmethod + def with_components(cls, *components, id=None): + return cls._raw_construct( + type=ComponentType.action_row, id=id, children=[c for c in components] + ) + + +class InputText(Component): + """Represents an Input Text field from the Discord Bot UI Kit. + This inherits from :class:`Component`. + + Attributes + ---------- + style: :class:`.InputTextStyle` + The style of the input text field. + custom_id: Optional[:class:`str`] + The custom ID of the input text field that gets received during an interaction. + label: :class:`str` + The label for the input text field. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_length: Optional[:class:`int`] + The minimum number of characters that must be entered + Defaults to 0 + max_length: Optional[:class:`int`] + The maximum number of characters that can be entered + required: Optional[:class:`bool`] + Whether the input text field is required or not. Defaults to `True`. + value: Optional[:class:`str`] + The value that has been entered in the input text field. + id: Optional[:class:`int`] + The input text's ID. + """ + + __slots__: tuple[str, ...] = ( + "type", + "style", + "custom_id", + "label", + "placeholder", + "min_length", + "max_length", + "required", + "value", + "id", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (1, 2) + + def __init__(self, data: InputTextComponentPayload): + self.type = ComponentType.input_text + self.id: int | None = data.get("id") + self.style: InputTextStyle = try_enum(InputTextStyle, data["style"]) + self.custom_id = data["custom_id"] + self.label: str | None = data.get("label", None) + self.placeholder: str | None = data.get("placeholder", None) + self.min_length: int | None = data.get("min_length", None) + self.max_length: int | None = data.get("max_length", None) + self.required: bool = data.get("required", True) + self.value: str | None = data.get("value", None) + + def to_dict(self) -> InputTextComponentPayload: + payload = { + "type": 4, + "id": self.id, + "style": self.style.value, + } + if self.custom_id: + payload["custom_id"] = self.custom_id + + if self.placeholder: + payload["placeholder"] = self.placeholder + + if self.min_length: + payload["min_length"] = self.min_length + + if self.max_length: + payload["max_length"] = self.max_length + + if not self.required: + payload["required"] = self.required + + if self.value: + payload["value"] = self.value + + if self.label: + payload["label"] = self.label + + return payload # type: ignore + + +class Button(Component): + """Represents a button from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.Button` instead. + + .. versionadded:: 2.0 + + Attributes + ---------- + style: :class:`.ButtonStyle` + The style of the button. + custom_id: Optional[:class:`str`] + The ID of the button that gets received during an interaction. + If this button is for a URL, it does not have a custom ID. + url: Optional[:class:`str`] + The URL this button sends you to. + disabled: :class:`bool` + Whether the button is disabled or not. + label: Optional[:class:`str`] + The label of the button, if any. + emoji: Optional[:class:`PartialEmoji`] + The emoji of the button, if available. + sku_id: Optional[:class:`int`] + The ID of the SKU this button refers to. + """ + + __slots__: tuple[str, ...] = ( + "style", + "custom_id", + "url", + "disabled", + "label", + "emoji", + "sku_id", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (1, 2) + + def __init__(self, data: ButtonComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.style: ButtonStyle = try_enum(ButtonStyle, data["style"]) + self.custom_id: str | None = data.get("custom_id") + self.url: str | None = data.get("url") + self.disabled: bool = data.get("disabled", False) + self.label: str | None = data.get("label") + self.emoji: PartialEmoji | None + if e := data.get("emoji"): + self.emoji = PartialEmoji.from_dict(e) + else: + self.emoji = None + self.sku_id: str | None = data.get("sku_id") + + def to_dict(self) -> ButtonComponentPayload: + payload = { + "type": 2, + "id": self.id, + "style": int(self.style), + "label": self.label, + "disabled": self.disabled, + } + if self.custom_id: + payload["custom_id"] = self.custom_id + + if self.url: + payload["url"] = self.url + + if self.emoji: + payload["emoji"] = self.emoji.to_dict() + + if self.sku_id: + payload["sku_id"] = self.sku_id + + return payload # type: ignore + + +class SelectMenu(Component): + """Represents a select menu from the Discord Bot UI Kit. + + A select menu is functionally the same as a dropdown, however + on mobile it renders a bit differently. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.Select` instead. + + .. versionadded:: 2.0 + + .. versionchanged:: 2.3 + + Added support for :attr:`ComponentType.user_select`, :attr:`ComponentType.role_select`, + :attr:`ComponentType.mentionable_select`, and :attr:`ComponentType.channel_select`. + + .. versionchanged:: 2.7 + + Added the :attr:`required` attribute for use in modals. + + Attributes + ---------- + type: :class:`ComponentType` + The select menu's type. + custom_id: Optional[:class:`str`] + The ID of the select menu that gets received during an interaction. + placeholder: Optional[:class:`str`] + The placeholder text that is shown if nothing is selected, if any. + min_values: :class:`int` + The minimum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 0 and 25. + max_values: :class:`int` + The maximum number of items that must be chosen for this select menu. + Defaults to 1 and must be between 1 and 25. + options: List[:class:`SelectOption`] + A list of options that can be selected in this menu. + Will be an empty list for all component types + except for :attr:`ComponentType.string_select`. + channel_types: List[:class:`ChannelType`] + A list of channel types that can be selected. + Will be an empty list for all component types + except for :attr:`ComponentType.channel_select`. + disabled: :class:`bool` + Whether the select is disabled or not. Not usable in modals. Defaults to ``False``. + required: Optional[:class:`bool`] + Whether the select is required or not. Only useable in modals. Defaults to ``True``. + """ + + __slots__: tuple[str, ...] = ( + "custom_id", + "placeholder", + "min_values", + "max_values", + "options", + "channel_types", + "disabled", + "required", + "default_values", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (1, 2) + + def __init__(self, data: SelectMenuPayload): + self.type = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.custom_id: str = data["custom_id"] + self.placeholder: str | None = data.get("placeholder") + self.min_values: int = data.get("min_values", 1) + self.max_values: int = data.get("max_values", 1) + self.disabled: bool = data.get("disabled", False) + self.options: list[SelectOption] = [ + SelectOption.from_dict(option) for option in data.get("options", []) + ] + self.channel_types: list[ChannelType] = [ + try_enum(ChannelType, ct) for ct in data.get("channel_types", []) + ] + self.required: bool | None = data.get("required") + self.default_values: list[SelectDefaultValue] = SelectDefaultValue._from_data( + data.get("default_values") + ) + + def to_dict(self) -> SelectMenuPayload: + payload: SelectMenuPayload = { + "type": self.type.value, + "id": self.id, + "custom_id": self.custom_id, + "min_values": self.min_values, + "max_values": self.max_values, + "disabled": self.disabled, + } + + if self.type is ComponentType.string_select: + payload["options"] = [op.to_dict() for op in self.options] + if self.type is ComponentType.channel_select and self.channel_types: + payload["channel_types"] = [ct.value for ct in self.channel_types] + if self.placeholder: + payload["placeholder"] = self.placeholder + if self.required is not None: + payload["required"] = self.required + if self.type is not ComponentType.string_select: + payload["default_values"] = [dv.to_dict() for dv in self.default_values] + + return payload + + +class SelectDefaultValue: + r"""Represents a :class:`discord.SelectMenu`\s default value. + + This is only applicable to selects of type other than :attr:`ComponentType.string_select`. + + .. versionadded:: 2.7 + + Parameters + ---------- + object: :class:`abc.Snowflake` + The model type this select default value is based of. + + Below, is a table defining the model instance type and the default value type it will be mapped: + + +-----------------------------------+--------------------------------------------------------------------------+ + | Model Type | Default Value Type | + +-----------------------------------+--------------------------------------------------------------------------+ + | :class:`discord.User` | :attr:`discord.SelectDefaultValueType.user` | + +-----------------------------------+--------------------------------------------------------------------------+ + | :class:`discord.Member` | :attr:`discord.SelectDefaultValueType.user` | + +-----------------------------------+--------------------------------------------------------------------------+ + | :class:`discord.Role` | :attr:`discord.SelectDefaultValueType.role` | + +-----------------------------------+--------------------------------------------------------------------------+ + | :class:`discord.abc.GuildChannel` | :attr:`discord.SelectDefaultValueType.channel` | + +-----------------------------------+--------------------------------------------------------------------------+ + | :class:`discord.Object` | depending on :attr:`discord.Object.type`, it will be mapped to any above | + +-----------------------------------+--------------------------------------------------------------------------+ + + If you pass a model that is not defined in the table, ``TypeError`` will be raised. + + .. note:: + + The :class:`discord.abc.GuildChannel` protocol includes :class:`discord.TextChannel`, :class:`discord.VoiceChannel`, :class:`discord.StageChannel`, + :class:`discord.ForumChannel`, :class:`discord.Thread`, :class:`discord.MediaChannel`. This list is not exhaustive, and is bound to change + based of the new channel types Discord adds. + + id: :class:`int` + The ID of the default value. This cannot be used with ``object``. + type: :class:`SelectDefaultValueType` + The default value type. This cannot be used with ``object``. + + Raises + ------ + TypeError + You did not provide any parameter, you provided all parameters, or you provided ``id`` but not ``type``. + """ + + __slots__ = ("id", "type") + + @overload + def __init__( + self, + object: abc.Snowflake, + /, + ) -> None: ... + + @overload + def __init__( + self, + /, + *, + id: int, + type: SelectDefaultValueType, + ) -> None: ... + + def __init__( + self, + object: abc.Snowflake = MISSING, + /, + *, + id: int = MISSING, + type: SelectDefaultValueType = MISSING, + ) -> None: + self.id: int = id + self.type: SelectDefaultValueType = type + if object is not MISSING: + if any(p is not MISSING for p in (id, type)): + raise TypeError("you cannot pass id or type when passing object") + self._handle_model(object, inst=self) + elif id is not MISSING and type is not MISSING: + self.id = id + self.type = type + else: + raise TypeError("you must provide an object model, or an id and type") + + def __repr__(self) -> str: + return f"" + + @classmethod + def _from_data( + cls, default_values: list[SelectDefaultValuePayload] | None + ) -> list[SelectDefaultValue]: + if not default_values: + return [] + return [ + cls(id=int(d["id"]), type=try_enum(SelectDefaultValueType, d["type"])) + for d in default_values + ] + + @classmethod + def _handle_model( + cls, + model: abc.Snowflake, + select_type: ComponentType | None = None, + inst: SelectDefaultValue | None = None, + ) -> SelectDefaultValue: + # preventing >circular imports< + from discord import Member, Object, Role, User, abc + from discord.user import _UserTag + + instances_mapping: dict[ + type, tuple[tuple[ComponentType, ...], SelectDefaultValueType] + ] = { + Role: ( + (ComponentType.role_select, ComponentType.mentionable_select), + SelectDefaultValueType.role, + ), + User: ( + (ComponentType.user_select, ComponentType.mentionable_select), + SelectDefaultValueType.user, + ), + Member: ( + (ComponentType.user_select, ComponentType.mentionable_select), + SelectDefaultValueType.user, + ), + _UserTag: ( + (ComponentType.user_select, ComponentType.mentionable_select), + SelectDefaultValueType.user, + ), + abc.GuildChannel: ( + (ComponentType.channel_select,), + SelectDefaultValueType.channel, + ), + } + + obj_id = model.id + obj_type = model.__class__ + + if isinstance(model, Object): + obj_type = model.type + + sel_types = None + def_type = None + + for typ, (st, dt) in instances_mapping.items(): + if issubclass(obj_type, typ): + sel_types = st + def_type = dt + break + + if sel_types is None or def_type is None: + raise TypeError( + f"{obj_type.__name__} is not a valid instance for a select default value" + ) + + # we can't actually check select types when not in a select context + if select_type is not None and select_type not in sel_types: + raise TypeError( + f"{model.__class__.__name__} objects can not be set as a default value for {select_type.value} selects", + ) + + if inst is None: + return cls(id=obj_id, type=def_type) + else: + inst.id = obj_id + inst.type = def_type + return inst + + def to_dict(self) -> SelectDefaultValuePayload: + return { + "id": self.id, + "type": self.type.value, + } + + +class SelectOption: + """Represents a :class:`discord.SelectMenu`'s option. + + These can be created by users. + + .. versionadded:: 2.0 + + Attributes + ---------- + label: :class:`str` + The label of the option. This is displayed to users. + Can only be up to 100 characters. + value: :class:`str` + The value of the option. This is not displayed to users. + If not provided when constructed then it defaults to the + label. Can only be up to 100 characters. + description: Optional[:class:`str`] + An additional description of the option, if any. + Can only be up to 100 characters. + default: :class:`bool` + Whether this option is selected by default. + """ + + __slots__: tuple[str, ...] = ( + "label", + "value", + "description", + "_emoji", + "default", + ) + + def __init__( + self, + *, + label: str, + value: str = MISSING, + description: str | None = None, + emoji: str | GuildEmoji | AppEmoji | PartialEmoji | None = None, + default: bool = False, + ) -> None: + if len(label) > 100: + raise ValueError("label must be 100 characters or fewer") + + if value is not MISSING and len(value) > 100: + raise ValueError("value must be 100 characters or fewer") + + if description is not None and len(description) > 100: + raise ValueError("description must be 100 characters or fewer") + + self.label = label + self.value = label if value is MISSING else value + self.description = description + self.emoji = emoji + self.default = default + + def __repr__(self) -> str: + return ( + "" + ) + + def __str__(self) -> str: + base = f"{self.emoji} {self.label}" if self.emoji else self.label + if self.description: + return f"{base}\n{self.description}" + return base + + @property + def emoji(self) -> str | GuildEmoji | AppEmoji | PartialEmoji | None: + """The emoji of the option, if available.""" + return self._emoji + + @emoji.setter + def emoji(self, value) -> None: + if value is not None: + if isinstance(value, str): + value = PartialEmoji.from_str(value) + elif isinstance(value, _EmojiTag): + value = value._to_partial() + else: + raise TypeError( + "expected emoji to be str, GuildEmoji, AppEmoji, or PartialEmoji, not" + f" {value.__class__}" + ) + + self._emoji = value + + @classmethod + def from_dict(cls, data: SelectOptionPayload) -> SelectOption: + if e := data.get("emoji"): + emoji = PartialEmoji.from_dict(e) + else: + emoji = None + + return cls( + label=data["label"], + value=data["value"], + description=data.get("description"), + emoji=emoji, + default=data.get("default", False), + ) + + def to_dict(self) -> SelectOptionPayload: + payload: SelectOptionPayload = { + "label": self.label, + "value": self.value, + "default": self.default, + } + + if self.emoji: + payload["emoji"] = self.emoji.to_dict() # type: ignore + + if self.description: + payload["description"] = self.description + + return payload + + +class Section(Component): + """Represents a Section from Components V2. + + This is a component that groups other components together with an additional component to the right as the accessory. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.Section` instead. + + .. versionadded:: 2.7 + + Attributes + ---------- + components: List[:class:`Component`] + The components contained in this section. Currently supports :class:`TextDisplay`. + accessory: Optional[:class:`Component`] + The accessory attached to this Section. Currently supports :class:`Button` and :class:`Thumbnail`. + """ + + __slots__: tuple[str, ...] = ("components", "accessory") + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: SectionComponentPayload, state=None): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.components: list[Component] = [ + _component_factory(d, state=state) for d in data.get("components", []) + ] + self.accessory: Component | None = None + if _accessory := data.get("accessory"): + self.accessory = _component_factory(_accessory, state=state) + + def to_dict(self) -> SectionComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "components": [c.to_dict() for c in self.components], + } + if self.accessory: + payload["accessory"] = self.accessory.to_dict() + return payload + + def walk_components(self) -> Iterator[Component]: + r = self.components + if self.accessory: + yield from r + [self.accessory] + yield from r + + def get_component(self, id: str | int) -> Component | None: + """Get a component from this section. Roughly equivalent to `utils.get(section.walk_components(), ...)`. + If an ``int`` is provided, the component will be retrieved by ``id``, otherwise by ``custom_id``. + + Parameters + ---------- + id: Union[:class:`str`, :class:`int`] + The custom_id or id of the component to get. + + Returns + ------- + Optional[:class:`Component`] + The component with the matching ``id`` or ``custom_id`` if it exists. + """ + if not id: + return None + attr = "id" if isinstance(id, int) else "custom_id" + if self.accessory and id == getattr(self.accessory, attr, None): + return self.accessory + component = find(lambda i: getattr(i, attr, None) == id, self.components) + return component + + +class TextDisplay(Component): + """Represents a Text Display from Components V2. + + This is a component that displays text. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.TextDisplay` instead. + + .. versionadded:: 2.7 + + Attributes + ---------- + content: :class:`str` + The component's text content. + """ + + __slots__: tuple[str, ...] = ("content",) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: TextDisplayComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.content: str = data.get("content") + + def to_dict(self) -> TextDisplayComponentPayload: + return {"type": int(self.type), "id": self.id, "content": self.content} + + +class UnfurledMediaItem(AssetMixin): + """Represents an Unfurled Media Item used in Components V2. + + This is used as an underlying component for other media-based components such as :class:`Thumbnail`, :class:`FileComponent`, and :class:`MediaGalleryItem`. + + .. versionadded:: 2.7 + + Attributes + ---------- + url: :class:`str` + The URL of this media item. This can either be an arbitrary URL or an ``attachment://`` URL to work with local files. + """ + + def __init__(self, url: str): + self._state = None + self._url: str = url + self._static_url: str | None = ( + url if url and url.startswith("attachment://") else None + ) + self.proxy_url: str | None = None + self.height: int | None = None + self.width: int | None = None + self.content_type: str | None = None + self.flags: AttachmentFlags | None = None + self.attachment_id: int | None = None + + def __repr__(self) -> str: + return ( + f"" + ) + + def __str__(self) -> str: + return self.url or self.__repr__() + + @property + def url(self) -> str: + return self._url + + @url.setter + def url(self, value: str) -> None: + self._url = value + self._static_url = ( + value if value and value.startswith("attachment://") else None + ) + + @classmethod + def from_dict(cls, data: UnfurledMediaItemPayload, state=None) -> UnfurledMediaItem: + r = cls(data.get("url")) + r.proxy_url = data.get("proxy_url") + r.height = data.get("height") + r.width = data.get("width") + r.content_type = data.get("content_type") + r.flags = AttachmentFlags._from_value(data.get("flags", 0)) + r.attachment_id = data.get("attachment_id") + r._state = state + return r + + def to_dict(self) -> dict[str, str]: + return {"url": self._static_url or self.url} + + +class Thumbnail(Component): + """Represents a Thumbnail from Components V2. + + This is a component that displays media, such as images and videos. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.Thumbnail` instead. + + .. versionadded:: 2.7 + + Attributes + ---------- + media: :class:`UnfurledMediaItem` + The component's underlying media object. + description: Optional[:class:`str`] + The thumbnail's description, up to 1024 characters. + spoiler: Optional[:class:`bool`] + Whether the thumbnail has the spoiler overlay. + """ + + __slots__: tuple[str, ...] = ( + "media", + "description", + "spoiler", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: ThumbnailComponentPayload, state=None): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.media: UnfurledMediaItem = ( + umi := data.get("media") + ) and UnfurledMediaItem.from_dict(umi, state=state) + self.description: str | None = data.get("description") + self.spoiler: bool | None = data.get("spoiler") + + @property + def url(self) -> str: + """Returns the URL of this thumbnail's underlying media item.""" + return self.media.url + + def to_dict(self) -> ThumbnailComponentPayload: + payload = {"type": int(self.type), "id": self.id, "media": self.media.to_dict()} + if self.description: + payload["description"] = self.description + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + +class MediaGalleryItem: + """Represents an item used in the :class:`MediaGallery` component. + + This is used as an underlying component for other media-based components such as :class:`Thumbnail`, :class:`FileComponent`, and :class:`MediaGalleryItem`. + + .. versionadded:: 2.7 + + Attributes + ---------- + url: :class:`str` + The URL of this gallery item. This can either be an arbitrary URL or an ``attachment://`` URL to work with local files. + description: Optional[:class:`str`] + The gallery item's description, up to 1024 characters. + spoiler: Optional[:class:`bool`] + Whether the gallery item is a spoiler. + """ + + def __init__(self, url, *, description=None, spoiler=False): + self._state = None + self.media: UnfurledMediaItem = UnfurledMediaItem(url) + self.description: str | None = description + self.spoiler: bool = spoiler + + @property + def url(self) -> str: + return self.media.url + + def is_dispatchable(self) -> bool: + return False + + @classmethod + def from_dict(cls, data: MediaGalleryItemPayload, state=None) -> MediaGalleryItem: + media = (umi := data.get("media")) and UnfurledMediaItem.from_dict( + umi, state=state + ) + description = data.get("description") + spoiler = data.get("spoiler", False) + + r = cls( + url=media.url, + description=description, + spoiler=spoiler, + ) + r._state = state + r.media = media + return r + + def to_dict(self) -> dict[str, Any]: + payload = {"media": self.media.to_dict()} + if self.description: + payload["description"] = self.description + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + +class MediaGallery(Component): + """Represents a Media Gallery from Components V2. + + This is a component that displays up to 10 different :class:`MediaGalleryItem` objects. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.MediaGallery` instead. + + .. versionadded:: 2.7 + + Attributes + ---------- + items: List[:class:`MediaGalleryItem`] + The media this gallery contains. + """ + + __slots__: tuple[str, ...] = ("items",) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: MediaGalleryComponentPayload, state=None): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.items: list[MediaGalleryItem] = [ + MediaGalleryItem.from_dict(d, state=state) for d in data.get("items", []) + ] + + def to_dict(self) -> MediaGalleryComponentPayload: + return { + "type": int(self.type), + "id": self.id, + "items": [i.to_dict() for i in self.items], + } + + +class FileComponent(Component): + """Represents a File from Components V2. + + This component displays a downloadable file in a message. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.File` instead. + + .. versionadded:: 2.7 + + Attributes + ---------- + file: :class:`UnfurledMediaItem` + The file's media item. + name: Optional[:class:`str`] + The file's name. + size: Optional[:class:`int`] + The file's size in bytes. + spoiler: Optional[:class:`bool`] + Whether the file has the spoiler overlay. + """ + + __slots__: tuple[str, ...] = ( + "file", + "spoiler", + "name", + "size", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: FileComponentPayload, state=None): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.name: str | None = data.get("name") + self.size: int | None = data.get("size") + self.file: UnfurledMediaItem = UnfurledMediaItem.from_dict( + data.get("file", {}), state=state + ) + self.spoiler: bool | None = data.get("spoiler") + + def to_dict(self) -> FileComponentPayload: + payload = {"type": int(self.type), "id": self.id, "file": self.file.to_dict()} + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + +class Separator(Component): + """Represents a Separator from Components V2. + + This is a component that visually separates components. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.Separator` instead. + + .. versionadded:: 2.7 + + Attributes + ---------- + divider: :class:`bool` + Whether the separator will show a horizontal line in addition to vertical spacing. + spacing: Optional[:class:`SeparatorSpacingSize`] + The separator's spacing size. + """ + + __slots__: tuple[str, ...] = ( + "divider", + "spacing", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: SeparatorComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.divider: bool = data.get("divider") + self.spacing: SeparatorSpacingSize = try_enum( + SeparatorSpacingSize, data.get("spacing", 1) + ) + + def to_dict(self) -> SeparatorComponentPayload: + return { + "type": int(self.type), + "id": self.id, + "divider": self.divider, + "spacing": int(self.spacing), + } + + +class Container(Component): + """Represents a Container from Components V2. + + This is a component that contains different :class:`Component` objects. + It may only contain: + + - :class:`ActionRow` + - :class:`TextDisplay` + - :class:`Section` + - :class:`MediaGallery` + - :class:`Separator` + - :class:`FileComponent` + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.Container` instead. + + .. versionadded:: 2.7 + + Attributes + ---------- + components: List[:class:`Component`] + The components contained in this container. + accent_color: Optional[:class:`Colour`] + The accent color of the container. + spoiler: Optional[:class:`bool`] + Whether the entire container has the spoiler overlay. + """ + + __slots__: tuple[str, ...] = ( + "accent_color", + "spoiler", + "components", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: ContainerComponentPayload, state=None): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data.get("id") + self.accent_color: Colour | None = (c := data.get("accent_color")) and Colour( + c + ) # at this point, not adding alternative spelling + self.spoiler: bool | None = data.get("spoiler") + self.components: list[Component] = [ + _component_factory(d, state=state) for d in data.get("components", []) + ] + + def to_dict(self) -> ContainerComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "components": [c.to_dict() for c in self.components], + } + if self.accent_color: + payload["accent_color"] = self.accent_color.value + if self.spoiler is not None: + payload["spoiler"] = self.spoiler + return payload + + def walk_components(self) -> Iterator[Component]: + for c in self.components: + if hasattr(c, "walk_components"): + yield from c.walk_components() + else: + yield c + + def get_component(self, id: str | int) -> Component | None: + """Get a component from this container. Roughly equivalent to `utils.get(container.components, ...)`. + If an ``int`` is provided, the component will be retrieved by ``id``, otherwise by ``custom_id``. + This method will also search for nested components. + + Parameters + ---------- + id: Union[:class:`str`, :class:`int`] + The custom_id or id of the component to get. + + Returns + ------- + Optional[:class:`Component`] + The component with the matching ``id`` or ``custom_id`` if it exists. + """ + if not id: + return None + attr = "id" if isinstance(id, int) else "custom_id" + for i in self.components: + if getattr(i, attr, None) == id: + return i + elif hasattr(i, "get_component"): + if component := i.get_component(id): + return component + return None + + +class Label(Component): + """Represents a Label used in modals as the top-level component. + + This is a component that allows you to add additional text to another component. + ``component`` may only be: + + - :class:`InputText` + - :class:`SelectMenu` + + This inherits from :class:`Component`. + + .. versionadded:: 2.7 + + Attributes + ---------- + component: :class:`Component` + The component contained in this label. Currently supports :class:`InputText` and :class:`SelectMenu`. + label: :class:`str` + The main text associated with this label's ``component``, up to 45 characters. + description: Optional[:class:`str`] + The description associated with this label's ``component``, up to 100 characters. + """ + + __slots__: tuple[str, ...] = ("component", "label", "description") + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: LabelComponentPayload): + self.type: ComponentType = try_enum(ComponentType, data["type"]) + self.id: int = data["id"] + self.component: Component = _component_factory(data["component"]) + self.label: str = data["label"] + self.description: str | None = data.get("description") + + def to_dict(self) -> LabelComponentPayload: + payload = { + "type": int(self.type), + "id": self.id, + "component": self.component.to_dict(), + "label": self.label, + "description": self.description, + } + return payload + + def walk_components(self) -> Iterator[Component]: + yield from [self.component] + + +class FileUpload(Component): + """Represents an File Upload component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.FileUpload` instead. + + .. versionadded:: 2.7 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The custom ID of the file upload field that gets received during an interaction. + min_values: Optional[:class:`int`] + The minimum number of files that must be uploaded. + max_values: Optional[:class:`int`] + The maximum number of files that can be uploaded. + required: Optional[:class:`bool`] + Whether the file upload field is required or not. Defaults to `True`. + id: Optional[:class:`int`] + The file upload's ID. + """ + + __slots__: tuple[str, ...] = ( + "type", + "custom_id", + "min_values", + "max_values", + "required", + "id", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: FileUploadComponentPayload): + self.type = ComponentType.file_upload + self.id: int | None = data.get("id") + self.custom_id = data["custom_id"] + self.min_values: int | None = data.get("min_values", None) + self.max_values: int | None = data.get("max_values", None) + self.required: bool = data.get("required", True) + + def to_dict(self) -> FileUploadComponentPayload: + payload = { + "type": 19, + "custom_id": self.custom_id, + } + if self.id is not None: + payload["id"] = self.id + + if self.min_values is not None: + payload["min_values"] = self.min_values + + if self.max_values is not None: + payload["max_values"] = self.max_values + + if not self.required: + payload["required"] = self.required + + return payload # type: ignore + + +class RadioGroup(Component): + """Represents an Radio Group component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.RadioGroup` instead. + + .. versionadded:: 2.8 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The custom ID of the radio group that gets received during an interaction. + options: List[:class:`RadioGroupOption`] + A list of options that can be selected in this group, between 2 and 10. + required: Optional[:class:`bool`] + Whether the radio group requires a selection or not. Defaults to ``True``. + id: Optional[:class:`int`] + The radio group's ID. + """ + + __slots__: tuple[str, ...] = ( + "type", + "custom_id", + "options", + "required", + "id", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: RadioGroupComponentPayload): + self.type = ComponentType.radio_group + self.id: int | None = data.get("id") + self.custom_id = data.get("custom_id") + self.options: list[RadioGroupOption] = [ + RadioGroupOption.from_dict(option) for option in data.get("options", []) + ] + self.required: bool = data.get("required", True) + + def to_dict(self) -> RadioGroupComponentPayload: + payload = { + "type": 21, + "custom_id": self.custom_id, + "options": [opt.to_dict() for opt in self.options], + } + if self.id is not None: + payload["id"] = self.id + + if not self.required: + payload["required"] = self.required + + return payload + + +class RadioGroupOption: + """Represents a :class:`discord.RadioGroup`'s option. + + These can be created by users. + + .. versionadded:: 2.8 + + Attributes + ---------- + label: :class:`str` + The label of the option. This is displayed to users. + Can only be up to 100 characters. + value: :class:`str` + The value of the option. This is not displayed to users. + If not provided when constructed then it defaults to the + label. Can only be up to 100 characters. + description: Optional[:class:`str`] + An additional description of the option, if any. + Can only be up to 100 characters. + default: :class:`bool` + Whether this option is selected by default. Only 1 option should be set to default within a :class:`discord.RadioGroup`. + """ + + __slots__: tuple[str, ...] = ( + "label", + "value", + "description", + "default", + ) + + def __init__( + self, + *, + label: str, + value: str = MISSING, + description: str | None = None, + default: bool = False, + ) -> None: + if len(label) > 100: + raise ValueError("label must be 100 characters or fewer") + + if value is not MISSING and len(value) > 100: + raise ValueError("value must be 100 characters or fewer") + + if description is not None and len(description) > 100: + raise ValueError("description must be 100 characters or fewer") + + self.label = label + self.value = label if value is MISSING else value + self.description = description + self.default = default + + def __repr__(self) -> str: + return ( + "" + ) + + def __str__(self) -> str: + if self.description: + return f"{self.label}\n{self.description}" + return self.label + + @classmethod + def from_dict(cls, data: RadioGroupOptionPayload) -> RadioGroupOption: + return cls( + label=data["label"], + value=data["value"], + description=data.get("description"), + default=data.get("default", False), + ) + + def to_dict(self) -> RadioGroupOptionPayload: + payload: RadioGroupOptionPayload = { + "label": self.label, + "value": self.value, + "default": self.default, + } + + if self.description: + payload["description"] = self.description + + return payload + + +class CheckboxGroup(Component): + """Represents an Checkbox Group component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.CheckboxGroup` instead. + + .. versionadded:: 2.8 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The custom ID of the checkbox group that gets received during an interaction. + options: List[:class:`CheckboxGroupOption`] + A list of options that can be selected in this group. + min_values: Optional[:class:`int`] + The minimum number of options that must be selected. + Defaults to 1 and must be between 0 and 25. If set to 0, :attr:`required` must be ``False``. + max_values: Optional[:class:`int`] + The maximum number of options that can be selected. + Must be between 1 and 10. + required: Optional[:class:`bool`] + Whether the checkbox group requires a selection or not. Defaults to ``True``. + id: Optional[:class:`int`] + The checkbox group's ID. + """ + + __slots__: tuple[str, ...] = ( + "type", + "custom_id", + "options", + "min_values", + "max_values", + "required", + "id", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: CheckboxGroupComponentPayload): + self.type = ComponentType.checkbox_group + self.id: int | None = data.get("id") + self.custom_id = data.get("custom_id") + self.options: list[CheckboxGroupOption] = [ + CheckboxGroupOption.from_dict(option) for option in data.get("options", []) + ] + self.min_values: int | None = data.get("min_values", None) + self.max_values: int | None = data.get("max_values", None) + self.required: bool = data.get("required", True) + + def to_dict(self) -> CheckboxGroupComponentPayload: + payload = { + "type": 22, + "custom_id": self.custom_id, + "options": [opt.to_dict() for opt in self.options], + } + if self.id is not None: + payload["id"] = self.id + + if self.min_values is not None: + payload["min_values"] = self.min_values + + if self.max_values is not None: + payload["max_values"] = self.max_values + + if not self.required: + payload["required"] = self.required + + return payload + + +class CheckboxGroupOption: + """Represents a :class:`discord.CheckboxGroup`'s option. + + These can be created by users. + + .. versionadded:: 2.8 + + Attributes + ---------- + label: :class:`str` + The label of the option. This is displayed to users. + Can only be up to 100 characters. + value: :class:`str` + The value of the option. This is not displayed to users. + If not provided when constructed then it defaults to the + label. Can only be up to 100 characters. + description: Optional[:class:`str`] + An additional description of the option, if any. + Can only be up to 100 characters. + default: :class:`bool` + Whether this option is selected by default. + """ + + __slots__: tuple[str, ...] = ( + "label", + "value", + "description", + "default", + ) + + def __init__( + self, + *, + label: str, + value: str = MISSING, + description: str | None = None, + default: bool = False, + ) -> None: + if len(label) > 100: + raise ValueError("label must be 100 characters or fewer") + + if value is not MISSING and len(value) > 100: + raise ValueError("value must be 100 characters or fewer") + + if description is not None and len(description) > 100: + raise ValueError("description must be 100 characters or fewer") + + self.label = label + self.value = label if value is MISSING else value + self.description = description + self.default = default + + def __repr__(self) -> str: + return ( + "" + ) + + def __str__(self) -> str: + if self.description: + return f"{self.label}\n{self.description}" + return self.label + + @classmethod + def from_dict(cls, data: CheckboxGroupOptionPayload) -> CheckboxGroupOption: + return cls( + label=data["label"], + value=data["value"], + description=data.get("description"), + default=data.get("default", False), + ) + + def to_dict(self) -> CheckboxGroupOptionPayload: + payload: CheckboxGroupOptionPayload = { + "label": self.label, + "value": self.value, + "default": self.default, + } + + if self.description: + payload["description"] = self.description + + return payload + + +class Checkbox(Component): + """Represents an Checkbox component from the Discord Bot UI Kit. + + This inherits from :class:`Component`. + + .. note:: + + This class is not useable by end-users; see :class:`discord.ui.Checkbox` instead. + + .. versionadded:: 2.8 + + Attributes + ---------- + custom_id: Optional[:class:`str`] + The custom ID of the checkbox group that gets received during an interaction. + default: Optional[:class:`bool`] + Whether this checkbox is selected by default. + id: Optional[:class:`int`] + The checkbox group's ID. + """ + + __slots__: tuple[str, ...] = ( + "type", + "custom_id", + "default", + "id", + ) + + __repr_info__: ClassVar[tuple[str, ...]] = __slots__ + versions: tuple[int, ...] = (2,) + + def __init__(self, data: CheckboxComponentPayload): + self.type = ComponentType.checkbox + self.id: int | None = data.get("id") + self.custom_id = data.get("custom_id") + self.default: bool = data.get("default", False) + + def to_dict(self) -> CheckboxComponentPayload: + payload = { + "type": 23, + "custom_id": self.custom_id, + } + if self.id is not None: + payload["id"] = self.id + + if self.default is not None: + payload["default"] = self.default + + return payload + + +COMPONENT_MAPPINGS = { + 1: ActionRow, + 2: Button, + 3: SelectMenu, + 4: InputText, + 5: SelectMenu, + 6: SelectMenu, + 7: SelectMenu, + 8: SelectMenu, + 9: Section, + 10: TextDisplay, + 11: Thumbnail, + 12: MediaGallery, + 13: FileComponent, + 14: Separator, + 17: Container, + 18: Label, + 19: FileUpload, + 21: RadioGroup, + 22: CheckboxGroup, + 23: Checkbox, +} + +STATE_COMPONENTS = (Section, Container, Thumbnail, MediaGallery, FileComponent) + + +def _component_factory(data: ComponentPayload, state=None) -> Component: + component_type = data["type"] + if cls := COMPONENT_MAPPINGS.get(component_type): + if issubclass(cls, STATE_COMPONENTS): + return cls(data, state=state) + else: + return cls(data) + else: + as_enum = try_enum(ComponentType, component_type) + return Component._raw_construct(type=as_enum) diff --git a/venv/lib/python3.11/site-packages/discord/context_managers.py b/venv/lib/python3.11/site-packages/discord/context_managers.py new file mode 100644 index 0000000..9f1a282 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/context_managers.py @@ -0,0 +1,90 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import asyncio +from typing import TYPE_CHECKING, TypeVar + +if TYPE_CHECKING: + from types import TracebackType + + from .abc import Messageable + + TypingT = TypeVar("TypingT", bound="Typing") + +__all__ = ("Typing",) + + +def _typing_done_callback(fut: asyncio.Task) -> None: + # just retrieve any exception and call it a day + try: + fut.exception() + except (asyncio.CancelledError, Exception): + pass + + +class Typing: + def __init__(self, messageable: Messageable) -> None: + self.loop: asyncio.AbstractEventLoop = messageable._state.loop + self.messageable: Messageable = messageable + + async def do_typing(self) -> None: + try: + channel = self._channel + except AttributeError: + channel = await self.messageable._get_channel() + + typing = channel._state.http.send_typing + + while True: + await typing(channel.id) + await asyncio.sleep(5) + + def __enter__(self: TypingT) -> TypingT: + self.task: asyncio.Task = self.loop.create_task(self.do_typing()) + self.task.add_done_callback(_typing_done_callback) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.task.cancel() + + async def __aenter__(self: TypingT) -> TypingT: + self._channel = channel = await self.messageable._get_channel() + await channel._state.http.send_typing(channel.id) + return self.__enter__() + + async def __aexit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + traceback: TracebackType | None, + ) -> None: + self.task.cancel() diff --git a/venv/lib/python3.11/site-packages/discord/embeds.py b/venv/lib/python3.11/site-packages/discord/embeds.py new file mode 100644 index 0000000..fbdf35c --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/embeds.py @@ -0,0 +1,1080 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import datetime +from typing import TYPE_CHECKING, Any, Mapping, TypeVar + +from . import utils +from .colour import Colour + +__all__ = ( + "Embed", + "EmbedField", + "EmbedAuthor", + "EmbedFooter", + "EmbedMedia", + "EmbedProvider", +) + + +E = TypeVar("E", bound="Embed") + +if TYPE_CHECKING: + from discord.types.embed import Embed as EmbedData + from discord.types.embed import EmbedType + + +class EmbedAuthor: + """Represents the author on the :class:`Embed` object. + + .. versionadded:: 2.5 + + Attributes + ---------- + name: :class:`str` + The name of the author. + url: :class:`str` + The URL of the hyperlink created in the author's name. + icon_url: :class:`str` + The URL of the author icon image. + proxy_icon_url: :class:`str` + The proxied URL of the author icon image. This can't be set directly, it is set by Discord. + """ + + def __init__( + self, + name: str, + url: str | None = None, + icon_url: str | None = None, + ) -> None: + self.name: str = name + self.url: str | None = url + self.icon_url: str | None = icon_url + self.proxy_icon_url: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, str | None]) -> EmbedAuthor: + self = cls.__new__(cls) + name = data.get("name") + if not name: + raise ValueError("name field is required") + self.name = name + self.url = data.get("url") + self.icon_url = data.get("icon_url") + self.proxy_icon_url = data.get("proxy_icon_url") + return self + + def to_dict(self) -> dict[str, str]: + d = {"name": str(self.name)} + if self.url: + d["url"] = str(self.url) + if self.icon_url: + d["icon_url"] = str(self.icon_url) + return d + + def __len__(self) -> int: + """Returns the total number of characters in the author name.""" + return len(self.name) + + def __repr__(self) -> str: + return f"" + + +class EmbedFooter: + """Represents the footer on the :class:`Embed` object. + + .. versionadded:: 2.5 + + Attributes + ---------- + text: :class:`str` + The text inside the footer. + icon_url: :class:`str` + The URL of the footer icon image. + proxy_icon_url: :class:`str` + The proxied URL of the footer icon image. This can't be set directly, it is set by Discord. + """ + + def __init__( + self, + text: str, + icon_url: str | None = None, + ) -> None: + self.text: str = text + self.icon_url: str | None = icon_url + self.proxy_icon_url: str | None = None + + @classmethod + def from_dict(cls, data: dict[str, str | None]) -> EmbedFooter: + self = cls.__new__(cls) + text = data.get("text") + if not text: + raise ValueError("text field is required") + self.text = text + self.icon_url = data.get("icon_url") + self.proxy_icon_url = data.get("proxy_icon_url") + return self + + def to_dict(self) -> dict[str, Any]: + d = {"text": str(self.text)} + if self.icon_url: + d["icon_url"] = str(self.icon_url) + return d + + def __len__(self) -> int: + """Returns the total number of characters in the footer text.""" + return len(self.text) + + def __repr__(self) -> str: + return f"" + + +class EmbedMedia: # Thumbnail, Image, Video + """Represents a media on the :class:`Embed` object. + This includes thumbnails, images, and videos. + + .. versionadded:: 2.5 + + Attributes + ---------- + url: :class:`str` + The source URL of the media. + proxy_url: :class:`str` + The proxied URL of the media. + height: :class:`int` + The height of the media. + width: :class:`int` + The width of the media. + """ + + url: str + proxy_url: str | None + height: int | None + width: int | None + + def __init__(self, url: str): + self.url = url + self.proxy_url = None + self.height = None + self.width = None + + @classmethod + def from_dict(cls, data: dict[str, str | int]) -> EmbedMedia: + self = cls.__new__(cls) + self.url = str(data.get("url")) + self.proxy_url = ( + str(proxy_url) if (proxy_url := data.get("proxy_url")) else None + ) + self.height = int(height) if (height := data.get("height")) else None + self.width = int(width) if (width := data.get("width")) else None + return self + + def __repr__(self) -> str: + return f" height={self.height!r} width={self.width!r}>" + + +class EmbedProvider: + """Represents a provider on the :class:`Embed` object. + + .. versionadded:: 2.5 + + Attributes + ---------- + name: :class:`str` + The name of the provider. + url: :class:`str` + The URL of the provider. + """ + + name: str | None + url: str | None + + @classmethod + def from_dict(cls, data: dict[str, str | None]) -> EmbedProvider: + self = cls.__new__(cls) + self.name = data.get("name") + self.url = data.get("url") + return self + + def __repr__(self) -> str: + return f"" + + +class EmbedField: + """Represents a field on the :class:`Embed` object. + + .. versionadded:: 2.0 + + Attributes + ---------- + name: :class:`str` + The name of the field. + value: :class:`str` + The value of the field. + inline: :class:`bool` + Whether the field should be displayed inline. + """ + + def __init__(self, name: str, value: str, inline: bool | None = False): + self.name = name + self.value = value + self.inline = inline + + @classmethod + def from_dict(cls, data: dict[str, str | bool]) -> EmbedField: + """Converts a :class:`dict` to a :class:`EmbedField` provided it is in the + format that Discord expects it to be in. + + You can find out about this format in the `official Discord documentation`__. + + .. _DiscordDocsEF: https://docs.discord.com/developers/resources/message#embed-object-embed-field-structure + + __ DiscordDocsEF_ + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into an EmbedField object. + """ + self = cls.__new__(cls) + + self.name = data["name"] + self.value = data["value"] + self.inline = data.get("inline", False) + + return self + + def to_dict(self) -> dict[str, str | bool | None]: + """Converts this EmbedField object into a dict. + + Returns + ------- + Dict[:class:`str`, Union[:class:`str`, :class:`bool`]] + A dictionary of :class:`str` embed field keys bound to the respective value. + """ + return { + "name": self.name, + "value": self.value, + "inline": self.inline, + } + + def __repr__(self) -> str: + return f"" + + +class Embed: + """Represents a Discord embed. + + .. container:: operations + + .. describe:: len(x) + + Returns the total size of the embed. + Useful for checking if it's within the 6000 character limit. + + .. describe:: bool(b) + + Returns whether the embed has any data set. + + .. versionadded:: 2.0 + + For ease of use, all parameters that expect a :class:`str` are implicitly + cast to :class:`str` for you. + + Attributes + ---------- + title: :class:`str` + The title of the embed. + This can be set during initialisation. + Must be 256 characters or fewer. + type: :class:`str` + The type of embed. Usually "rich". + This can be set during initialisation. + Possible strings for embed types can be found on discord's + `api docs `_ + description: :class:`str` + The description of the embed. + This can be set during initialisation. + Must be 4096 characters or fewer. + url: :class:`str` + The URL of the embed. + This can be set during initialisation. + timestamp: :class:`datetime.datetime` + The timestamp of the embed content. This is an aware datetime. + If a naive datetime is passed, it is converted to an aware + datetime with the local timezone. + colour: Union[:class:`Colour`, :class:`int`] + The colour code of the embed. Aliased to ``color`` as well. + This can be set during initialisation. + """ + + __slots__ = ( + "title", + "url", + "type", + "_timestamp", + "_colour", + "_footer", + "_image", + "_thumbnail", + "_video", + "_provider", + "_author", + "_fields", + "description", + ) + + def __init__( + self, + *, + colour: int | Colour | None = None, + color: int | Colour | None = None, + title: Any | None = None, + type: EmbedType = "rich", + url: Any | None = None, + description: Any | None = None, + timestamp: datetime.datetime | None = None, + fields: list[EmbedField] | None = None, + author: EmbedAuthor | None = None, + footer: EmbedFooter | None = None, + image: str | EmbedMedia | None = None, + thumbnail: str | EmbedMedia | None = None, + ): + self.colour = colour if colour else color + self.title = title + self.type = type + self.url = url + self.description = description + + if self.title: + self.title = str(self.title) + + if self.description: + self.description = str(self.description) + + if self.url: + self.url = str(self.url) + + if timestamp: + self.timestamp = timestamp + + self._fields: list[EmbedField] = fields if fields is not None else [] + + self.author = author + self.footer = footer + self.image = image + self.thumbnail = thumbnail + + @classmethod + def from_dict(cls: type[E], data: Mapping[str, Any]) -> E: + """Converts a :class:`dict` to a :class:`Embed` provided it is in the + format that Discord expects it to be in. + + You can find out about this format in the `official Discord documentation`__. + + .. _DiscordDocs: https://docs.discord.com/developers/resources/message#embed-object + + __ DiscordDocs_ + + Parameters + ---------- + data: :class:`dict` + The dictionary to convert into an embed. + + Returns + ------- + :class:`Embed` + The converted embed object. + """ + # we are bypassing __init__ here since it doesn't apply here + self: E = cls.__new__(cls) + + # fill in the basic fields + + self.title = data.get("title", None) + self.type = data.get("type", None) + self.description = data.get("description", None) + self.url = data.get("url", None) + + if self.title: + self.title = str(self.title) + + if self.description: + self.description = str(self.description) + + if self.url: + self.url = str(self.url) + + # try to fill in the more rich fields + + try: + self._colour = Colour(value=data["color"]) + except KeyError: + pass + + try: + self._timestamp = utils.parse_time(data["timestamp"]) + except KeyError: + pass + + for attr in ( + "thumbnail", + "video", + "provider", + "author", + "fields", + "image", + "footer", + ): + if attr == "fields": + value = data.get(attr, []) + self._fields = [EmbedField.from_dict(d) for d in value] if value else [] + else: + try: + value = data[attr] + except KeyError: + continue + else: + setattr(self, f"_{attr}", value) + + return self + + def copy(self: E) -> E: + """Creates a shallow copy of the :class:`Embed` object. + + Returns + ------- + :class:`Embed` + The copied embed object. + """ + return self.__class__.from_dict(self.to_dict()) + + def __len__(self) -> int: + total = 0 + if self.title: + total += len(self.title) + if self.description: + total += len(self.description) + for field in getattr(self, "_fields", []): + total += len(field.name) + len(field.value) + + try: + footer_text = self._footer["text"] + except (AttributeError, KeyError): + pass + else: + total += len(footer_text) + + try: + author = self._author + except AttributeError: + pass + else: + total += len(author["name"]) + + return total + + def __bool__(self) -> bool: + return any( + ( + self.title, + self.url, + self.description, + self.colour, + self.fields, + self.timestamp, + self.author, + self.thumbnail, + self.footer, + self.image, + self.provider, + self.video, + ) + ) + + @property + def colour(self) -> Colour | None: + return getattr(self, "_colour", None) + + @colour.setter + def colour(self, value: int | Colour | None): # type: ignore + self._colour = Colour.resolve_value(value) + + color = colour + + @property + def timestamp(self) -> datetime.datetime | None: + return getattr(self, "_timestamp", None) + + @timestamp.setter + def timestamp(self, value: datetime.datetime | None): + if isinstance(value, datetime.datetime): + if value.tzinfo is None: + value = value.astimezone() + self._timestamp = value + elif value is None: + self._timestamp = value + else: + raise TypeError( + "Expected datetime.datetime or None. Received" + f" {value.__class__.__name__} instead" + ) + + @property + def footer(self) -> EmbedFooter | None: + """Returns an :class:`EmbedFooter` denoting the footer contents. + + See :meth:`set_footer` for possible values you can access. + + If the footer is not set then `None` is returned. + """ + foot = getattr(self, "_footer", None) + if not foot: + return None + return EmbedFooter.from_dict(foot) + + @footer.setter + def footer(self, value: EmbedFooter | None): + if value is None: + self.remove_footer() + elif isinstance(value, EmbedFooter): + self._footer = value.to_dict() + else: + raise TypeError( + "Expected EmbedFooter or None. Received" + f" {value.__class__.__name__} instead" + ) + + def set_footer( + self: E, + *, + text: Any | None = None, + icon_url: Any | None = None, + ) -> E: + """Sets the footer for the embed content. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + text: :class:`str` + The footer text. + Must be 2048 characters or fewer. + icon_url: :class:`str` + The URL of the footer icon. Only HTTP(S) is supported. + """ + + self._footer = {} + if text: + self._footer["text"] = str(text) + + if icon_url: + self._footer["icon_url"] = str(icon_url) + + return self + + def remove_footer(self: E) -> E: + """Clears embed's footer information. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionadded:: 2.0 + """ + try: + del self._footer + except AttributeError: + pass + + return self + + @property + def image(self) -> EmbedMedia | None: + """Returns an :class:`EmbedMedia` denoting the image contents. + + Attributes you can access are: + + - ``url`` + - ``proxy_url`` + - ``width`` + - ``height`` + + If the image is not set then `None` is returned. + """ + img = getattr(self, "_image", None) + if not img: + return None + return EmbedMedia.from_dict(img) + + @image.setter + def image(self, value: str | EmbedMedia | None): + if value is None: + self.remove_image() + elif isinstance(value, str): + self.set_image(url=value) + elif isinstance(value, EmbedMedia): + self.set_image(url=value.url) + else: + raise TypeError( + "Expected discord.EmbedMedia, or None but received" + f" {value.__class__.__name__} instead." + ) + + def set_image(self: E, *, url: Any | None) -> E: + """Sets the image for the embed content. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionchanged:: 1.4 + Passing `None` removes the image. + + Parameters + ---------- + url: :class:`str` + The source URL for the image. Only HTTP(S) is supported. + """ + + if url is None: + try: + del self._image + except AttributeError: + pass + else: + self._image = { + "url": str(url), + } + + return self + + def remove_image(self: E) -> E: + """Removes the embed's image. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionadded:: 2.0 + """ + try: + del self._image + except AttributeError: + pass + + return self + + @property + def thumbnail(self) -> EmbedMedia | None: + """Returns an :class:`EmbedMedia` denoting the thumbnail contents. + + Attributes you can access are: + + - ``url`` + - ``proxy_url`` + - ``width`` + - ``height`` + + If the thumbnail is not set then `None` is returned. + """ + thumb = getattr(self, "_thumbnail", None) + if not thumb: + return None + return EmbedMedia.from_dict(thumb) + + @thumbnail.setter + def thumbnail(self, value: str | EmbedMedia | None): + if value is None: + self.remove_thumbnail() + elif isinstance(value, str): + self.set_thumbnail(url=value) + elif isinstance(value, EmbedMedia): + self.set_thumbnail(url=value.url) + else: + raise TypeError( + "Expected discord.EmbedMedia, or None but received" + f" {value.__class__.__name__} instead." + ) + + def set_thumbnail(self: E, *, url: Any | None) -> E: + """Sets the thumbnail for the embed content. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionchanged:: 1.4 + Passing `None` removes the thumbnail. + + Parameters + ---------- + url: :class:`str` + The source URL for the thumbnail. Only HTTP(S) is supported. + """ + + if url is None: + try: + del self._thumbnail + except AttributeError: + pass + else: + self._thumbnail = { + "url": str(url), + } + + return self + + def remove_thumbnail(self: E) -> E: + """Removes the embed's thumbnail. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionadded:: 2.0 + """ + try: + del self._thumbnail + except AttributeError: + pass + + return self + + @property + def video(self) -> EmbedMedia | None: + """Returns an :class:`EmbedMedia` denoting the video contents. + + Attributes include: + + - ``url`` for the video URL. + - ``height`` for the video height. + - ``width`` for the video width. + + If the video is not set then `None` is returned. + """ + vid = getattr(self, "_video", None) + if not vid: + return None + return EmbedMedia.from_dict(vid) + + @property + def provider(self) -> EmbedProvider | None: + """Returns an :class:`EmbedProvider` denoting the provider contents. + + The only attributes that might be accessed are ``name`` and ``url``. + + If the provider is not set then `None` is returned. + """ + prov = getattr(self, "_provider", None) + if not prov: + return None + return EmbedProvider.from_dict(prov) + + @property + def author(self) -> EmbedAuthor | None: + """Returns an :class:`EmbedAuthor` denoting the author contents. + + See :meth:`set_author` for possible values you can access. + + If the author is not set then `None` is returned. + """ + auth = getattr(self, "_author", None) + if not auth: + return None + return EmbedAuthor.from_dict(auth) + + @author.setter + def author(self, value: EmbedAuthor | None): + if value is None: + self.remove_author() + elif isinstance(value, EmbedAuthor): + self._author = value.to_dict() + else: + raise TypeError( + "Expected discord.EmbedAuthor, or None but received" + f" {value.__class__.__name__} instead." + ) + + def set_author( + self: E, + *, + name: Any, + url: Any | None = None, + icon_url: Any | None = None, + ) -> E: + """Sets the author for the embed content. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + name: :class:`str` + The name of the author. + Must be 256 characters or fewer. + url: :class:`str` + The URL for the author. + icon_url: :class:`str` + The URL of the author icon. Only HTTP(S) is supported. + """ + + self._author = { + "name": str(name), + } + + if url: + self._author["url"] = str(url) + + if icon_url: + self._author["icon_url"] = str(icon_url) + + return self + + def remove_author(self: E) -> E: + """Clears embed's author information. + + This function returns the class instance to allow for fluent-style + chaining. + + .. versionadded:: 1.4 + """ + try: + del self._author + except AttributeError: + pass + + return self + + @property + def fields(self) -> list[EmbedField]: + """Returns a :class:`list` of :class:`EmbedField` objects denoting the field contents. + + See :meth:`add_field` for possible values you can access. + + If the attribute has no value then ``None`` is returned. + """ + return self._fields + + @fields.setter + def fields(self, value: list[EmbedField]) -> None: + """Sets the fields for the embed. This overwrites any existing fields. + + Parameters + ---------- + value: List[:class:`EmbedField`] + The list of :class:`EmbedField` objects to include in the embed. + """ + if not all(isinstance(x, EmbedField) for x in value): + raise TypeError("Expected a list of EmbedField objects.") + + self._fields = value + + def append_field(self, field: EmbedField) -> None: + """Appends an :class:`EmbedField` object to the embed. + + .. versionadded:: 2.0 + + Parameters + ---------- + field: :class:`EmbedField` + The field to add. + """ + if not isinstance(field, EmbedField): + raise TypeError("Expected an EmbedField object.") + + self._fields.append(field) + + def add_field(self: E, *, name: str, value: str, inline: bool = True) -> E: + """Adds a field to the embed object. + + This function returns the class instance to allow for fluent-style + chaining. There must be 25 fields or fewer. + + Parameters + ---------- + name: :class:`str` + The name of the field. + Must be 256 characters or fewer. + value: :class:`str` + The value of the field. + Must be 1024 characters or fewer. + inline: :class:`bool` + Whether the field should be displayed inline. + """ + self._fields.append(EmbedField(name=str(name), value=str(value), inline=inline)) + + return self + + def insert_field_at( + self: E, index: int, *, name: Any, value: Any, inline: bool = True + ) -> E: + """Inserts a field before a specified index to the embed. + + This function returns the class instance to allow for fluent-style + chaining. There must be 25 fields or fewer. + + .. versionadded:: 1.2 + + Parameters + ---------- + index: :class:`int` + The index of where to insert the field. + name: :class:`str` + The name of the field. + Must be 256 characters or fewer. + value: :class:`str` + The value of the field. + Must be 1024 characters or fewer. + inline: :class:`bool` + Whether the field should be displayed inline. + """ + + field = EmbedField(name=str(name), value=str(value), inline=inline) + + self._fields.insert(index, field) + + return self + + def clear_fields(self) -> None: + """Removes all fields from this embed.""" + self._fields.clear() + + def remove_field(self, index: int) -> None: + """Removes a field at a specified index. + + If the index is invalid or out of bounds then the error is + silently swallowed. + + .. note:: + + When deleting a field by index, the index of the other fields + shift to fill the gap just like a regular list. + + Parameters + ---------- + index: :class:`int` + The index of the field to remove. + """ + try: + del self._fields[index] + except IndexError: + pass + + def set_field_at( + self: E, index: int, *, name: Any, value: Any, inline: bool = True + ) -> E: + """Modifies a field to the embed object. + + The index must point to a valid pre-existing field. There must be 25 fields or fewer. + + This function returns the class instance to allow for fluent-style + chaining. + + Parameters + ---------- + index: :class:`int` + The index of the field to modify. + name: :class:`str` + The name of the field. + Must be 256 characters or fewer. + value: :class:`str` + The value of the field. + Must be 1024 characters or fewer. + inline: :class:`bool` + Whether the field should be displayed inline. + + Raises + ------ + IndexError + An invalid index was provided. + """ + + try: + field = self._fields[index] + except (TypeError, IndexError): + raise IndexError("field index out of range") + + field.name = str(name) + field.value = str(value) + field.inline = inline + return self + + def to_dict(self) -> EmbedData: + """Converts this embed object into a dict. + + Returns + ------- + Dict[:class:`str`, Union[:class:`str`, :class:`int`, :class:`bool`]] + A dictionary of :class:`str` embed keys bound to the respective value. + """ + + # add in the raw data into the dict + result = { + key[1:]: getattr(self, key) + for key in self.__slots__ + if key != "_fields" and key[0] == "_" and hasattr(self, key) + } + + # add in the fields + result["fields"] = [field.to_dict() for field in self._fields] + + # deal with basic convenience wrappers + + try: + colour = result.pop("colour") + except KeyError: + pass + else: + if colour: + result["color"] = colour.value + + try: + timestamp = result.pop("timestamp") + except KeyError: + pass + else: + if timestamp: + if timestamp.tzinfo: + result["timestamp"] = timestamp.astimezone( + tz=datetime.timezone.utc + ).isoformat() + else: + result["timestamp"] = timestamp.replace( + tzinfo=datetime.timezone.utc + ).isoformat() + + # add in the non-raw attribute ones + if self.type: + result["type"] = self.type + + if self.description: + result["description"] = self.description + + if self.url: + result["url"] = self.url + + if self.title: + result["title"] = self.title + + return result # type: ignore diff --git a/venv/lib/python3.11/site-packages/discord/emoji.py b/venv/lib/python3.11/site-packages/discord/emoji.py new file mode 100644 index 0000000..39a224b --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/emoji.py @@ -0,0 +1,435 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Iterator, Literal + +from .asset import Asset, AssetMixin +from .partial_emoji import PartialEmoji, _EmojiTag +from .user import User +from .utils import MISSING, SnowflakeList, snowflake_time + +__all__ = ( + "Emoji", + "GuildEmoji", + "AppEmoji", +) + +if TYPE_CHECKING: + from datetime import datetime + + from .abc import Snowflake + from .guild import Guild + from .role import Role + from .state import ConnectionState + from .types.emoji import Emoji as EmojiPayload + + +class BaseEmoji(_EmojiTag, AssetMixin): + __slots__: tuple[str, ...] = ( + "require_colons", + "animated", + "managed", + "id", + "name", + "_state", + "user", + "available", + ) + + def __init__(self, *, state: ConnectionState, data: EmojiPayload): + self._state: ConnectionState = state + self._from_data(data) + + def _from_data(self, emoji: EmojiPayload): + self.require_colons: bool = emoji.get("require_colons", False) + self.managed: bool = emoji.get("managed", False) + self.id: int = int(emoji["id"]) # type: ignore + self.name: str = emoji["name"] # type: ignore + self.animated: bool = emoji.get("animated", False) + self.available: bool = emoji.get("available", True) + user = emoji.get("user") + self.user: User | None = User(state=self._state, data=user) if user else None + + def _to_partial(self) -> PartialEmoji: + return PartialEmoji(name=self.name, animated=self.animated, id=self.id) + + def __iter__(self) -> Iterator[tuple[str, Any]]: + for attr in self.__slots__: + if attr[0] != "_": + value = getattr(self, attr, None) + if value is not None: + yield attr, value + + def __str__(self) -> str: + if self.animated: + return f"" + return f"<:{self.name}:{self.id}>" + + def __repr__(self) -> str: + return f"" + + def __eq__(self, other: Any) -> bool: + return isinstance(other, _EmojiTag) and self.id == other.id + + def __hash__(self) -> int: + return self.id >> 22 + + @property + def created_at(self) -> datetime: + """Returns the emoji's creation time in UTC.""" + return snowflake_time(self.id) + + @property + def url(self) -> str: + """Returns the URL of the emoji.""" + url = f"{Asset.BASE}/emojis/{self.id}.{self.extension}" + if self.animated: + url += "?animated=true" + return url + + @property + def mention(self) -> str: + """Return a string that allows you to mention the emoji in a message.""" + if self.animated: + return f"" + return f"<:{self.name}:{self.id}>" + + @property + def extension(self) -> Literal["webp", "png"]: + """Return the file extension of the emoji. + + .. versionadded:: 2.7.1 + """ + return "webp" if self.animated else "png" + + +class GuildEmoji(BaseEmoji): + """Represents a custom emoji in a guild. + + Depending on the way this object was created, some attributes can + have a value of ``None``. + + .. container:: operations + + .. describe:: x == y + + Checks if two emoji are the same. + + .. describe:: x != y + + Checks if two emoji are not the same. + + .. describe:: hash(x) + + Return the emoji's hash. + + .. describe:: iter(x) + + Returns an iterator of ``(field, value)`` pairs. This allows this class + to be used as an iterable in list/dict/etc constructions. + + .. describe:: str(x) + + Returns the emoji rendered for discord. + + Attributes + ---------- + name: :class:`str` + The name of the emoji. + id: :class:`int` + The emoji's ID. + require_colons: :class:`bool` + If colons are required to use this emoji in the client (:PJSalt: vs PJSalt). + animated: :class:`bool` + Whether an emoji is animated or not. + managed: :class:`bool` + If this emoji is managed by a Twitch integration. + guild_id: :class:`int` + The guild ID the emoji belongs to. + available: :class:`bool` + Whether the emoji is available for use. + user: Optional[:class:`User`] + The user that created the emoji. This can only be retrieved using :meth:`Guild.fetch_emoji` and + having the :attr:`~Permissions.manage_emojis` permission. + """ + + __slots__: tuple[str, ...] = ( + "_roles", + "guild_id", + ) + + def __init__(self, *, guild: Guild, state: ConnectionState, data: EmojiPayload): + self.guild_id: int = guild.id + self._roles: SnowflakeList = SnowflakeList(map(int, data.get("roles", []))) + super().__init__(state=state, data=data) + + def __repr__(self) -> str: + return ( + "" + ) + + @property + def roles(self) -> list[Role]: + """A :class:`list` of roles that is allowed to use this emoji. + + If roles is empty, the emoji is unrestricted. + """ + guild = self.guild + if guild is None: + return [] + + return [role for role in guild.roles if self._roles.has(role.id)] + + @property + def guild(self) -> Guild: + """The guild this emoji belongs to.""" + return self._state._get_guild(self.guild_id) + + def is_usable(self) -> bool: + """Whether the bot can use this emoji. + + .. versionadded:: 1.3 + """ + if not self.available: + return False + if not self._roles: + return True + emoji_roles, my_roles = self._roles, self.guild.me._roles + return any(my_roles.has(role_id) for role_id in emoji_roles) + + async def delete(self, *, reason: str | None = None) -> None: + """|coro| + + Deletes the custom emoji. + + You must have :attr:`~Permissions.manage_emojis` permission to + do this. + + Parameters + ---------- + reason: Optional[:class:`str`] + The reason for deleting this emoji. Shows up on the audit log. + + Raises + ------ + Forbidden + You are not allowed to delete emojis. + HTTPException + An error occurred deleting the emoji. + """ + + await self._state.http.delete_custom_emoji( + self.guild.id, self.id, reason=reason + ) + + async def edit( + self, + *, + name: str = MISSING, + roles: list[Snowflake] = MISSING, + reason: str | None = None, + ) -> GuildEmoji: + r"""|coro| + + Edits the custom emoji. + + You must have :attr:`~Permissions.manage_emojis` permission to + do this. + + .. versionchanged:: 2.0 + The newly updated emoji is returned. + + Parameters + ----------- + name: :class:`str` + The new emoji name. + roles: Optional[List[:class:`~discord.abc.Snowflake`]] + A list of roles that can use this emoji. An empty list can be passed to make it available to everyone. + reason: Optional[:class:`str`] + The reason for editing this emoji. Shows up on the audit log. + + Raises + ------- + Forbidden + You are not allowed to edit emojis. + HTTPException + An error occurred editing the emoji. + + Returns + -------- + :class:`GuildEmoji` + The newly updated emoji. + """ + + payload = {} + if name is not MISSING: + payload["name"] = name + if roles is not MISSING: + payload["roles"] = [role.id for role in roles] + + data = await self._state.http.edit_custom_emoji( + self.guild.id, self.id, payload=payload, reason=reason + ) + return GuildEmoji(guild=self.guild, data=data, state=self._state) + + +Emoji = GuildEmoji + + +class AppEmoji(BaseEmoji): + """Represents a custom emoji from an application. + + Depending on the way this object was created, some attributes can + have a value of ``None``. + + .. versionadded:: 2.7 + + .. container:: operations + + .. describe:: x == y + + Checks if two emoji are the same. + + .. describe:: x != y + + Checks if two emoji are not the same. + + .. describe:: hash(x) + + Return the emoji's hash. + + .. describe:: iter(x) + + Returns an iterator of ``(field, value)`` pairs. This allows this class + to be used as an iterable in list/dict/etc constructions. + + .. describe:: str(x) + + Returns the emoji rendered for discord. + + Attributes + ---------- + name: :class:`str` + The name of the emoji. + id: :class:`int` + The emoji's ID. + require_colons: :class:`bool` + If colons are required to use this emoji in the client (:PJSalt: vs PJSalt). + animated: :class:`bool` + Whether an emoji is animated or not. + managed: :class:`bool` + If this emoji is managed by a Twitch integration. + application_id: Optional[:class:`int`] + The application ID the emoji belongs to, if available. + available: :class:`bool` + Whether the emoji is available for use. + user: Optional[:class:`User`] + The user that created the emoji. + """ + + __slots__: tuple[str, ...] = ("application_id",) + + def __init__( + self, *, application_id: int, state: ConnectionState, data: EmojiPayload + ): + self.application_id: int = application_id + super().__init__(state=state, data=data) + + def __repr__(self) -> str: + return f"" + + @property + def guild(self) -> Guild: + """The guild this emoji belongs to. This is always `None` for :class:`AppEmoji`.""" + return None + + @property + def roles(self) -> list[Role]: + """A :class:`list` of roles that is allowed to use this emoji. This is always empty for :class:`AppEmoji`.""" + return [] + + def is_usable(self) -> bool: + """Whether the bot can use this emoji.""" + return self.application_id == self._state.application_id + + async def delete(self) -> None: + """|coro| + + Deletes the application emoji. + + You must own the emoji to do this. + + Raises + ------ + Forbidden + You are not allowed to delete the emoji. + HTTPException + An error occurred deleting the emoji. + """ + + await self._state.http.delete_application_emoji(self.application_id, self.id) + if self._state.cache_app_emojis and self._state.get_emoji(self.id): + self._state._remove_emoji(self) + + async def edit( + self, + *, + name: str = MISSING, + ) -> AppEmoji: + r"""|coro| + + Edits the application emoji. + + You must own the emoji to do this. + + Parameters + ----------- + name: :class:`str` + The new emoji name. + + Raises + ------- + Forbidden + You are not allowed to edit the emoji. + HTTPException + An error occurred editing the emoji. + + Returns + -------- + :class:`AppEmoji` + The newly updated emoji. + """ + + payload = {} + if name is not MISSING: + payload["name"] = name + + data = await self._state.http.edit_application_emoji( + self.application_id, self.id, payload=payload + ) + return self._state.maybe_store_app_emoji(self.application_id, data) diff --git a/venv/lib/python3.11/site-packages/discord/emojis.json b/venv/lib/python3.11/site-packages/discord/emojis.json new file mode 100644 index 0000000..2cc7c8d --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/emojis.json @@ -0,0 +1 @@ +{"grinning": "😀", "grinning_face": "😀", "smiley": "😃", "smile": "😄", "grin": "😁", "laughing": "😆", "satisfied": "😆", "face_holding_back_tears": "🥹", "sweat_smile": "😅", "joy": "😂", "rofl": "🤣", "rolling_on_the_floor_laughing": "🤣", "smiling_face_with_tear": "🥲", "relaxed": "☺️", "smiling_face": "☺️", "blush": "😊", "innocent": "😇", "slight_smile": "🙂", "slightly_smiling_face": "🙂", "upside_down": "🙃", "upside_down_face": "🙃", "wink": "😉", "winking_face": "😉", "relieved": "😌", "relieved_face": "😌", "heart_eyes": "😍", "smiling_face_with_3_hearts": "🥰", "kissing_heart": "😘", "kissing": "😗", "kissing_face": "😗", "kissing_smiling_eyes": "😙", "kissing_closed_eyes": "😚", "yum": "😋", "stuck_out_tongue": "😛", "stuck_out_tongue_closed_eyes": "😝", "stuck_out_tongue_winking_eye": "😜", "zany_face": "🤪", "face_with_raised_eyebrow": "🤨", "face_with_monocle": "🧐", "nerd": "🤓", "nerd_face": "🤓", "sunglasses": "😎", "disguised_face": "🥸", "star_struck": "🤩", "partying_face": "🥳", "smirk": "😏", "smirking_face": "😏", "unamused": "😒", "unamused_face": "😒", "disappointed": "😞", "pensive": "😔", "pensive_face": "😔", "worried": "😟", "worried_face": "😟", "confused": "😕", "confused_face": "😕", "slight_frown": "🙁", "slightly_frowning_face": "🙁", "frowning2": "☹️", "white_frowning_face": "☹️", "frowning_face": "☹️", "persevere": "😣", "confounded": "😖", "tired_face": "😫", "weary": "😩", "weary_face": "😩", "pleading_face": "🥺", "cry": "😢", "crying_face": "😢", "sob": "😭", "triumph": "😤", "angry": "😠", "angry_face": "😠", "rage": "😡", "pouting_face": "😡", "face_with_symbols_over_mouth": "🤬", "exploding_head": "🤯", "flushed": "😳", "flushed_face": "😳", "hot_face": "🥵", "cold_face": "🥶", "face_in_clouds": "😶‍🌫️", "scream": "😱", "fearful": "😨", "fearful_face": "😨", "cold_sweat": "😰", "disappointed_relieved": "😥", "sweat": "😓", "hugging": "🤗", "hugging_face": "🤗", "thinking": "🤔", "thinking_face": "🤔", "face_with_peeking_eye": "🫣", "face_with_hand_over_mouth": "🤭", "face_with_open_eyes_and_hand_over_mouth": "🫢", "saluting_face": "🫡", "shushing_face": "🤫", "melting_face": "🫠", "lying_face": "🤥", "liar": "🤥", "no_mouth": "😶", "dotted_line_face": "🫥", "neutral_face": "😐", "face_with_diagonal_mouth": "🫤", "expressionless": "😑", "shaking_face": "🫨", "head_shaking_horizontally": "🙂‍↔️", "head_shaking_vertically": "🙂‍↕️", "grimacing": "😬", "rolling_eyes": "🙄", "face_with_rolling_eyes": "🙄", "hushed": "😯", "hushed_face": "😯", "frowning": "😦", "anguished": "😧", "open_mouth": "😮", "astonished": "😲", "yawning_face": "🥱", "sleeping": "😴", "sleeping_face": "😴", "drooling_face": "🤤", "drool": "🤤", "sleepy": "😪", "sleepy_face": "😪", "face_exhaling": "😮‍💨", "dizzy_face": "😵", "face_with_spiral_eyes": "😵‍💫", "zipper_mouth": "🤐", "zipper_mouth_face": "🤐", "woozy_face": "🥴", "nauseated_face": "🤢", "sick": "🤢", "face_vomiting": "🤮", "sneezing_face": "🤧", "sneeze": "🤧", "mask": "😷", "thermometer_face": "🤒", "face_with_thermometer": "🤒", "head_bandage": "🤕", "face_with_head_bandage": "🤕", "money_mouth": "🤑", "money_mouth_face": "🤑", "cowboy": "🤠", "face_with_cowboy_hat": "🤠", "smiling_imp": "😈", "imp": "👿", "japanese_ogre": "👹", "ogre": "👹", "japanese_goblin": "👺", "goblin": "👺", "clown": "🤡", "clown_face": "🤡", "poop": "💩", "shit": "💩", "hankey": "💩", "poo": "💩", "pile_of_poo": "💩", "ghost": "👻", "skull": "💀", "skeleton": "💀", "skull_crossbones": "☠️", "skull_and_crossbones": "☠️", "alien": "👽", "space_invader": "👾", "alien_monster": "👾", "robot": "🤖", "robot_face": "🤖", "jack_o_lantern": "🎃", "smiley_cat": "😺", "grinning_cat": "😺", "smile_cat": "😸", "joy_cat": "😹", "heart_eyes_cat": "😻", "smirk_cat": "😼", "kissing_cat": "😽", "scream_cat": "🙀", "weary_cat": "🙀", "crying_cat_face": "😿", "crying_cat": "😿", "pouting_cat": "😾", "handshake": "🤝", "shaking_hands": "🤝", "heart_hands": "🫶", "palms_up_together": "🤲", "open_hands": "👐", "raised_hands": "🙌", "raising_hands": "🙌", "clap": "👏", "thumbsup": "👍", "+1": "👍", "thumbup": "👍", "thumbs_up": "👍", "thumbsdown": "👎", "-1": "👎", "thumbdown": "👎", "thumbs_down": "👎", "punch": "👊", "oncoming_fist": "👊", "fist": "✊", "raised_fist": "✊", "left_facing_fist": "🤛", "left_fist": "🤛", "right_facing_fist": "🤜", "right_fist": "🤜", "leftwards_pushing_hand": "🫷", "rightwards_pushing_hand": "🫸", "fingers_crossed": "🤞", "hand_with_index_and_middle_finger_crossed": "🤞", "v": "✌️", "victory_hand": "✌️", "hand_with_index_finger_and_thumb_crossed": "🫰", "love_you_gesture": "🤟", "metal": "🤘", "sign_of_the_horns": "🤘", "ok_hand": "👌", "pinched_fingers": "🤌", "pinching_hand": "🤏", "palm_down_hand": "🫳", "palm_up_hand": "🫴", "point_left": "👈", "point_right": "👉", "point_up_2": "👆", "point_down": "👇", "point_up": "☝️", "raised_hand": "✋", "raised_back_of_hand": "🤚", "back_of_hand": "🤚", "hand_splayed": "🖐️", "raised_hand_with_fingers_splayed": "🖐️", "vulcan": "🖖", "raised_hand_with_part_between_middle_and_ring_fingers": "🖖", "vulcan_salute": "🖖", "wave": "👋", "waving_hand": "👋", "call_me": "🤙", "call_me_hand": "🤙", "leftwards_hand": "🫲", "rightwards_hand": "🫱", "muscle": "💪", "flexed_biceps": "💪", "mechanical_arm": "🦾", "middle_finger": "🖕", "reversed_hand_with_middle_finger_extended": "🖕", "writing_hand": "✍️", "pray": "🙏", "folded_hands": "🙏", "index_pointing_at_the_viewer": "🫵", "foot": "🦶", "leg": "🦵", "mechanical_leg": "🦿", "lipstick": "💄", "kiss": "💋", "kiss_mark": "💋", "lips": "👄", "mouth": "👄", "biting_lip": "🫦", "tooth": "🦷", "tongue": "👅", "ear": "👂", "ear_with_hearing_aid": "🦻", "nose": "👃", "footprints": "👣", "eye": "👁️", "eyes": "👀", "anatomical_heart": "🫀", "lungs": "🫁", "brain": "🧠", "speaking_head": "🗣️", "speaking_head_in_silhouette": "🗣️", "bust_in_silhouette": "👤", "busts_in_silhouette": "👥", "people_hugging": "🫂", "baby": "👶", "child": "🧒", "girl": "👧", "boy": "👦", "adult": "🧑", "person": "🧑", "woman": "👩", "man": "👨", "person_curly_hair": "🧑‍🦱", "woman_curly_haired": "👩‍🦱", "man_curly_haired": "👨‍🦱", "person_red_hair": "🧑‍🦰", "woman_red_haired": "👩‍🦰", "man_red_haired": "👨‍🦰", "man_red_hair": "👨‍🦰", "blond_haired_person": "👱", "person_with_blond_hair": "👱", "blond_haired_woman": "👱‍♀️", "blond_haired_man": "👱‍♂️", "person_white_hair": "🧑‍🦳", "woman_white_haired": "👩‍🦳", "man_white_haired": "👨‍🦳", "person_bald": "🧑‍🦲", "woman_bald": "👩‍🦲", "man_bald": "👨‍🦲", "bearded_person": "🧔", "person_beard": "🧔", "woman_beard": "🧔‍♀️", "man_beard": "🧔‍♂️", "older_adult": "🧓", "older_person": "🧓", "older_woman": "👵", "grandma": "👵", "old_woman": "👵", "older_man": "👴", "old_man": "👴", "man_with_chinese_cap": "👲", "man_with_gua_pi_mao": "👲", "person_wearing_turban": "👳", "man_with_turban": "👳", "woman_wearing_turban": "👳‍♀️", "man_wearing_turban": "👳‍♂️", "woman_with_headscarf": "🧕", "police_officer": "👮", "cop": "👮", "woman_police_officer": "👮‍♀️", "man_police_officer": "👮‍♂️", "construction_worker": "👷", "woman_construction_worker": "👷‍♀️", "man_construction_worker": "👷‍♂️", "guard": "💂", "guardsman": "💂", "woman_guard": "💂‍♀️", "man_guard": "💂‍♂️", "detective": "🕵️", "spy": "🕵️", "sleuth_or_spy": "🕵️", "woman_detective": "🕵️‍♀️", "man_detective": "🕵️‍♂️", "health_worker": "🧑‍⚕️", "woman_health_worker": "👩‍⚕️", "man_health_worker": "👨‍⚕️", "farmer": "🧑‍🌾", "woman_farmer": "👩‍🌾", "man_farmer": "👨‍🌾", "cook": "🧑‍🍳", "woman_cook": "👩‍🍳", "man_cook": "👨‍🍳", "student": "🧑‍🎓", "woman_student": "👩‍🎓", "man_student": "👨‍🎓", "singer": "🧑‍🎤", "woman_singer": "👩‍🎤", "man_singer": "👨‍🎤", "teacher": "🧑‍🏫", "woman_teacher": "👩‍🏫", "man_teacher": "👨‍🏫", "factory_worker": "🧑‍🏭", "woman_factory_worker": "👩‍🏭", "man_factory_worker": "👨‍🏭", "technologist": "🧑‍💻", "woman_technologist": "👩‍💻", "man_technologist": "👨‍💻", "office_worker": "🧑‍💼", "woman_office_worker": "👩‍💼", "man_office_worker": "👨‍💼", "mechanic": "🧑‍🔧", "woman_mechanic": "👩‍🔧", "man_mechanic": "👨‍🔧", "scientist": "🧑‍🔬", "woman_scientist": "👩‍🔬", "man_scientist": "👨‍🔬", "artist": "🧑‍🎨", "woman_artist": "👩‍🎨", "man_artist": "👨‍🎨", "firefighter": "🧑‍🚒", "woman_firefighter": "👩‍🚒", "man_firefighter": "👨‍🚒", "pilot": "🧑‍✈️", "woman_pilot": "👩‍✈️", "man_pilot": "👨‍✈️", "astronaut": "🧑‍🚀", "woman_astronaut": "👩‍🚀", "man_astronaut": "👨‍🚀", "judge": "🧑‍⚖️", "woman_judge": "👩‍⚖️", "man_judge": "👨‍⚖️", "person_with_veil": "👰", "woman_with_veil": "👰‍♀️", "bride_with_veil": "👰‍♀️", "man_with_veil": "👰‍♂️", "person_in_tuxedo": "🤵", "woman_in_tuxedo": "🤵‍♀️", "man_in_tuxedo": "🤵‍♂️", "person_with_crown": "🫅", "princess": "👸", "prince": "🤴", "superhero": "🦸", "woman_superhero": "🦸‍♀️", "man_superhero": "🦸‍♂️", "supervillain": "🦹", "woman_supervillain": "🦹‍♀️", "man_supervillain": "🦹‍♂️", "ninja": "🥷", "mx_claus": "🧑‍🎄", "mrs_claus": "🤶", "mother_christmas": "🤶", "santa": "🎅", "santa_claus": "🎅", "mage": "🧙", "woman_mage": "🧙‍♀️", "man_mage": "🧙‍♂️", "elf": "🧝", "woman_elf": "🧝‍♀️", "man_elf": "🧝‍♂️", "troll": "🧌", "vampire": "🧛", "woman_vampire": "🧛‍♀️", "man_vampire": "🧛‍♂️", "zombie": "🧟", "woman_zombie": "🧟‍♀️", "man_zombie": "🧟‍♂️", "genie": "🧞", "woman_genie": "🧞‍♀️", "man_genie": "🧞‍♂️", "merperson": "🧜", "mermaid": "🧜‍♀️", "merman": "🧜‍♂️", "fairy": "🧚", "woman_fairy": "🧚‍♀️", "man_fairy": "🧚‍♂️", "angel": "👼", "baby_angel": "👼", "pregnant_person": "🫄", "pregnant_woman": "🤰", "expecting_woman": "🤰", "pregnant_man": "🫃", "breast_feeding": "🤱", "person_feeding_baby": "🧑‍🍼", "woman_feeding_baby": "👩‍🍼", "man_feeding_baby": "👨‍🍼", "person_bowing": "🙇", "bow": "🙇", "woman_bowing": "🙇‍♀️", "man_bowing": "🙇‍♂️", "person_tipping_hand": "💁", "information_desk_person": "💁", "woman_tipping_hand": "💁‍♀️", "man_tipping_hand": "💁‍♂️", "person_gesturing_no": "🙅", "no_good": "🙅", "woman_gesturing_no": "🙅‍♀️", "man_gesturing_no": "🙅‍♂️", "person_gesturing_ok": "🙆", "woman_gesturing_ok": "🙆‍♀️", "man_gesturing_ok": "🙆‍♂️", "person_raising_hand": "🙋", "raising_hand": "🙋", "woman_raising_hand": "🙋‍♀️", "man_raising_hand": "🙋‍♂️", "deaf_person": "🧏", "deaf_woman": "🧏‍♀️", "deaf_man": "🧏‍♂️", "person_facepalming": "🤦", "face_palm": "🤦", "facepalm": "🤦", "woman_facepalming": "🤦‍♀️", "man_facepalming": "🤦‍♂️", "person_shrugging": "🤷", "shrug": "🤷", "woman_shrugging": "🤷‍♀️", "man_shrugging": "🤷‍♂️", "person_pouting": "🙎", "person_with_pouting_face": "🙎", "woman_pouting": "🙎‍♀️", "man_pouting": "🙎‍♂️", "person_frowning": "🙍", "woman_frowning": "🙍‍♀️", "man_frowning": "🙍‍♂️", "person_getting_haircut": "💇", "haircut": "💇", "woman_getting_haircut": "💇‍♀️", "man_getting_haircut": "💇‍♂️", "person_getting_massage": "💆", "massage": "💆", "woman_getting_face_massage": "💆‍♀️", "man_getting_face_massage": "💆‍♂️", "person_in_steamy_room": "🧖", "woman_in_steamy_room": "🧖‍♀️", "man_in_steamy_room": "🧖‍♂️", "nail_care": "💅", "nail_polish": "💅", "selfie": "🤳", "dancer": "💃", "woman_dancing": "💃", "man_dancing": "🕺", "male_dancer": "🕺", "people_with_bunny_ears_partying": "👯", "dancers": "👯", "women_with_bunny_ears_partying": "👯‍♀️", "men_with_bunny_ears_partying": "👯‍♂️", "levitate": "🕴️", "man_in_business_suit_levitating": "🕴️", "person_in_manual_wheelchair": "🧑‍🦽", "woman_in_manual_wheelchair": "👩‍🦽", "man_in_manual_wheelchair": "👨‍🦽", "person_in_manual_wheelchair_facing_right": "🧑‍🦽‍➡️", "man_in_manual_wheelchair_facing_right": "👨‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right": "👩‍🦽‍➡️", "person_in_motorized_wheelchair": "🧑‍🦼", "woman_in_motorized_wheelchair": "👩‍🦼", "man_in_motorized_wheelchair": "👨‍🦼", "person_in_motorized_wheelchair_facing_right": "🧑‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right": "👨‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right": "👩‍🦼‍➡️", "person_walking": "🚶", "walking": "🚶", "woman_walking": "🚶‍♀️", "man_walking": "🚶‍♂️", "person_walking_facing_right": "🚶‍➡️", "woman_walking_facing_right": "🚶‍♀️‍➡️", "man_walking_facing_right": "🚶‍♂️‍➡️", "person_with_probing_cane": "🧑‍🦯", "woman_with_probing_cane": "👩‍🦯", "man_with_probing_cane": "👨‍🦯", "person_with_white_cane_facing_right": "🧑‍🦯‍➡️", "man_with_white_cane_facing_right": "👨‍🦯‍➡️", "woman_with_white_cane_facing_right": "👩‍🦯‍➡️", "person_kneeling": "🧎", "woman_kneeling": "🧎‍♀️", "man_kneeling": "🧎‍♂️", "person_kneeling_facing_right": "🧎‍➡️", "woman_kneeling_facing_right": "🧎‍♀️‍➡️", "man_kneeling_facing_right": "🧎‍♂️‍➡️", "person_running": "🏃", "runner": "🏃", "woman_running": "🏃‍♀️", "man_running": "🏃‍♂️", "person_running_facing_right": "🏃‍➡️", "woman_running_facing_right": "🏃‍♀️‍➡️", "man_running_facing_right": "🏃‍♂️‍➡️", "person_standing": "🧍", "woman_standing": "🧍‍♀️", "man_standing": "🧍‍♂️", "people_holding_hands": "🧑‍🤝‍🧑", "couple": "👫", "two_women_holding_hands": "👭", "two_men_holding_hands": "👬", "couple_with_heart": "💑", "couple_with_heart_woman_man": "👩‍❤️‍👨", "couple_ww": "👩‍❤️‍👩", "couple_with_heart_ww": "👩‍❤️‍👩", "couple_mm": "👨‍❤️‍👨", "couple_with_heart_mm": "👨‍❤️‍👨", "couplekiss": "💏", "kiss_woman_man": "👩‍❤️‍💋‍👨", "kiss_ww": "👩‍❤️‍💋‍👩", "couplekiss_ww": "👩‍❤️‍💋‍👩", "kiss_mm": "👨‍❤️‍💋‍👨", "couplekiss_mm": "👨‍❤️‍💋‍👨", "kiss_man_man": "👨‍❤️‍💋‍👨", "family_adult_adult_child_child": "🧑‍🧑‍🧒‍🧒", "family_adult_adult_child": "🧑‍🧑‍🧒", "family_adult_child_child": "🧑‍🧒‍🧒", "family_adult_child": "🧑‍🧒", "family": "👪", "family_man_woman_boy": "👨‍👩‍👦", "family_mwg": "👨‍👩‍👧", "family_mwgb": "👨‍👩‍👧‍👦", "family_mwbb": "👨‍👩‍👦‍👦", "family_mwgg": "👨‍👩‍👧‍👧", "family_wwb": "👩‍👩‍👦", "family_wwg": "👩‍👩‍👧", "family_wwgb": "👩‍👩‍👧‍👦", "family_wwbb": "👩‍👩‍👦‍👦", "family_wwgg": "👩‍👩‍👧‍👧", "family_mmb": "👨‍👨‍👦", "family_mmg": "👨‍👨‍👧", "family_mmgb": "👨‍👨‍👧‍👦", "family_mmbb": "👨‍👨‍👦‍👦", "family_mmgg": "👨‍👨‍👧‍👧", "family_woman_boy": "👩‍👦", "family_woman_girl": "👩‍👧", "family_woman_girl_boy": "👩‍👧‍👦", "family_woman_boy_boy": "👩‍👦‍👦", "family_woman_girl_girl": "👩‍👧‍👧", "family_man_boy": "👨‍👦", "family_man_girl": "👨‍👧", "family_man_girl_boy": "👨‍👧‍👦", "family_man_boy_boy": "👨‍👦‍👦", "family_man_girl_girl": "👨‍👧‍👧", "knot": "🪢", "yarn": "🧶", "thread": "🧵", "sewing_needle": "🪡", "coat": "🧥", "lab_coat": "🥼", "safety_vest": "🦺", "womans_clothes": "👚", "shirt": "👕", "t_shirt": "👕", "jeans": "👖", "briefs": "🩲", "shorts": "🩳", "necktie": "👔", "dress": "👗", "bikini": "👙", "one_piece_swimsuit": "🩱", "kimono": "👘", "sari": "🥻", "thong_sandal": "🩴", "womans_flat_shoe": "🥿", "flat_shoe": "🥿", "high_heel": "👠", "sandal": "👡", "womans_sandal": "👡", "boot": "👢", "womans_boot": "👢", "mans_shoe": "👞", "athletic_shoe": "👟", "running_shoe": "👟", "hiking_boot": "🥾", "socks": "🧦", "gloves": "🧤", "scarf": "🧣", "tophat": "🎩", "top_hat": "🎩", "billed_cap": "🧢", "womans_hat": "👒", "mortar_board": "🎓", "helmet_with_cross": "⛑️", "helmet_with_white_cross": "⛑️", "military_helmet": "🪖", "crown": "👑", "ring": "💍", "pouch": "👝", "clutch_bag": "👝", "purse": "👛", "handbag": "👜", "briefcase": "💼", "school_satchel": "🎒", "backpack": "🎒", "luggage": "🧳", "eyeglasses": "👓", "glasses": "👓", "dark_sunglasses": "🕶️", "goggles": "🥽", "closed_umbrella": "🌂", "dog": "🐶", "dog_face": "🐶", "cat": "🐱", "cat_face": "🐱", "mouse": "🐭", "mouse_face": "🐭", "hamster": "🐹", "rabbit": "🐰", "rabbit_face": "🐰", "fox": "🦊", "fox_face": "🦊", "bear": "🐻", "panda_face": "🐼", "panda": "🐼", "polar_bear": "🐻‍❄️", "koala": "🐨", "tiger": "🐯", "tiger_face": "🐯", "lion_face": "🦁", "lion": "🦁", "cow": "🐮", "cow_face": "🐮", "pig": "🐷", "pig_face": "🐷", "pig_nose": "🐽", "frog": "🐸", "monkey_face": "🐵", "see_no_evil": "🙈", "hear_no_evil": "🙉", "speak_no_evil": "🙊", "monkey": "🐒", "chicken": "🐔", "penguin": "🐧", "bird": "🐦", "baby_chick": "🐤", "hatching_chick": "🐣", "hatched_chick": "🐥", "goose": "🪿", "duck": "🦆", "black_bird": "🐦‍⬛", "eagle": "🦅", "owl": "🦉", "bat": "🦇", "wolf": "🐺", "boar": "🐗", "horse": "🐴", "horse_face": "🐴", "unicorn": "🦄", "unicorn_face": "🦄", "moose": "🫎", "bee": "🐝", "honeybee": "🐝", "worm": "🪱", "bug": "🐛", "butterfly": "🦋", "snail": "🐌", "lady_beetle": "🐞", "ant": "🐜", "fly": "🪰", "beetle": "🪲", "cockroach": "🪳", "mosquito": "🦟", "cricket": "🦗", "spider": "🕷️", "spider_web": "🕸️", "scorpion": "🦂", "turtle": "🐢", "snake": "🐍", "lizard": "🦎", "t_rex": "🦖", "sauropod": "🦕", "octopus": "🐙", "squid": "🦑", "jellyfish": "🪼", "shrimp": "🦐", "lobster": "🦞", "crab": "🦀", "blowfish": "🐡", "tropical_fish": "🐠", "fish": "🐟", "dolphin": "🐬", "whale": "🐳", "whale2": "🐋", "shark": "🦈", "seal": "🦭", "crocodile": "🐊", "tiger2": "🐅", "leopard": "🐆", "zebra": "🦓", "gorilla": "🦍", "orangutan": "🦧", "mammoth": "🦣", "elephant": "🐘", "hippopotamus": "🦛", "rhino": "🦏", "rhinoceros": "🦏", "dromedary_camel": "🐪", "camel": "🐫", "giraffe": "🦒", "kangaroo": "🦘", "bison": "🦬", "water_buffalo": "🐃", "ox": "🐂", "cow2": "🐄", "donkey": "🫏", "racehorse": "🐎", "pig2": "🐖", "ram": "🐏", "sheep": "🐑", "ewe": "🐑", "llama": "🦙", "goat": "🐐", "deer": "🦌", "dog2": "🐕", "poodle": "🐩", "guide_dog": "🦮", "service_dog": "🐕‍🦺", "cat2": "🐈", "black_cat": "🐈‍⬛", "feather": "🪶", "wing": "🪽", "rooster": "🐓", "turkey": "🦃", "dodo": "🦤", "peacock": "🦚", "parrot": "🦜", "swan": "🦢", "flamingo": "🦩", "dove": "🕊️", "dove_of_peace": "🕊️", "rabbit2": "🐇", "raccoon": "🦝", "skunk": "🦨", "badger": "🦡", "beaver": "🦫", "otter": "🦦", "sloth": "🦥", "mouse2": "🐁", "rat": "🐀", "chipmunk": "🐿️", "hedgehog": "🦔", "feet": "🐾", "paw_prints": "🐾", "dragon": "🐉", "dragon_face": "🐲", "phoenix": "🐦‍🔥", "cactus": "🌵", "christmas_tree": "🎄", "evergreen_tree": "🌲", "deciduous_tree": "🌳", "palm_tree": "🌴", "wood": "🪵", "seedling": "🌱", "herb": "🌿", "shamrock": "☘️", "four_leaf_clover": "🍀", "bamboo": "🎍", "potted_plant": "🪴", "tanabata_tree": "🎋", "leaves": "🍃", "fallen_leaf": "🍂", "maple_leaf": "🍁", "nest_with_eggs": "🪺", "empty_nest": "🪹", "mushroom": "🍄", "brown_mushroom": "🍄‍🟫", "shell": "🐚", "spiral_shell": "🐚", "coral": "🪸", "rock": "🪨", "ear_of_rice": "🌾", "sheaf_of_rice": "🌾", "bouquet": "💐", "tulip": "🌷", "rose": "🌹", "wilted_rose": "🥀", "wilted_flower": "🥀", "hyacinth": "🪻", "lotus": "🪷", "hibiscus": "🌺", "cherry_blossom": "🌸", "blossom": "🌼", "sunflower": "🌻", "sun_with_face": "🌞", "full_moon_with_face": "🌝", "first_quarter_moon_with_face": "🌛", "last_quarter_moon_with_face": "🌜", "new_moon_with_face": "🌚", "new_moon_face": "🌚", "full_moon": "🌕", "waning_gibbous_moon": "🌖", "last_quarter_moon": "🌗", "waning_crescent_moon": "🌘", "new_moon": "🌑", "waxing_crescent_moon": "🌒", "first_quarter_moon": "🌓", "waxing_gibbous_moon": "🌔", "crescent_moon": "🌙", "earth_americas": "🌎", "earth_africa": "🌍", "earth_asia": "🌏", "ringed_planet": "🪐", "dizzy": "💫", "star": "⭐", "star2": "🌟", "glowing_star": "🌟", "sparkles": "✨", "zap": "⚡", "high_voltage": "⚡", "comet": "☄️", "boom": "💥", "collision": "💥", "fire": "🔥", "flame": "🔥", "cloud_tornado": "🌪️", "cloud_with_tornado": "🌪️", "tornado": "🌪️", "rainbow": "🌈", "sunny": "☀️", "sun": "☀️", "white_sun_small_cloud": "🌤️", "white_sun_with_small_cloud": "🌤️", "partly_sunny": "⛅", "white_sun_cloud": "🌥️", "white_sun_behind_cloud": "🌥️", "cloud": "☁️", "white_sun_rain_cloud": "🌦️", "white_sun_behind_cloud_with_rain": "🌦️", "cloud_rain": "🌧️", "cloud_with_rain": "🌧️", "thunder_cloud_rain": "⛈️", "thunder_cloud_and_rain": "⛈️", "cloud_lightning": "🌩️", "cloud_with_lightning": "🌩️", "cloud_snow": "🌨️", "cloud_with_snow": "🌨️", "snowflake": "❄️", "snowman2": "☃️", "snowman": "⛄", "wind_blowing_face": "🌬️", "wind_face": "🌬️", "dash": "💨", "dashing_away": "💨", "droplet": "💧", "sweat_drops": "💦", "bubbles": "🫧", "umbrella": "☔", "umbrella2": "☂️", "ocean": "🌊", "water_wave": "🌊", "fog": "🌫️", "green_apple": "🍏", "apple": "🍎", "red_apple": "🍎", "pear": "🍐", "tangerine": "🍊", "lemon": "🍋", "lime": "🍋‍🟩", "banana": "🍌", "watermelon": "🍉", "grapes": "🍇", "strawberry": "🍓", "blueberries": "🫐", "melon": "🍈", "cherries": "🍒", "peach": "🍑", "mango": "🥭", "pineapple": "🍍", "coconut": "🥥", "kiwi": "🥝", "kiwifruit": "🥝", "kiwi_fruit": "🥝", "tomato": "🍅", "eggplant": "🍆", "avocado": "🥑", "pea_pod": "🫛", "broccoli": "🥦", "leafy_green": "🥬", "cucumber": "🥒", "hot_pepper": "🌶️", "bell_pepper": "🫑", "corn": "🌽", "ear_of_corn": "🌽", "carrot": "🥕", "olive": "🫒", "garlic": "🧄", "onion": "🧅", "potato": "🥔", "sweet_potato": "🍠", "ginger_root": "🫚", "croissant": "🥐", "bagel": "🥯", "bread": "🍞", "french_bread": "🥖", "baguette_bread": "🥖", "pretzel": "🥨", "cheese": "🧀", "cheese_wedge": "🧀", "egg": "🥚", "cooking": "🍳", "butter": "🧈", "pancakes": "🥞", "waffle": "🧇", "bacon": "🥓", "cut_of_meat": "🥩", "poultry_leg": "🍗", "meat_on_bone": "🍖", "bone": "🦴", "hotdog": "🌭", "hot_dog": "🌭", "hamburger": "🍔", "fries": "🍟", "french_fries": "🍟", "pizza": "🍕", "flatbread": "🫓", "sandwich": "🥪", "stuffed_flatbread": "🥙", "stuffed_pita": "🥙", "falafel": "🧆", "taco": "🌮", "burrito": "🌯", "tamale": "🫔", "salad": "🥗", "green_salad": "🥗", "shallow_pan_of_food": "🥘", "paella": "🥘", "fondue": "🫕", "canned_food": "🥫", "jar": "🫙", "spaghetti": "🍝", "ramen": "🍜", "steaming_bowl": "🍜", "stew": "🍲", "pot_of_food": "🍲", "curry": "🍛", "curry_rice": "🍛", "sushi": "🍣", "bento": "🍱", "bento_box": "🍱", "dumpling": "🥟", "oyster": "🦪", "fried_shrimp": "🍤", "rice_ball": "🍙", "rice": "🍚", "cooked_rice": "🍚", "rice_cracker": "🍘", "fish_cake": "🍥", "fortune_cookie": "🥠", "moon_cake": "🥮", "oden": "🍢", "dango": "🍡", "shaved_ice": "🍧", "ice_cream": "🍨", "icecream": "🍦", "pie": "🥧", "cupcake": "🧁", "cake": "🍰", "shortcake": "🍰", "birthday": "🎂", "birthday_cake": "🎂", "custard": "🍮", "pudding": "🍮", "flan": "🍮", "lollipop": "🍭", "candy": "🍬", "chocolate_bar": "🍫", "popcorn": "🍿", "doughnut": "🍩", "cookie": "🍪", "chestnut": "🌰", "peanuts": "🥜", "shelled_peanut": "🥜", "beans": "🫘", "honey_pot": "🍯", "milk": "🥛", "glass_of_milk": "🥛", "pouring_liquid": "🫗", "baby_bottle": "🍼", "teapot": "🫖", "coffee": "☕", "hot_beverage": "☕", "tea": "🍵", "mate": "🧉", "beverage_box": "🧃", "cup_with_straw": "🥤", "bubble_tea": "🧋", "sake": "🍶", "beer": "🍺", "beer_mug": "🍺", "beers": "🍻", "champagne_glass": "🥂", "clinking_glass": "🥂", "wine_glass": "🍷", "tumbler_glass": "🥃", "whisky": "🥃", "cocktail": "🍸", "tropical_drink": "🍹", "champagne": "🍾", "bottle_with_popping_cork": "🍾", "ice_cube": "🧊", "spoon": "🥄", "fork_and_knife": "🍴", "fork_knife_plate": "🍽️", "fork_and_knife_with_plate": "🍽️", "bowl_with_spoon": "🥣", "takeout_box": "🥡", "chopsticks": "🥢", "salt": "🧂", "soccer": "⚽", "soccer_ball": "⚽", "basketball": "🏀", "football": "🏈", "baseball": "⚾", "softball": "🥎", "tennis": "🎾", "volleyball": "🏐", "rugby_football": "🏉", "flying_disc": "🥏", "8ball": "🎱", "yo_yo": "🪀", "ping_pong": "🏓", "table_tennis": "🏓", "badminton": "🏸", "hockey": "🏒", "ice_hockey": "🏒", "field_hockey": "🏑", "lacrosse": "🥍", "cricket_game": "🏏", "cricket_bat_ball": "🏏", "boomerang": "🪃", "goal": "🥅", "goal_net": "🥅", "golf": "⛳", "flag_in_hole": "⛳", "kite": "🪁", "playground_slide": "🛝", "bow_and_arrow": "🏹", "archery": "🏹", "fishing_pole_and_fish": "🎣", "fishing_pole": "🎣", "diving_mask": "🤿", "boxing_glove": "🥊", "boxing_gloves": "🥊", "martial_arts_uniform": "🥋", "karate_uniform": "🥋", "running_shirt_with_sash": "🎽", "running_shirt": "🎽", "skateboard": "🛹", "roller_skate": "🛼", "sled": "🛷", "ice_skate": "⛸️", "curling_stone": "🥌", "ski": "🎿", "skis": "🎿", "skier": "⛷️", "snowboarder": "🏂", "parachute": "🪂", "person_lifting_weights": "🏋️", "lifter": "🏋️", "weight_lifter": "🏋️", "woman_lifting_weights": "🏋️‍♀️", "man_lifting_weights": "🏋️‍♂️", "people_wrestling": "🤼", "wrestlers": "🤼", "wrestling": "🤼", "women_wrestling": "🤼‍♀️", "men_wrestling": "🤼‍♂️", "person_doing_cartwheel": "🤸", "cartwheel": "🤸", "woman_cartwheeling": "🤸‍♀️", "man_cartwheeling": "🤸‍♂️", "person_bouncing_ball": "⛹️", "basketball_player": "⛹️", "person_with_ball": "⛹️", "woman_bouncing_ball": "⛹️‍♀️", "man_bouncing_ball": "⛹️‍♂️", "person_fencing": "🤺", "fencer": "🤺", "fencing": "🤺", "person_playing_handball": "🤾", "handball": "🤾", "woman_playing_handball": "🤾‍♀️", "man_playing_handball": "🤾‍♂️", "person_golfing": "🏌️", "golfer": "🏌️", "woman_golfing": "🏌️‍♀️", "man_golfing": "🏌️‍♂️", "horse_racing": "🏇", "person_in_lotus_position": "🧘", "woman_in_lotus_position": "🧘‍♀️", "man_in_lotus_position": "🧘‍♂️", "person_surfing": "🏄", "surfer": "🏄", "woman_surfing": "🏄‍♀️", "man_surfing": "🏄‍♂️", "person_swimming": "🏊", "swimmer": "🏊", "woman_swimming": "🏊‍♀️", "man_swimming": "🏊‍♂️", "person_playing_water_polo": "🤽", "water_polo": "🤽", "woman_playing_water_polo": "🤽‍♀️", "man_playing_water_polo": "🤽‍♂️", "person_rowing_boat": "🚣", "rowboat": "🚣", "woman_rowing_boat": "🚣‍♀️", "man_rowing_boat": "🚣‍♂️", "person_climbing": "🧗", "woman_climbing": "🧗‍♀️", "man_climbing": "🧗‍♂️", "person_mountain_biking": "🚵", "mountain_bicyclist": "🚵", "woman_mountain_biking": "🚵‍♀️", "man_mountain_biking": "🚵‍♂️", "person_biking": "🚴", "bicyclist": "🚴", "woman_biking": "🚴‍♀️", "man_biking": "🚴‍♂️", "trophy": "🏆", "first_place": "🥇", "first_place_medal": "🥇", "second_place": "🥈", "second_place_medal": "🥈", "third_place": "🥉", "third_place_medal": "🥉", "medal": "🏅", "sports_medal": "🏅", "military_medal": "🎖️", "rosette": "🏵️", "reminder_ribbon": "🎗️", "ticket": "🎫", "tickets": "🎟️", "admission_tickets": "🎟️", "circus_tent": "🎪", "person_juggling": "🤹", "juggling": "🤹", "juggler": "🤹", "woman_juggling": "🤹‍♀️", "man_juggling": "🤹‍♂️", "performing_arts": "🎭", "ballet_shoes": "🩰", "art": "🎨", "clapper": "🎬", "clapper_board": "🎬", "microphone": "🎤", "headphones": "🎧", "headphone": "🎧", "musical_score": "🎼", "musical_keyboard": "🎹", "maracas": "🪇", "drum": "🥁", "drum_with_drumsticks": "🥁", "long_drum": "🪘", "saxophone": "🎷", "trumpet": "🎺", "accordion": "🪗", "guitar": "🎸", "banjo": "🪕", "violin": "🎻", "flute": "🪈", "game_die": "🎲", "chess_pawn": "♟️", "dart": "🎯", "direct_hit": "🎯", "bowling": "🎳", "video_game": "🎮", "slot_machine": "🎰", "jigsaw": "🧩", "puzzle_piece": "🧩", "red_car": "🚗", "automobile": "🚗", "taxi": "🚕", "blue_car": "🚙", "pickup_truck": "🛻", "minibus": "🚐", "bus": "🚌", "trolleybus": "🚎", "race_car": "🏎️", "racing_car": "🏎️", "police_car": "🚓", "ambulance": "🚑", "fire_engine": "🚒", "truck": "🚚", "articulated_lorry": "🚛", "tractor": "🚜", "probing_cane": "🦯", "manual_wheelchair": "🦽", "motorized_wheelchair": "🦼", "crutch": "🩼", "scooter": "🛴", "kick_scooter": "🛴", "bike": "🚲", "bicycle": "🚲", "motor_scooter": "🛵", "motorbike": "🛵", "motorcycle": "🏍️", "racing_motorcycle": "🏍️", "auto_rickshaw": "🛺", "wheel": "🛞", "rotating_light": "🚨", "oncoming_police_car": "🚔", "oncoming_bus": "🚍", "oncoming_automobile": "🚘", "oncoming_taxi": "🚖", "aerial_tramway": "🚡", "mountain_cableway": "🚠", "suspension_railway": "🚟", "railway_car": "🚃", "train": "🚋", "tram_car": "🚋", "mountain_railway": "🚞", "monorail": "🚝", "bullettrain_side": "🚄", "bullettrain_front": "🚅", "bullet_train": "🚅", "light_rail": "🚈", "steam_locomotive": "🚂", "locomotive": "🚂", "train2": "🚆", "metro": "🚇", "tram": "🚊", "station": "🚉", "airplane": "✈️", "airplane_departure": "🛫", "airplane_arriving": "🛬", "airplane_small": "🛩️", "small_airplane": "🛩️", "seat": "💺", "satellite_orbital": "🛰️", "rocket": "🚀", "flying_saucer": "🛸", "helicopter": "🚁", "canoe": "🛶", "kayak": "🛶", "sailboat": "⛵", "speedboat": "🚤", "motorboat": "🛥️", "motor_boat": "🛥️", "cruise_ship": "🛳️", "passenger_ship": "🛳️", "ferry": "⛴️", "ship": "🚢", "ring_buoy": "🛟", "anchor": "⚓", "hook": "🪝", "fuelpump": "⛽", "fuel_pump": "⛽", "construction": "🚧", "vertical_traffic_light": "🚦", "traffic_light": "🚥", "busstop": "🚏", "bus_stop": "🚏", "map": "🗺️", "world_map": "🗺️", "moyai": "🗿", "moai": "🗿", "statue_of_liberty": "🗽", "tokyo_tower": "🗼", "european_castle": "🏰", "castle": "🏰", "japanese_castle": "🏯", "stadium": "🏟️", "ferris_wheel": "🎡", "roller_coaster": "🎢", "carousel_horse": "🎠", "fountain": "⛲", "beach_umbrella": "⛱️", "umbrella_on_ground": "⛱️", "beach": "🏖️", "beach_with_umbrella": "🏖️", "island": "🏝️", "desert_island": "🏝️", "desert": "🏜️", "volcano": "🌋", "mountain": "⛰️", "mountain_snow": "🏔️", "snow_capped_mountain": "🏔️", "mount_fuji": "🗻", "camping": "🏕️", "tent": "⛺", "house": "🏠", "house_with_garden": "🏡", "homes": "🏘️", "house_buildings": "🏘️", "houses": "🏘️", "house_abandoned": "🏚️", "derelict_house_building": "🏚️", "hut": "🛖", "construction_site": "🏗️", "building_construction": "🏗️", "factory": "🏭", "office": "🏢", "department_store": "🏬", "post_office": "🏣", "european_post_office": "🏤", "hospital": "🏥", "bank": "🏦", "hotel": "🏨", "convenience_store": "🏪", "school": "🏫", "love_hotel": "🏩", "wedding": "💒", "classical_building": "🏛️", "church": "⛪", "mosque": "🕌", "synagogue": "🕍", "hindu_temple": "🛕", "kaaba": "🕋", "shinto_shrine": "⛩️", "railway_track": "🛤️", "railroad_track": "🛤️", "motorway": "🛣️", "japan": "🗾", "map_of_japan": "🗾", "rice_scene": "🎑", "park": "🏞️", "national_park": "🏞️", "sunrise": "🌅", "sunrise_over_mountains": "🌄", "stars": "🌠", "shooting_star": "🌠", "sparkler": "🎇", "fireworks": "🎆", "city_sunset": "🌇", "city_sunrise": "🌇", "sunset": "🌇", "city_dusk": "🌆", "cityscape": "🏙️", "night_with_stars": "🌃", "milky_way": "🌌", "bridge_at_night": "🌉", "foggy": "🌁", "watch": "⌚", "mobile_phone": "📱", "iphone": "📱", "calling": "📲", "computer": "💻", "keyboard": "⌨️", "desktop": "🖥️", "desktop_computer": "🖥️", "printer": "🖨️", "mouse_three_button": "🖱️", "three_button_mouse": "🖱️", "trackball": "🖲️", "joystick": "🕹️", "compression": "🗜️", "clamp": "🗜️", "minidisc": "💽", "computer_disk": "💽", "floppy_disk": "💾", "cd": "💿", "optical_disk": "💿", "dvd": "📀", "vhs": "📼", "videocassette": "📼", "camera": "📷", "camera_with_flash": "📸", "video_camera": "📹", "movie_camera": "🎥", "projector": "📽️", "film_projector": "📽️", "film_frames": "🎞️", "telephone_receiver": "📞", "telephone": "☎️", "pager": "📟", "fax": "📠", "fax_machine": "📠", "tv": "📺", "television": "📺", "radio": "📻", "microphone2": "🎙️", "studio_microphone": "🎙️", "level_slider": "🎚️", "control_knobs": "🎛️", "compass": "🧭", "stopwatch": "⏱️", "timer": "⏲️", "timer_clock": "⏲️", "alarm_clock": "⏰", "clock": "🕰️", "mantlepiece_clock": "🕰️", "hourglass": "⌛", "hourglass_flowing_sand": "⏳", "satellite": "📡", "battery": "🔋", "low_battery": "🪫", "electric_plug": "🔌", "bulb": "💡", "light_bulb": "💡", "flashlight": "🔦", "candle": "🕯️", "diya_lamp": "🪔", "fire_extinguisher": "🧯", "oil": "🛢️", "oil_drum": "🛢️", "money_with_wings": "💸", "dollar": "💵", "yen": "💴", "yen_banknote": "💴", "euro": "💶", "euro_banknote": "💶", "pound": "💷", "coin": "🪙", "moneybag": "💰", "money_bag": "💰", "credit_card": "💳", "identification_card": "🪪", "gem": "💎", "gem_stone": "💎", "scales": "⚖️", "balance_scale": "⚖️", "ladder": "🪜", "toolbox": "🧰", "screwdriver": "🪛", "wrench": "🔧", "hammer": "🔨", "hammer_pick": "⚒️", "hammer_and_pick": "⚒️", "tools": "🛠️", "hammer_and_wrench": "🛠️", "pick": "⛏️", "carpentry_saw": "🪚", "nut_and_bolt": "🔩", "gear": "⚙️", "mouse_trap": "🪤", "bricks": "🧱", "brick": "🧱", "chains": "⛓️", "link": "🔗", "broken_chain": "⛓️‍💥", "magnet": "🧲", "gun": "🔫", "pistol": "🔫", "bomb": "💣", "firecracker": "🧨", "axe": "🪓", "knife": "🔪", "kitchen_knife": "🔪", "dagger": "🗡️", "dagger_knife": "🗡️", "crossed_swords": "⚔️", "shield": "🛡️", "smoking": "🚬", "cigarette": "🚬", "coffin": "⚰️", "headstone": "🪦", "urn": "⚱️", "funeral_urn": "⚱️", "amphora": "🏺", "crystal_ball": "🔮", "prayer_beads": "📿", "nazar_amulet": "🧿", "hamsa": "🪬", "barber": "💈", "barber_pole": "💈", "alembic": "⚗️", "telescope": "🔭", "microscope": "🔬", "hole": "🕳️", "x_ray": "🩻", "adhesive_bandage": "🩹", "stethoscope": "🩺", "pill": "💊", "syringe": "💉", "drop_of_blood": "🩸", "dna": "🧬", "microbe": "🦠", "petri_dish": "🧫", "test_tube": "🧪", "thermometer": "🌡️", "broom": "🧹", "plunger": "🪠", "basket": "🧺", "roll_of_paper": "🧻", "toilet": "🚽", "potable_water": "🚰", "shower": "🚿", "bathtub": "🛁", "bath": "🛀", "soap": "🧼", "toothbrush": "🪥", "razor": "🪒", "hair_pick": "🪮", "sponge": "🧽", "bucket": "🪣", "squeeze_bottle": "🧴", "lotion_bottle": "🧴", "bellhop": "🛎️", "bellhop_bell": "🛎️", "key": "🔑", "key2": "🗝️", "old_key": "🗝️", "door": "🚪", "chair": "🪑", "couch": "🛋️", "couch_and_lamp": "🛋️", "bed": "🛏️", "sleeping_accommodation": "🛌", "person_in_bed": "🛌", "teddy_bear": "🧸", "nesting_dolls": "🪆", "frame_photo": "🖼️", "frame_with_picture": "🖼️", "mirror": "🪞", "window": "🪟", "shopping_bags": "🛍️", "shopping_cart": "🛒", "shopping_trolley": "🛒", "gift": "🎁", "wrapped_gift": "🎁", "balloon": "🎈", "flags": "🎏", "carp_streamer": "🎏", "ribbon": "🎀", "magic_wand": "🪄", "piñata": "🪅", "confetti_ball": "🎊", "tada": "🎉", "party_popper": "🎉", "dolls": "🎎", "folding_hand_fan": "🪭", "izakaya_lantern": "🏮", "wind_chime": "🎐", "mirror_ball": "🪩", "red_envelope": "🧧", "envelope": "✉️", "envelope_with_arrow": "📩", "incoming_envelope": "📨", "e_mail": "📧", "email": "📧", "love_letter": "💌", "inbox_tray": "📥", "outbox_tray": "📤", "package": "📦", "label": "🏷️", "placard": "🪧", "mailbox_closed": "📪", "mailbox": "📫", "mailbox_with_mail": "📬", "mailbox_with_no_mail": "📭", "postbox": "📮", "postal_horn": "📯", "scroll": "📜", "page_with_curl": "📃", "page_facing_up": "📄", "bookmark_tabs": "📑", "receipt": "🧾", "bar_chart": "📊", "chart_with_upwards_trend": "📈", "chart_with_downwards_trend": "📉", "notepad_spiral": "🗒️", "spiral_note_pad": "🗒️", "calendar_spiral": "🗓️", "spiral_calendar_pad": "🗓️", "calendar": "📆", "date": "📅", "wastebasket": "🗑️", "card_index": "📇", "card_box": "🗃️", "card_file_box": "🗃️", "ballot_box": "🗳️", "ballot_box_with_ballot": "🗳️", "file_cabinet": "🗄️", "clipboard": "📋", "file_folder": "📁", "open_file_folder": "📂", "dividers": "🗂️", "card_index_dividers": "🗂️", "newspaper2": "🗞️", "rolled_up_newspaper": "🗞️", "newspaper": "📰", "notebook": "📓", "notebook_with_decorative_cover": "📔", "ledger": "📒", "closed_book": "📕", "green_book": "📗", "blue_book": "📘", "orange_book": "📙", "books": "📚", "book": "📖", "open_book": "📖", "bookmark": "🔖", "safety_pin": "🧷", "paperclip": "📎", "paperclips": "🖇️", "linked_paperclips": "🖇️", "triangular_ruler": "📐", "straight_ruler": "📏", "abacus": "🧮", "pushpin": "📌", "round_pushpin": "📍", "scissors": "✂️", "pen_ballpoint": "🖊️", "lower_left_ballpoint_pen": "🖊️", "pen": "🖊️", "pen_fountain": "🖋️", "lower_left_fountain_pen": "🖋️", "fountain_pen": "🖋️", "black_nib": "✒️", "paintbrush": "🖌️", "lower_left_paintbrush": "🖌️", "crayon": "🖍️", "lower_left_crayon": "🖍️", "pencil": "📝", "memo": "📝", "pencil2": "✏️", "mag": "🔍", "mag_right": "🔎", "lock_with_ink_pen": "🔏", "closed_lock_with_key": "🔐", "lock": "🔒", "locked": "🔒", "unlock": "🔓", "unlocked": "🔓", "pink_heart": "🩷", "heart": "❤️", "red_heart": "❤️", "orange_heart": "🧡", "yellow_heart": "💛", "green_heart": "💚", "light_blue_heart": "🩵", "blue_heart": "💙", "purple_heart": "💜", "black_heart": "🖤", "grey_heart": "🩶", "white_heart": "🤍", "brown_heart": "🤎", "broken_heart": "💔", "heart_exclamation": "❣️", "heavy_heart_exclamation_mark_ornament": "❣️", "two_hearts": "💕", "revolving_hearts": "💞", "heartbeat": "💓", "beating_heart": "💓", "heartpulse": "💗", "growing_heart": "💗", "sparkling_heart": "💖", "cupid": "💘", "gift_heart": "💝", "mending_heart": "❤️‍🩹", "heart_on_fire": "❤️‍🔥", "heart_decoration": "💟", "peace": "☮️", "peace_symbol": "☮️", "cross": "✝️", "latin_cross": "✝️", "star_and_crescent": "☪️", "om_symbol": "🕉️", "wheel_of_dharma": "☸️", "khanda": "🪯", "star_of_david": "✡️", "six_pointed_star": "🔯", "menorah": "🕎", "yin_yang": "☯️", "orthodox_cross": "☦️", "place_of_worship": "🛐", "worship_symbol": "🛐", "ophiuchus": "⛎", "aries": "♈", "taurus": "♉", "gemini": "♊", "cancer": "♋", "leo": "♌", "virgo": "♍", "libra": "♎", "scorpius": "♏", "scorpio": "♏", "sagittarius": "♐", "capricorn": "♑", "aquarius": "♒", "pisces": "♓", "id": "🆔", "atom": "⚛️", "atom_symbol": "⚛️", "accept": "🉑", "radioactive": "☢️", "radioactive_sign": "☢️", "biohazard": "☣️", "biohazard_sign": "☣️", "mobile_phone_off": "📴", "vibration_mode": "📳", "u6709": "🈶", "u7121": "🈚", "u7533": "🈸", "u55b6": "🈺", "u6708": "🈷️", "eight_pointed_black_star": "✴️", "vs": "🆚", "white_flower": "💮", "ideograph_advantage": "🉐", "secret": "㊙️", "congratulations": "㊗️", "u5408": "🈴", "u6e80": "🈵", "u5272": "🈹", "u7981": "🈲", "a": "🅰️", "b": "🅱️", "ab": "🆎", "cl": "🆑", "o2": "🅾️", "sos": "🆘", "x": "❌", "cross_mark": "❌", "o": "⭕", "octagonal_sign": "🛑", "stop_sign": "🛑", "no_entry": "⛔", "name_badge": "📛", "no_entry_sign": "🚫", "prohibited": "🚫", "100": "💯", "anger": "💢", "hotsprings": "♨️", "hot_springs": "♨️", "no_pedestrians": "🚷", "do_not_litter": "🚯", "no_littering": "🚯", "no_bicycles": "🚳", "non_potable_water": "🚱", "underage": "🔞", "no_mobile_phones": "📵", "no_smoking": "🚭", "exclamation": "❗", "grey_exclamation": "❕", "question": "❓", "question_mark": "❓", "grey_question": "❔", "bangbang": "‼️", "interrobang": "⁉️", "low_brightness": "🔅", "high_brightness": "🔆", "part_alternation_mark": "〽️", "warning": "⚠️", "children_crossing": "🚸", "trident": "🔱", "fleur_de_lis": "⚜️", "beginner": "🔰", "recycle": "♻️", "white_check_mark": "✅", "u6307": "🈯", "chart": "💹", "sparkle": "❇️", "eight_spoked_asterisk": "✳️", "negative_squared_cross_mark": "❎", "globe_with_meridians": "🌐", "diamond_shape_with_a_dot_inside": "💠", "m": "Ⓜ️", "circled_m": "Ⓜ️", "cyclone": "🌀", "zzz": "💤", "atm": "🏧", "wc": "🚾", "water_closet": "🚾", "wheelchair": "♿", "parking": "🅿️", "elevator": "🛗", "u7a7a": "🈳", "sa": "🈂️", "passport_control": "🛂", "customs": "🛃", "baggage_claim": "🛄", "left_luggage": "🛅", "wireless": "🛜", "mens": "🚹", "mens_room": "🚹", "womens": "🚺", "womens_room": "🚺", "baby_symbol": "🚼", "restroom": "🚻", "put_litter_in_its_place": "🚮", "cinema": "🎦", "signal_strength": "📶", "antenna_bars": "📶", "koko": "🈁", "symbols": "🔣", "input_symbols": "🔣", "information_source": "ℹ️", "information": "ℹ️", "abc": "🔤", "abcd": "🔡", "capital_abcd": "🔠", "ng": "🆖", "ok": "🆗", "up": "🆙", "cool": "🆒", "new": "🆕", "free": "🆓", "zero": "0️⃣", "number_0": "0️⃣", "one": "1️⃣", "number_1": "1️⃣", "two": "2️⃣", "number_2": "2️⃣", "three": "3️⃣", "number_3": "3️⃣", "four": "4️⃣", "number_4": "4️⃣", "five": "5️⃣", "number_5": "5️⃣", "six": "6️⃣", "number_6": "6️⃣", "seven": "7️⃣", "number_7": "7️⃣", "eight": "8️⃣", "number_8": "8️⃣", "nine": "9️⃣", "number_9": "9️⃣", "keycap_ten": "🔟", "number_10": "🔟", "1234": "🔢", "input_numbers": "🔢", "hash": "#️⃣", "asterisk": "*️⃣", "keycap_asterisk": "*️⃣", "eject": "⏏️", "eject_symbol": "⏏️", "arrow_forward": "▶️", "pause_button": "⏸️", "double_vertical_bar": "⏸️", "play_pause": "⏯️", "stop_button": "⏹️", "record_button": "⏺️", "track_next": "⏭️", "next_track": "⏭️", "track_previous": "⏮️", "previous_track": "⏮️", "fast_forward": "⏩", "rewind": "⏪", "arrow_double_up": "⏫", "arrow_double_down": "⏬", "arrow_backward": "◀️", "arrow_up_small": "🔼", "arrow_down_small": "🔽", "arrow_right": "➡️", "right_arrow": "➡️", "arrow_left": "⬅️", "left_arrow": "⬅️", "arrow_up": "⬆️", "up_arrow": "⬆️", "arrow_down": "⬇️", "down_arrow": "⬇️", "arrow_upper_right": "↗️", "arrow_lower_right": "↘️", "arrow_lower_left": "↙️", "arrow_upper_left": "↖️", "up_left_arrow": "↖️", "arrow_up_down": "↕️", "up_down_arrow": "↕️", "left_right_arrow": "↔️", "arrow_right_hook": "↪️", "leftwards_arrow_with_hook": "↩️", "arrow_heading_up": "⤴️", "arrow_heading_down": "⤵️", "twisted_rightwards_arrows": "🔀", "repeat": "🔁", "repeat_one": "🔂", "arrows_counterclockwise": "🔄", "arrows_clockwise": "🔃", "musical_note": "🎵", "notes": "🎶", "musical_notes": "🎶", "heavy_plus_sign": "➕", "heavy_minus_sign": "➖", "heavy_division_sign": "➗", "heavy_multiplication_x": "✖️", "heavy_equals_sign": "🟰", "infinity": "♾️", "heavy_dollar_sign": "💲", "currency_exchange": "💱", "tm": "™", "trade_mark": "™", "copyright": "©", "c": "©", "registered": "®", "r": "®", "wavy_dash": "〰️", "curly_loop": "➰", "loop": "➿", "end": "🔚", "end_arrow": "🔚", "back": "🔙", "back_arrow": "🔙", "on": "🔛", "on_arrow": "🔛", "top": "🔝", "top_arrow": "🔝", "soon": "🔜", "soon_arrow": "🔜", "heavy_check_mark": "✔️", "check_mark": "✔️", "ballot_box_with_check": "☑️", "radio_button": "🔘", "white_circle": "⚪", "black_circle": "⚫", "red_circle": "🔴", "blue_circle": "🔵", "brown_circle": "🟤", "purple_circle": "🟣", "green_circle": "🟢", "yellow_circle": "🟡", "orange_circle": "🟠", "small_red_triangle": "🔺", "small_red_triangle_down": "🔻", "small_orange_diamond": "🔸", "small_blue_diamond": "🔹", "large_orange_diamond": "🔶", "large_blue_diamond": "🔷", "white_square_button": "🔳", "black_square_button": "🔲", "black_small_square": "▪️", "white_small_square": "▫️", "black_medium_small_square": "◾", "white_medium_small_square": "◽", "black_medium_square": "◼️", "white_medium_square": "◻️", "black_large_square": "⬛", "white_large_square": "⬜", "orange_square": "🟧", "blue_square": "🟦", "red_square": "🟥", "brown_square": "🟫", "purple_square": "🟪", "green_square": "🟩", "yellow_square": "🟨", "speaker": "🔈", "mute": "🔇", "muted_speaker": "🔇", "sound": "🔉", "loud_sound": "🔊", "bell": "🔔", "no_bell": "🔕", "mega": "📣", "megaphone": "📣", "loudspeaker": "📢", "speech_left": "🗨️", "left_speech_bubble": "🗨️", "eye_in_speech_bubble": "👁‍🗨", "speech_balloon": "💬", "thought_balloon": "💭", "anger_right": "🗯️", "right_anger_bubble": "🗯️", "spades": "♠️", "spade_suit": "♠️", "clubs": "♣️", "club_suit": "♣️", "hearts": "♥️", "heart_suit": "♥️", "diamonds": "♦️", "diamond_suit": "♦️", "black_joker": "🃏", "joker": "🃏", "flower_playing_cards": "🎴", "mahjong": "🀄", "clock1": "🕐", "one_oclock": "🕐", "clock2": "🕑", "two_oclock": "🕑", "clock3": "🕒", "three_oclock": "🕒", "clock4": "🕓", "four_oclock": "🕓", "clock5": "🕔", "five_oclock": "🕔", "clock6": "🕕", "six_oclock": "🕕", "clock7": "🕖", "seven_oclock": "🕖", "clock8": "🕗", "eight_oclock": "🕗", "clock9": "🕘", "nine_oclock": "🕘", "clock10": "🕙", "ten_oclock": "🕙", "clock11": "🕚", "eleven_oclock": "🕚", "clock12": "🕛", "twelve_oclock": "🕛", "clock130": "🕜", "one_thirty": "🕜", "clock230": "🕝", "two_thirty": "🕝", "clock330": "🕞", "three_thirty": "🕞", "clock430": "🕟", "four_thirty": "🕟", "clock530": "🕠", "five_thirty": "🕠", "clock630": "🕡", "six_thirty": "🕡", "clock730": "🕢", "seven_thirty": "🕢", "clock830": "🕣", "eight_thirty": "🕣", "clock930": "🕤", "nine_thirty": "🕤", "clock1030": "🕥", "ten_thirty": "🕥", "clock1130": "🕦", "eleven_thirty": "🕦", "clock1230": "🕧", "twelve_thirty": "🕧", "female_sign": "♀️", "male_sign": "♂️", "transgender_symbol": "⚧", "medical_symbol": "⚕️", "regional_indicator_z": "🇿", "regional_indicator_y": "🇾", "regional_indicator_x": "🇽", "regional_indicator_w": "🇼", "regional_indicator_v": "🇻", "regional_indicator_u": "🇺", "regional_indicator_t": "🇹", "regional_indicator_s": "🇸", "regional_indicator_r": "🇷", "regional_indicator_q": "🇶", "regional_indicator_p": "🇵", "regional_indicator_o": "🇴", "regional_indicator_n": "🇳", "regional_indicator_m": "🇲", "regional_indicator_l": "🇱", "regional_indicator_k": "🇰", "regional_indicator_j": "🇯", "regional_indicator_i": "🇮", "regional_indicator_h": "🇭", "regional_indicator_g": "🇬", "regional_indicator_f": "🇫", "regional_indicator_e": "🇪", "regional_indicator_d": "🇩", "regional_indicator_c": "🇨", "regional_indicator_b": "🇧", "regional_indicator_a": "🇦", "flag_white": "🏳️", "flag_black": "🏴", "pirate_flag": "🏴‍☠️", "checkered_flag": "🏁", "triangular_flag_on_post": "🚩", "rainbow_flag": "🏳️‍🌈", "gay_pride_flag": "🏳️‍🌈", "transgender_flag": "🏳️‍⚧️", "united_nations": "🇺🇳", "flag_af": "🇦🇫", "flag_ax": "🇦🇽", "flag_al": "🇦🇱", "flag_dz": "🇩🇿", "flag_as": "🇦🇸", "flag_ad": "🇦🇩", "flag_ao": "🇦🇴", "flag_ai": "🇦🇮", "flag_aq": "🇦🇶", "flag_ag": "🇦🇬", "flag_ar": "🇦🇷", "flag_am": "🇦🇲", "flag_aw": "🇦🇼", "flag_au": "🇦🇺", "flag_at": "🇦🇹", "flag_az": "🇦🇿", "flag_bs": "🇧🇸", "flag_bh": "🇧🇭", "flag_bd": "🇧🇩", "flag_bb": "🇧🇧", "flag_by": "🇧🇾", "flag_be": "🇧🇪", "flag_bz": "🇧🇿", "flag_bj": "🇧🇯", "flag_bm": "🇧🇲", "flag_bt": "🇧🇹", "flag_bo": "🇧🇴", "flag_ba": "🇧🇦", "flag_bw": "🇧🇼", "flag_br": "🇧🇷", "flag_io": "🇮🇴", "flag_vg": "🇻🇬", "flag_bn": "🇧🇳", "flag_bg": "🇧🇬", "flag_bf": "🇧🇫", "flag_bi": "🇧🇮", "flag_kh": "🇰🇭", "flag_cm": "🇨🇲", "flag_ca": "🇨🇦", "flag_ic": "🇮🇨", "flag_cv": "🇨🇻", "flag_bq": "🇧🇶", "flag_ky": "🇰🇾", "flag_cf": "🇨🇫", "flag_td": "🇹🇩", "flag_cl": "🇨🇱", "flag_cn": "🇨🇳", "flag_cx": "🇨🇽", "flag_cc": "🇨🇨", "flag_co": "🇨🇴", "flag_km": "🇰🇲", "flag_cg": "🇨🇬", "flag_cd": "🇨🇩", "flag_ck": "🇨🇰", "flag_cr": "🇨🇷", "flag_ci": "🇨🇮", "flag_hr": "🇭🇷", "flag_cu": "🇨🇺", "flag_cw": "🇨🇼", "flag_cy": "🇨🇾", "flag_cz": "🇨🇿", "flag_dk": "🇩🇰", "flag_dj": "🇩🇯", "flag_dm": "🇩🇲", "flag_do": "🇩🇴", "flag_ec": "🇪🇨", "flag_eg": "🇪🇬", "flag_sv": "🇸🇻", "flag_gq": "🇬🇶", "flag_er": "🇪🇷", "flag_ee": "🇪🇪", "flag_et": "🇪🇹", "flag_eu": "🇪🇺", "flag_fk": "🇫🇰", "flag_fo": "🇫🇴", "flag_fj": "🇫🇯", "flag_fi": "🇫🇮", "flag_fr": "🇫🇷", "flag_gf": "🇬🇫", "flag_pf": "🇵🇫", "flag_tf": "🇹🇫", "flag_ga": "🇬🇦", "flag_gm": "🇬🇲", "flag_ge": "🇬🇪", "flag_de": "🇩🇪", "flag_gh": "🇬🇭", "flag_gi": "🇬🇮", "flag_gr": "🇬🇷", "flag_gl": "🇬🇱", "flag_gd": "🇬🇩", "flag_gp": "🇬🇵", "flag_gu": "🇬🇺", "flag_gt": "🇬🇹", "flag_gg": "🇬🇬", "flag_gn": "🇬🇳", "flag_gw": "🇬🇼", "flag_gy": "🇬🇾", "flag_ht": "🇭🇹", "flag_hn": "🇭🇳", "flag_hk": "🇭🇰", "flag_hu": "🇭🇺", "flag_is": "🇮🇸", "flag_in": "🇮🇳", "flag_id": "🇮🇩", "flag_ir": "🇮🇷", "flag_iq": "🇮🇶", "flag_ie": "🇮🇪", "flag_im": "🇮🇲", "flag_il": "🇮🇱", "flag_it": "🇮🇹", "flag_jm": "🇯🇲", "flag_jp": "🇯🇵", "crossed_flags": "🎌", "flag_je": "🇯🇪", "flag_jo": "🇯🇴", "flag_kz": "🇰🇿", "flag_ke": "🇰🇪", "flag_ki": "🇰🇮", "flag_xk": "🇽🇰", "flag_kw": "🇰🇼", "flag_kg": "🇰🇬", "flag_la": "🇱🇦", "flag_lv": "🇱🇻", "flag_lb": "🇱🇧", "flag_ls": "🇱🇸", "flag_lr": "🇱🇷", "flag_ly": "🇱🇾", "flag_li": "🇱🇮", "flag_lt": "🇱🇹", "flag_lu": "🇱🇺", "flag_mo": "🇲🇴", "flag_mk": "🇲🇰", "flag_mg": "🇲🇬", "flag_mw": "🇲🇼", "flag_my": "🇲🇾", "flag_mv": "🇲🇻", "flag_ml": "🇲🇱", "flag_mt": "🇲🇹", "flag_mh": "🇲🇭", "flag_mq": "🇲🇶", "flag_mr": "🇲🇷", "flag_mu": "🇲🇺", "flag_yt": "🇾🇹", "flag_mx": "🇲🇽", "flag_fm": "🇫🇲", "flag_md": "🇲🇩", "flag_mc": "🇲🇨", "flag_mn": "🇲🇳", "flag_me": "🇲🇪", "flag_ms": "🇲🇸", "flag_ma": "🇲🇦", "flag_mz": "🇲🇿", "flag_mm": "🇲🇲", "flag_na": "🇳🇦", "flag_nr": "🇳🇷", "flag_np": "🇳🇵", "flag_nl": "🇳🇱", "flag_nc": "🇳🇨", "flag_nz": "🇳🇿", "flag_ni": "🇳🇮", "flag_ne": "🇳🇪", "flag_ng": "🇳🇬", "flag_nu": "🇳🇺", "flag_nf": "🇳🇫", "flag_kp": "🇰🇵", "flag_mp": "🇲🇵", "flag_no": "🇳🇴", "flag_om": "🇴🇲", "flag_pk": "🇵🇰", "flag_pw": "🇵🇼", "flag_ps": "🇵🇸", "flag_pa": "🇵🇦", "flag_pg": "🇵🇬", "flag_py": "🇵🇾", "flag_pe": "🇵🇪", "flag_ph": "🇵🇭", "flag_pn": "🇵🇳", "flag_pl": "🇵🇱", "flag_pt": "🇵🇹", "flag_pr": "🇵🇷", "flag_qa": "🇶🇦", "flag_re": "🇷🇪", "flag_ro": "🇷🇴", "flag_ru": "🇷🇺", "flag_rw": "🇷🇼", "flag_ws": "🇼🇸", "flag_sm": "🇸🇲", "flag_st": "🇸🇹", "flag_sa": "🇸🇦", "flag_sn": "🇸🇳", "flag_rs": "🇷🇸", "flag_sc": "🇸🇨", "flag_sl": "🇸🇱", "flag_sg": "🇸🇬", "flag_sx": "🇸🇽", "flag_sk": "🇸🇰", "flag_si": "🇸🇮", "flag_gs": "🇬🇸", "flag_sb": "🇸🇧", "flag_so": "🇸🇴", "flag_za": "🇿🇦", "flag_kr": "🇰🇷", "flag_ss": "🇸🇸", "flag_es": "🇪🇸", "flag_lk": "🇱🇰", "flag_bl": "🇧🇱", "flag_sh": "🇸🇭", "flag_kn": "🇰🇳", "flag_lc": "🇱🇨", "flag_pm": "🇵🇲", "flag_vc": "🇻🇨", "flag_sd": "🇸🇩", "flag_sr": "🇸🇷", "flag_sz": "🇸🇿", "flag_se": "🇸🇪", "flag_ch": "🇨🇭", "flag_sy": "🇸🇾", "flag_tw": "🇹🇼", "flag_tj": "🇹🇯", "flag_tz": "🇹🇿", "flag_th": "🇹🇭", "flag_tl": "🇹🇱", "flag_tg": "🇹🇬", "flag_tk": "🇹🇰", "flag_to": "🇹🇴", "flag_tt": "🇹🇹", "flag_tn": "🇹🇳", "flag_tr": "🇹🇷", "flag_tm": "🇹🇲", "flag_tc": "🇹🇨", "flag_vi": "🇻🇮", "flag_tv": "🇹🇻", "flag_ug": "🇺🇬", "flag_ua": "🇺🇦", "flag_ae": "🇦🇪", "flag_gb": "🇬🇧", "england": "🏴󠁧󠁢󠁥󠁮󠁧󠁿", "scotland": "🏴󠁧󠁢󠁳󠁣󠁴󠁿", "wales": "🏴󠁧󠁢󠁷󠁬󠁳󠁿", "flag_us": "🇺🇸", "flag_uy": "🇺🇾", "flag_uz": "🇺🇿", "flag_vu": "🇻🇺", "flag_va": "🇻🇦", "flag_ve": "🇻🇪", "flag_vn": "🇻🇳", "flag_wf": "🇼🇫", "flag_eh": "🇪🇭", "flag_ye": "🇾🇪", "flag_zm": "🇿🇲", "flag_zw": "🇿🇼", "flag_ac": "🇦🇨", "flag_bv": "🇧🇻", "flag_cp": "🇨🇵", "flag_ea": "🇪🇦", "flag_dg": "🇩🇬", "flag_hm": "🇭🇲", "flag_mf": "🇲🇫", "flag_sj": "🇸🇯", "flag_ta": "🇹🇦", "flag_um": "🇺🇲", "handshake_tone1": "🤝🏻", "handshake_light_skin_tone": "🤝🏻", "handshake_tone1_tone2": "🫱🏻‍🫲🏼", "handshake_light_skin_tone_medium_light_skin_tone": "🫱🏻‍🫲🏼", "handshake_tone1_tone3": "🫱🏻‍🫲🏽", "handshake_light_skin_tone_medium_skin_tone": "🫱🏻‍🫲🏽", "handshake_tone1_tone4": "🫱🏻‍🫲🏾", "handshake_light_skin_tone_medium_dark_skin_tone": "🫱🏻‍🫲🏾", "handshake_tone1_tone5": "🫱🏻‍🫲🏿", "handshake_light_skin_tone_dark_skin_tone": "🫱🏻‍🫲🏿", "handshake_tone2_tone1": "🫱🏼‍🫲🏻", "handshake_medium_light_skin_tone_light_skin_tone": "🫱🏼‍🫲🏻", "handshake_tone2": "🤝🏼", "handshake_medium_light_skin_tone": "🤝🏼", "handshake_tone2_tone3": "🫱🏼‍🫲🏽", "handshake_medium_light_skin_tone_medium_skin_tone": "🫱🏼‍🫲🏽", "handshake_tone2_tone4": "🫱🏼‍🫲🏾", "handshake_medium_light_skin_tone_medium_dark_skin_tone": "🫱🏼‍🫲🏾", "handshake_tone2_tone5": "🫱🏼‍🫲🏿", "handshake_medium_light_skin_tone_dark_skin_tone": "🫱🏼‍🫲🏿", "handshake_tone3_tone1": "🫱🏽‍🫲🏻", "handshake_medium_skin_tone_light_skin_tone": "🫱🏽‍🫲🏻", "handshake_tone3_tone2": "🫱🏽‍🫲🏼", "handshake_medium_skin_tone_medium_light_skin_tone": "🫱🏽‍🫲🏼", "handshake_tone3": "🤝🏽", "handshake_medium_skin_tone": "🤝🏽", "handshake_tone3_tone4": "🫱🏽‍🫲🏾", "handshake_medium_skin_tone_medium_dark_skin_tone": "🫱🏽‍🫲🏾", "handshake_tone3_tone5": "🫱🏽‍🫲🏿", "handshake_medium_skin_tone_dark_skin_tone": "🫱🏽‍🫲🏿", "handshake_tone4_tone1": "🫱🏾‍🫲🏻", "handshake_medium_dark_skin_tone_light_skin_tone": "🫱🏾‍🫲🏻", "handshake_tone4_tone2": "🫱🏾‍🫲🏼", "handshake_medium_dark_skin_tone_medium_light_skin_tone": "🫱🏾‍🫲🏼", "handshake_tone4_tone3": "🫱🏾‍🫲🏽", "handshake_medium_dark_skin_tone_medium_skin_tone": "🫱🏾‍🫲🏽", "handshake_tone4": "🤝🏾", "handshake_medium_dark_skin_tone": "🤝🏾", "handshake_tone4_tone5": "🫱🏾‍🫲🏿", "handshake_medium_dark_skin_tone_dark_skin_tone": "🫱🏾‍🫲🏿", "handshake_tone5_tone1": "🫱🏿‍🫲🏻", "handshake_dark_skin_tone_light_skin_tone": "🫱🏿‍🫲🏻", "handshake_tone5_tone2": "🫱🏿‍🫲🏼", "handshake_dark_skin_tone_medium_light_skin_tone": "🫱🏿‍🫲🏼", "handshake_tone5_tone3": "🫱🏿‍🫲🏽", "handshake_dark_skin_tone_medium_skin_tone": "🫱🏿‍🫲🏽", "handshake_tone5_tone4": "🫱🏿‍🫲🏾", "handshake_dark_skin_tone_medium_dark_skin_tone": "🫱🏿‍🫲🏾", "handshake_tone5": "🤝🏿", "handshake_dark_skin_tone": "🤝🏿", "heart_hands_tone1": "🫶🏻", "heart_hands_light_skin_tone": "🫶🏻", "heart_hands_tone2": "🫶🏼", "heart_hands_medium_light_skin_tone": "🫶🏼", "heart_hands_tone3": "🫶🏽", "heart_hands_medium_skin_tone": "🫶🏽", "heart_hands_tone4": "🫶🏾", "heart_hands_medium_dark_skin_tone": "🫶🏾", "heart_hands_tone5": "🫶🏿", "heart_hands_dark_skin_tone": "🫶🏿", "palms_up_together_tone1": "🤲🏻", "palms_up_together_light_skin_tone": "🤲🏻", "palms_up_together_tone2": "🤲🏼", "palms_up_together_medium_light_skin_tone": "🤲🏼", "palms_up_together_tone3": "🤲🏽", "palms_up_together_medium_skin_tone": "🤲🏽", "palms_up_together_tone4": "🤲🏾", "palms_up_together_medium_dark_skin_tone": "🤲🏾", "palms_up_together_tone5": "🤲🏿", "palms_up_together_dark_skin_tone": "🤲🏿", "open_hands_tone1": "👐🏻", "open_hands_tone2": "👐🏼", "open_hands_tone3": "👐🏽", "open_hands_tone4": "👐🏾", "open_hands_tone5": "👐🏿", "raised_hands_tone1": "🙌🏻", "raised_hands_tone2": "🙌🏼", "raised_hands_tone3": "🙌🏽", "raised_hands_tone4": "🙌🏾", "raised_hands_tone5": "🙌🏿", "clap_tone1": "👏🏻", "clap_tone2": "👏🏼", "clap_tone3": "👏🏽", "clap_tone4": "👏🏾", "clap_tone5": "👏🏿", "thumbsup_tone1": "👍🏻", "+1_tone1": "👍🏻", "thumbup_tone1": "👍🏻", "thumbsup_tone2": "👍🏼", "+1_tone2": "👍🏼", "thumbup_tone2": "👍🏼", "thumbsup_tone3": "👍🏽", "+1_tone3": "👍🏽", "thumbup_tone3": "👍🏽", "thumbsup_tone4": "👍🏾", "+1_tone4": "👍🏾", "thumbup_tone4": "👍🏾", "thumbsup_tone5": "👍🏿", "+1_tone5": "👍🏿", "thumbup_tone5": "👍🏿", "thumbsdown_tone1": "👎🏻", "_1_tone1": "👎🏻", "thumbdown_tone1": "👎🏻", "thumbsdown_tone2": "👎🏼", "_1_tone2": "👎🏼", "thumbdown_tone2": "👎🏼", "thumbsdown_tone3": "👎🏽", "_1_tone3": "👎🏽", "thumbdown_tone3": "👎🏽", "thumbsdown_tone4": "👎🏾", "_1_tone4": "👎🏾", "thumbdown_tone4": "👎🏾", "thumbsdown_tone5": "👎🏿", "_1_tone5": "👎🏿", "thumbdown_tone5": "👎🏿", "punch_tone1": "👊🏻", "punch_tone2": "👊🏼", "punch_tone3": "👊🏽", "punch_tone4": "👊🏾", "punch_tone5": "👊🏿", "fist_tone1": "✊🏻", "fist_tone2": "✊🏼", "fist_tone3": "✊🏽", "fist_tone4": "✊🏾", "fist_tone5": "✊🏿", "left_facing_fist_tone1": "🤛🏻", "left_fist_tone1": "🤛🏻", "left_facing_fist_tone2": "🤛🏼", "left_fist_tone2": "🤛🏼", "left_facing_fist_tone3": "🤛🏽", "left_fist_tone3": "🤛🏽", "left_facing_fist_tone4": "🤛🏾", "left_fist_tone4": "🤛🏾", "left_facing_fist_tone5": "🤛🏿", "left_fist_tone5": "🤛🏿", "right_facing_fist_tone1": "🤜🏻", "right_fist_tone1": "🤜🏻", "right_facing_fist_tone2": "🤜🏼", "right_fist_tone2": "🤜🏼", "right_facing_fist_tone3": "🤜🏽", "right_fist_tone3": "🤜🏽", "right_facing_fist_tone4": "🤜🏾", "right_fist_tone4": "🤜🏾", "right_facing_fist_tone5": "🤜🏿", "right_fist_tone5": "🤜🏿", "leftwards_pushing_hand_tone1": "🫷🏻", "leftwards_pushing_hand_light_skin_tone": "🫷🏻", "leftwards_pushing_hand_tone2": "🫷🏼", "leftwards_pushing_hand_medium_light_skin_tone": "🫷🏼", "leftwards_pushing_hand_tone3": "🫷🏽", "leftwards_pushing_hand_medium_skin_tone": "🫷🏽", "leftwards_pushing_hand_tone4": "🫷🏾", "leftwards_pushing_hand_medium_dark_skin_tone": "🫷🏾", "leftwards_pushing_hand_tone5": "🫷🏿", "leftwards_pushing_hand_dark_skin_tone": "🫷🏿", "rightwards_pushing_hand_tone1": "🫸🏻", "rightwards_pushing_hand_light_skin_tone": "🫸🏻", "rightwards_pushing_hand_tone2": "🫸🏼", "rightwards_pushing_hand_medium_light_skin_tone": "🫸🏼", "rightwards_pushing_hand_tone3": "🫸🏽", "rightwards_pushing_hand_medium_skin_tone": "🫸🏽", "rightwards_pushing_hand_tone4": "🫸🏾", "rightwards_pushing_hand_medium_dark_skin_tone": "🫸🏾", "rightwards_pushing_hand_tone5": "🫸🏿", "rightwards_pushing_hand_dark_skin_tone": "🫸🏿", "fingers_crossed_tone1": "🤞🏻", "hand_with_index_and_middle_fingers_crossed_tone1": "🤞🏻", "fingers_crossed_tone2": "🤞🏼", "hand_with_index_and_middle_fingers_crossed_tone2": "🤞🏼", "fingers_crossed_tone3": "🤞🏽", "hand_with_index_and_middle_fingers_crossed_tone3": "🤞🏽", "fingers_crossed_tone4": "🤞🏾", "hand_with_index_and_middle_fingers_crossed_tone4": "🤞🏾", "fingers_crossed_tone5": "🤞🏿", "hand_with_index_and_middle_fingers_crossed_tone5": "🤞🏿", "v_tone1": "✌🏻", "v_tone2": "✌🏼", "v_tone3": "✌🏽", "v_tone4": "✌🏾", "v_tone5": "✌🏿", "hand_with_index_finger_and_thumb_crossed_tone1": "🫰🏻", "hand_with_index_finger_and_thumb_crossed_light_skin_tone": "🫰🏻", "hand_with_index_finger_and_thumb_crossed_tone2": "🫰🏼", "hand_with_index_finger_and_thumb_crossed_medium_light_skin_tone": "🫰🏼", "hand_with_index_finger_and_thumb_crossed_tone3": "🫰🏽", "hand_with_index_finger_and_thumb_crossed_medium_skin_tone": "🫰🏽", "hand_with_index_finger_and_thumb_crossed_tone4": "🫰🏾", "hand_with_index_finger_and_thumb_crossed_medium_dark_skin_tone": "🫰🏾", "hand_with_index_finger_and_thumb_crossed_tone5": "🫰🏿", "hand_with_index_finger_and_thumb_crossed_dark_skin_tone": "🫰🏿", "love_you_gesture_tone1": "🤟🏻", "love_you_gesture_light_skin_tone": "🤟🏻", "love_you_gesture_tone2": "🤟🏼", "love_you_gesture_medium_light_skin_tone": "🤟🏼", "love_you_gesture_tone3": "🤟🏽", "love_you_gesture_medium_skin_tone": "🤟🏽", "love_you_gesture_tone4": "🤟🏾", "love_you_gesture_medium_dark_skin_tone": "🤟🏾", "love_you_gesture_tone5": "🤟🏿", "love_you_gesture_dark_skin_tone": "🤟🏿", "metal_tone1": "🤘🏻", "sign_of_the_horns_tone1": "🤘🏻", "metal_tone2": "🤘🏼", "sign_of_the_horns_tone2": "🤘🏼", "metal_tone3": "🤘🏽", "sign_of_the_horns_tone3": "🤘🏽", "metal_tone4": "🤘🏾", "sign_of_the_horns_tone4": "🤘🏾", "metal_tone5": "🤘🏿", "sign_of_the_horns_tone5": "🤘🏿", "ok_hand_tone1": "👌🏻", "ok_hand_tone2": "👌🏼", "ok_hand_tone3": "👌🏽", "ok_hand_tone4": "👌🏾", "ok_hand_tone5": "👌🏿", "pinched_fingers_tone2": "🤌🏼", "pinched_fingers_medium_light_skin_tone": "🤌🏼", "pinched_fingers_tone1": "🤌🏻", "pinched_fingers_light_skin_tone": "🤌🏻", "pinched_fingers_tone3": "🤌🏽", "pinched_fingers_medium_skin_tone": "🤌🏽", "pinched_fingers_tone4": "🤌🏾", "pinched_fingers_medium_dark_skin_tone": "🤌🏾", "pinched_fingers_tone5": "🤌🏿", "pinched_fingers_dark_skin_tone": "🤌🏿", "pinching_hand_tone1": "🤏🏻", "pinching_hand_light_skin_tone": "🤏🏻", "pinching_hand_tone2": "🤏🏼", "pinching_hand_medium_light_skin_tone": "🤏🏼", "pinching_hand_tone3": "🤏🏽", "pinching_hand_medium_skin_tone": "🤏🏽", "pinching_hand_tone4": "🤏🏾", "pinching_hand_medium_dark_skin_tone": "🤏🏾", "pinching_hand_tone5": "🤏🏿", "pinching_hand_dark_skin_tone": "🤏🏿", "palm_down_hand_tone1": "🫳🏻", "palm_down_hand_light_skin_tone": "🫳🏻", "palm_down_hand_tone2": "🫳🏼", "palm_down_hand_medium_light_skin_tone": "🫳🏼", "palm_down_hand_tone3": "🫳🏽", "palm_down_hand_medium_skin_tone": "🫳🏽", "palm_down_hand_tone4": "🫳🏾", "palm_down_hand_medium_dark_skin_tone": "🫳🏾", "palm_down_hand_tone5": "🫳🏿", "palm_down_hand_dark_skin_tone": "🫳🏿", "palm_up_hand_tone1": "🫴🏻", "palm_up_hand_light_skin_tone": "🫴🏻", "palm_up_hand_tone2": "🫴🏼", "palm_up_hand_medium_light_skin_tone": "🫴🏼", "palm_up_hand_tone3": "🫴🏽", "palm_up_hand_medium_skin_tone": "🫴🏽", "palm_up_hand_tone4": "🫴🏾", "palm_up_hand_medium_dark_skin_tone": "🫴🏾", "palm_up_hand_tone5": "🫴🏿", "palm_up_hand_dark_skin_tone": "🫴🏿", "point_left_tone1": "👈🏻", "point_left_tone2": "👈🏼", "point_left_tone3": "👈🏽", "point_left_tone4": "👈🏾", "point_left_tone5": "👈🏿", "point_right_tone1": "👉🏻", "point_right_tone2": "👉🏼", "point_right_tone3": "👉🏽", "point_right_tone4": "👉🏾", "point_right_tone5": "👉🏿", "point_up_2_tone1": "👆🏻", "point_up_2_tone2": "👆🏼", "point_up_2_tone3": "👆🏽", "point_up_2_tone4": "👆🏾", "point_up_2_tone5": "👆🏿", "point_down_tone1": "👇🏻", "point_down_tone2": "👇🏼", "point_down_tone3": "👇🏽", "point_down_tone4": "👇🏾", "point_down_tone5": "👇🏿", "point_up_tone1": "☝🏻", "point_up_tone2": "☝🏼", "point_up_tone3": "☝🏽", "point_up_tone4": "☝🏾", "point_up_tone5": "☝🏿", "raised_hand_tone1": "✋🏻", "raised_hand_tone2": "✋🏼", "raised_hand_tone3": "✋🏽", "raised_hand_tone4": "✋🏾", "raised_hand_tone5": "✋🏿", "raised_back_of_hand_tone1": "🤚🏻", "back_of_hand_tone1": "🤚🏻", "raised_back_of_hand_tone2": "🤚🏼", "back_of_hand_tone2": "🤚🏼", "raised_back_of_hand_tone3": "🤚🏽", "back_of_hand_tone3": "🤚🏽", "raised_back_of_hand_tone4": "🤚🏾", "back_of_hand_tone4": "🤚🏾", "raised_back_of_hand_tone5": "🤚🏿", "back_of_hand_tone5": "🤚🏿", "hand_splayed_tone1": "🖐🏻", "raised_hand_with_fingers_splayed_tone1": "🖐🏻", "hand_splayed_tone2": "🖐🏼", "raised_hand_with_fingers_splayed_tone2": "🖐🏼", "hand_splayed_tone3": "🖐🏽", "raised_hand_with_fingers_splayed_tone3": "🖐🏽", "hand_splayed_tone4": "🖐🏾", "raised_hand_with_fingers_splayed_tone4": "🖐🏾", "hand_splayed_tone5": "🖐🏿", "raised_hand_with_fingers_splayed_tone5": "🖐🏿", "vulcan_tone1": "🖖🏻", "raised_hand_with_part_between_middle_and_ring_fingers_tone1": "🖖🏻", "vulcan_tone2": "🖖🏼", "raised_hand_with_part_between_middle_and_ring_fingers_tone2": "🖖🏼", "vulcan_tone3": "🖖🏽", "raised_hand_with_part_between_middle_and_ring_fingers_tone3": "🖖🏽", "vulcan_tone4": "🖖🏾", "raised_hand_with_part_between_middle_and_ring_fingers_tone4": "🖖🏾", "vulcan_tone5": "🖖🏿", "raised_hand_with_part_between_middle_and_ring_fingers_tone5": "🖖🏿", "wave_tone1": "👋🏻", "wave_tone2": "👋🏼", "wave_tone3": "👋🏽", "wave_tone4": "👋🏾", "wave_tone5": "👋🏿", "call_me_tone1": "🤙🏻", "call_me_hand_tone1": "🤙🏻", "call_me_tone2": "🤙🏼", "call_me_hand_tone2": "🤙🏼", "call_me_tone3": "🤙🏽", "call_me_hand_tone3": "🤙🏽", "call_me_tone4": "🤙🏾", "call_me_hand_tone4": "🤙🏾", "call_me_tone5": "🤙🏿", "call_me_hand_tone5": "🤙🏿", "leftwards_hand_tone1": "🫲🏻", "leftwards_hand_light_skin_tone": "🫲🏻", "leftwards_hand_tone2": "🫲🏼", "leftwards_hand_medium_light_skin_tone": "🫲🏼", "leftwards_hand_tone3": "🫲🏽", "leftwards_hand_medium_skin_tone": "🫲🏽", "leftwards_hand_tone4": "🫲🏾", "leftwards_hand_medium_dark_skin_tone": "🫲🏾", "leftwards_hand_tone5": "🫲🏿", "leftwards_hand_dark_skin_tone": "🫲🏿", "rightwards_hand_tone1": "🫱🏻", "rightwards_hand_light_skin_tone": "🫱🏻", "rightwards_hand_tone2": "🫱🏼", "rightwards_hand_medium_light_skin_tone": "🫱🏼", "rightwards_hand_tone3": "🫱🏽", "rightwards_hand_medium_skin_tone": "🫱🏽", "rightwards_hand_tone4": "🫱🏾", "rightwards_hand_medium_dark_skin_tone": "🫱🏾", "rightwards_hand_tone5": "🫱🏿", "rightwards_hand_dark_skin_tone": "🫱🏿", "muscle_tone1": "💪🏻", "muscle_tone2": "💪🏼", "muscle_tone3": "💪🏽", "muscle_tone4": "💪🏾", "muscle_tone5": "💪🏿", "middle_finger_tone1": "🖕🏻", "reversed_hand_with_middle_finger_extended_tone1": "🖕🏻", "middle_finger_tone2": "🖕🏼", "reversed_hand_with_middle_finger_extended_tone2": "🖕🏼", "middle_finger_tone3": "🖕🏽", "reversed_hand_with_middle_finger_extended_tone3": "🖕🏽", "middle_finger_tone4": "🖕🏾", "reversed_hand_with_middle_finger_extended_tone4": "🖕🏾", "middle_finger_tone5": "🖕🏿", "reversed_hand_with_middle_finger_extended_tone5": "🖕🏿", "writing_hand_tone1": "✍🏻", "writing_hand_tone2": "✍🏼", "writing_hand_tone3": "✍🏽", "writing_hand_tone4": "✍🏾", "writing_hand_tone5": "✍🏿", "pray_tone1": "🙏🏻", "pray_tone2": "🙏🏼", "pray_tone3": "🙏🏽", "pray_tone4": "🙏🏾", "pray_tone5": "🙏🏿", "index_pointing_at_the_viewer_tone1": "🫵🏻", "index_pointing_at_the_viewer_light_skin_tone": "🫵🏻", "index_pointing_at_the_viewer_tone2": "🫵🏼", "index_pointing_at_the_viewer_medium_light_skin_tone": "🫵🏼", "index_pointing_at_the_viewer_tone3": "🫵🏽", "index_pointing_at_the_viewer_medium_skin_tone": "🫵🏽", "index_pointing_at_the_viewer_tone4": "🫵🏾", "index_pointing_at_the_viewer_medium_dark_skin_tone": "🫵🏾", "index_pointing_at_the_viewer_tone5": "🫵🏿", "index_pointing_at_the_viewer_dark_skin_tone": "🫵🏿", "foot_tone1": "🦶🏻", "foot_light_skin_tone": "🦶🏻", "foot_tone2": "🦶🏼", "foot_medium_light_skin_tone": "🦶🏼", "foot_tone3": "🦶🏽", "foot_medium_skin_tone": "🦶🏽", "foot_tone4": "🦶🏾", "foot_medium_dark_skin_tone": "🦶🏾", "foot_tone5": "🦶🏿", "foot_dark_skin_tone": "🦶🏿", "leg_tone1": "🦵🏻", "leg_light_skin_tone": "🦵🏻", "leg_tone2": "🦵🏼", "leg_medium_light_skin_tone": "🦵🏼", "leg_tone3": "🦵🏽", "leg_medium_skin_tone": "🦵🏽", "leg_tone4": "🦵🏾", "leg_medium_dark_skin_tone": "🦵🏾", "leg_tone5": "🦵🏿", "leg_dark_skin_tone": "🦵🏿", "ear_tone1": "👂🏻", "ear_tone2": "👂🏼", "ear_tone3": "👂🏽", "ear_tone4": "👂🏾", "ear_tone5": "👂🏿", "ear_with_hearing_aid_tone1": "🦻🏻", "ear_with_hearing_aid_light_skin_tone": "🦻🏻", "ear_with_hearing_aid_tone2": "🦻🏼", "ear_with_hearing_aid_medium_light_skin_tone": "🦻🏼", "ear_with_hearing_aid_tone3": "🦻🏽", "ear_with_hearing_aid_medium_skin_tone": "🦻🏽", "ear_with_hearing_aid_tone4": "🦻🏾", "ear_with_hearing_aid_medium_dark_skin_tone": "🦻🏾", "ear_with_hearing_aid_tone5": "🦻🏿", "ear_with_hearing_aid_dark_skin_tone": "🦻🏿", "nose_tone1": "👃🏻", "nose_tone2": "👃🏼", "nose_tone3": "👃🏽", "nose_tone4": "👃🏾", "nose_tone5": "👃🏿", "baby_tone1": "👶🏻", "baby_tone2": "👶🏼", "baby_tone3": "👶🏽", "baby_tone4": "👶🏾", "baby_tone5": "👶🏿", "child_tone1": "🧒🏻", "child_light_skin_tone": "🧒🏻", "child_tone2": "🧒🏼", "child_medium_light_skin_tone": "🧒🏼", "child_tone3": "🧒🏽", "child_medium_skin_tone": "🧒🏽", "child_tone4": "🧒🏾", "child_medium_dark_skin_tone": "🧒🏾", "child_tone5": "🧒🏿", "child_dark_skin_tone": "🧒🏿", "girl_tone1": "👧🏻", "girl_tone2": "👧🏼", "girl_tone3": "👧🏽", "girl_tone4": "👧🏾", "girl_tone5": "👧🏿", "boy_tone1": "👦🏻", "boy_tone2": "👦🏼", "boy_tone3": "👦🏽", "boy_tone4": "👦🏾", "boy_tone5": "👦🏿", "adult_tone1": "🧑🏻", "adult_light_skin_tone": "🧑🏻", "adult_tone2": "🧑🏼", "adult_medium_light_skin_tone": "🧑🏼", "adult_tone3": "🧑🏽", "adult_medium_skin_tone": "🧑🏽", "adult_tone4": "🧑🏾", "adult_medium_dark_skin_tone": "🧑🏾", "adult_tone5": "🧑🏿", "adult_dark_skin_tone": "🧑🏿", "woman_tone1": "👩🏻", "woman_tone2": "👩🏼", "woman_tone3": "👩🏽", "woman_tone4": "👩🏾", "woman_tone5": "👩🏿", "man_tone1": "👨🏻", "man_tone2": "👨🏼", "man_tone3": "👨🏽", "man_tone4": "👨🏾", "man_tone5": "👨🏿", "person_tone1_curly_hair": "🧑🏻‍🦱", "person_light_skin_tone_curly_hair": "🧑🏻‍🦱", "person_tone2_curly_hair": "🧑🏼‍🦱", "person_medium_light_skin_tone_curly_hair": "🧑🏼‍🦱", "person_tone3_curly_hair": "🧑🏽‍🦱", "person_medium_skin_tone_curly_hair": "🧑🏽‍🦱", "person_tone4_curly_hair": "🧑🏾‍🦱", "person_medium_dark_skin_tone_curly_hair": "🧑🏾‍🦱", "person_tone5_curly_hair": "🧑🏿‍🦱", "person_dark_skin_tone_curly_hair": "🧑🏿‍🦱", "woman_curly_haired_tone1": "👩🏻‍🦱", "woman_curly_haired_light_skin_tone": "👩🏻‍🦱", "woman_curly_haired_tone2": "👩🏼‍🦱", "woman_curly_haired_medium_light_skin_tone": "👩🏼‍🦱", "woman_curly_haired_tone3": "👩🏽‍🦱", "woman_curly_haired_medium_skin_tone": "👩🏽‍🦱", "woman_curly_haired_tone4": "👩🏾‍🦱", "woman_curly_haired_medium_dark_skin_tone": "👩🏾‍🦱", "woman_curly_haired_tone5": "👩🏿‍🦱", "woman_curly_haired_dark_skin_tone": "👩🏿‍🦱", "man_curly_haired_tone1": "👨🏻‍🦱", "man_curly_haired_light_skin_tone": "👨🏻‍🦱", "man_curly_haired_tone2": "👨🏼‍🦱", "man_curly_haired_medium_light_skin_tone": "👨🏼‍🦱", "man_curly_haired_tone3": "👨🏽‍🦱", "man_curly_haired_medium_skin_tone": "👨🏽‍🦱", "man_curly_haired_tone4": "👨🏾‍🦱", "man_curly_haired_medium_dark_skin_tone": "👨🏾‍🦱", "man_curly_haired_tone5": "👨🏿‍🦱", "man_curly_haired_dark_skin_tone": "👨🏿‍🦱", "person_tone1_red_hair": "🧑🏻‍🦰", "person_light_skin_tone_red_hair": "🧑🏻‍🦰", "person_tone2_red_hair": "🧑🏼‍🦰", "person_medium_light_skin_tone_red_hair": "🧑🏼‍🦰", "person_tone3_red_hair": "🧑🏽‍🦰", "person_medium_skin_tone_red_hair": "🧑🏽‍🦰", "person_tone4_red_hair": "🧑🏾‍🦰", "person_medium_dark_skin_tone_red_hair": "🧑🏾‍🦰", "person_tone5_red_hair": "🧑🏿‍🦰", "person_dark_skin_tone_red_hair": "🧑🏿‍🦰", "woman_red_haired_tone1": "👩🏻‍🦰", "woman_red_haired_light_skin_tone": "👩🏻‍🦰", "woman_red_haired_tone2": "👩🏼‍🦰", "woman_red_haired_medium_light_skin_tone": "👩🏼‍🦰", "woman_red_haired_tone3": "👩🏽‍🦰", "woman_red_haired_medium_skin_tone": "👩🏽‍🦰", "woman_red_haired_tone4": "👩🏾‍🦰", "woman_red_haired_medium_dark_skin_tone": "👩🏾‍🦰", "woman_red_haired_tone5": "👩🏿‍🦰", "woman_red_haired_dark_skin_tone": "👩🏿‍🦰", "man_red_haired_tone1": "👨🏻‍🦰", "man_red_haired_light_skin_tone": "👨🏻‍🦰", "man_red_haired_tone2": "👨🏼‍🦰", "man_red_haired_medium_light_skin_tone": "👨🏼‍🦰", "man_red_haired_tone3": "👨🏽‍🦰", "man_red_haired_medium_skin_tone": "👨🏽‍🦰", "man_red_haired_tone4": "👨🏾‍🦰", "man_red_haired_medium_dark_skin_tone": "👨🏾‍🦰", "man_red_haired_tone5": "👨🏿‍🦰", "man_red_haired_dark_skin_tone": "👨🏿‍🦰", "blond_haired_person_tone1": "👱🏻", "person_with_blond_hair_tone1": "👱🏻", "blond_haired_person_tone2": "👱🏼", "person_with_blond_hair_tone2": "👱🏼", "blond_haired_person_tone3": "👱🏽", "person_with_blond_hair_tone3": "👱🏽", "blond_haired_person_tone4": "👱🏾", "person_with_blond_hair_tone4": "👱🏾", "blond_haired_person_tone5": "👱🏿", "person_with_blond_hair_tone5": "👱🏿", "blond_haired_woman_tone1": "👱🏻‍♀️", "blond_haired_woman_light_skin_tone": "👱🏻‍♀️", "blond_haired_woman_tone2": "👱🏼‍♀️", "blond_haired_woman_medium_light_skin_tone": "👱🏼‍♀️", "blond_haired_woman_tone3": "👱🏽‍♀️", "blond_haired_woman_medium_skin_tone": "👱🏽‍♀️", "blond_haired_woman_tone4": "👱🏾‍♀️", "blond_haired_woman_medium_dark_skin_tone": "👱🏾‍♀️", "blond_haired_woman_tone5": "👱🏿‍♀️", "blond_haired_woman_dark_skin_tone": "👱🏿‍♀️", "blond_haired_man_tone1": "👱🏻‍♂️", "blond_haired_man_light_skin_tone": "👱🏻‍♂️", "blond_haired_man_tone2": "👱🏼‍♂️", "blond_haired_man_medium_light_skin_tone": "👱🏼‍♂️", "blond_haired_man_tone3": "👱🏽‍♂️", "blond_haired_man_medium_skin_tone": "👱🏽‍♂️", "blond_haired_man_tone4": "👱🏾‍♂️", "blond_haired_man_medium_dark_skin_tone": "👱🏾‍♂️", "blond_haired_man_tone5": "👱🏿‍♂️", "blond_haired_man_dark_skin_tone": "👱🏿‍♂️", "person_tone1_white_hair": "🧑🏻‍🦳", "person_light_skin_tone_white_hair": "🧑🏻‍🦳", "person_tone2_white_hair": "🧑🏼‍🦳", "person_medium_light_skin_tone_white_hair": "🧑🏼‍🦳", "person_tone3_white_hair": "🧑🏽‍🦳", "person_medium_skin_tone_white_hair": "🧑🏽‍🦳", "person_tone4_white_hair": "🧑🏾‍🦳", "person_medium_dark_skin_tone_white_hair": "🧑🏾‍🦳", "person_tone5_white_hair": "🧑🏿‍🦳", "person_dark_skin_tone_white_hair": "🧑🏿‍🦳", "woman_white_haired_tone1": "👩🏻‍🦳", "woman_white_haired_light_skin_tone": "👩🏻‍🦳", "woman_white_haired_tone2": "👩🏼‍🦳", "woman_white_haired_medium_light_skin_tone": "👩🏼‍🦳", "woman_white_haired_tone3": "👩🏽‍🦳", "woman_white_haired_medium_skin_tone": "👩🏽‍🦳", "woman_white_haired_tone4": "👩🏾‍🦳", "woman_white_haired_medium_dark_skin_tone": "👩🏾‍🦳", "woman_white_haired_tone5": "👩🏿‍🦳", "woman_white_haired_dark_skin_tone": "👩🏿‍🦳", "man_white_haired_tone1": "👨🏻‍🦳", "man_white_haired_light_skin_tone": "👨🏻‍🦳", "man_white_haired_tone2": "👨🏼‍🦳", "man_white_haired_medium_light_skin_tone": "👨🏼‍🦳", "man_white_haired_tone3": "👨🏽‍🦳", "man_white_haired_medium_skin_tone": "👨🏽‍🦳", "man_white_haired_tone4": "👨🏾‍🦳", "man_white_haired_medium_dark_skin_tone": "👨🏾‍🦳", "man_white_haired_tone5": "👨🏿‍🦳", "man_white_haired_dark_skin_tone": "👨🏿‍🦳", "person_tone1_bald": "🧑🏻‍🦲", "person_light_skin_tone_bald": "🧑🏻‍🦲", "person_tone2_bald": "🧑🏼‍🦲", "person_medium_light_skin_tone_bald": "🧑🏼‍🦲", "person_tone3_bald": "🧑🏽‍🦲", "person_medium_skin_tone_bald": "🧑🏽‍🦲", "person_tone4_bald": "🧑🏾‍🦲", "person_medium_dark_skin_tone_bald": "🧑🏾‍🦲", "person_tone5_bald": "🧑🏿‍🦲", "person_dark_skin_tone_bald": "🧑🏿‍🦲", "woman_bald_tone1": "👩🏻‍🦲", "woman_bald_light_skin_tone": "👩🏻‍🦲", "woman_bald_tone2": "👩🏼‍🦲", "woman_bald_medium_light_skin_tone": "👩🏼‍🦲", "woman_bald_tone3": "👩🏽‍🦲", "woman_bald_medium_skin_tone": "👩🏽‍🦲", "woman_bald_tone4": "👩🏾‍🦲", "woman_bald_medium_dark_skin_tone": "👩🏾‍🦲", "woman_bald_tone5": "👩🏿‍🦲", "woman_bald_dark_skin_tone": "👩🏿‍🦲", "man_bald_tone1": "👨🏻‍🦲", "man_bald_light_skin_tone": "👨🏻‍🦲", "man_bald_tone2": "👨🏼‍🦲", "man_bald_medium_light_skin_tone": "👨🏼‍🦲", "man_bald_tone3": "👨🏽‍🦲", "man_bald_medium_skin_tone": "👨🏽‍🦲", "man_bald_tone4": "👨🏾‍🦲", "man_bald_medium_dark_skin_tone": "👨🏾‍🦲", "man_bald_tone5": "👨🏿‍🦲", "man_bald_dark_skin_tone": "👨🏿‍🦲", "bearded_person_tone1": "🧔🏻", "bearded_person_light_skin_tone": "🧔🏻", "bearded_person_tone2": "🧔🏼", "bearded_person_medium_light_skin_tone": "🧔🏼", "bearded_person_tone3": "🧔🏽", "bearded_person_medium_skin_tone": "🧔🏽", "bearded_person_tone4": "🧔🏾", "bearded_person_medium_dark_skin_tone": "🧔🏾", "bearded_person_tone5": "🧔🏿", "bearded_person_dark_skin_tone": "🧔🏿", "woman_tone1_beard": "🧔🏻‍♀️", "woman_light_skin_tone_beard": "🧔🏻‍♀️", "woman_tone2_beard": "🧔🏼‍♀️", "woman_medium_light_skin_tone_beard": "🧔🏼‍♀️", "woman_tone3_beard": "🧔🏽‍♀️", "woman_medium_skin_tone_beard": "🧔🏽‍♀️", "woman_tone4_beard": "🧔🏾‍♀️", "woman_medium_dark_skin_tone_beard": "🧔🏾‍♀️", "woman_tone5_beard": "🧔🏿‍♀️", "woman_dark_skin_tone_beard": "🧔🏿‍♀️", "man_tone1_beard": "🧔🏻‍♂️", "man_light_skin_tone_beard": "🧔🏻‍♂️", "man_tone2_beard": "🧔🏼‍♂️", "man_medium_light_skin_tone_beard": "🧔🏼‍♂️", "man_tone3_beard": "🧔🏽‍♂️", "man_medium_skin_tone_beard": "🧔🏽‍♂️", "man_tone4_beard": "🧔🏾‍♂️", "man_medium_dark_skin_tone_beard": "🧔🏾‍♂️", "man_tone5_beard": "🧔🏿‍♂️", "man_dark_skin_tone_beard": "🧔🏿‍♂️", "older_adult_tone1": "🧓🏻", "older_adult_light_skin_tone": "🧓🏻", "older_adult_tone2": "🧓🏼", "older_adult_medium_light_skin_tone": "🧓🏼", "older_adult_tone3": "🧓🏽", "older_adult_medium_skin_tone": "🧓🏽", "older_adult_tone4": "🧓🏾", "older_adult_medium_dark_skin_tone": "🧓🏾", "older_adult_tone5": "🧓🏿", "older_adult_dark_skin_tone": "🧓🏿", "older_woman_tone1": "👵🏻", "grandma_tone1": "👵🏻", "older_woman_tone2": "👵🏼", "grandma_tone2": "👵🏼", "older_woman_tone3": "👵🏽", "grandma_tone3": "👵🏽", "older_woman_tone4": "👵🏾", "grandma_tone4": "👵🏾", "older_woman_tone5": "👵🏿", "grandma_tone5": "👵🏿", "older_man_tone1": "👴🏻", "older_man_tone2": "👴🏼", "older_man_tone3": "👴🏽", "older_man_tone4": "👴🏾", "older_man_tone5": "👴🏿", "man_with_chinese_cap_tone1": "👲🏻", "man_with_gua_pi_mao_tone1": "👲🏻", "man_with_chinese_cap_tone2": "👲🏼", "man_with_gua_pi_mao_tone2": "👲🏼", "man_with_chinese_cap_tone3": "👲🏽", "man_with_gua_pi_mao_tone3": "👲🏽", "man_with_chinese_cap_tone4": "👲🏾", "man_with_gua_pi_mao_tone4": "👲🏾", "man_with_chinese_cap_tone5": "👲🏿", "man_with_gua_pi_mao_tone5": "👲🏿", "person_wearing_turban_tone1": "👳🏻", "man_with_turban_tone1": "👳🏻", "person_wearing_turban_tone2": "👳🏼", "man_with_turban_tone2": "👳🏼", "person_wearing_turban_tone3": "👳🏽", "man_with_turban_tone3": "👳🏽", "person_wearing_turban_tone4": "👳🏾", "man_with_turban_tone4": "👳🏾", "person_wearing_turban_tone5": "👳🏿", "man_with_turban_tone5": "👳🏿", "woman_wearing_turban_tone1": "👳🏻‍♀️", "woman_wearing_turban_light_skin_tone": "👳🏻‍♀️", "woman_wearing_turban_tone2": "👳🏼‍♀️", "woman_wearing_turban_medium_light_skin_tone": "👳🏼‍♀️", "woman_wearing_turban_tone3": "👳🏽‍♀️", "woman_wearing_turban_medium_skin_tone": "👳🏽‍♀️", "woman_wearing_turban_tone4": "👳🏾‍♀️", "woman_wearing_turban_medium_dark_skin_tone": "👳🏾‍♀️", "woman_wearing_turban_tone5": "👳🏿‍♀️", "woman_wearing_turban_dark_skin_tone": "👳🏿‍♀️", "man_wearing_turban_tone1": "👳🏻‍♂️", "man_wearing_turban_light_skin_tone": "👳🏻‍♂️", "man_wearing_turban_tone2": "👳🏼‍♂️", "man_wearing_turban_medium_light_skin_tone": "👳🏼‍♂️", "man_wearing_turban_tone3": "👳🏽‍♂️", "man_wearing_turban_medium_skin_tone": "👳🏽‍♂️", "man_wearing_turban_tone4": "👳🏾‍♂️", "man_wearing_turban_medium_dark_skin_tone": "👳🏾‍♂️", "man_wearing_turban_tone5": "👳🏿‍♂️", "man_wearing_turban_dark_skin_tone": "👳🏿‍♂️", "woman_with_headscarf_tone1": "🧕🏻", "woman_with_headscarf_light_skin_tone": "🧕🏻", "woman_with_headscarf_tone2": "🧕🏼", "woman_with_headscarf_medium_light_skin_tone": "🧕🏼", "woman_with_headscarf_tone3": "🧕🏽", "woman_with_headscarf_medium_skin_tone": "🧕🏽", "woman_with_headscarf_tone4": "🧕🏾", "woman_with_headscarf_medium_dark_skin_tone": "🧕🏾", "woman_with_headscarf_tone5": "🧕🏿", "woman_with_headscarf_dark_skin_tone": "🧕🏿", "police_officer_tone1": "👮🏻", "cop_tone1": "👮🏻", "police_officer_tone2": "👮🏼", "cop_tone2": "👮🏼", "police_officer_tone3": "👮🏽", "cop_tone3": "👮🏽", "police_officer_tone4": "👮🏾", "cop_tone4": "👮🏾", "police_officer_tone5": "👮🏿", "cop_tone5": "👮🏿", "woman_police_officer_tone1": "👮🏻‍♀️", "woman_police_officer_light_skin_tone": "👮🏻‍♀️", "woman_police_officer_tone2": "👮🏼‍♀️", "woman_police_officer_medium_light_skin_tone": "👮🏼‍♀️", "woman_police_officer_tone3": "👮🏽‍♀️", "woman_police_officer_medium_skin_tone": "👮🏽‍♀️", "woman_police_officer_tone4": "👮🏾‍♀️", "woman_police_officer_medium_dark_skin_tone": "👮🏾‍♀️", "woman_police_officer_tone5": "👮🏿‍♀️", "woman_police_officer_dark_skin_tone": "👮🏿‍♀️", "man_police_officer_tone1": "👮🏻‍♂️", "man_police_officer_light_skin_tone": "👮🏻‍♂️", "man_police_officer_tone2": "👮🏼‍♂️", "man_police_officer_medium_light_skin_tone": "👮🏼‍♂️", "man_police_officer_tone3": "👮🏽‍♂️", "man_police_officer_medium_skin_tone": "👮🏽‍♂️", "man_police_officer_tone4": "👮🏾‍♂️", "man_police_officer_medium_dark_skin_tone": "👮🏾‍♂️", "man_police_officer_tone5": "👮🏿‍♂️", "man_police_officer_dark_skin_tone": "👮🏿‍♂️", "construction_worker_tone1": "👷🏻", "construction_worker_tone2": "👷🏼", "construction_worker_tone3": "👷🏽", "construction_worker_tone4": "👷🏾", "construction_worker_tone5": "👷🏿", "woman_construction_worker_tone1": "👷🏻‍♀️", "woman_construction_worker_light_skin_tone": "👷🏻‍♀️", "woman_construction_worker_tone2": "👷🏼‍♀️", "woman_construction_worker_medium_light_skin_tone": "👷🏼‍♀️", "woman_construction_worker_tone3": "👷🏽‍♀️", "woman_construction_worker_medium_skin_tone": "👷🏽‍♀️", "woman_construction_worker_tone4": "👷🏾‍♀️", "woman_construction_worker_medium_dark_skin_tone": "👷🏾‍♀️", "woman_construction_worker_tone5": "👷🏿‍♀️", "woman_construction_worker_dark_skin_tone": "👷🏿‍♀️", "man_construction_worker_tone1": "👷🏻‍♂️", "man_construction_worker_light_skin_tone": "👷🏻‍♂️", "man_construction_worker_tone2": "👷🏼‍♂️", "man_construction_worker_medium_light_skin_tone": "👷🏼‍♂️", "man_construction_worker_tone3": "👷🏽‍♂️", "man_construction_worker_medium_skin_tone": "👷🏽‍♂️", "man_construction_worker_tone4": "👷🏾‍♂️", "man_construction_worker_medium_dark_skin_tone": "👷🏾‍♂️", "man_construction_worker_tone5": "👷🏿‍♂️", "man_construction_worker_dark_skin_tone": "👷🏿‍♂️", "guard_tone1": "💂🏻", "guardsman_tone1": "💂🏻", "guard_tone2": "💂🏼", "guardsman_tone2": "💂🏼", "guard_tone3": "💂🏽", "guardsman_tone3": "💂🏽", "guard_tone4": "💂🏾", "guardsman_tone4": "💂🏾", "guard_tone5": "💂🏿", "guardsman_tone5": "💂🏿", "woman_guard_tone1": "💂🏻‍♀️", "woman_guard_light_skin_tone": "💂🏻‍♀️", "woman_guard_tone2": "💂🏼‍♀️", "woman_guard_medium_light_skin_tone": "💂🏼‍♀️", "woman_guard_tone3": "💂🏽‍♀️", "woman_guard_medium_skin_tone": "💂🏽‍♀️", "woman_guard_tone4": "💂🏾‍♀️", "woman_guard_medium_dark_skin_tone": "💂🏾‍♀️", "woman_guard_tone5": "💂🏿‍♀️", "woman_guard_dark_skin_tone": "💂🏿‍♀️", "man_guard_tone1": "💂🏻‍♂️", "man_guard_light_skin_tone": "💂🏻‍♂️", "man_guard_tone2": "💂🏼‍♂️", "man_guard_medium_light_skin_tone": "💂🏼‍♂️", "man_guard_tone3": "💂🏽‍♂️", "man_guard_medium_skin_tone": "💂🏽‍♂️", "man_guard_tone4": "💂🏾‍♂️", "man_guard_medium_dark_skin_tone": "💂🏾‍♂️", "man_guard_tone5": "💂🏿‍♂️", "man_guard_dark_skin_tone": "💂🏿‍♂️", "detective_tone1": "🕵🏻", "spy_tone1": "🕵🏻", "sleuth_or_spy_tone1": "🕵🏻", "detective_tone2": "🕵🏼", "spy_tone2": "🕵🏼", "sleuth_or_spy_tone2": "🕵🏼", "detective_tone3": "🕵🏽", "spy_tone3": "🕵🏽", "sleuth_or_spy_tone3": "🕵🏽", "detective_tone4": "🕵🏾", "spy_tone4": "🕵🏾", "sleuth_or_spy_tone4": "🕵🏾", "detective_tone5": "🕵🏿", "spy_tone5": "🕵🏿", "sleuth_or_spy_tone5": "🕵🏿", "woman_detective_tone1": "🕵🏻‍♀️", "woman_detective_light_skin_tone": "🕵🏻‍♀️", "woman_detective_tone2": "🕵🏼‍♀️", "woman_detective_medium_light_skin_tone": "🕵🏼‍♀️", "woman_detective_tone3": "🕵🏽‍♀️", "woman_detective_medium_skin_tone": "🕵🏽‍♀️", "woman_detective_tone4": "🕵🏾‍♀️", "woman_detective_medium_dark_skin_tone": "🕵🏾‍♀️", "woman_detective_tone5": "🕵🏿‍♀️", "woman_detective_dark_skin_tone": "🕵🏿‍♀️", "man_detective_tone1": "🕵🏻‍♂️", "man_detective_light_skin_tone": "🕵🏻‍♂️", "man_detective_tone2": "🕵🏼‍♂️", "man_detective_medium_light_skin_tone": "🕵🏼‍♂️", "man_detective_tone3": "🕵🏽‍♂️", "man_detective_medium_skin_tone": "🕵🏽‍♂️", "man_detective_tone4": "🕵🏾‍♂️", "man_detective_medium_dark_skin_tone": "🕵🏾‍♂️", "man_detective_tone5": "🕵🏿‍♂️", "man_detective_dark_skin_tone": "🕵🏿‍♂️", "health_worker_tone1": "🧑🏻‍⚕️", "health_worker_light_skin_tone": "🧑🏻‍⚕️", "health_worker_tone2": "🧑🏼‍⚕️", "health_worker_medium_light_skin_tone": "🧑🏼‍⚕️", "health_worker_tone3": "🧑🏽‍⚕️", "health_worker_medium_skin_tone": "🧑🏽‍⚕️", "health_worker_tone4": "🧑🏾‍⚕️", "health_worker_medium_dark_skin_tone": "🧑🏾‍⚕️", "health_worker_tone5": "🧑🏿‍⚕️", "health_worker_dark_skin_tone": "🧑🏿‍⚕️", "woman_health_worker_tone1": "👩🏻‍⚕️", "woman_health_worker_light_skin_tone": "👩🏻‍⚕️", "woman_health_worker_tone2": "👩🏼‍⚕️", "woman_health_worker_medium_light_skin_tone": "👩🏼‍⚕️", "woman_health_worker_tone3": "👩🏽‍⚕️", "woman_health_worker_medium_skin_tone": "👩🏽‍⚕️", "woman_health_worker_tone4": "👩🏾‍⚕️", "woman_health_worker_medium_dark_skin_tone": "👩🏾‍⚕️", "woman_health_worker_tone5": "👩🏿‍⚕️", "woman_health_worker_dark_skin_tone": "👩🏿‍⚕️", "man_health_worker_tone1": "👨🏻‍⚕️", "man_health_worker_light_skin_tone": "👨🏻‍⚕️", "man_health_worker_tone2": "👨🏼‍⚕️", "man_health_worker_medium_light_skin_tone": "👨🏼‍⚕️", "man_health_worker_tone3": "👨🏽‍⚕️", "man_health_worker_medium_skin_tone": "👨🏽‍⚕️", "man_health_worker_tone4": "👨🏾‍⚕️", "man_health_worker_medium_dark_skin_tone": "👨🏾‍⚕️", "man_health_worker_tone5": "👨🏿‍⚕️", "man_health_worker_dark_skin_tone": "👨🏿‍⚕️", "farmer_tone1": "🧑🏻‍🌾", "farmer_light_skin_tone": "🧑🏻‍🌾", "farmer_tone2": "🧑🏼‍🌾", "farmer_medium_light_skin_tone": "🧑🏼‍🌾", "farmer_tone3": "🧑🏽‍🌾", "farmer_medium_skin_tone": "🧑🏽‍🌾", "farmer_tone4": "🧑🏾‍🌾", "farmer_medium_dark_skin_tone": "🧑🏾‍🌾", "farmer_tone5": "🧑🏿‍🌾", "farmer_dark_skin_tone": "🧑🏿‍🌾", "woman_farmer_tone1": "👩🏻‍🌾", "woman_farmer_light_skin_tone": "👩🏻‍🌾", "woman_farmer_tone2": "👩🏼‍🌾", "woman_farmer_medium_light_skin_tone": "👩🏼‍🌾", "woman_farmer_tone3": "👩🏽‍🌾", "woman_farmer_medium_skin_tone": "👩🏽‍🌾", "woman_farmer_tone4": "👩🏾‍🌾", "woman_farmer_medium_dark_skin_tone": "👩🏾‍🌾", "woman_farmer_tone5": "👩🏿‍🌾", "woman_farmer_dark_skin_tone": "👩🏿‍🌾", "man_farmer_tone1": "👨🏻‍🌾", "man_farmer_light_skin_tone": "👨🏻‍🌾", "man_farmer_tone2": "👨🏼‍🌾", "man_farmer_medium_light_skin_tone": "👨🏼‍🌾", "man_farmer_tone3": "👨🏽‍🌾", "man_farmer_medium_skin_tone": "👨🏽‍🌾", "man_farmer_tone4": "👨🏾‍🌾", "man_farmer_medium_dark_skin_tone": "👨🏾‍🌾", "man_farmer_tone5": "👨🏿‍🌾", "man_farmer_dark_skin_tone": "👨🏿‍🌾", "cook_tone1": "🧑🏻‍🍳", "cook_light_skin_tone": "🧑🏻‍🍳", "cook_tone2": "🧑🏼‍🍳", "cook_medium_light_skin_tone": "🧑🏼‍🍳", "cook_tone3": "🧑🏽‍🍳", "cook_medium_skin_tone": "🧑🏽‍🍳", "cook_tone4": "🧑🏾‍🍳", "cook_medium_dark_skin_tone": "🧑🏾‍🍳", "cook_tone5": "🧑🏿‍🍳", "cook_dark_skin_tone": "🧑🏿‍🍳", "woman_cook_tone1": "👩🏻‍🍳", "woman_cook_light_skin_tone": "👩🏻‍🍳", "woman_cook_tone2": "👩🏼‍🍳", "woman_cook_medium_light_skin_tone": "👩🏼‍🍳", "woman_cook_tone3": "👩🏽‍🍳", "woman_cook_medium_skin_tone": "👩🏽‍🍳", "woman_cook_tone4": "👩🏾‍🍳", "woman_cook_medium_dark_skin_tone": "👩🏾‍🍳", "woman_cook_tone5": "👩🏿‍🍳", "woman_cook_dark_skin_tone": "👩🏿‍🍳", "man_cook_tone1": "👨🏻‍🍳", "man_cook_light_skin_tone": "👨🏻‍🍳", "man_cook_tone2": "👨🏼‍🍳", "man_cook_medium_light_skin_tone": "👨🏼‍🍳", "man_cook_tone3": "👨🏽‍🍳", "man_cook_medium_skin_tone": "👨🏽‍🍳", "man_cook_tone4": "👨🏾‍🍳", "man_cook_medium_dark_skin_tone": "👨🏾‍🍳", "man_cook_tone5": "👨🏿‍🍳", "man_cook_dark_skin_tone": "👨🏿‍🍳", "student_tone1": "🧑🏻‍🎓", "student_light_skin_tone": "🧑🏻‍🎓", "student_tone2": "🧑🏼‍🎓", "student_medium_light_skin_tone": "🧑🏼‍🎓", "student_tone3": "🧑🏽‍🎓", "student_medium_skin_tone": "🧑🏽‍🎓", "student_tone4": "🧑🏾‍🎓", "student_medium_dark_skin_tone": "🧑🏾‍🎓", "student_tone5": "🧑🏿‍🎓", "student_dark_skin_tone": "🧑🏿‍🎓", "woman_student_tone1": "👩🏻‍🎓", "woman_student_light_skin_tone": "👩🏻‍🎓", "woman_student_tone2": "👩🏼‍🎓", "woman_student_medium_light_skin_tone": "👩🏼‍🎓", "woman_student_tone3": "👩🏽‍🎓", "woman_student_medium_skin_tone": "👩🏽‍🎓", "woman_student_tone4": "👩🏾‍🎓", "woman_student_medium_dark_skin_tone": "👩🏾‍🎓", "woman_student_tone5": "👩🏿‍🎓", "woman_student_dark_skin_tone": "👩🏿‍🎓", "man_student_tone1": "👨🏻‍🎓", "man_student_light_skin_tone": "👨🏻‍🎓", "man_student_tone2": "👨🏼‍🎓", "man_student_medium_light_skin_tone": "👨🏼‍🎓", "man_student_tone3": "👨🏽‍🎓", "man_student_medium_skin_tone": "👨🏽‍🎓", "man_student_tone4": "👨🏾‍🎓", "man_student_medium_dark_skin_tone": "👨🏾‍🎓", "man_student_tone5": "👨🏿‍🎓", "man_student_dark_skin_tone": "👨🏿‍🎓", "singer_tone1": "🧑🏻‍🎤", "singer_light_skin_tone": "🧑🏻‍🎤", "singer_tone2": "🧑🏼‍🎤", "singer_medium_light_skin_tone": "🧑🏼‍🎤", "singer_tone3": "🧑🏽‍🎤", "singer_medium_skin_tone": "🧑🏽‍🎤", "singer_tone4": "🧑🏾‍🎤", "singer_medium_dark_skin_tone": "🧑🏾‍🎤", "singer_tone5": "🧑🏿‍🎤", "singer_dark_skin_tone": "🧑🏿‍🎤", "woman_singer_tone1": "👩🏻‍🎤", "woman_singer_light_skin_tone": "👩🏻‍🎤", "woman_singer_tone2": "👩🏼‍🎤", "woman_singer_medium_light_skin_tone": "👩🏼‍🎤", "woman_singer_tone3": "👩🏽‍🎤", "woman_singer_medium_skin_tone": "👩🏽‍🎤", "woman_singer_tone4": "👩🏾‍🎤", "woman_singer_medium_dark_skin_tone": "👩🏾‍🎤", "woman_singer_tone5": "👩🏿‍🎤", "woman_singer_dark_skin_tone": "👩🏿‍🎤", "man_singer_tone1": "👨🏻‍🎤", "man_singer_light_skin_tone": "👨🏻‍🎤", "man_singer_tone2": "👨🏼‍🎤", "man_singer_medium_light_skin_tone": "👨🏼‍🎤", "man_singer_tone3": "👨🏽‍🎤", "man_singer_medium_skin_tone": "👨🏽‍🎤", "man_singer_tone4": "👨🏾‍🎤", "man_singer_medium_dark_skin_tone": "👨🏾‍🎤", "man_singer_tone5": "👨🏿‍🎤", "man_singer_dark_skin_tone": "👨🏿‍🎤", "teacher_tone1": "🧑🏻‍🏫", "teacher_light_skin_tone": "🧑🏻‍🏫", "teacher_tone2": "🧑🏼‍🏫", "teacher_medium_light_skin_tone": "🧑🏼‍🏫", "teacher_tone3": "🧑🏽‍🏫", "teacher_medium_skin_tone": "🧑🏽‍🏫", "teacher_tone4": "🧑🏾‍🏫", "teacher_medium_dark_skin_tone": "🧑🏾‍🏫", "teacher_tone5": "🧑🏿‍🏫", "teacher_dark_skin_tone": "🧑🏿‍🏫", "woman_teacher_tone1": "👩🏻‍🏫", "woman_teacher_light_skin_tone": "👩🏻‍🏫", "woman_teacher_tone2": "👩🏼‍🏫", "woman_teacher_medium_light_skin_tone": "👩🏼‍🏫", "woman_teacher_tone3": "👩🏽‍🏫", "woman_teacher_medium_skin_tone": "👩🏽‍🏫", "woman_teacher_tone4": "👩🏾‍🏫", "woman_teacher_medium_dark_skin_tone": "👩🏾‍🏫", "woman_teacher_tone5": "👩🏿‍🏫", "woman_teacher_dark_skin_tone": "👩🏿‍🏫", "man_teacher_tone1": "👨🏻‍🏫", "man_teacher_light_skin_tone": "👨🏻‍🏫", "man_teacher_tone2": "👨🏼‍🏫", "man_teacher_medium_light_skin_tone": "👨🏼‍🏫", "man_teacher_tone3": "👨🏽‍🏫", "man_teacher_medium_skin_tone": "👨🏽‍🏫", "man_teacher_tone4": "👨🏾‍🏫", "man_teacher_medium_dark_skin_tone": "👨🏾‍🏫", "man_teacher_tone5": "👨🏿‍🏫", "man_teacher_dark_skin_tone": "👨🏿‍🏫", "factory_worker_tone1": "🧑🏻‍🏭", "factory_worker_light_skin_tone": "🧑🏻‍🏭", "factory_worker_tone2": "🧑🏼‍🏭", "factory_worker_medium_light_skin_tone": "🧑🏼‍🏭", "factory_worker_tone3": "🧑🏽‍🏭", "factory_worker_medium_skin_tone": "🧑🏽‍🏭", "factory_worker_tone4": "🧑🏾‍🏭", "factory_worker_medium_dark_skin_tone": "🧑🏾‍🏭", "factory_worker_tone5": "🧑🏿‍🏭", "factory_worker_dark_skin_tone": "🧑🏿‍🏭", "woman_factory_worker_tone1": "👩🏻‍🏭", "woman_factory_worker_light_skin_tone": "👩🏻‍🏭", "woman_factory_worker_tone2": "👩🏼‍🏭", "woman_factory_worker_medium_light_skin_tone": "👩🏼‍🏭", "woman_factory_worker_tone3": "👩🏽‍🏭", "woman_factory_worker_medium_skin_tone": "👩🏽‍🏭", "woman_factory_worker_tone4": "👩🏾‍🏭", "woman_factory_worker_medium_dark_skin_tone": "👩🏾‍🏭", "woman_factory_worker_tone5": "👩🏿‍🏭", "woman_factory_worker_dark_skin_tone": "👩🏿‍🏭", "man_factory_worker_tone1": "👨🏻‍🏭", "man_factory_worker_light_skin_tone": "👨🏻‍🏭", "man_factory_worker_tone2": "👨🏼‍🏭", "man_factory_worker_medium_light_skin_tone": "👨🏼‍🏭", "man_factory_worker_tone3": "👨🏽‍🏭", "man_factory_worker_medium_skin_tone": "👨🏽‍🏭", "man_factory_worker_tone4": "👨🏾‍🏭", "man_factory_worker_medium_dark_skin_tone": "👨🏾‍🏭", "man_factory_worker_tone5": "👨🏿‍🏭", "man_factory_worker_dark_skin_tone": "👨🏿‍🏭", "technologist_tone1": "🧑🏻‍💻", "technologist_light_skin_tone": "🧑🏻‍💻", "technologist_tone2": "🧑🏼‍💻", "technologist_medium_light_skin_tone": "🧑🏼‍💻", "technologist_tone3": "🧑🏽‍💻", "technologist_medium_skin_tone": "🧑🏽‍💻", "technologist_tone4": "🧑🏾‍💻", "technologist_medium_dark_skin_tone": "🧑🏾‍💻", "technologist_tone5": "🧑🏿‍💻", "technologist_dark_skin_tone": "🧑🏿‍💻", "woman_technologist_tone1": "👩🏻‍💻", "woman_technologist_light_skin_tone": "👩🏻‍💻", "woman_technologist_tone2": "👩🏼‍💻", "woman_technologist_medium_light_skin_tone": "👩🏼‍💻", "woman_technologist_tone3": "👩🏽‍💻", "woman_technologist_medium_skin_tone": "👩🏽‍💻", "woman_technologist_tone4": "👩🏾‍💻", "woman_technologist_medium_dark_skin_tone": "👩🏾‍💻", "woman_technologist_tone5": "👩🏿‍💻", "woman_technologist_dark_skin_tone": "👩🏿‍💻", "man_technologist_tone1": "👨🏻‍💻", "man_technologist_light_skin_tone": "👨🏻‍💻", "man_technologist_tone2": "👨🏼‍💻", "man_technologist_medium_light_skin_tone": "👨🏼‍💻", "man_technologist_tone3": "👨🏽‍💻", "man_technologist_medium_skin_tone": "👨🏽‍💻", "man_technologist_tone4": "👨🏾‍💻", "man_technologist_medium_dark_skin_tone": "👨🏾‍💻", "man_technologist_tone5": "👨🏿‍💻", "man_technologist_dark_skin_tone": "👨🏿‍💻", "office_worker_tone1": "🧑🏻‍💼", "office_worker_light_skin_tone": "🧑🏻‍💼", "office_worker_tone2": "🧑🏼‍💼", "office_worker_medium_light_skin_tone": "🧑🏼‍💼", "office_worker_tone3": "🧑🏽‍💼", "office_worker_medium_skin_tone": "🧑🏽‍💼", "office_worker_tone4": "🧑🏾‍💼", "office_worker_medium_dark_skin_tone": "🧑🏾‍💼", "office_worker_tone5": "🧑🏿‍💼", "office_worker_dark_skin_tone": "🧑🏿‍💼", "woman_office_worker_tone1": "👩🏻‍💼", "woman_office_worker_light_skin_tone": "👩🏻‍💼", "woman_office_worker_tone2": "👩🏼‍💼", "woman_office_worker_medium_light_skin_tone": "👩🏼‍💼", "woman_office_worker_tone3": "👩🏽‍💼", "woman_office_worker_medium_skin_tone": "👩🏽‍💼", "woman_office_worker_tone4": "👩🏾‍💼", "woman_office_worker_medium_dark_skin_tone": "👩🏾‍💼", "woman_office_worker_tone5": "👩🏿‍💼", "woman_office_worker_dark_skin_tone": "👩🏿‍💼", "man_office_worker_tone1": "👨🏻‍💼", "man_office_worker_light_skin_tone": "👨🏻‍💼", "man_office_worker_tone2": "👨🏼‍💼", "man_office_worker_medium_light_skin_tone": "👨🏼‍💼", "man_office_worker_tone3": "👨🏽‍💼", "man_office_worker_medium_skin_tone": "👨🏽‍💼", "man_office_worker_tone4": "👨🏾‍💼", "man_office_worker_medium_dark_skin_tone": "👨🏾‍💼", "man_office_worker_tone5": "👨🏿‍💼", "man_office_worker_dark_skin_tone": "👨🏿‍💼", "mechanic_tone1": "🧑🏻‍🔧", "mechanic_light_skin_tone": "🧑🏻‍🔧", "mechanic_tone2": "🧑🏼‍🔧", "mechanic_medium_light_skin_tone": "🧑🏼‍🔧", "mechanic_tone3": "🧑🏽‍🔧", "mechanic_medium_skin_tone": "🧑🏽‍🔧", "mechanic_tone4": "🧑🏾‍🔧", "mechanic_medium_dark_skin_tone": "🧑🏾‍🔧", "mechanic_tone5": "🧑🏿‍🔧", "mechanic_dark_skin_tone": "🧑🏿‍🔧", "woman_mechanic_tone1": "👩🏻‍🔧", "woman_mechanic_light_skin_tone": "👩🏻‍🔧", "woman_mechanic_tone2": "👩🏼‍🔧", "woman_mechanic_medium_light_skin_tone": "👩🏼‍🔧", "woman_mechanic_tone3": "👩🏽‍🔧", "woman_mechanic_medium_skin_tone": "👩🏽‍🔧", "woman_mechanic_tone4": "👩🏾‍🔧", "woman_mechanic_medium_dark_skin_tone": "👩🏾‍🔧", "woman_mechanic_tone5": "👩🏿‍🔧", "woman_mechanic_dark_skin_tone": "👩🏿‍🔧", "man_mechanic_tone1": "👨🏻‍🔧", "man_mechanic_light_skin_tone": "👨🏻‍🔧", "man_mechanic_tone2": "👨🏼‍🔧", "man_mechanic_medium_light_skin_tone": "👨🏼‍🔧", "man_mechanic_tone3": "👨🏽‍🔧", "man_mechanic_medium_skin_tone": "👨🏽‍🔧", "man_mechanic_tone4": "👨🏾‍🔧", "man_mechanic_medium_dark_skin_tone": "👨🏾‍🔧", "man_mechanic_tone5": "👨🏿‍🔧", "man_mechanic_dark_skin_tone": "👨🏿‍🔧", "scientist_tone1": "🧑🏻‍🔬", "scientist_light_skin_tone": "🧑🏻‍🔬", "scientist_tone2": "🧑🏼‍🔬", "scientist_medium_light_skin_tone": "🧑🏼‍🔬", "scientist_tone3": "🧑🏽‍🔬", "scientist_medium_skin_tone": "🧑🏽‍🔬", "scientist_tone4": "🧑🏾‍🔬", "scientist_medium_dark_skin_tone": "🧑🏾‍🔬", "scientist_tone5": "🧑🏿‍🔬", "scientist_dark_skin_tone": "🧑🏿‍🔬", "woman_scientist_tone1": "👩🏻‍🔬", "woman_scientist_light_skin_tone": "👩🏻‍🔬", "woman_scientist_tone2": "👩🏼‍🔬", "woman_scientist_medium_light_skin_tone": "👩🏼‍🔬", "woman_scientist_tone3": "👩🏽‍🔬", "woman_scientist_medium_skin_tone": "👩🏽‍🔬", "woman_scientist_tone4": "👩🏾‍🔬", "woman_scientist_medium_dark_skin_tone": "👩🏾‍🔬", "woman_scientist_tone5": "👩🏿‍🔬", "woman_scientist_dark_skin_tone": "👩🏿‍🔬", "man_scientist_tone1": "👨🏻‍🔬", "man_scientist_light_skin_tone": "👨🏻‍🔬", "man_scientist_tone2": "👨🏼‍🔬", "man_scientist_medium_light_skin_tone": "👨🏼‍🔬", "man_scientist_tone3": "👨🏽‍🔬", "man_scientist_medium_skin_tone": "👨🏽‍🔬", "man_scientist_tone4": "👨🏾‍🔬", "man_scientist_medium_dark_skin_tone": "👨🏾‍🔬", "man_scientist_tone5": "👨🏿‍🔬", "man_scientist_dark_skin_tone": "👨🏿‍🔬", "artist_tone1": "🧑🏻‍🎨", "artist_light_skin_tone": "🧑🏻‍🎨", "artist_tone2": "🧑🏼‍🎨", "artist_medium_light_skin_tone": "🧑🏼‍🎨", "artist_tone3": "🧑🏽‍🎨", "artist_medium_skin_tone": "🧑🏽‍🎨", "artist_tone4": "🧑🏾‍🎨", "artist_medium_dark_skin_tone": "🧑🏾‍🎨", "artist_tone5": "🧑🏿‍🎨", "artist_dark_skin_tone": "🧑🏿‍🎨", "woman_artist_tone1": "👩🏻‍🎨", "woman_artist_light_skin_tone": "👩🏻‍🎨", "woman_artist_tone2": "👩🏼‍🎨", "woman_artist_medium_light_skin_tone": "👩🏼‍🎨", "woman_artist_tone3": "👩🏽‍🎨", "woman_artist_medium_skin_tone": "👩🏽‍🎨", "woman_artist_tone4": "👩🏾‍🎨", "woman_artist_medium_dark_skin_tone": "👩🏾‍🎨", "woman_artist_tone5": "👩🏿‍🎨", "woman_artist_dark_skin_tone": "👩🏿‍🎨", "man_artist_tone1": "👨🏻‍🎨", "man_artist_light_skin_tone": "👨🏻‍🎨", "man_artist_tone2": "👨🏼‍🎨", "man_artist_medium_light_skin_tone": "👨🏼‍🎨", "man_artist_tone3": "👨🏽‍🎨", "man_artist_medium_skin_tone": "👨🏽‍🎨", "man_artist_tone4": "👨🏾‍🎨", "man_artist_medium_dark_skin_tone": "👨🏾‍🎨", "man_artist_tone5": "👨🏿‍🎨", "man_artist_dark_skin_tone": "👨🏿‍🎨", "firefighter_tone1": "🧑🏻‍🚒", "firefighter_light_skin_tone": "🧑🏻‍🚒", "firefighter_tone2": "🧑🏼‍🚒", "firefighter_medium_light_skin_tone": "🧑🏼‍🚒", "firefighter_tone3": "🧑🏽‍🚒", "firefighter_medium_skin_tone": "🧑🏽‍🚒", "firefighter_tone4": "🧑🏾‍🚒", "firefighter_medium_dark_skin_tone": "🧑🏾‍🚒", "firefighter_tone5": "🧑🏿‍🚒", "firefighter_dark_skin_tone": "🧑🏿‍🚒", "woman_firefighter_tone1": "👩🏻‍🚒", "woman_firefighter_light_skin_tone": "👩🏻‍🚒", "woman_firefighter_tone2": "👩🏼‍🚒", "woman_firefighter_medium_light_skin_tone": "👩🏼‍🚒", "woman_firefighter_tone3": "👩🏽‍🚒", "woman_firefighter_medium_skin_tone": "👩🏽‍🚒", "woman_firefighter_tone4": "👩🏾‍🚒", "woman_firefighter_medium_dark_skin_tone": "👩🏾‍🚒", "woman_firefighter_tone5": "👩🏿‍🚒", "woman_firefighter_dark_skin_tone": "👩🏿‍🚒", "man_firefighter_tone1": "👨🏻‍🚒", "man_firefighter_light_skin_tone": "👨🏻‍🚒", "man_firefighter_tone2": "👨🏼‍🚒", "man_firefighter_medium_light_skin_tone": "👨🏼‍🚒", "man_firefighter_tone3": "👨🏽‍🚒", "man_firefighter_medium_skin_tone": "👨🏽‍🚒", "man_firefighter_tone4": "👨🏾‍🚒", "man_firefighter_medium_dark_skin_tone": "👨🏾‍🚒", "man_firefighter_tone5": "👨🏿‍🚒", "man_firefighter_dark_skin_tone": "👨🏿‍🚒", "pilot_tone1": "🧑🏻‍✈️", "pilot_light_skin_tone": "🧑🏻‍✈️", "pilot_tone2": "🧑🏼‍✈️", "pilot_medium_light_skin_tone": "🧑🏼‍✈️", "pilot_tone3": "🧑🏽‍✈️", "pilot_medium_skin_tone": "🧑🏽‍✈️", "pilot_tone4": "🧑🏾‍✈️", "pilot_medium_dark_skin_tone": "🧑🏾‍✈️", "pilot_tone5": "🧑🏿‍✈️", "pilot_dark_skin_tone": "🧑🏿‍✈️", "woman_pilot_tone1": "👩🏻‍✈️", "woman_pilot_light_skin_tone": "👩🏻‍✈️", "woman_pilot_tone2": "👩🏼‍✈️", "woman_pilot_medium_light_skin_tone": "👩🏼‍✈️", "woman_pilot_tone3": "👩🏽‍✈️", "woman_pilot_medium_skin_tone": "👩🏽‍✈️", "woman_pilot_tone4": "👩🏾‍✈️", "woman_pilot_medium_dark_skin_tone": "👩🏾‍✈️", "woman_pilot_tone5": "👩🏿‍✈️", "woman_pilot_dark_skin_tone": "👩🏿‍✈️", "man_pilot_tone1": "👨🏻‍✈️", "man_pilot_light_skin_tone": "👨🏻‍✈️", "man_pilot_tone2": "👨🏼‍✈️", "man_pilot_medium_light_skin_tone": "👨🏼‍✈️", "man_pilot_tone3": "👨🏽‍✈️", "man_pilot_medium_skin_tone": "👨🏽‍✈️", "man_pilot_tone4": "👨🏾‍✈️", "man_pilot_medium_dark_skin_tone": "👨🏾‍✈️", "man_pilot_tone5": "👨🏿‍✈️", "man_pilot_dark_skin_tone": "👨🏿‍✈️", "astronaut_tone1": "🧑🏻‍🚀", "astronaut_light_skin_tone": "🧑🏻‍🚀", "astronaut_tone2": "🧑🏼‍🚀", "astronaut_medium_light_skin_tone": "🧑🏼‍🚀", "astronaut_tone3": "🧑🏽‍🚀", "astronaut_medium_skin_tone": "🧑🏽‍🚀", "astronaut_tone4": "🧑🏾‍🚀", "astronaut_medium_dark_skin_tone": "🧑🏾‍🚀", "astronaut_tone5": "🧑🏿‍🚀", "astronaut_dark_skin_tone": "🧑🏿‍🚀", "woman_astronaut_tone1": "👩🏻‍🚀", "woman_astronaut_light_skin_tone": "👩🏻‍🚀", "woman_astronaut_tone2": "👩🏼‍🚀", "woman_astronaut_medium_light_skin_tone": "👩🏼‍🚀", "woman_astronaut_tone3": "👩🏽‍🚀", "woman_astronaut_medium_skin_tone": "👩🏽‍🚀", "woman_astronaut_tone4": "👩🏾‍🚀", "woman_astronaut_medium_dark_skin_tone": "👩🏾‍🚀", "woman_astronaut_tone5": "👩🏿‍🚀", "woman_astronaut_dark_skin_tone": "👩🏿‍🚀", "man_astronaut_tone1": "👨🏻‍🚀", "man_astronaut_light_skin_tone": "👨🏻‍🚀", "man_astronaut_tone2": "👨🏼‍🚀", "man_astronaut_medium_light_skin_tone": "👨🏼‍🚀", "man_astronaut_tone3": "👨🏽‍🚀", "man_astronaut_medium_skin_tone": "👨🏽‍🚀", "man_astronaut_tone4": "👨🏾‍🚀", "man_astronaut_medium_dark_skin_tone": "👨🏾‍🚀", "man_astronaut_tone5": "👨🏿‍🚀", "man_astronaut_dark_skin_tone": "👨🏿‍🚀", "judge_tone1": "🧑🏻‍⚖️", "judge_light_skin_tone": "🧑🏻‍⚖️", "judge_tone2": "🧑🏼‍⚖️", "judge_medium_light_skin_tone": "🧑🏼‍⚖️", "judge_tone3": "🧑🏽‍⚖️", "judge_medium_skin_tone": "🧑🏽‍⚖️", "judge_tone4": "🧑🏾‍⚖️", "judge_medium_dark_skin_tone": "🧑🏾‍⚖️", "judge_tone5": "🧑🏿‍⚖️", "judge_dark_skin_tone": "🧑🏿‍⚖️", "woman_judge_tone1": "👩🏻‍⚖️", "woman_judge_light_skin_tone": "👩🏻‍⚖️", "woman_judge_tone2": "👩🏼‍⚖️", "woman_judge_medium_light_skin_tone": "👩🏼‍⚖️", "woman_judge_tone3": "👩🏽‍⚖️", "woman_judge_medium_skin_tone": "👩🏽‍⚖️", "woman_judge_tone4": "👩🏾‍⚖️", "woman_judge_medium_dark_skin_tone": "👩🏾‍⚖️", "woman_judge_tone5": "👩🏿‍⚖️", "woman_judge_dark_skin_tone": "👩🏿‍⚖️", "man_judge_tone1": "👨🏻‍⚖️", "man_judge_light_skin_tone": "👨🏻‍⚖️", "man_judge_tone2": "👨🏼‍⚖️", "man_judge_medium_light_skin_tone": "👨🏼‍⚖️", "man_judge_tone3": "👨🏽‍⚖️", "man_judge_medium_skin_tone": "👨🏽‍⚖️", "man_judge_tone4": "👨🏾‍⚖️", "man_judge_medium_dark_skin_tone": "👨🏾‍⚖️", "man_judge_tone5": "👨🏿‍⚖️", "man_judge_dark_skin_tone": "👨🏿‍⚖️", "person_with_veil_tone1": "👰🏻", "person_with_veil_tone2": "👰🏼", "person_with_veil_tone3": "👰🏽", "person_with_veil_tone4": "👰🏾", "person_with_veil_tone5": "👰🏿", "woman_with_veil_tone1": "👰🏻‍♀️", "woman_with_veil_light_skin_tone": "👰🏻‍♀️", "woman_with_veil_tone2": "👰🏼‍♀️", "woman_with_veil_medium_light_skin_tone": "👰🏼‍♀️", "woman_with_veil_tone3": "👰🏽‍♀️", "woman_with_veil_medium_skin_tone": "👰🏽‍♀️", "woman_with_veil_tone4": "👰🏾‍♀️", "woman_with_veil_medium_dark_skin_tone": "👰🏾‍♀️", "woman_with_veil_tone5": "👰🏿‍♀️", "woman_with_veil_dark_skin_tone": "👰🏿‍♀️", "man_with_veil_tone1": "👰🏻‍♂️", "man_with_veil_light_skin_tone": "👰🏻‍♂️", "man_with_veil_tone2": "👰🏼‍♂️", "man_with_veil_medium_light_skin_tone": "👰🏼‍♂️", "man_with_veil_tone3": "👰🏽‍♂️", "man_with_veil_medium_skin_tone": "👰🏽‍♂️", "man_with_veil_tone4": "👰🏾‍♂️", "man_with_veil_medium_dark_skin_tone": "👰🏾‍♂️", "man_with_veil_tone5": "👰🏿‍♂️", "man_with_veil_dark_skin_tone": "👰🏿‍♂️", "person_in_tuxedo_tone1": "🤵🏻", "tuxedo_tone1": "🤵🏻", "person_in_tuxedo_tone2": "🤵🏼", "tuxedo_tone2": "🤵🏼", "person_in_tuxedo_tone3": "🤵🏽", "tuxedo_tone3": "🤵🏽", "person_in_tuxedo_tone4": "🤵🏾", "tuxedo_tone4": "🤵🏾", "person_in_tuxedo_tone5": "🤵🏿", "tuxedo_tone5": "🤵🏿", "woman_in_tuxedo_tone1": "🤵🏻‍♀️", "woman_in_tuxedo_light_skin_tone": "🤵🏻‍♀️", "woman_in_tuxedo_tone2": "🤵🏼‍♀️", "woman_in_tuxedo_medium_light_skin_tone": "🤵🏼‍♀️", "woman_in_tuxedo_tone3": "🤵🏽‍♀️", "woman_in_tuxedo_medium_skin_tone": "🤵🏽‍♀️", "woman_in_tuxedo_tone4": "🤵🏾‍♀️", "woman_in_tuxedo_medium_dark_skin_tone": "🤵🏾‍♀️", "woman_in_tuxedo_tone5": "🤵🏿‍♀️", "woman_in_tuxedo_dark_skin_tone": "🤵🏿‍♀️", "man_in_tuxedo_tone1": "🤵🏻‍♂️", "man_in_tuxedo_light_skin_tone": "🤵🏻‍♂️", "man_in_tuxedo_tone2": "🤵🏼‍♂️", "man_in_tuxedo_medium_light_skin_tone": "🤵🏼‍♂️", "man_in_tuxedo_tone3": "🤵🏽‍♂️", "man_in_tuxedo_medium_skin_tone": "🤵🏽‍♂️", "man_in_tuxedo_tone4": "🤵🏾‍♂️", "man_in_tuxedo_medium_dark_skin_tone": "🤵🏾‍♂️", "man_in_tuxedo_tone5": "🤵🏿‍♂️", "man_in_tuxedo_dark_skin_tone": "🤵🏿‍♂️", "person_with_crown_tone1": "🫅🏻", "person_with_crown_light_skin_tone": "🫅🏻", "person_with_crown_tone2": "🫅🏼", "person_with_crown_medium_light_skin_tone": "🫅🏼", "person_with_crown_tone3": "🫅🏽", "person_with_crown_medium_skin_tone": "🫅🏽", "person_with_crown_tone4": "🫅🏾", "person_with_crown_medium_dark_skin_tone": "🫅🏾", "person_with_crown_tone5": "🫅🏿", "person_with_crown_dark_skin_tone": "🫅🏿", "princess_tone1": "👸🏻", "princess_tone2": "👸🏼", "princess_tone3": "👸🏽", "princess_tone4": "👸🏾", "princess_tone5": "👸🏿", "prince_tone1": "🤴🏻", "prince_tone2": "🤴🏼", "prince_tone3": "🤴🏽", "prince_tone4": "🤴🏾", "prince_tone5": "🤴🏿", "superhero_tone1": "🦸🏻", "superhero_light_skin_tone": "🦸🏻", "superhero_tone2": "🦸🏼", "superhero_medium_light_skin_tone": "🦸🏼", "superhero_tone3": "🦸🏽", "superhero_medium_skin_tone": "🦸🏽", "superhero_tone4": "🦸🏾", "superhero_medium_dark_skin_tone": "🦸🏾", "superhero_tone5": "🦸🏿", "superhero_dark_skin_tone": "🦸🏿", "woman_superhero_tone1": "🦸🏻‍♀️", "woman_superhero_light_skin_tone": "🦸🏻‍♀️", "woman_superhero_tone2": "🦸🏼‍♀️", "woman_superhero_medium_light_skin_tone": "🦸🏼‍♀️", "woman_superhero_tone3": "🦸🏽‍♀️", "woman_superhero_medium_skin_tone": "🦸🏽‍♀️", "woman_superhero_tone4": "🦸🏾‍♀️", "woman_superhero_medium_dark_skin_tone": "🦸🏾‍♀️", "woman_superhero_tone5": "🦸🏿‍♀️", "woman_superhero_dark_skin_tone": "🦸🏿‍♀️", "man_superhero_tone1": "🦸🏻‍♂️", "man_superhero_light_skin_tone": "🦸🏻‍♂️", "man_superhero_tone2": "🦸🏼‍♂️", "man_superhero_medium_light_skin_tone": "🦸🏼‍♂️", "man_superhero_tone3": "🦸🏽‍♂️", "man_superhero_medium_skin_tone": "🦸🏽‍♂️", "man_superhero_tone4": "🦸🏾‍♂️", "man_superhero_medium_dark_skin_tone": "🦸🏾‍♂️", "man_superhero_tone5": "🦸🏿‍♂️", "man_superhero_dark_skin_tone": "🦸🏿‍♂️", "supervillain_tone1": "🦹🏻", "supervillain_light_skin_tone": "🦹🏻", "supervillain_tone2": "🦹🏼", "supervillain_medium_light_skin_tone": "🦹🏼", "supervillain_tone3": "🦹🏽", "supervillain_medium_skin_tone": "🦹🏽", "supervillain_tone4": "🦹🏾", "supervillain_medium_dark_skin_tone": "🦹🏾", "supervillain_tone5": "🦹🏿", "supervillain_dark_skin_tone": "🦹🏿", "woman_supervillain_tone1": "🦹🏻‍♀️", "woman_supervillain_light_skin_tone": "🦹🏻‍♀️", "woman_supervillain_tone2": "🦹🏼‍♀️", "woman_supervillain_medium_light_skin_tone": "🦹🏼‍♀️", "woman_supervillain_tone3": "🦹🏽‍♀️", "woman_supervillain_medium_skin_tone": "🦹🏽‍♀️", "woman_supervillain_tone4": "🦹🏾‍♀️", "woman_supervillain_medium_dark_skin_tone": "🦹🏾‍♀️", "woman_supervillain_tone5": "🦹🏿‍♀️", "woman_supervillain_dark_skin_tone": "🦹🏿‍♀️", "man_supervillain_tone1": "🦹🏻‍♂️", "man_supervillain_light_skin_tone": "🦹🏻‍♂️", "man_supervillain_tone2": "🦹🏼‍♂️", "man_supervillain_medium_light_skin_tone": "🦹🏼‍♂️", "man_supervillain_tone3": "🦹🏽‍♂️", "man_supervillain_medium_skin_tone": "🦹🏽‍♂️", "man_supervillain_tone4": "🦹🏾‍♂️", "man_supervillain_medium_dark_skin_tone": "🦹🏾‍♂️", "man_supervillain_tone5": "🦹🏿‍♂️", "man_supervillain_dark_skin_tone": "🦹🏿‍♂️", "ninja_tone1": "🥷🏻", "ninja_light_skin_tone": "🥷🏻", "ninja_tone2": "🥷🏼", "ninja_medium_light_skin_tone": "🥷🏼", "ninja_tone3": "🥷🏽", "ninja_medium_skin_tone": "🥷🏽", "ninja_tone4": "🥷🏾", "ninja_medium_dark_skin_tone": "🥷🏾", "ninja_tone5": "🥷🏿", "ninja_dark_skin_tone": "🥷🏿", "mx_claus_tone1": "🧑🏻‍🎄", "mx_claus_light_skin_tone": "🧑🏻‍🎄", "mx_claus_tone2": "🧑🏼‍🎄", "mx_claus_medium_light_skin_tone": "🧑🏼‍🎄", "mx_claus_tone3": "🧑🏽‍🎄", "mx_claus_medium_skin_tone": "🧑🏽‍🎄", "mx_claus_tone4": "🧑🏾‍🎄", "mx_claus_medium_dark_skin_tone": "🧑🏾‍🎄", "mx_claus_tone5": "🧑🏿‍🎄", "mx_claus_dark_skin_tone": "🧑🏿‍🎄", "mrs_claus_tone1": "🤶🏻", "mother_christmas_tone1": "🤶🏻", "mrs_claus_tone2": "🤶🏼", "mother_christmas_tone2": "🤶🏼", "mrs_claus_tone3": "🤶🏽", "mother_christmas_tone3": "🤶🏽", "mrs_claus_tone4": "🤶🏾", "mother_christmas_tone4": "🤶🏾", "mrs_claus_tone5": "🤶🏿", "mother_christmas_tone5": "🤶🏿", "santa_tone1": "🎅🏻", "santa_tone2": "🎅🏼", "santa_tone3": "🎅🏽", "santa_tone4": "🎅🏾", "santa_tone5": "🎅🏿", "mage_tone1": "🧙🏻", "mage_light_skin_tone": "🧙🏻", "mage_tone2": "🧙🏼", "mage_medium_light_skin_tone": "🧙🏼", "mage_tone3": "🧙🏽", "mage_medium_skin_tone": "🧙🏽", "mage_tone4": "🧙🏾", "mage_medium_dark_skin_tone": "🧙🏾", "mage_tone5": "🧙🏿", "mage_dark_skin_tone": "🧙🏿", "woman_mage_tone1": "🧙🏻‍♀️", "woman_mage_light_skin_tone": "🧙🏻‍♀️", "woman_mage_tone2": "🧙🏼‍♀️", "woman_mage_medium_light_skin_tone": "🧙🏼‍♀️", "woman_mage_tone3": "🧙🏽‍♀️", "woman_mage_medium_skin_tone": "🧙🏽‍♀️", "woman_mage_tone4": "🧙🏾‍♀️", "woman_mage_medium_dark_skin_tone": "🧙🏾‍♀️", "woman_mage_tone5": "🧙🏿‍♀️", "woman_mage_dark_skin_tone": "🧙🏿‍♀️", "man_mage_tone1": "🧙🏻‍♂️", "man_mage_light_skin_tone": "🧙🏻‍♂️", "man_mage_tone2": "🧙🏼‍♂️", "man_mage_medium_light_skin_tone": "🧙🏼‍♂️", "man_mage_tone3": "🧙🏽‍♂️", "man_mage_medium_skin_tone": "🧙🏽‍♂️", "man_mage_tone4": "🧙🏾‍♂️", "man_mage_medium_dark_skin_tone": "🧙🏾‍♂️", "man_mage_tone5": "🧙🏿‍♂️", "man_mage_dark_skin_tone": "🧙🏿‍♂️", "elf_tone1": "🧝🏻", "elf_light_skin_tone": "🧝🏻", "elf_tone2": "🧝🏼", "elf_medium_light_skin_tone": "🧝🏼", "elf_tone3": "🧝🏽", "elf_medium_skin_tone": "🧝🏽", "elf_tone4": "🧝🏾", "elf_medium_dark_skin_tone": "🧝🏾", "elf_tone5": "🧝🏿", "elf_dark_skin_tone": "🧝🏿", "woman_elf_tone1": "🧝🏻‍♀️", "woman_elf_light_skin_tone": "🧝🏻‍♀️", "woman_elf_tone2": "🧝🏼‍♀️", "woman_elf_medium_light_skin_tone": "🧝🏼‍♀️", "woman_elf_tone3": "🧝🏽‍♀️", "woman_elf_medium_skin_tone": "🧝🏽‍♀️", "woman_elf_tone4": "🧝🏾‍♀️", "woman_elf_medium_dark_skin_tone": "🧝🏾‍♀️", "woman_elf_tone5": "🧝🏿‍♀️", "woman_elf_dark_skin_tone": "🧝🏿‍♀️", "man_elf_tone1": "🧝🏻‍♂️", "man_elf_light_skin_tone": "🧝🏻‍♂️", "man_elf_tone2": "🧝🏼‍♂️", "man_elf_medium_light_skin_tone": "🧝🏼‍♂️", "man_elf_tone3": "🧝🏽‍♂️", "man_elf_medium_skin_tone": "🧝🏽‍♂️", "man_elf_tone4": "🧝🏾‍♂️", "man_elf_medium_dark_skin_tone": "🧝🏾‍♂️", "man_elf_tone5": "🧝🏿‍♂️", "man_elf_dark_skin_tone": "🧝🏿‍♂️", "vampire_tone1": "🧛🏻", "vampire_light_skin_tone": "🧛🏻", "vampire_tone2": "🧛🏼", "vampire_medium_light_skin_tone": "🧛🏼", "vampire_tone3": "🧛🏽", "vampire_medium_skin_tone": "🧛🏽", "vampire_tone4": "🧛🏾", "vampire_medium_dark_skin_tone": "🧛🏾", "vampire_tone5": "🧛🏿", "vampire_dark_skin_tone": "🧛🏿", "woman_vampire_tone1": "🧛🏻‍♀️", "woman_vampire_light_skin_tone": "🧛🏻‍♀️", "woman_vampire_tone2": "🧛🏼‍♀️", "woman_vampire_medium_light_skin_tone": "🧛🏼‍♀️", "woman_vampire_tone3": "🧛🏽‍♀️", "woman_vampire_medium_skin_tone": "🧛🏽‍♀️", "woman_vampire_tone4": "🧛🏾‍♀️", "woman_vampire_medium_dark_skin_tone": "🧛🏾‍♀️", "woman_vampire_tone5": "🧛🏿‍♀️", "woman_vampire_dark_skin_tone": "🧛🏿‍♀️", "man_vampire_tone1": "🧛🏻‍♂️", "man_vampire_light_skin_tone": "🧛🏻‍♂️", "man_vampire_tone2": "🧛🏼‍♂️", "man_vampire_medium_light_skin_tone": "🧛🏼‍♂️", "man_vampire_tone3": "🧛🏽‍♂️", "man_vampire_medium_skin_tone": "🧛🏽‍♂️", "man_vampire_tone4": "🧛🏾‍♂️", "man_vampire_medium_dark_skin_tone": "🧛🏾‍♂️", "man_vampire_tone5": "🧛🏿‍♂️", "man_vampire_dark_skin_tone": "🧛🏿‍♂️", "merperson_tone1": "🧜🏻", "merperson_light_skin_tone": "🧜🏻", "merperson_tone2": "🧜🏼", "merperson_medium_light_skin_tone": "🧜🏼", "merperson_tone3": "🧜🏽", "merperson_medium_skin_tone": "🧜🏽", "merperson_tone4": "🧜🏾", "merperson_medium_dark_skin_tone": "🧜🏾", "merperson_tone5": "🧜🏿", "merperson_dark_skin_tone": "🧜🏿", "mermaid_tone1": "🧜🏻‍♀️", "mermaid_light_skin_tone": "🧜🏻‍♀️", "mermaid_tone2": "🧜🏼‍♀️", "mermaid_medium_light_skin_tone": "🧜🏼‍♀️", "mermaid_tone3": "🧜🏽‍♀️", "mermaid_medium_skin_tone": "🧜🏽‍♀️", "mermaid_tone4": "🧜🏾‍♀️", "mermaid_medium_dark_skin_tone": "🧜🏾‍♀️", "mermaid_tone5": "🧜🏿‍♀️", "mermaid_dark_skin_tone": "🧜🏿‍♀️", "merman_tone1": "🧜🏻‍♂️", "merman_light_skin_tone": "🧜🏻‍♂️", "merman_tone2": "🧜🏼‍♂️", "merman_medium_light_skin_tone": "🧜🏼‍♂️", "merman_tone3": "🧜🏽‍♂️", "merman_medium_skin_tone": "🧜🏽‍♂️", "merman_tone4": "🧜🏾‍♂️", "merman_medium_dark_skin_tone": "🧜🏾‍♂️", "merman_tone5": "🧜🏿‍♂️", "merman_dark_skin_tone": "🧜🏿‍♂️", "fairy_tone1": "🧚🏻", "fairy_light_skin_tone": "🧚🏻", "fairy_tone2": "🧚🏼", "fairy_medium_light_skin_tone": "🧚🏼", "fairy_tone3": "🧚🏽", "fairy_medium_skin_tone": "🧚🏽", "fairy_tone4": "🧚🏾", "fairy_medium_dark_skin_tone": "🧚🏾", "fairy_tone5": "🧚🏿", "fairy_dark_skin_tone": "🧚🏿", "woman_fairy_tone1": "🧚🏻‍♀️", "woman_fairy_light_skin_tone": "🧚🏻‍♀️", "woman_fairy_tone2": "🧚🏼‍♀️", "woman_fairy_medium_light_skin_tone": "🧚🏼‍♀️", "woman_fairy_tone3": "🧚🏽‍♀️", "woman_fairy_medium_skin_tone": "🧚🏽‍♀️", "woman_fairy_tone4": "🧚🏾‍♀️", "woman_fairy_medium_dark_skin_tone": "🧚🏾‍♀️", "woman_fairy_tone5": "🧚🏿‍♀️", "woman_fairy_dark_skin_tone": "🧚🏿‍♀️", "man_fairy_tone1": "🧚🏻‍♂️", "man_fairy_light_skin_tone": "🧚🏻‍♂️", "man_fairy_tone2": "🧚🏼‍♂️", "man_fairy_medium_light_skin_tone": "🧚🏼‍♂️", "man_fairy_tone3": "🧚🏽‍♂️", "man_fairy_medium_skin_tone": "🧚🏽‍♂️", "man_fairy_tone4": "🧚🏾‍♂️", "man_fairy_medium_dark_skin_tone": "🧚🏾‍♂️", "man_fairy_tone5": "🧚🏿‍♂️", "man_fairy_dark_skin_tone": "🧚🏿‍♂️", "angel_tone1": "👼🏻", "angel_tone2": "👼🏼", "angel_tone3": "👼🏽", "angel_tone4": "👼🏾", "angel_tone5": "👼🏿", "pregnant_person_tone1": "🫄🏻", "pregnant_person_light_skin_tone": "🫄🏻", "pregnant_person_tone2": "🫄🏼", "pregnant_person_medium_light_skin_tone": "🫄🏼", "pregnant_person_tone3": "🫄🏽", "pregnant_person_medium_skin_tone": "🫄🏽", "pregnant_person_tone4": "🫄🏾", "pregnant_person_medium_dark_skin_tone": "🫄🏾", "pregnant_person_tone5": "🫄🏿", "pregnant_person_dark_skin_tone": "🫄🏿", "pregnant_woman_tone1": "🤰🏻", "expecting_woman_tone1": "🤰🏻", "pregnant_woman_tone2": "🤰🏼", "expecting_woman_tone2": "🤰🏼", "pregnant_woman_tone3": "🤰🏽", "expecting_woman_tone3": "🤰🏽", "pregnant_woman_tone4": "🤰🏾", "expecting_woman_tone4": "🤰🏾", "pregnant_woman_tone5": "🤰🏿", "expecting_woman_tone5": "🤰🏿", "pregnant_man_tone1": "🫃🏻", "pregnant_man_light_skin_tone": "🫃🏻", "pregnant_man_tone2": "🫃🏼", "pregnant_man_medium_light_skin_tone": "🫃🏼", "pregnant_man_tone3": "🫃🏽", "pregnant_man_medium_skin_tone": "🫃🏽", "pregnant_man_tone4": "🫃🏾", "pregnant_man_medium_dark_skin_tone": "🫃🏾", "pregnant_man_tone5": "🫃🏿", "pregnant_man_dark_skin_tone": "🫃🏿", "breast_feeding_tone1": "🤱🏻", "breast_feeding_light_skin_tone": "🤱🏻", "breast_feeding_tone2": "🤱🏼", "breast_feeding_medium_light_skin_tone": "🤱🏼", "breast_feeding_tone3": "🤱🏽", "breast_feeding_medium_skin_tone": "🤱🏽", "breast_feeding_tone4": "🤱🏾", "breast_feeding_medium_dark_skin_tone": "🤱🏾", "breast_feeding_tone5": "🤱🏿", "breast_feeding_dark_skin_tone": "🤱🏿", "person_feeding_baby_tone1": "🧑🏻‍🍼", "person_feeding_baby_light_skin_tone": "🧑🏻‍🍼", "person_feeding_baby_tone2": "🧑🏼‍🍼", "person_feeding_baby_medium_light_skin_tone": "🧑🏼‍🍼", "person_feeding_baby_tone3": "🧑🏽‍🍼", "person_feeding_baby_medium_skin_tone": "🧑🏽‍🍼", "person_feeding_baby_tone4": "🧑🏾‍🍼", "person_feeding_baby_medium_dark_skin_tone": "🧑🏾‍🍼", "person_feeding_baby_tone5": "🧑🏿‍🍼", "person_feeding_baby_dark_skin_tone": "🧑🏿‍🍼", "woman_feeding_baby_tone1": "👩🏻‍🍼", "woman_feeding_baby_light_skin_tone": "👩🏻‍🍼", "woman_feeding_baby_tone2": "👩🏼‍🍼", "woman_feeding_baby_medium_light_skin_tone": "👩🏼‍🍼", "woman_feeding_baby_tone3": "👩🏽‍🍼", "woman_feeding_baby_medium_skin_tone": "👩🏽‍🍼", "woman_feeding_baby_tone4": "👩🏾‍🍼", "woman_feeding_baby_medium_dark_skin_tone": "👩🏾‍🍼", "woman_feeding_baby_tone5": "👩🏿‍🍼", "woman_feeding_baby_dark_skin_tone": "👩🏿‍🍼", "man_feeding_baby_tone1": "👨🏻‍🍼", "man_feeding_baby_light_skin_tone": "👨🏻‍🍼", "man_feeding_baby_tone2": "👨🏼‍🍼", "man_feeding_baby_medium_light_skin_tone": "👨🏼‍🍼", "man_feeding_baby_tone3": "👨🏽‍🍼", "man_feeding_baby_medium_skin_tone": "👨🏽‍🍼", "man_feeding_baby_tone4": "👨🏾‍🍼", "man_feeding_baby_medium_dark_skin_tone": "👨🏾‍🍼", "man_feeding_baby_tone5": "👨🏿‍🍼", "man_feeding_baby_dark_skin_tone": "👨🏿‍🍼", "person_bowing_tone1": "🙇🏻", "bow_tone1": "🙇🏻", "person_bowing_tone2": "🙇🏼", "bow_tone2": "🙇🏼", "person_bowing_tone3": "🙇🏽", "bow_tone3": "🙇🏽", "person_bowing_tone4": "🙇🏾", "bow_tone4": "🙇🏾", "person_bowing_tone5": "🙇🏿", "bow_tone5": "🙇🏿", "woman_bowing_tone1": "🙇🏻‍♀️", "woman_bowing_light_skin_tone": "🙇🏻‍♀️", "woman_bowing_tone2": "🙇🏼‍♀️", "woman_bowing_medium_light_skin_tone": "🙇🏼‍♀️", "woman_bowing_tone3": "🙇🏽‍♀️", "woman_bowing_medium_skin_tone": "🙇🏽‍♀️", "woman_bowing_tone4": "🙇🏾‍♀️", "woman_bowing_medium_dark_skin_tone": "🙇🏾‍♀️", "woman_bowing_tone5": "🙇🏿‍♀️", "woman_bowing_dark_skin_tone": "🙇🏿‍♀️", "man_bowing_tone1": "🙇🏻‍♂️", "man_bowing_light_skin_tone": "🙇🏻‍♂️", "man_bowing_tone2": "🙇🏼‍♂️", "man_bowing_medium_light_skin_tone": "🙇🏼‍♂️", "man_bowing_tone3": "🙇🏽‍♂️", "man_bowing_medium_skin_tone": "🙇🏽‍♂️", "man_bowing_tone4": "🙇🏾‍♂️", "man_bowing_medium_dark_skin_tone": "🙇🏾‍♂️", "man_bowing_tone5": "🙇🏿‍♂️", "man_bowing_dark_skin_tone": "🙇🏿‍♂️", "person_tipping_hand_tone1": "💁🏻", "information_desk_person_tone1": "💁🏻", "person_tipping_hand_tone2": "💁🏼", "information_desk_person_tone2": "💁🏼", "person_tipping_hand_tone3": "💁🏽", "information_desk_person_tone3": "💁🏽", "person_tipping_hand_tone4": "💁🏾", "information_desk_person_tone4": "💁🏾", "person_tipping_hand_tone5": "💁🏿", "information_desk_person_tone5": "💁🏿", "woman_tipping_hand_tone1": "💁🏻‍♀️", "woman_tipping_hand_light_skin_tone": "💁🏻‍♀️", "woman_tipping_hand_tone2": "💁🏼‍♀️", "woman_tipping_hand_medium_light_skin_tone": "💁🏼‍♀️", "woman_tipping_hand_tone3": "💁🏽‍♀️", "woman_tipping_hand_medium_skin_tone": "💁🏽‍♀️", "woman_tipping_hand_tone4": "💁🏾‍♀️", "woman_tipping_hand_medium_dark_skin_tone": "💁🏾‍♀️", "woman_tipping_hand_tone5": "💁🏿‍♀️", "woman_tipping_hand_dark_skin_tone": "💁🏿‍♀️", "man_tipping_hand_tone1": "💁🏻‍♂️", "man_tipping_hand_light_skin_tone": "💁🏻‍♂️", "man_tipping_hand_tone2": "💁🏼‍♂️", "man_tipping_hand_medium_light_skin_tone": "💁🏼‍♂️", "man_tipping_hand_tone3": "💁🏽‍♂️", "man_tipping_hand_medium_skin_tone": "💁🏽‍♂️", "man_tipping_hand_tone4": "💁🏾‍♂️", "man_tipping_hand_medium_dark_skin_tone": "💁🏾‍♂️", "man_tipping_hand_tone5": "💁🏿‍♂️", "man_tipping_hand_dark_skin_tone": "💁🏿‍♂️", "person_gesturing_no_tone1": "🙅🏻", "no_good_tone1": "🙅🏻", "person_gesturing_no_tone2": "🙅🏼", "no_good_tone2": "🙅🏼", "person_gesturing_no_tone3": "🙅🏽", "no_good_tone3": "🙅🏽", "person_gesturing_no_tone4": "🙅🏾", "no_good_tone4": "🙅🏾", "person_gesturing_no_tone5": "🙅🏿", "no_good_tone5": "🙅🏿", "woman_gesturing_no_tone1": "🙅🏻‍♀️", "woman_gesturing_no_light_skin_tone": "🙅🏻‍♀️", "woman_gesturing_no_tone2": "🙅🏼‍♀️", "woman_gesturing_no_medium_light_skin_tone": "🙅🏼‍♀️", "woman_gesturing_no_tone3": "🙅🏽‍♀️", "woman_gesturing_no_medium_skin_tone": "🙅🏽‍♀️", "woman_gesturing_no_tone4": "🙅🏾‍♀️", "woman_gesturing_no_medium_dark_skin_tone": "🙅🏾‍♀️", "woman_gesturing_no_tone5": "🙅🏿‍♀️", "woman_gesturing_no_dark_skin_tone": "🙅🏿‍♀️", "man_gesturing_no_tone1": "🙅🏻‍♂️", "man_gesturing_no_light_skin_tone": "🙅🏻‍♂️", "man_gesturing_no_tone2": "🙅🏼‍♂️", "man_gesturing_no_medium_light_skin_tone": "🙅🏼‍♂️", "man_gesturing_no_tone3": "🙅🏽‍♂️", "man_gesturing_no_medium_skin_tone": "🙅🏽‍♂️", "man_gesturing_no_tone4": "🙅🏾‍♂️", "man_gesturing_no_medium_dark_skin_tone": "🙅🏾‍♂️", "man_gesturing_no_tone5": "🙅🏿‍♂️", "man_gesturing_no_dark_skin_tone": "🙅🏿‍♂️", "person_gesturing_ok_tone1": "🙆🏻", "person_gesturing_ok_tone2": "🙆🏼", "person_gesturing_ok_tone3": "🙆🏽", "person_gesturing_ok_tone4": "🙆🏾", "person_gesturing_ok_tone5": "🙆🏿", "woman_gesturing_ok_tone1": "🙆🏻‍♀️", "woman_gesturing_ok_light_skin_tone": "🙆🏻‍♀️", "woman_gesturing_ok_tone2": "🙆🏼‍♀️", "woman_gesturing_ok_medium_light_skin_tone": "🙆🏼‍♀️", "woman_gesturing_ok_tone3": "🙆🏽‍♀️", "woman_gesturing_ok_medium_skin_tone": "🙆🏽‍♀️", "woman_gesturing_ok_tone4": "🙆🏾‍♀️", "woman_gesturing_ok_medium_dark_skin_tone": "🙆🏾‍♀️", "woman_gesturing_ok_tone5": "🙆🏿‍♀️", "woman_gesturing_ok_dark_skin_tone": "🙆🏿‍♀️", "man_gesturing_ok_tone1": "🙆🏻‍♂️", "man_gesturing_ok_light_skin_tone": "🙆🏻‍♂️", "man_gesturing_ok_tone2": "🙆🏼‍♂️", "man_gesturing_ok_medium_light_skin_tone": "🙆🏼‍♂️", "man_gesturing_ok_tone3": "🙆🏽‍♂️", "man_gesturing_ok_medium_skin_tone": "🙆🏽‍♂️", "man_gesturing_ok_tone4": "🙆🏾‍♂️", "man_gesturing_ok_medium_dark_skin_tone": "🙆🏾‍♂️", "man_gesturing_ok_tone5": "🙆🏿‍♂️", "man_gesturing_ok_dark_skin_tone": "🙆🏿‍♂️", "person_raising_hand_tone1": "🙋🏻", "raising_hand_tone1": "🙋🏻", "person_raising_hand_tone2": "🙋🏼", "raising_hand_tone2": "🙋🏼", "person_raising_hand_tone3": "🙋🏽", "raising_hand_tone3": "🙋🏽", "person_raising_hand_tone4": "🙋🏾", "raising_hand_tone4": "🙋🏾", "person_raising_hand_tone5": "🙋🏿", "raising_hand_tone5": "🙋🏿", "woman_raising_hand_tone1": "🙋🏻‍♀️", "woman_raising_hand_light_skin_tone": "🙋🏻‍♀️", "woman_raising_hand_tone2": "🙋🏼‍♀️", "woman_raising_hand_medium_light_skin_tone": "🙋🏼‍♀️", "woman_raising_hand_tone3": "🙋🏽‍♀️", "woman_raising_hand_medium_skin_tone": "🙋🏽‍♀️", "woman_raising_hand_tone4": "🙋🏾‍♀️", "woman_raising_hand_medium_dark_skin_tone": "🙋🏾‍♀️", "woman_raising_hand_tone5": "🙋🏿‍♀️", "woman_raising_hand_dark_skin_tone": "🙋🏿‍♀️", "man_raising_hand_tone1": "🙋🏻‍♂️", "man_raising_hand_light_skin_tone": "🙋🏻‍♂️", "man_raising_hand_tone2": "🙋🏼‍♂️", "man_raising_hand_medium_light_skin_tone": "🙋🏼‍♂️", "man_raising_hand_tone3": "🙋🏽‍♂️", "man_raising_hand_medium_skin_tone": "🙋🏽‍♂️", "man_raising_hand_tone4": "🙋🏾‍♂️", "man_raising_hand_medium_dark_skin_tone": "🙋🏾‍♂️", "man_raising_hand_tone5": "🙋🏿‍♂️", "man_raising_hand_dark_skin_tone": "🙋🏿‍♂️", "deaf_person_tone1": "🧏🏻", "deaf_person_light_skin_tone": "🧏🏻", "deaf_person_tone2": "🧏🏼", "deaf_person_medium_light_skin_tone": "🧏🏼", "deaf_person_tone3": "🧏🏽", "deaf_person_medium_skin_tone": "🧏🏽", "deaf_person_tone4": "🧏🏾", "deaf_person_medium_dark_skin_tone": "🧏🏾", "deaf_person_tone5": "🧏🏿", "deaf_person_dark_skin_tone": "🧏🏿", "deaf_woman_tone1": "🧏🏻‍♀️", "deaf_woman_light_skin_tone": "🧏🏻‍♀️", "deaf_woman_tone2": "🧏🏼‍♀️", "deaf_woman_medium_light_skin_tone": "🧏🏼‍♀️", "deaf_woman_tone3": "🧏🏽‍♀️", "deaf_woman_medium_skin_tone": "🧏🏽‍♀️", "deaf_woman_tone4": "🧏🏾‍♀️", "deaf_woman_medium_dark_skin_tone": "🧏🏾‍♀️", "deaf_woman_tone5": "🧏🏿‍♀️", "deaf_woman_dark_skin_tone": "🧏🏿‍♀️", "deaf_man_tone1": "🧏🏻‍♂️", "deaf_man_light_skin_tone": "🧏🏻‍♂️", "deaf_man_tone2": "🧏🏼‍♂️", "deaf_man_medium_light_skin_tone": "🧏🏼‍♂️", "deaf_man_tone3": "🧏🏽‍♂️", "deaf_man_medium_skin_tone": "🧏🏽‍♂️", "deaf_man_tone4": "🧏🏾‍♂️", "deaf_man_medium_dark_skin_tone": "🧏🏾‍♂️", "deaf_man_tone5": "🧏🏿‍♂️", "deaf_man_dark_skin_tone": "🧏🏿‍♂️", "person_facepalming_tone1": "🤦🏻", "face_palm_tone1": "🤦🏻", "facepalm_tone1": "🤦🏻", "person_facepalming_tone2": "🤦🏼", "face_palm_tone2": "🤦🏼", "facepalm_tone2": "🤦🏼", "person_facepalming_tone3": "🤦🏽", "face_palm_tone3": "🤦🏽", "facepalm_tone3": "🤦🏽", "person_facepalming_tone4": "🤦🏾", "face_palm_tone4": "🤦🏾", "facepalm_tone4": "🤦🏾", "person_facepalming_tone5": "🤦🏿", "face_palm_tone5": "🤦🏿", "facepalm_tone5": "🤦🏿", "woman_facepalming_tone1": "🤦🏻‍♀️", "woman_facepalming_light_skin_tone": "🤦🏻‍♀️", "woman_facepalming_tone2": "🤦🏼‍♀️", "woman_facepalming_medium_light_skin_tone": "🤦🏼‍♀️", "woman_facepalming_tone3": "🤦🏽‍♀️", "woman_facepalming_medium_skin_tone": "🤦🏽‍♀️", "woman_facepalming_tone4": "🤦🏾‍♀️", "woman_facepalming_medium_dark_skin_tone": "🤦🏾‍♀️", "woman_facepalming_tone5": "🤦🏿‍♀️", "woman_facepalming_dark_skin_tone": "🤦🏿‍♀️", "man_facepalming_tone1": "🤦🏻‍♂️", "man_facepalming_light_skin_tone": "🤦🏻‍♂️", "man_facepalming_tone2": "🤦🏼‍♂️", "man_facepalming_medium_light_skin_tone": "🤦🏼‍♂️", "man_facepalming_tone3": "🤦🏽‍♂️", "man_facepalming_medium_skin_tone": "🤦🏽‍♂️", "man_facepalming_tone4": "🤦🏾‍♂️", "man_facepalming_medium_dark_skin_tone": "🤦🏾‍♂️", "man_facepalming_tone5": "🤦🏿‍♂️", "man_facepalming_dark_skin_tone": "🤦🏿‍♂️", "person_shrugging_tone1": "🤷🏻", "shrug_tone1": "🤷🏻", "person_shrugging_tone2": "🤷🏼", "shrug_tone2": "🤷🏼", "person_shrugging_tone3": "🤷🏽", "shrug_tone3": "🤷🏽", "person_shrugging_tone4": "🤷🏾", "shrug_tone4": "🤷🏾", "person_shrugging_tone5": "🤷🏿", "shrug_tone5": "🤷🏿", "woman_shrugging_tone1": "🤷🏻‍♀️", "woman_shrugging_light_skin_tone": "🤷🏻‍♀️", "woman_shrugging_tone2": "🤷🏼‍♀️", "woman_shrugging_medium_light_skin_tone": "🤷🏼‍♀️", "woman_shrugging_tone3": "🤷🏽‍♀️", "woman_shrugging_medium_skin_tone": "🤷🏽‍♀️", "woman_shrugging_tone4": "🤷🏾‍♀️", "woman_shrugging_medium_dark_skin_tone": "🤷🏾‍♀️", "woman_shrugging_tone5": "🤷🏿‍♀️", "woman_shrugging_dark_skin_tone": "🤷🏿‍♀️", "man_shrugging_tone1": "🤷🏻‍♂️", "man_shrugging_light_skin_tone": "🤷🏻‍♂️", "man_shrugging_tone2": "🤷🏼‍♂️", "man_shrugging_medium_light_skin_tone": "🤷🏼‍♂️", "man_shrugging_tone3": "🤷🏽‍♂️", "man_shrugging_medium_skin_tone": "🤷🏽‍♂️", "man_shrugging_tone4": "🤷🏾‍♂️", "man_shrugging_medium_dark_skin_tone": "🤷🏾‍♂️", "man_shrugging_tone5": "🤷🏿‍♂️", "man_shrugging_dark_skin_tone": "🤷🏿‍♂️", "person_pouting_tone1": "🙎🏻", "person_with_pouting_face_tone1": "🙎🏻", "person_pouting_tone2": "🙎🏼", "person_with_pouting_face_tone2": "🙎🏼", "person_pouting_tone3": "🙎🏽", "person_with_pouting_face_tone3": "🙎🏽", "person_pouting_tone4": "🙎🏾", "person_with_pouting_face_tone4": "🙎🏾", "person_pouting_tone5": "🙎🏿", "person_with_pouting_face_tone5": "🙎🏿", "woman_pouting_tone1": "🙎🏻‍♀️", "woman_pouting_light_skin_tone": "🙎🏻‍♀️", "woman_pouting_tone2": "🙎🏼‍♀️", "woman_pouting_medium_light_skin_tone": "🙎🏼‍♀️", "woman_pouting_tone3": "🙎🏽‍♀️", "woman_pouting_medium_skin_tone": "🙎🏽‍♀️", "woman_pouting_tone4": "🙎🏾‍♀️", "woman_pouting_medium_dark_skin_tone": "🙎🏾‍♀️", "woman_pouting_tone5": "🙎🏿‍♀️", "woman_pouting_dark_skin_tone": "🙎🏿‍♀️", "man_pouting_tone1": "🙎🏻‍♂️", "man_pouting_light_skin_tone": "🙎🏻‍♂️", "man_pouting_tone2": "🙎🏼‍♂️", "man_pouting_medium_light_skin_tone": "🙎🏼‍♂️", "man_pouting_tone3": "🙎🏽‍♂️", "man_pouting_medium_skin_tone": "🙎🏽‍♂️", "man_pouting_tone4": "🙎🏾‍♂️", "man_pouting_medium_dark_skin_tone": "🙎🏾‍♂️", "man_pouting_tone5": "🙎🏿‍♂️", "man_pouting_dark_skin_tone": "🙎🏿‍♂️", "person_frowning_tone1": "🙍🏻", "person_frowning_tone2": "🙍🏼", "person_frowning_tone3": "🙍🏽", "person_frowning_tone4": "🙍🏾", "person_frowning_tone5": "🙍🏿", "woman_frowning_tone1": "🙍🏻‍♀️", "woman_frowning_light_skin_tone": "🙍🏻‍♀️", "woman_frowning_tone2": "🙍🏼‍♀️", "woman_frowning_medium_light_skin_tone": "🙍🏼‍♀️", "woman_frowning_tone3": "🙍🏽‍♀️", "woman_frowning_medium_skin_tone": "🙍🏽‍♀️", "woman_frowning_tone4": "🙍🏾‍♀️", "woman_frowning_medium_dark_skin_tone": "🙍🏾‍♀️", "woman_frowning_tone5": "🙍🏿‍♀️", "woman_frowning_dark_skin_tone": "🙍🏿‍♀️", "man_frowning_tone1": "🙍🏻‍♂️", "man_frowning_light_skin_tone": "🙍🏻‍♂️", "man_frowning_tone2": "🙍🏼‍♂️", "man_frowning_medium_light_skin_tone": "🙍🏼‍♂️", "man_frowning_tone3": "🙍🏽‍♂️", "man_frowning_medium_skin_tone": "🙍🏽‍♂️", "man_frowning_tone4": "🙍🏾‍♂️", "man_frowning_medium_dark_skin_tone": "🙍🏾‍♂️", "man_frowning_tone5": "🙍🏿‍♂️", "man_frowning_dark_skin_tone": "🙍🏿‍♂️", "person_getting_haircut_tone1": "💇🏻", "haircut_tone1": "💇🏻", "person_getting_haircut_tone2": "💇🏼", "haircut_tone2": "💇🏼", "person_getting_haircut_tone3": "💇🏽", "haircut_tone3": "💇🏽", "person_getting_haircut_tone4": "💇🏾", "haircut_tone4": "💇🏾", "person_getting_haircut_tone5": "💇🏿", "haircut_tone5": "💇🏿", "woman_getting_haircut_tone1": "💇🏻‍♀️", "woman_getting_haircut_light_skin_tone": "💇🏻‍♀️", "woman_getting_haircut_tone2": "💇🏼‍♀️", "woman_getting_haircut_medium_light_skin_tone": "💇🏼‍♀️", "woman_getting_haircut_tone3": "💇🏽‍♀️", "woman_getting_haircut_medium_skin_tone": "💇🏽‍♀️", "woman_getting_haircut_tone4": "💇🏾‍♀️", "woman_getting_haircut_medium_dark_skin_tone": "💇🏾‍♀️", "woman_getting_haircut_tone5": "💇🏿‍♀️", "woman_getting_haircut_dark_skin_tone": "💇🏿‍♀️", "man_getting_haircut_tone1": "💇🏻‍♂️", "man_getting_haircut_light_skin_tone": "💇🏻‍♂️", "man_getting_haircut_tone2": "💇🏼‍♂️", "man_getting_haircut_medium_light_skin_tone": "💇🏼‍♂️", "man_getting_haircut_tone3": "💇🏽‍♂️", "man_getting_haircut_medium_skin_tone": "💇🏽‍♂️", "man_getting_haircut_tone4": "💇🏾‍♂️", "man_getting_haircut_medium_dark_skin_tone": "💇🏾‍♂️", "man_getting_haircut_tone5": "💇🏿‍♂️", "man_getting_haircut_dark_skin_tone": "💇🏿‍♂️", "person_getting_massage_tone1": "💆🏻", "massage_tone1": "💆🏻", "person_getting_massage_tone2": "💆🏼", "massage_tone2": "💆🏼", "person_getting_massage_tone3": "💆🏽", "massage_tone3": "💆🏽", "person_getting_massage_tone4": "💆🏾", "massage_tone4": "💆🏾", "person_getting_massage_tone5": "💆🏿", "massage_tone5": "💆🏿", "woman_getting_face_massage_tone1": "💆🏻‍♀️", "woman_getting_face_massage_light_skin_tone": "💆🏻‍♀️", "woman_getting_face_massage_tone2": "💆🏼‍♀️", "woman_getting_face_massage_medium_light_skin_tone": "💆🏼‍♀️", "woman_getting_face_massage_tone3": "💆🏽‍♀️", "woman_getting_face_massage_medium_skin_tone": "💆🏽‍♀️", "woman_getting_face_massage_tone4": "💆🏾‍♀️", "woman_getting_face_massage_medium_dark_skin_tone": "💆🏾‍♀️", "woman_getting_face_massage_tone5": "💆🏿‍♀️", "woman_getting_face_massage_dark_skin_tone": "💆🏿‍♀️", "man_getting_face_massage_tone1": "💆🏻‍♂️", "man_getting_face_massage_light_skin_tone": "💆🏻‍♂️", "man_getting_face_massage_tone2": "💆🏼‍♂️", "man_getting_face_massage_medium_light_skin_tone": "💆🏼‍♂️", "man_getting_face_massage_tone3": "💆🏽‍♂️", "man_getting_face_massage_medium_skin_tone": "💆🏽‍♂️", "man_getting_face_massage_tone4": "💆🏾‍♂️", "man_getting_face_massage_medium_dark_skin_tone": "💆🏾‍♂️", "man_getting_face_massage_tone5": "💆🏿‍♂️", "man_getting_face_massage_dark_skin_tone": "💆🏿‍♂️", "person_in_steamy_room_tone1": "🧖🏻", "person_in_steamy_room_light_skin_tone": "🧖🏻", "person_in_steamy_room_tone2": "🧖🏼", "person_in_steamy_room_medium_light_skin_tone": "🧖🏼", "person_in_steamy_room_tone3": "🧖🏽", "person_in_steamy_room_medium_skin_tone": "🧖🏽", "person_in_steamy_room_tone4": "🧖🏾", "person_in_steamy_room_medium_dark_skin_tone": "🧖🏾", "person_in_steamy_room_tone5": "🧖🏿", "person_in_steamy_room_dark_skin_tone": "🧖🏿", "woman_in_steamy_room_tone1": "🧖🏻‍♀️", "woman_in_steamy_room_light_skin_tone": "🧖🏻‍♀️", "woman_in_steamy_room_tone2": "🧖🏼‍♀️", "woman_in_steamy_room_medium_light_skin_tone": "🧖🏼‍♀️", "woman_in_steamy_room_tone3": "🧖🏽‍♀️", "woman_in_steamy_room_medium_skin_tone": "🧖🏽‍♀️", "woman_in_steamy_room_tone4": "🧖🏾‍♀️", "woman_in_steamy_room_medium_dark_skin_tone": "🧖🏾‍♀️", "woman_in_steamy_room_tone5": "🧖🏿‍♀️", "woman_in_steamy_room_dark_skin_tone": "🧖🏿‍♀️", "man_in_steamy_room_tone1": "🧖🏻‍♂️", "man_in_steamy_room_light_skin_tone": "🧖🏻‍♂️", "man_in_steamy_room_tone2": "🧖🏼‍♂️", "man_in_steamy_room_medium_light_skin_tone": "🧖🏼‍♂️", "man_in_steamy_room_tone3": "🧖🏽‍♂️", "man_in_steamy_room_medium_skin_tone": "🧖🏽‍♂️", "man_in_steamy_room_tone4": "🧖🏾‍♂️", "man_in_steamy_room_medium_dark_skin_tone": "🧖🏾‍♂️", "man_in_steamy_room_tone5": "🧖🏿‍♂️", "man_in_steamy_room_dark_skin_tone": "🧖🏿‍♂️", "nail_care_tone1": "💅🏻", "nail_care_tone2": "💅🏼", "nail_care_tone3": "💅🏽", "nail_care_tone4": "💅🏾", "nail_care_tone5": "💅🏿", "selfie_tone1": "🤳🏻", "selfie_tone2": "🤳🏼", "selfie_tone3": "🤳🏽", "selfie_tone4": "🤳🏾", "selfie_tone5": "🤳🏿", "dancer_tone1": "💃🏻", "dancer_tone2": "💃🏼", "dancer_tone3": "💃🏽", "dancer_tone4": "💃🏾", "dancer_tone5": "💃🏿", "man_dancing_tone1": "🕺🏻", "male_dancer_tone1": "🕺🏻", "man_dancing_tone2": "🕺🏼", "male_dancer_tone2": "🕺🏼", "man_dancing_tone3": "🕺🏽", "male_dancer_tone3": "🕺🏽", "man_dancing_tone5": "🕺🏿", "male_dancer_tone5": "🕺🏿", "man_dancing_tone4": "🕺🏾", "male_dancer_tone4": "🕺🏾", "levitate_tone1": "🕴🏻", "man_in_business_suit_levitating_tone1": "🕴🏻", "man_in_business_suit_levitating_light_skin_tone": "🕴🏻", "levitate_tone2": "🕴🏼", "man_in_business_suit_levitating_tone2": "🕴🏼", "man_in_business_suit_levitating_medium_light_skin_tone": "🕴🏼", "levitate_tone3": "🕴🏽", "man_in_business_suit_levitating_tone3": "🕴🏽", "man_in_business_suit_levitating_medium_skin_tone": "🕴🏽", "levitate_tone4": "🕴🏾", "man_in_business_suit_levitating_tone4": "🕴🏾", "man_in_business_suit_levitating_medium_dark_skin_tone": "🕴🏾", "levitate_tone5": "🕴🏿", "man_in_business_suit_levitating_tone5": "🕴🏿", "man_in_business_suit_levitating_dark_skin_tone": "🕴🏿", "person_in_manual_wheelchair_tone1": "🧑🏻‍🦽", "person_in_manual_wheelchair_light_skin_tone": "🧑🏻‍🦽", "person_in_manual_wheelchair_tone2": "🧑🏼‍🦽", "person_in_manual_wheelchair_medium_light_skin_tone": "🧑🏼‍🦽", "person_in_manual_wheelchair_tone3": "🧑🏽‍🦽", "person_in_manual_wheelchair_medium_skin_tone": "🧑🏽‍🦽", "person_in_manual_wheelchair_tone4": "🧑🏾‍🦽", "person_in_manual_wheelchair_medium_dark_skin_tone": "🧑🏾‍🦽", "person_in_manual_wheelchair_tone5": "🧑🏿‍🦽", "person_in_manual_wheelchair_dark_skin_tone": "🧑🏿‍🦽", "woman_in_manual_wheelchair_tone1": "👩🏻‍🦽", "woman_in_manual_wheelchair_light_skin_tone": "👩🏻‍🦽", "woman_in_manual_wheelchair_tone2": "👩🏼‍🦽", "woman_in_manual_wheelchair_medium_light_skin_tone": "👩🏼‍🦽", "woman_in_manual_wheelchair_tone3": "👩🏽‍🦽", "woman_in_manual_wheelchair_medium_skin_tone": "👩🏽‍🦽", "woman_in_manual_wheelchair_tone4": "👩🏾‍🦽", "woman_in_manual_wheelchair_medium_dark_skin_tone": "👩🏾‍🦽", "woman_in_manual_wheelchair_tone5": "👩🏿‍🦽", "woman_in_manual_wheelchair_dark_skin_tone": "👩🏿‍🦽", "man_in_manual_wheelchair_tone1": "👨🏻‍🦽", "man_in_manual_wheelchair_light_skin_tone": "👨🏻‍🦽", "man_in_manual_wheelchair_tone2": "👨🏼‍🦽", "man_in_manual_wheelchair_medium_light_skin_tone": "👨🏼‍🦽", "man_in_manual_wheelchair_tone3": "👨🏽‍🦽", "man_in_manual_wheelchair_medium_skin_tone": "👨🏽‍🦽", "man_in_manual_wheelchair_tone4": "👨🏾‍🦽", "man_in_manual_wheelchair_medium_dark_skin_tone": "👨🏾‍🦽", "man_in_manual_wheelchair_tone5": "👨🏿‍🦽", "man_in_manual_wheelchair_dark_skin_tone": "👨🏿‍🦽", "person_in_manual_wheelchair_facing_right_tone1": "🧑🏻‍🦽‍➡️", "person_in_manual_wheelchair_facing_right_light_skin_tone": "🧑🏻‍🦽‍➡️", "person_in_manual_wheelchair_facing_right_tone2": "🧑🏼‍🦽‍➡️", "person_in_manual_wheelchair_facing_right_medium_light_skin_tone": "🧑🏼‍🦽‍➡️", "person_in_manual_wheelchair_facing_right_tone3": "🧑🏽‍🦽‍➡️", "person_in_manual_wheelchair_facing_right_medium_skin_tone": "🧑🏽‍🦽‍➡️", "person_in_manual_wheelchair_facing_right_tone4": "🧑🏾‍🦽‍➡️", "person_in_manual_wheelchair_facing_right_medium_dark_skin_tone": "🧑🏾‍🦽‍➡️", "person_in_manual_wheelchair_facing_right_tone5": "🧑🏿‍🦽‍➡️", "person_in_manual_wheelchair_facing_right_dark_skin_tone": "🧑🏿‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_tone2": "👨🏼‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_medium_light_skin_tone": "👨🏼‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_tone1": "👨🏻‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_light_skin_tone": "👨🏻‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_tone3": "👨🏽‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_medium_skin_tone": "👨🏽‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_tone4": "👨🏾‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_medium_dark_skin_tone": "👨🏾‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_tone5": "👨🏿‍🦽‍➡️", "man_in_manual_wheelchair_facing_right_dark_skin_tone": "👨🏿‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_tone1": "👩🏻‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_light_skin_tone": "👩🏻‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_tone2": "👩🏼‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_medium_light_skin_tone": "👩🏼‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_tone3": "👩🏽‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_medium_skin_tone": "👩🏽‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_tone4": "👩🏾‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_medium_dark_skin_tone": "👩🏾‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_tone5": "👩🏿‍🦽‍➡️", "woman_in_manual_wheelchair_facing_right_dark_skin_tone": "👩🏿‍🦽‍➡️", "person_in_motorized_wheelchair_tone1": "🧑🏻‍🦼", "person_in_motorized_wheelchair_light_skin_tone": "🧑🏻‍🦼", "person_in_motorized_wheelchair_tone2": "🧑🏼‍🦼", "person_in_motorized_wheelchair_medium_light_skin_tone": "🧑🏼‍🦼", "person_in_motorized_wheelchair_tone3": "🧑🏽‍🦼", "person_in_motorized_wheelchair_medium_skin_tone": "🧑🏽‍🦼", "person_in_motorized_wheelchair_tone4": "🧑🏾‍🦼", "person_in_motorized_wheelchair_medium_dark_skin_tone": "🧑🏾‍🦼", "person_in_motorized_wheelchair_tone5": "🧑🏿‍🦼", "person_in_motorized_wheelchair_dark_skin_tone": "🧑🏿‍🦼", "woman_in_motorized_wheelchair_tone1": "👩🏻‍🦼", "woman_in_motorized_wheelchair_light_skin_tone": "👩🏻‍🦼", "woman_in_motorized_wheelchair_tone2": "👩🏼‍🦼", "woman_in_motorized_wheelchair_medium_light_skin_tone": "👩🏼‍🦼", "woman_in_motorized_wheelchair_tone3": "👩🏽‍🦼", "woman_in_motorized_wheelchair_medium_skin_tone": "👩🏽‍🦼", "woman_in_motorized_wheelchair_tone4": "👩🏾‍🦼", "woman_in_motorized_wheelchair_medium_dark_skin_tone": "👩🏾‍🦼", "woman_in_motorized_wheelchair_tone5": "👩🏿‍🦼", "woman_in_motorized_wheelchair_dark_skin_tone": "👩🏿‍🦼", "man_in_motorized_wheelchair_tone1": "👨🏻‍🦼", "man_in_motorized_wheelchair_light_skin_tone": "👨🏻‍🦼", "man_in_motorized_wheelchair_tone2": "👨🏼‍🦼", "man_in_motorized_wheelchair_medium_light_skin_tone": "👨🏼‍🦼", "man_in_motorized_wheelchair_tone3": "👨🏽‍🦼", "man_in_motorized_wheelchair_medium_skin_tone": "👨🏽‍🦼", "man_in_motorized_wheelchair_tone4": "👨🏾‍🦼", "man_in_motorized_wheelchair_medium_dark_skin_tone": "👨🏾‍🦼", "man_in_motorized_wheelchair_tone5": "👨🏿‍🦼", "man_in_motorized_wheelchair_dark_skin_tone": "👨🏿‍🦼", "person_in_motorized_wheelchair_facing_right_tone1": "🧑🏻‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right_light_skin_tone": "🧑🏻‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right_tone2": "🧑🏼‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right_medium_light_skin_tone": "🧑🏼‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right_tone3": "🧑🏽‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right_medium_skin_tone": "🧑🏽‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right_tone4": "🧑🏾‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right_medium_dark_skin_tone": "🧑🏾‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right_tone5": "🧑🏿‍🦼‍➡️", "person_in_motorized_wheelchair_facing_right_dark_skin_tone": "🧑🏿‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_tone1": "👨🏻‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_light_skin_tone": "👨🏻‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_tone2": "👨🏼‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_medium_light_skin_tone": "👨🏼‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_tone3": "👨🏽‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_medium_skin_tone": "👨🏽‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_tone4": "👨🏾‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_medium_dark_skin_tone": "👨🏾‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_tone5": "👨🏿‍🦼‍➡️", "man_in_motorized_wheelchair_facing_right_dark_skin_tone": "👨🏿‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_tone1": "👩🏻‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_light_skin_tone": "👩🏻‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_tone2": "👩🏼‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_medium_light_skin_tone": "👩🏼‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_tone3": "👩🏽‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_medium_skin_tone": "👩🏽‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_tone4": "👩🏾‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_medium_dark_skin_tone": "👩🏾‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_tone5": "👩🏿‍🦼‍➡️", "woman_in_motorized_wheelchair_facing_right_dark_skin_tone": "👩🏿‍🦼‍➡️", "person_walking_tone1": "🚶🏻", "walking_tone1": "🚶🏻", "person_walking_tone2": "🚶🏼", "walking_tone2": "🚶🏼", "person_walking_tone3": "🚶🏽", "walking_tone3": "🚶🏽", "person_walking_tone4": "🚶🏾", "walking_tone4": "🚶🏾", "person_walking_tone5": "🚶🏿", "walking_tone5": "🚶🏿", "woman_walking_tone1": "🚶🏻‍♀️", "woman_walking_light_skin_tone": "🚶🏻‍♀️", "woman_walking_tone2": "🚶🏼‍♀️", "woman_walking_medium_light_skin_tone": "🚶🏼‍♀️", "woman_walking_tone3": "🚶🏽‍♀️", "woman_walking_medium_skin_tone": "🚶🏽‍♀️", "woman_walking_tone4": "🚶🏾‍♀️", "woman_walking_medium_dark_skin_tone": "🚶🏾‍♀️", "woman_walking_tone5": "🚶🏿‍♀️", "woman_walking_dark_skin_tone": "🚶🏿‍♀️", "man_walking_tone1": "🚶🏻‍♂️", "man_walking_light_skin_tone": "🚶🏻‍♂️", "man_walking_tone2": "🚶🏼‍♂️", "man_walking_medium_light_skin_tone": "🚶🏼‍♂️", "man_walking_tone3": "🚶🏽‍♂️", "man_walking_medium_skin_tone": "🚶🏽‍♂️", "man_walking_tone4": "🚶🏾‍♂️", "man_walking_medium_dark_skin_tone": "🚶🏾‍♂️", "man_walking_tone5": "🚶🏿‍♂️", "man_walking_dark_skin_tone": "🚶🏿‍♂️", "person_walking_facing_right_tone1": "🚶🏻‍➡️", "person_walking_facing_right_light_skin_tone": "🚶🏻‍➡️", "person_walking_facing_right_tone2": "🚶🏼‍➡️", "person_walking_facing_right_medium_light_skin_tone": "🚶🏼‍➡️", "person_walking_facing_right_tone3": "🚶🏽‍➡️", "person_walking_facing_right_medium_skin_tone": "🚶🏽‍➡️", "person_walking_facing_right_tone4": "🚶🏾‍➡️", "person_walking_facing_right_medium_dark_skin_tone": "🚶🏾‍➡️", "person_walking_facing_right_tone5": "🚶🏿‍➡️", "person_walking_facing_right_dark_skin_tone": "🚶🏿‍➡️", "woman_walking_facing_right_tone1": "🚶🏻‍♀️‍➡️", "woman_walking_facing_right_light_skin_tone": "🚶🏻‍♀️‍➡️", "woman_walking_facing_right_tone2": "🚶🏼‍♀️‍➡️", "woman_walking_facing_right_medium_light_skin_tone": "🚶🏼‍♀️‍➡️", "woman_walking_facing_right_tone3": "🚶🏽‍♀️‍➡️", "woman_walking_facing_right_medium_skin_tone": "🚶🏽‍♀️‍➡️", "woman_walking_facing_right_tone4": "🚶🏾‍♀️‍➡️", "woman_walking_facing_right_medium_dark_skin_tone": "🚶🏾‍♀️‍➡️", "woman_walking_facing_right_tone5": "🚶🏿‍♀️‍➡️", "woman_walking_facing_right_dark_skin_tone": "🚶🏿‍♀️‍➡️", "man_walking_facing_right_tone1": "🚶🏻‍♂️‍➡️", "man_walking_facing_right_light_skin_tone": "🚶🏻‍♂️‍➡️", "man_walking_facing_right_tone2": "🚶🏼‍♂️‍➡️", "man_walking_facing_right_medium_light_skin_tone": "🚶🏼‍♂️‍➡️", "man_walking_facing_right_tone3": "🚶🏽‍♂️‍➡️", "man_walking_facing_right_medium_skin_tone": "🚶🏽‍♂️‍➡️", "man_walking_facing_right_tone4": "🚶🏾‍♂️‍➡️", "man_walking_facing_right_medium_dark_skin_tone": "🚶🏾‍♂️‍➡️", "man_walking_facing_right_tone5": "🚶🏿‍♂️‍➡️", "man_walking_facing_right_dark_skin_tone": "🚶🏿‍♂️‍➡️", "person_with_probing_cane_tone1": "🧑🏻‍🦯", "person_with_probing_cane_light_skin_tone": "🧑🏻‍🦯", "person_with_probing_cane_tone2": "🧑🏼‍🦯", "person_with_probing_cane_medium_light_skin_tone": "🧑🏼‍🦯", "person_with_probing_cane_tone3": "🧑🏽‍🦯", "person_with_probing_cane_medium_skin_tone": "🧑🏽‍🦯", "person_with_probing_cane_tone4": "🧑🏾‍🦯", "person_with_probing_cane_medium_dark_skin_tone": "🧑🏾‍🦯", "person_with_probing_cane_tone5": "🧑🏿‍🦯", "person_with_probing_cane_dark_skin_tone": "🧑🏿‍🦯", "woman_with_probing_cane_tone1": "👩🏻‍🦯", "woman_with_probing_cane_light_skin_tone": "👩🏻‍🦯", "woman_with_probing_cane_tone2": "👩🏼‍🦯", "woman_with_probing_cane_medium_light_skin_tone": "👩🏼‍🦯", "woman_with_probing_cane_tone3": "👩🏽‍🦯", "woman_with_probing_cane_medium_skin_tone": "👩🏽‍🦯", "woman_with_probing_cane_tone4": "👩🏾‍🦯", "woman_with_probing_cane_medium_dark_skin_tone": "👩🏾‍🦯", "woman_with_probing_cane_tone5": "👩🏿‍🦯", "woman_with_probing_cane_dark_skin_tone": "👩🏿‍🦯", "man_with_probing_cane_tone1": "👨🏻‍🦯", "man_with_probing_cane_light_skin_tone": "👨🏻‍🦯", "man_with_probing_cane_tone2": "👨🏼‍🦯", "man_with_probing_cane_medium_light_skin_tone": "👨🏼‍🦯", "man_with_probing_cane_tone3": "👨🏽‍🦯", "man_with_probing_cane_medium_skin_tone": "👨🏽‍🦯", "man_with_probing_cane_tone4": "👨🏾‍🦯", "man_with_probing_cane_medium_dark_skin_tone": "👨🏾‍🦯", "man_with_probing_cane_tone5": "👨🏿‍🦯", "man_with_probing_cane_dark_skin_tone": "👨🏿‍🦯", "person_with_white_cane_facing_right_tone1": "🧑🏻‍🦯‍➡️", "person_with_white_cane_facing_right_light_skin_tone": "🧑🏻‍🦯‍➡️", "person_with_white_cane_facing_right_tone2": "🧑🏼‍🦯‍➡️", "person_with_white_cane_facing_right_medium_light_skin_tone": "🧑🏼‍🦯‍➡️", "person_with_white_cane_facing_right_tone3": "🧑🏽‍🦯‍➡️", "person_with_white_cane_facing_right_medium_skin_tone": "🧑🏽‍🦯‍➡️", "person_with_white_cane_facing_right_tone4": "🧑🏾‍🦯‍➡️", "person_with_white_cane_facing_right_medium_dark_skin_tone": "🧑🏾‍🦯‍➡️", "person_with_white_cane_facing_right_tone5": "🧑🏿‍🦯‍➡️", "person_with_white_cane_facing_right_dark_skin_tone": "🧑🏿‍🦯‍➡️", "man_with_white_cane_facing_right_tone1": "👨🏻‍🦯‍➡️", "man_with_white_cane_facing_right_light_skin_tone": "👨🏻‍🦯‍➡️", "man_with_white_cane_facing_right_tone2": "👨🏼‍🦯‍➡️", "man_with_white_cane_facing_right_medium_light_skin_tone": "👨🏼‍🦯‍➡️", "man_with_white_cane_facing_right_tone3": "👨🏽‍🦯‍➡️", "man_with_white_cane_facing_right_medium_skin_tone": "👨🏽‍🦯‍➡️", "man_with_white_cane_facing_right_tone4": "👨🏾‍🦯‍➡️", "man_with_white_cane_facing_right_medium_dark_skin_tone": "👨🏾‍🦯‍➡️", "man_with_white_cane_facing_right_tone5": "👨🏿‍🦯‍➡️", "man_with_white_cane_facing_right_dark_skin_tone": "👨🏿‍🦯‍➡️", "woman_with_white_cane_facing_right_tone1": "👩🏻‍🦯‍➡️", "woman_with_white_cane_facing_right_light_skin_tone": "👩🏻‍🦯‍➡️", "woman_with_white_cane_facing_right_tone2": "👩🏼‍🦯‍➡️", "woman_with_white_cane_facing_right_medium_light_skin_tone": "👩🏼‍🦯‍➡️", "woman_with_white_cane_facing_right_tone3": "👩🏽‍🦯‍➡️", "woman_with_white_cane_facing_right_medium_skin_tone": "👩🏽‍🦯‍➡️", "woman_with_white_cane_facing_right_tone4": "👩🏾‍🦯‍➡️", "woman_with_white_cane_facing_right_medium_dark_skin_tone": "👩🏾‍🦯‍➡️", "woman_with_white_cane_facing_right_tone5": "👩🏿‍🦯‍➡️", "woman_with_white_cane_facing_right_dark_skin_tone": "👩🏿‍🦯‍➡️", "person_kneeling_tone1": "🧎🏻", "person_kneeling_light_skin_tone": "🧎🏻", "person_kneeling_tone2": "🧎🏼", "person_kneeling_medium_light_skin_tone": "🧎🏼", "person_kneeling_tone3": "🧎🏽", "person_kneeling_medium_skin_tone": "🧎🏽", "person_kneeling_tone4": "🧎🏾", "person_kneeling_medium_dark_skin_tone": "🧎🏾", "person_kneeling_tone5": "🧎🏿", "person_kneeling_dark_skin_tone": "🧎🏿", "woman_kneeling_tone1": "🧎🏻‍♀️", "woman_kneeling_light_skin_tone": "🧎🏻‍♀️", "woman_kneeling_tone2": "🧎🏼‍♀️", "woman_kneeling_medium_light_skin_tone": "🧎🏼‍♀️", "woman_kneeling_tone3": "🧎🏽‍♀️", "woman_kneeling_medium_skin_tone": "🧎🏽‍♀️", "woman_kneeling_tone4": "🧎🏾‍♀️", "woman_kneeling_medium_dark_skin_tone": "🧎🏾‍♀️", "woman_kneeling_tone5": "🧎🏿‍♀️", "woman_kneeling_dark_skin_tone": "🧎🏿‍♀️", "man_kneeling_tone1": "🧎🏻‍♂️", "man_kneeling_light_skin_tone": "🧎🏻‍♂️", "man_kneeling_tone2": "🧎🏼‍♂️", "man_kneeling_medium_light_skin_tone": "🧎🏼‍♂️", "man_kneeling_tone3": "🧎🏽‍♂️", "man_kneeling_medium_skin_tone": "🧎🏽‍♂️", "man_kneeling_tone4": "🧎🏾‍♂️", "man_kneeling_medium_dark_skin_tone": "🧎🏾‍♂️", "man_kneeling_tone5": "🧎🏿‍♂️", "man_kneeling_dark_skin_tone": "🧎🏿‍♂️", "person_kneeling_facing_right_tone1": "🧎🏻‍➡️", "person_kneeling_facing_right_light_skin_tone": "🧎🏻‍➡️", "person_kneeling_facing_right_tone2": "🧎🏼‍➡️", "person_kneeling_facing_right_medium_light_skin_tone": "🧎🏼‍➡️", "person_kneeling_facing_right_tone3": "🧎🏽‍➡️", "person_kneeling_facing_right_medium_skin_tone": "🧎🏽‍➡️", "person_kneeling_facing_right_tone4": "🧎🏾‍➡️", "person_kneeling_facing_right_medium_dark_skin_tone": "🧎🏾‍➡️", "person_kneeling_facing_right_tone5": "🧎🏿‍➡️", "person_kneeling_facing_right_dark_skin_tone": "🧎🏿‍➡️", "woman_kneeling_facing_right_tone1": "🧎🏻‍♀️‍➡️", "woman_kneeling_facing_right_light_skin_tone": "🧎🏻‍♀️‍➡️", "woman_kneeling_facing_right_tone2": "🧎🏼‍♀️‍➡️", "woman_kneeling_facing_right_medium_light_skin_tone": "🧎🏼‍♀️‍➡️", "woman_kneeling_facing_right_tone3": "🧎🏽‍♀️‍➡️", "woman_kneeling_facing_right_medium_skin_tone": "🧎🏽‍♀️‍➡️", "woman_kneeling_facing_right_tone4": "🧎🏾‍♀️‍➡️", "woman_kneeling_facing_right_medium_dark_skin_tone": "🧎🏾‍♀️‍➡️", "woman_kneeling_facing_right_tone5": "🧎🏿‍♀️‍➡️", "woman_kneeling_facing_right_dark_skin_tone": "🧎🏿‍♀️‍➡️", "man_kneeling_facing_right_tone1": "🧎🏻‍♂️‍➡️", "man_kneeling_facing_right_light_skin_tone": "🧎🏻‍♂️‍➡️", "man_kneeling_facing_right_tone2": "🧎🏼‍♂️‍➡️", "man_kneeling_facing_right_medium_light_skin_tone": "🧎🏼‍♂️‍➡️", "man_kneeling_facing_right_tone3": "🧎🏽‍♂️‍➡️", "man_kneeling_facing_right_medium_skin_tone": "🧎🏽‍♂️‍➡️", "man_kneeling_facing_right_tone4": "🧎🏾‍♂️‍➡️", "man_kneeling_facing_right_medium_dark_skin_tone": "🧎🏾‍♂️‍➡️", "man_kneeling_facing_right_tone5": "🧎🏿‍♂️‍➡️", "man_kneeling_facing_right_dark_skin_tone": "🧎🏿‍♂️‍➡️", "person_running_tone1": "🏃🏻", "runner_tone1": "🏃🏻", "person_running_tone2": "🏃🏼", "runner_tone2": "🏃🏼", "person_running_tone3": "🏃🏽", "runner_tone3": "🏃🏽", "person_running_tone4": "🏃🏾", "runner_tone4": "🏃🏾", "person_running_tone5": "🏃🏿", "runner_tone5": "🏃🏿", "woman_running_tone1": "🏃🏻‍♀️", "woman_running_light_skin_tone": "🏃🏻‍♀️", "woman_running_tone2": "🏃🏼‍♀️", "woman_running_medium_light_skin_tone": "🏃🏼‍♀️", "woman_running_tone3": "🏃🏽‍♀️", "woman_running_medium_skin_tone": "🏃🏽‍♀️", "woman_running_tone4": "🏃🏾‍♀️", "woman_running_medium_dark_skin_tone": "🏃🏾‍♀️", "woman_running_tone5": "🏃🏿‍♀️", "woman_running_dark_skin_tone": "🏃🏿‍♀️", "man_running_tone1": "🏃🏻‍♂️", "man_running_light_skin_tone": "🏃🏻‍♂️", "man_running_tone2": "🏃🏼‍♂️", "man_running_medium_light_skin_tone": "🏃🏼‍♂️", "man_running_tone3": "🏃🏽‍♂️", "man_running_medium_skin_tone": "🏃🏽‍♂️", "man_running_tone4": "🏃🏾‍♂️", "man_running_medium_dark_skin_tone": "🏃🏾‍♂️", "man_running_tone5": "🏃🏿‍♂️", "man_running_dark_skin_tone": "🏃🏿‍♂️", "person_running_facing_right_tone1": "🏃🏻‍➡️", "person_running_facing_right_light_skin_tone": "🏃🏻‍➡️", "person_running_facing_right_tone2": "🏃🏼‍➡️", "person_running_facing_right_medium_light_skin_tone": "🏃🏼‍➡️", "person_running_facing_right_tone3": "🏃🏽‍➡️", "person_running_facing_right_medium_skin_tone": "🏃🏽‍➡️", "person_running_facing_right_tone4": "🏃🏾‍➡️", "person_running_facing_right_medium_dark_skin_tone": "🏃🏾‍➡️", "person_running_facing_right_tone5": "🏃🏿‍➡️", "person_running_facing_right_dark_skin_tone": "🏃🏿‍➡️", "woman_running_facing_right_tone1": "🏃🏻‍♀️‍➡️", "woman_running_facing_right_light_skin_tone": "🏃🏻‍♀️‍➡️", "woman_running_facing_right_tone2": "🏃🏼‍♀️‍➡️", "woman_running_facing_right_medium_light_skin_tone": "🏃🏼‍♀️‍➡️", "woman_running_facing_right_tone3": "🏃🏽‍♀️‍➡️", "woman_running_facing_right_medium_skin_tone": "🏃🏽‍♀️‍➡️", "woman_running_facing_right_tone4": "🏃🏾‍♀️‍➡️", "woman_running_facing_right_medium_dark_skin_tone": "🏃🏾‍♀️‍➡️", "woman_running_facing_right_tone5": "🏃🏿‍♀️‍➡️", "woman_running_facing_right_dark_skin_tone": "🏃🏿‍♀️‍➡️", "man_running_facing_right_tone1": "🏃🏻‍♂️‍➡️", "man_running_facing_right_light_skin_tone": "🏃🏻‍♂️‍➡️", "man_running_facing_right_tone2": "🏃🏼‍♂️‍➡️", "man_running_facing_right_medium_light_skin_tone": "🏃🏼‍♂️‍➡️", "man_running_facing_right_tone3": "🏃🏽‍♂️‍➡️", "man_running_facing_right_medium_skin_tone": "🏃🏽‍♂️‍➡️", "man_running_facing_right_tone4": "🏃🏾‍♂️‍➡️", "man_running_facing_right_medium_dark_skin_tone": "🏃🏾‍♂️‍➡️", "man_running_facing_right_tone5": "🏃🏿‍♂️‍➡️", "man_running_facing_right_dark_skin_tone": "🏃🏿‍♂️‍➡️", "person_standing_tone1": "🧍🏻", "person_standing_light_skin_tone": "🧍🏻", "person_standing_tone2": "🧍🏼", "person_standing_medium_light_skin_tone": "🧍🏼", "person_standing_tone3": "🧍🏽", "person_standing_medium_skin_tone": "🧍🏽", "person_standing_tone4": "🧍🏾", "person_standing_medium_dark_skin_tone": "🧍🏾", "person_standing_tone5": "🧍🏿", "person_standing_dark_skin_tone": "🧍🏿", "woman_standing_tone1": "🧍🏻‍♀️", "woman_standing_light_skin_tone": "🧍🏻‍♀️", "woman_standing_tone2": "🧍🏼‍♀️", "woman_standing_medium_light_skin_tone": "🧍🏼‍♀️", "woman_standing_tone3": "🧍🏽‍♀️", "woman_standing_medium_skin_tone": "🧍🏽‍♀️", "woman_standing_tone4": "🧍🏾‍♀️", "woman_standing_medium_dark_skin_tone": "🧍🏾‍♀️", "woman_standing_tone5": "🧍🏿‍♀️", "woman_standing_dark_skin_tone": "🧍🏿‍♀️", "man_standing_tone1": "🧍🏻‍♂️", "man_standing_light_skin_tone": "🧍🏻‍♂️", "man_standing_tone2": "🧍🏼‍♂️", "man_standing_medium_light_skin_tone": "🧍🏼‍♂️", "man_standing_tone3": "🧍🏽‍♂️", "man_standing_medium_skin_tone": "🧍🏽‍♂️", "man_standing_tone4": "🧍🏾‍♂️", "man_standing_medium_dark_skin_tone": "🧍🏾‍♂️", "man_standing_tone5": "🧍🏿‍♂️", "man_standing_dark_skin_tone": "🧍🏿‍♂️", "people_holding_hands_tone1": "🧑🏻‍🤝‍🧑🏻", "people_holding_hands_light_skin_tone": "🧑🏻‍🤝‍🧑🏻", "people_holding_hands_tone1_tone2": "🧑🏻‍🤝‍🧑🏼", "people_holding_hands_light_skin_tone_medium_light_skin_tone": "🧑🏻‍🤝‍🧑🏼", "people_holding_hands_tone1_tone3": "🧑🏻‍🤝‍🧑🏽", "people_holding_hands_light_skin_tone_medium_skin_tone": "🧑🏻‍🤝‍🧑🏽", "people_holding_hands_tone1_tone4": "🧑🏻‍🤝‍🧑🏾", "people_holding_hands_light_skin_tone_medium_dark_skin_tone": "🧑🏻‍🤝‍🧑🏾", "people_holding_hands_tone1_tone5": "🧑🏻‍🤝‍🧑🏿", "people_holding_hands_light_skin_tone_dark_skin_tone": "🧑🏻‍🤝‍🧑🏿", "people_holding_hands_tone2_tone1": "🧑🏼‍🤝‍🧑🏻", "people_holding_hands_medium_light_skin_tone_light_skin_tone": "🧑🏼‍🤝‍🧑🏻", "people_holding_hands_tone2": "🧑🏼‍🤝‍🧑🏼", "people_holding_hands_medium_light_skin_tone": "🧑🏼‍🤝‍🧑🏼", "people_holding_hands_tone2_tone3": "🧑🏼‍🤝‍🧑🏽", "people_holding_hands_medium_light_skin_tone_medium_skin_tone": "🧑🏼‍🤝‍🧑🏽", "people_holding_hands_tone2_tone4": "🧑🏼‍🤝‍🧑🏾", "people_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "🧑🏼‍🤝‍🧑🏾", "people_holding_hands_tone2_tone5": "🧑🏼‍🤝‍🧑🏿", "people_holding_hands_medium_light_skin_tone_dark_skin_tone": "🧑🏼‍🤝‍🧑🏿", "people_holding_hands_tone3_tone1": "🧑🏽‍🤝‍🧑🏻", "people_holding_hands_medium_skin_tone_light_skin_tone": "🧑🏽‍🤝‍🧑🏻", "people_holding_hands_tone3_tone2": "🧑🏽‍🤝‍🧑🏼", "people_holding_hands_medium_skin_tone_medium_light_skin_tone": "🧑🏽‍🤝‍🧑🏼", "people_holding_hands_tone3": "🧑🏽‍🤝‍🧑🏽", "people_holding_hands_medium_skin_tone": "🧑🏽‍🤝‍🧑🏽", "people_holding_hands_tone3_tone4": "🧑🏽‍🤝‍🧑🏾", "people_holding_hands_medium_skin_tone_medium_dark_skin_tone": "🧑🏽‍🤝‍🧑🏾", "people_holding_hands_tone3_tone5": "🧑🏽‍🤝‍🧑🏿", "people_holding_hands_medium_skin_tone_dark_skin_tone": "🧑🏽‍🤝‍🧑🏿", "people_holding_hands_tone4_tone1": "🧑🏾‍🤝‍🧑🏻", "people_holding_hands_medium_dark_skin_tone_light_skin_tone": "🧑🏾‍🤝‍🧑🏻", "people_holding_hands_tone4_tone2": "🧑🏾‍🤝‍🧑🏼", "people_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "🧑🏾‍🤝‍🧑🏼", "people_holding_hands_tone4_tone3": "🧑🏾‍🤝‍🧑🏽", "people_holding_hands_medium_dark_skin_tone_medium_skin_tone": "🧑🏾‍🤝‍🧑🏽", "people_holding_hands_tone4": "🧑🏾‍🤝‍🧑🏾", "people_holding_hands_medium_dark_skin_tone": "🧑🏾‍🤝‍🧑🏾", "people_holding_hands_tone4_tone5": "🧑🏾‍🤝‍🧑🏿", "people_holding_hands_medium_dark_skin_tone_dark_skin_tone": "🧑🏾‍🤝‍🧑🏿", "people_holding_hands_tone5_tone1": "🧑🏿‍🤝‍🧑🏻", "people_holding_hands_dark_skin_tone_light_skin_tone": "🧑🏿‍🤝‍🧑🏻", "people_holding_hands_tone5_tone2": "🧑🏿‍🤝‍🧑🏼", "people_holding_hands_dark_skin_tone_medium_light_skin_tone": "🧑🏿‍🤝‍🧑🏼", "people_holding_hands_tone5_tone3": "🧑🏿‍🤝‍🧑🏽", "people_holding_hands_dark_skin_tone_medium_skin_tone": "🧑🏿‍🤝‍🧑🏽", "people_holding_hands_tone5_tone4": "🧑🏿‍🤝‍🧑🏾", "people_holding_hands_dark_skin_tone_medium_dark_skin_tone": "🧑🏿‍🤝‍🧑🏾", "people_holding_hands_tone5": "🧑🏿‍🤝‍🧑🏿", "people_holding_hands_dark_skin_tone": "🧑🏿‍🤝‍🧑🏿", "woman_and_man_holding_hands_tone1": "👫🏻", "woman_and_man_holding_hands_light_skin_tone": "👫🏻", "woman_and_man_holding_hands_tone1_tone2": "👩🏻‍🤝‍👨🏼", "woman_and_man_holding_hands_light_skin_tone_medium_light_skin_tone": "👩🏻‍🤝‍👨🏼", "woman_and_man_holding_hands_tone1_tone3": "👩🏻‍🤝‍👨🏽", "woman_and_man_holding_hands_light_skin_tone_medium_skin_tone": "👩🏻‍🤝‍👨🏽", "woman_and_man_holding_hands_tone1_tone4": "👩🏻‍🤝‍👨🏾", "woman_and_man_holding_hands_light_skin_tone_medium_dark_skin_tone": "👩🏻‍🤝‍👨🏾", "woman_and_man_holding_hands_tone1_tone5": "👩🏻‍🤝‍👨🏿", "woman_and_man_holding_hands_light_skin_tone_dark_skin_tone": "👩🏻‍🤝‍👨🏿", "woman_and_man_holding_hands_tone2_tone1": "👩🏼‍🤝‍👨🏻", "woman_and_man_holding_hands_medium_light_skin_tone_light_skin_tone": "👩🏼‍🤝‍👨🏻", "woman_and_man_holding_hands_tone2": "👫🏼", "woman_and_man_holding_hands_medium_light_skin_tone": "👫🏼", "woman_and_man_holding_hands_tone2_tone3": "👩🏼‍🤝‍👨🏽", "woman_and_man_holding_hands_medium_light_skin_tone_medium_skin_tone": "👩🏼‍🤝‍👨🏽", "woman_and_man_holding_hands_tone2_tone4": "👩🏼‍🤝‍👨🏾", "woman_and_man_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "👩🏼‍🤝‍👨🏾", "woman_and_man_holding_hands_tone2_tone5": "👩🏼‍🤝‍👨🏿", "woman_and_man_holding_hands_medium_light_skin_tone_dark_skin_tone": "👩🏼‍🤝‍👨🏿", "woman_and_man_holding_hands_tone3_tone1": "👩🏽‍🤝‍👨🏻", "woman_and_man_holding_hands_medium_skin_tone_light_skin_tone": "👩🏽‍🤝‍👨🏻", "woman_and_man_holding_hands_tone3_tone2": "👩🏽‍🤝‍👨🏼", "woman_and_man_holding_hands_medium_skin_tone_medium_light_skin_tone": "👩🏽‍🤝‍👨🏼", "woman_and_man_holding_hands_tone3": "👫🏽", "woman_and_man_holding_hands_medium_skin_tone": "👫🏽", "woman_and_man_holding_hands_tone3_tone4": "👩🏽‍🤝‍👨🏾", "woman_and_man_holding_hands_medium_skin_tone_medium_dark_skin_tone": "👩🏽‍🤝‍👨🏾", "woman_and_man_holding_hands_tone3_tone5": "👩🏽‍🤝‍👨🏿", "woman_and_man_holding_hands_medium_skin_tone_dark_skin_tone": "👩🏽‍🤝‍👨🏿", "woman_and_man_holding_hands_tone4_tone1": "👩🏾‍🤝‍👨🏻", "woman_and_man_holding_hands_medium_dark_skin_tone_light_skin_tone": "👩🏾‍🤝‍👨🏻", "woman_and_man_holding_hands_tone4_tone2": "👩🏾‍🤝‍👨🏼", "woman_and_man_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "👩🏾‍🤝‍👨🏼", "woman_and_man_holding_hands_tone4_tone3": "👩🏾‍🤝‍👨🏽", "woman_and_man_holding_hands_medium_dark_skin_tone_medium_skin_tone": "👩🏾‍🤝‍👨🏽", "woman_and_man_holding_hands_tone4": "👫🏾", "woman_and_man_holding_hands_medium_dark_skin_tone": "👫🏾", "woman_and_man_holding_hands_tone4_tone5": "👩🏾‍🤝‍👨🏿", "woman_and_man_holding_hands_medium_dark_skin_tone_dark_skin_tone": "👩🏾‍🤝‍👨🏿", "woman_and_man_holding_hands_tone5_tone1": "👩🏿‍🤝‍👨🏻", "woman_and_man_holding_hands_dark_skin_tone_light_skin_tone": "👩🏿‍🤝‍👨🏻", "woman_and_man_holding_hands_tone5_tone2": "👩🏿‍🤝‍👨🏼", "woman_and_man_holding_hands_dark_skin_tone_medium_light_skin_tone": "👩🏿‍🤝‍👨🏼", "woman_and_man_holding_hands_tone5_tone3": "👩🏿‍🤝‍👨🏽", "woman_and_man_holding_hands_dark_skin_tone_medium_skin_tone": "👩🏿‍🤝‍👨🏽", "woman_and_man_holding_hands_tone5_tone4": "👩🏿‍🤝‍👨🏾", "woman_and_man_holding_hands_dark_skin_tone_medium_dark_skin_tone": "👩🏿‍🤝‍👨🏾", "woman_and_man_holding_hands_tone5": "👫🏿", "woman_and_man_holding_hands_dark_skin_tone": "👫🏿", "women_holding_hands_tone1": "👭🏻", "women_holding_hands_light_skin_tone": "👭🏻", "women_holding_hands_tone1_tone2": "👩🏻‍🤝‍👩🏼", "women_holding_hands_light_skin_tone_medium_light_skin_tone": "👩🏻‍🤝‍👩🏼", "women_holding_hands_tone1_tone3": "👩🏻‍🤝‍👩🏽", "women_holding_hands_light_skin_tone_medium_skin_tone": "👩🏻‍🤝‍👩🏽", "women_holding_hands_tone1_tone4": "👩🏻‍🤝‍👩🏾", "women_holding_hands_light_skin_tone_medium_dark_skin_tone": "👩🏻‍🤝‍👩🏾", "women_holding_hands_tone1_tone5": "👩🏻‍🤝‍👩🏿", "women_holding_hands_light_skin_tone_dark_skin_tone": "👩🏻‍🤝‍👩🏿", "women_holding_hands_tone2_tone1": "👩🏼‍🤝‍👩🏻", "women_holding_hands_medium_light_skin_tone_light_skin_tone": "👩🏼‍🤝‍👩🏻", "women_holding_hands_tone2": "👭🏼", "women_holding_hands_medium_light_skin_tone": "👭🏼", "women_holding_hands_tone2_tone3": "👩🏼‍🤝‍👩🏽", "women_holding_hands_medium_light_skin_tone_medium_skin_tone": "👩🏼‍🤝‍👩🏽", "women_holding_hands_tone2_tone4": "👩🏼‍🤝‍👩🏾", "women_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "👩🏼‍🤝‍👩🏾", "women_holding_hands_tone2_tone5": "👩🏼‍🤝‍👩🏿", "women_holding_hands_medium_light_skin_tone_dark_skin_tone": "👩🏼‍🤝‍👩🏿", "women_holding_hands_tone3_tone1": "👩🏽‍🤝‍👩🏻", "women_holding_hands_medium_skin_tone_light_skin_tone": "👩🏽‍🤝‍👩🏻", "women_holding_hands_tone3_tone2": "👩🏽‍🤝‍👩🏼", "women_holding_hands_medium_skin_tone_medium_light_skin_tone": "👩🏽‍🤝‍👩🏼", "women_holding_hands_tone3": "👭🏽", "women_holding_hands_medium_skin_tone": "👭🏽", "women_holding_hands_tone3_tone4": "👩🏽‍🤝‍👩🏾", "women_holding_hands_medium_skin_tone_medium_dark_skin_tone": "👩🏽‍🤝‍👩🏾", "women_holding_hands_tone3_tone5": "👩🏽‍🤝‍👩🏿", "women_holding_hands_medium_skin_tone_dark_skin_tone": "👩🏽‍🤝‍👩🏿", "women_holding_hands_tone4_tone1": "👩🏾‍🤝‍👩🏻", "women_holding_hands_medium_dark_skin_tone_light_skin_tone": "👩🏾‍🤝‍👩🏻", "women_holding_hands_tone4_tone2": "👩🏾‍🤝‍👩🏼", "women_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "👩🏾‍🤝‍👩🏼", "women_holding_hands_tone4_tone3": "👩🏾‍🤝‍👩🏽", "women_holding_hands_medium_dark_skin_tone_medium_skin_tone": "👩🏾‍🤝‍👩🏽", "women_holding_hands_tone4": "👭🏾", "women_holding_hands_medium_dark_skin_tone": "👭🏾", "women_holding_hands_tone4_tone5": "👩🏾‍🤝‍👩🏿", "women_holding_hands_medium_dark_skin_tone_dark_skin_tone": "👩🏾‍🤝‍👩🏿", "women_holding_hands_tone5_tone1": "👩🏿‍🤝‍👩🏻", "women_holding_hands_dark_skin_tone_light_skin_tone": "👩🏿‍🤝‍👩🏻", "women_holding_hands_tone5_tone2": "👩🏿‍🤝‍👩🏼", "women_holding_hands_dark_skin_tone_medium_light_skin_tone": "👩🏿‍🤝‍👩🏼", "women_holding_hands_tone5_tone3": "👩🏿‍🤝‍👩🏽", "women_holding_hands_dark_skin_tone_medium_skin_tone": "👩🏿‍🤝‍👩🏽", "women_holding_hands_tone5_tone4": "👩🏿‍🤝‍👩🏾", "women_holding_hands_dark_skin_tone_medium_dark_skin_tone": "👩🏿‍🤝‍👩🏾", "women_holding_hands_tone5": "👭🏿", "women_holding_hands_dark_skin_tone": "👭🏿", "men_holding_hands_tone1": "👬🏻", "men_holding_hands_light_skin_tone": "👬🏻", "men_holding_hands_tone1_tone2": "👨🏻‍🤝‍👨🏼", "men_holding_hands_light_skin_tone_medium_light_skin_tone": "👨🏻‍🤝‍👨🏼", "men_holding_hands_tone1_tone3": "👨🏻‍🤝‍👨🏽", "men_holding_hands_light_skin_tone_medium_skin_tone": "👨🏻‍🤝‍👨🏽", "men_holding_hands_tone1_tone4": "👨🏻‍🤝‍👨🏾", "men_holding_hands_light_skin_tone_medium_dark_skin_tone": "👨🏻‍🤝‍👨🏾", "men_holding_hands_tone1_tone5": "👨🏻‍🤝‍👨🏿", "men_holding_hands_light_skin_tone_dark_skin_tone": "👨🏻‍🤝‍👨🏿", "men_holding_hands_tone2_tone1": "👨🏼‍🤝‍👨🏻", "men_holding_hands_medium_light_skin_tone_light_skin_tone": "👨🏼‍🤝‍👨🏻", "men_holding_hands_tone2": "👬🏼", "men_holding_hands_medium_light_skin_tone": "👬🏼", "men_holding_hands_tone2_tone3": "👨🏼‍🤝‍👨🏽", "men_holding_hands_medium_light_skin_tone_medium_skin_tone": "👨🏼‍🤝‍👨🏽", "men_holding_hands_tone2_tone4": "👨🏼‍🤝‍👨🏾", "men_holding_hands_medium_light_skin_tone_medium_dark_skin_tone": "👨🏼‍🤝‍👨🏾", "men_holding_hands_tone2_tone5": "👨🏼‍🤝‍👨🏿", "men_holding_hands_medium_light_skin_tone_dark_skin_tone": "👨🏼‍🤝‍👨🏿", "men_holding_hands_tone3_tone1": "👨🏽‍🤝‍👨🏻", "men_holding_hands_medium_skin_tone_light_skin_tone": "👨🏽‍🤝‍👨🏻", "men_holding_hands_tone3_tone2": "👨🏽‍🤝‍👨🏼", "men_holding_hands_medium_skin_tone_medium_light_skin_tone": "👨🏽‍🤝‍👨🏼", "men_holding_hands_tone3": "👬🏽", "men_holding_hands_medium_skin_tone": "👬🏽", "men_holding_hands_tone3_tone4": "👨🏽‍🤝‍👨🏾", "men_holding_hands_medium_skin_tone_medium_dark_skin_tone": "👨🏽‍🤝‍👨🏾", "men_holding_hands_tone3_tone5": "👨🏽‍🤝‍👨🏿", "men_holding_hands_medium_skin_tone_dark_skin_tone": "👨🏽‍🤝‍👨🏿", "men_holding_hands_tone4_tone1": "👨🏾‍🤝‍👨🏻", "men_holding_hands_medium_dark_skin_tone_light_skin_tone": "👨🏾‍🤝‍👨🏻", "men_holding_hands_tone4_tone2": "👨🏾‍🤝‍👨🏼", "men_holding_hands_medium_dark_skin_tone_medium_light_skin_tone": "👨🏾‍🤝‍👨🏼", "men_holding_hands_tone4_tone3": "👨🏾‍🤝‍👨🏽", "men_holding_hands_medium_dark_skin_tone_medium_skin_tone": "👨🏾‍🤝‍👨🏽", "men_holding_hands_tone4": "👬🏾", "men_holding_hands_medium_dark_skin_tone": "👬🏾", "men_holding_hands_tone4_tone5": "👨🏾‍🤝‍👨🏿", "men_holding_hands_medium_dark_skin_tone_dark_skin_tone": "👨🏾‍🤝‍👨🏿", "men_holding_hands_tone5_tone1": "👨🏿‍🤝‍👨🏻", "men_holding_hands_dark_skin_tone_light_skin_tone": "👨🏿‍🤝‍👨🏻", "men_holding_hands_tone5_tone2": "👨🏿‍🤝‍👨🏼", "men_holding_hands_dark_skin_tone_medium_light_skin_tone": "👨🏿‍🤝‍👨🏼", "men_holding_hands_tone5_tone3": "👨🏿‍🤝‍👨🏽", "men_holding_hands_dark_skin_tone_medium_skin_tone": "👨🏿‍🤝‍👨🏽", "men_holding_hands_tone5_tone4": "👨🏿‍🤝‍👨🏾", "men_holding_hands_dark_skin_tone_medium_dark_skin_tone": "👨🏿‍🤝‍👨🏾", "men_holding_hands_tone5": "👬🏿", "men_holding_hands_dark_skin_tone": "👬🏿", "couple_with_heart_tone1": "💑🏻", "couple_with_heart_light_skin_tone": "💑🏻", "couple_with_heart_person_person_tone1_tone2": "🧑🏻‍❤️‍🧑🏼", "couple_with_heart_person_person_light_skin_tone_medium_light_skin_tone": "🧑🏻‍❤️‍🧑🏼", "couple_with_heart_person_person_tone1_tone3": "🧑🏻‍❤️‍🧑🏽", "couple_with_heart_person_person_light_skin_tone_medium_skin_tone": "🧑🏻‍❤️‍🧑🏽", "couple_with_heart_person_person_tone1_tone4": "🧑🏻‍❤️‍🧑🏾", "couple_with_heart_person_person_light_skin_tone_medium_dark_skin_tone": "🧑🏻‍❤️‍🧑🏾", "couple_with_heart_person_person_tone1_tone5": "🧑🏻‍❤️‍🧑🏿", "couple_with_heart_person_person_light_skin_tone_dark_skin_tone": "🧑🏻‍❤️‍🧑🏿", "couple_with_heart_person_person_tone2_tone1": "🧑🏼‍❤️‍🧑🏻", "couple_with_heart_person_person_medium_light_skin_tone_light_skin_tone": "🧑🏼‍❤️‍🧑🏻", "couple_with_heart_tone2": "💑🏼", "couple_with_heart_medium_light_skin_tone": "💑🏼", "couple_with_heart_person_person_tone2_tone3": "🧑🏼‍❤️‍🧑🏽", "couple_with_heart_person_person_medium_light_skin_tone_medium_skin_tone": "🧑🏼‍❤️‍🧑🏽", "couple_with_heart_person_person_tone2_tone4": "🧑🏼‍❤️‍🧑🏾", "couple_with_heart_person_person_medium_light_skin_tone_medium_dark_skin_tone": "🧑🏼‍❤️‍🧑🏾", "couple_with_heart_person_person_tone2_tone5": "🧑🏼‍❤️‍🧑🏿", "couple_with_heart_person_person_medium_light_skin_tone_dark_skin_tone": "🧑🏼‍❤️‍🧑🏿", "couple_with_heart_person_person_tone3_tone1": "🧑🏽‍❤️‍🧑🏻", "couple_with_heart_person_person_medium_skin_tone_light_skin_tone": "🧑🏽‍❤️‍🧑🏻", "couple_with_heart_person_person_tone3_tone2": "🧑🏽‍❤️‍🧑🏼", "couple_with_heart_person_person_medium_skin_tone_medium_light_skin_tone": "🧑🏽‍❤️‍🧑🏼", "couple_with_heart_tone3": "💑🏽", "couple_with_heart_medium_skin_tone": "💑🏽", "couple_with_heart_person_person_tone3_tone4": "🧑🏽‍❤️‍🧑🏾", "couple_with_heart_person_person_medium_skin_tone_medium_dark_skin_tone": "🧑🏽‍❤️‍🧑🏾", "couple_with_heart_person_person_tone3_tone5": "🧑🏽‍❤️‍🧑🏿", "couple_with_heart_person_person_medium_skin_tone_dark_skin_tone": "🧑🏽‍❤️‍🧑🏿", "couple_with_heart_person_person_tone4_tone1": "🧑🏾‍❤️‍🧑🏻", "couple_with_heart_person_person_medium_dark_skin_tone_light_skin_tone": "🧑🏾‍❤️‍🧑🏻", "couple_with_heart_person_person_tone4_tone2": "🧑🏾‍❤️‍🧑🏼", "couple_with_heart_person_person_medium_dark_skin_tone_medium_light_skin_tone": "🧑🏾‍❤️‍🧑🏼", "couple_with_heart_person_person_tone4_tone3": "🧑🏾‍❤️‍🧑🏽", "couple_with_heart_person_person_medium_dark_skin_tone_medium_skin_tone": "🧑🏾‍❤️‍🧑🏽", "couple_with_heart_tone4": "💑🏾", "couple_with_heart_medium_dark_skin_tone": "💑🏾", "couple_with_heart_person_person_tone4_tone5": "🧑🏾‍❤️‍🧑🏿", "couple_with_heart_person_person_medium_dark_skin_tone_dark_skin_tone": "🧑🏾‍❤️‍🧑🏿", "couple_with_heart_person_person_tone5_tone1": "🧑🏿‍❤️‍🧑🏻", "couple_with_heart_person_person_dark_skin_tone_light_skin_tone": "🧑🏿‍❤️‍🧑🏻", "couple_with_heart_person_person_tone5_tone2": "🧑🏿‍❤️‍🧑🏼", "couple_with_heart_person_person_dark_skin_tone_medium_light_skin_tone": "🧑🏿‍❤️‍🧑🏼", "couple_with_heart_person_person_tone5_tone3": "🧑🏿‍❤️‍🧑🏽", "couple_with_heart_person_person_dark_skin_tone_medium_skin_tone": "🧑🏿‍❤️‍🧑🏽", "couple_with_heart_person_person_tone5_tone4": "🧑🏿‍❤️‍🧑🏾", "couple_with_heart_person_person_dark_skin_tone_medium_dark_skin_tone": "🧑🏿‍❤️‍🧑🏾", "couple_with_heart_tone5": "💑🏿", "couple_with_heart_dark_skin_tone": "💑🏿", "couple_with_heart_woman_man_tone1": "👩🏻‍❤️‍👨🏻", "couple_with_heart_woman_man_light_skin_tone": "👩🏻‍❤️‍👨🏻", "couple_with_heart_woman_man_tone1_tone2": "👩🏻‍❤️‍👨🏼", "couple_with_heart_woman_man_light_skin_tone_medium_light_skin_tone": "👩🏻‍❤️‍👨🏼", "couple_with_heart_woman_man_tone1_tone3": "👩🏻‍❤️‍👨🏽", "couple_with_heart_woman_man_light_skin_tone_medium_skin_tone": "👩🏻‍❤️‍👨🏽", "couple_with_heart_woman_man_tone1_tone4": "👩🏻‍❤️‍👨🏾", "couple_with_heart_woman_man_light_skin_tone_medium_dark_skin_tone": "👩🏻‍❤️‍👨🏾", "couple_with_heart_woman_man_tone1_tone5": "👩🏻‍❤️‍👨🏿", "couple_with_heart_woman_man_light_skin_tone_dark_skin_tone": "👩🏻‍❤️‍👨🏿", "couple_with_heart_woman_man_tone2_tone1": "👩🏼‍❤️‍👨🏻", "couple_with_heart_woman_man_medium_light_skin_tone_light_skin_tone": "👩🏼‍❤️‍👨🏻", "couple_with_heart_woman_man_tone2": "👩🏼‍❤️‍👨🏼", "couple_with_heart_woman_man_medium_light_skin_tone": "👩🏼‍❤️‍👨🏼", "couple_with_heart_woman_man_tone2_tone3": "👩🏼‍❤️‍👨🏽", "couple_with_heart_woman_man_medium_light_skin_tone_medium_skin_tone": "👩🏼‍❤️‍👨🏽", "couple_with_heart_woman_man_tone2_tone4": "👩🏼‍❤️‍👨🏾", "couple_with_heart_woman_man_medium_light_skin_tone_medium_dark_skin_tone": "👩🏼‍❤️‍👨🏾", "couple_with_heart_woman_man_tone2_tone5": "👩🏼‍❤️‍👨🏿", "couple_with_heart_woman_man_medium_light_skin_tone_dark_skin_tone": "👩🏼‍❤️‍👨🏿", "couple_with_heart_woman_man_tone3_tone1": "👩🏽‍❤️‍👨🏻", "couple_with_heart_woman_man_medium_skin_tone_light_skin_tone": "👩🏽‍❤️‍👨🏻", "couple_with_heart_woman_man_tone3_tone2": "👩🏽‍❤️‍👨🏼", "couple_with_heart_woman_man_medium_skin_tone_medium_light_skin_tone": "👩🏽‍❤️‍👨🏼", "couple_with_heart_woman_man_tone3": "👩🏽‍❤️‍👨🏽", "couple_with_heart_woman_man_medium_skin_tone": "👩🏽‍❤️‍👨🏽", "couple_with_heart_woman_man_tone3_tone4": "👩🏽‍❤️‍👨🏾", "couple_with_heart_woman_man_medium_skin_tone_medium_dark_skin_tone": "👩🏽‍❤️‍👨🏾", "couple_with_heart_woman_man_tone3_tone5": "👩🏽‍❤️‍👨🏿", "couple_with_heart_woman_man_medium_skin_tone_dark_skin_tone": "👩🏽‍❤️‍👨🏿", "couple_with_heart_woman_man_tone4_tone1": "👩🏾‍❤️‍👨🏻", "couple_with_heart_woman_man_medium_dark_skin_tone_light_skin_tone": "👩🏾‍❤️‍👨🏻", "couple_with_heart_woman_man_tone4_tone2": "👩🏾‍❤️‍👨🏼", "couple_with_heart_woman_man_medium_dark_skin_tone_medium_light_skin_tone": "👩🏾‍❤️‍👨🏼", "couple_with_heart_woman_man_tone4_tone3": "👩🏾‍❤️‍👨🏽", "couple_with_heart_woman_man_medium_dark_skin_tone_medium_skin_tone": "👩🏾‍❤️‍👨🏽", "couple_with_heart_woman_man_tone4": "👩🏾‍❤️‍👨🏾", "couple_with_heart_woman_man_medium_dark_skin_tone": "👩🏾‍❤️‍👨🏾", "couple_with_heart_woman_man_tone4_tone5": "👩🏾‍❤️‍👨🏿", "couple_with_heart_woman_man_medium_dark_skin_tone_dark_skin_tone": "👩🏾‍❤️‍👨🏿", "couple_with_heart_woman_man_tone5_tone1": "👩🏿‍❤️‍👨🏻", "couple_with_heart_woman_man_dark_skin_tone_light_skin_tone": "👩🏿‍❤️‍👨🏻", "couple_with_heart_woman_man_tone5_tone2": "👩🏿‍❤️‍👨🏼", "couple_with_heart_woman_man_dark_skin_tone_medium_light_skin_tone": "👩🏿‍❤️‍👨🏼", "couple_with_heart_woman_man_tone5_tone3": "👩🏿‍❤️‍👨🏽", "couple_with_heart_woman_man_dark_skin_tone_medium_skin_tone": "👩🏿‍❤️‍👨🏽", "couple_with_heart_woman_man_tone5_tone4": "👩🏿‍❤️‍👨🏾", "couple_with_heart_woman_man_dark_skin_tone_medium_dark_skin_tone": "👩🏿‍❤️‍👨🏾", "couple_with_heart_woman_man_tone5": "👩🏿‍❤️‍👨🏿", "couple_with_heart_woman_man_dark_skin_tone": "👩🏿‍❤️‍👨🏿", "couple_with_heart_woman_woman_tone1": "👩🏻‍❤️‍👩🏻", "couple_with_heart_woman_woman_light_skin_tone": "👩🏻‍❤️‍👩🏻", "couple_with_heart_woman_woman_tone1_tone2": "👩🏻‍❤️‍👩🏼", "couple_with_heart_woman_woman_light_skin_tone_medium_light_skin_tone": "👩🏻‍❤️‍👩🏼", "couple_with_heart_woman_woman_tone1_tone3": "👩🏻‍❤️‍👩🏽", "couple_with_heart_woman_woman_light_skin_tone_medium_skin_tone": "👩🏻‍❤️‍👩🏽", "couple_with_heart_woman_woman_tone1_tone4": "👩🏻‍❤️‍👩🏾", "couple_with_heart_woman_woman_light_skin_tone_medium_dark_skin_tone": "👩🏻‍❤️‍👩🏾", "couple_with_heart_woman_woman_tone1_tone5": "👩🏻‍❤️‍👩🏿", "couple_with_heart_woman_woman_light_skin_tone_dark_skin_tone": "👩🏻‍❤️‍👩🏿", "couple_with_heart_woman_woman_tone2_tone1": "👩🏼‍❤️‍👩🏻", "couple_with_heart_woman_woman_medium_light_skin_tone_light_skin_tone": "👩🏼‍❤️‍👩🏻", "couple_with_heart_woman_woman_tone2": "👩🏼‍❤️‍👩🏼", "couple_with_heart_woman_woman_medium_light_skin_tone": "👩🏼‍❤️‍👩🏼", "couple_with_heart_woman_woman_tone2_tone3": "👩🏼‍❤️‍👩🏽", "couple_with_heart_woman_woman_medium_light_skin_tone_medium_skin_tone": "👩🏼‍❤️‍👩🏽", "couple_with_heart_woman_woman_tone2_tone4": "👩🏼‍❤️‍👩🏾", "couple_with_heart_woman_woman_medium_light_skin_tone_medium_dark_skin_tone": "👩🏼‍❤️‍👩🏾", "couple_with_heart_woman_woman_tone2_tone5": "👩🏼‍❤️‍👩🏿", "couple_with_heart_woman_woman_medium_light_skin_tone_dark_skin_tone": "👩🏼‍❤️‍👩🏿", "couple_with_heart_woman_woman_tone3_tone1": "👩🏽‍❤️‍👩🏻", "couple_with_heart_woman_woman_medium_skin_tone_light_skin_tone": "👩🏽‍❤️‍👩🏻", "couple_with_heart_woman_woman_tone3_tone2": "👩🏽‍❤️‍👩🏼", "couple_with_heart_woman_woman_medium_skin_tone_medium_light_skin_tone": "👩🏽‍❤️‍👩🏼", "couple_with_heart_woman_woman_tone3": "👩🏽‍❤️‍👩🏽", "couple_with_heart_woman_woman_medium_skin_tone": "👩🏽‍❤️‍👩🏽", "couple_with_heart_woman_woman_tone3_tone4": "👩🏽‍❤️‍👩🏾", "couple_with_heart_woman_woman_medium_skin_tone_medium_dark_skin_tone": "👩🏽‍❤️‍👩🏾", "couple_with_heart_woman_woman_tone3_tone5": "👩🏽‍❤️‍👩🏿", "couple_with_heart_woman_woman_medium_skin_tone_dark_skin_tone": "👩🏽‍❤️‍👩🏿", "couple_with_heart_woman_woman_tone4_tone1": "👩🏾‍❤️‍👩🏻", "couple_with_heart_woman_woman_medium_dark_skin_tone_light_skin_tone": "👩🏾‍❤️‍👩🏻", "couple_with_heart_woman_woman_tone4_tone2": "👩🏾‍❤️‍👩🏼", "couple_with_heart_woman_woman_medium_dark_skin_tone_medium_light_skin_tone": "👩🏾‍❤️‍👩🏼", "couple_with_heart_woman_woman_tone4_tone3": "👩🏾‍❤️‍👩🏽", "couple_with_heart_woman_woman_medium_dark_skin_tone_medium_skin_tone": "👩🏾‍❤️‍👩🏽", "couple_with_heart_woman_woman_tone4": "👩🏾‍❤️‍👩🏾", "couple_with_heart_woman_woman_medium_dark_skin_tone": "👩🏾‍❤️‍👩🏾", "couple_with_heart_woman_woman_tone4_tone5": "👩🏾‍❤️‍👩🏿", "couple_with_heart_woman_woman_medium_dark_skin_tone_dark_skin_tone": "👩🏾‍❤️‍👩🏿", "couple_with_heart_woman_woman_tone5_tone1": "👩🏿‍❤️‍👩🏻", "couple_with_heart_woman_woman_dark_skin_tone_light_skin_tone": "👩🏿‍❤️‍👩🏻", "couple_with_heart_woman_woman_tone5_tone2": "👩🏿‍❤️‍👩🏼", "couple_with_heart_woman_woman_dark_skin_tone_medium_light_skin_tone": "👩🏿‍❤️‍👩🏼", "couple_with_heart_woman_woman_tone5_tone3": "👩🏿‍❤️‍👩🏽", "couple_with_heart_woman_woman_dark_skin_tone_medium_skin_tone": "👩🏿‍❤️‍👩🏽", "couple_with_heart_woman_woman_tone5_tone4": "👩🏿‍❤️‍👩🏾", "couple_with_heart_woman_woman_dark_skin_tone_medium_dark_skin_tone": "👩🏿‍❤️‍👩🏾", "couple_with_heart_woman_woman_tone5": "👩🏿‍❤️‍👩🏿", "couple_with_heart_woman_woman_dark_skin_tone": "👩🏿‍❤️‍👩🏿", "couple_with_heart_man_man_tone1": "👨🏻‍❤️‍👨🏻", "couple_with_heart_man_man_light_skin_tone": "👨🏻‍❤️‍👨🏻", "couple_with_heart_man_man_tone1_tone2": "👨🏻‍❤️‍👨🏼", "couple_with_heart_man_man_light_skin_tone_medium_light_skin_tone": "👨🏻‍❤️‍👨🏼", "couple_with_heart_man_man_tone1_tone3": "👨🏻‍❤️‍👨🏽", "couple_with_heart_man_man_light_skin_tone_medium_skin_tone": "👨🏻‍❤️‍👨🏽", "couple_with_heart_man_man_tone1_tone4": "👨🏻‍❤️‍👨🏾", "couple_with_heart_man_man_light_skin_tone_medium_dark_skin_tone": "👨🏻‍❤️‍👨🏾", "couple_with_heart_man_man_tone1_tone5": "👨🏻‍❤️‍👨🏿", "couple_with_heart_man_man_light_skin_tone_dark_skin_tone": "👨🏻‍❤️‍👨🏿", "couple_with_heart_man_man_tone2_tone1": "👨🏼‍❤️‍👨🏻", "couple_with_heart_man_man_medium_light_skin_tone_light_skin_tone": "👨🏼‍❤️‍👨🏻", "couple_with_heart_man_man_tone2": "👨🏼‍❤️‍👨🏼", "couple_with_heart_man_man_medium_light_skin_tone": "👨🏼‍❤️‍👨🏼", "couple_with_heart_man_man_tone2_tone3": "👨🏼‍❤️‍👨🏽", "couple_with_heart_man_man_medium_light_skin_tone_medium_skin_tone": "👨🏼‍❤️‍👨🏽", "couple_with_heart_man_man_tone2_tone4": "👨🏼‍❤️‍👨🏾", "couple_with_heart_man_man_medium_light_skin_tone_medium_dark_skin_tone": "👨🏼‍❤️‍👨🏾", "couple_with_heart_man_man_tone2_tone5": "👨🏼‍❤️‍👨🏿", "couple_with_heart_man_man_medium_light_skin_tone_dark_skin_tone": "👨🏼‍❤️‍👨🏿", "couple_with_heart_man_man_tone3_tone1": "👨🏽‍❤️‍👨🏻", "couple_with_heart_man_man_medium_skin_tone_light_skin_tone": "👨🏽‍❤️‍👨🏻", "couple_with_heart_man_man_tone3_tone2": "👨🏽‍❤️‍👨🏼", "couple_with_heart_man_man_medium_skin_tone_medium_light_skin_tone": "👨🏽‍❤️‍👨🏼", "couple_with_heart_man_man_tone3": "👨🏽‍❤️‍👨🏽", "couple_with_heart_man_man_medium_skin_tone": "👨🏽‍❤️‍👨🏽", "couple_with_heart_man_man_tone3_tone4": "👨🏽‍❤️‍👨🏾", "couple_with_heart_man_man_medium_skin_tone_medium_dark_skin_tone": "👨🏽‍❤️‍👨🏾", "couple_with_heart_man_man_tone3_tone5": "👨🏽‍❤️‍👨🏿", "couple_with_heart_man_man_medium_skin_tone_dark_skin_tone": "👨🏽‍❤️‍👨🏿", "couple_with_heart_man_man_tone4_tone1": "👨🏾‍❤️‍👨🏻", "couple_with_heart_man_man_medium_dark_skin_tone_light_skin_tone": "👨🏾‍❤️‍👨🏻", "couple_with_heart_man_man_tone4_tone2": "👨🏾‍❤️‍👨🏼", "couple_with_heart_man_man_medium_dark_skin_tone_medium_light_skin_tone": "👨🏾‍❤️‍👨🏼", "couple_with_heart_man_man_tone4_tone3": "👨🏾‍❤️‍👨🏽", "couple_with_heart_man_man_medium_dark_skin_tone_medium_skin_tone": "👨🏾‍❤️‍👨🏽", "couple_with_heart_man_man_tone4": "👨🏾‍❤️‍👨🏾", "couple_with_heart_man_man_medium_dark_skin_tone": "👨🏾‍❤️‍👨🏾", "couple_with_heart_man_man_tone4_tone5": "👨🏾‍❤️‍👨🏿", "couple_with_heart_man_man_medium_dark_skin_tone_dark_skin_tone": "👨🏾‍❤️‍👨🏿", "couple_with_heart_man_man_tone5_tone1": "👨🏿‍❤️‍👨🏻", "couple_with_heart_man_man_dark_skin_tone_light_skin_tone": "👨🏿‍❤️‍👨🏻", "couple_with_heart_man_man_tone5_tone2": "👨🏿‍❤️‍👨🏼", "couple_with_heart_man_man_dark_skin_tone_medium_light_skin_tone": "👨🏿‍❤️‍👨🏼", "couple_with_heart_man_man_tone5_tone3": "👨🏿‍❤️‍👨🏽", "couple_with_heart_man_man_dark_skin_tone_medium_skin_tone": "👨🏿‍❤️‍👨🏽", "couple_with_heart_man_man_tone5_tone4": "👨🏿‍❤️‍👨🏾", "couple_with_heart_man_man_dark_skin_tone_medium_dark_skin_tone": "👨🏿‍❤️‍👨🏾", "couple_with_heart_man_man_tone5": "👨🏿‍❤️‍👨🏿", "couple_with_heart_man_man_dark_skin_tone": "👨🏿‍❤️‍👨🏿", "kiss_tone1": "💏🏻", "kiss_light_skin_tone": "💏🏻", "kiss_person_person_tone1_tone2": "🧑🏻‍❤️‍💋‍🧑🏼", "kiss_person_person_light_skin_tone_medium_light_skin_tone": "🧑🏻‍❤️‍💋‍🧑🏼", "kiss_person_person_tone1_tone3": "🧑🏻‍❤️‍💋‍🧑🏽", "kiss_person_person_light_skin_tone_medium_skin_tone": "🧑🏻‍❤️‍💋‍🧑🏽", "kiss_person_person_tone1_tone4": "🧑🏻‍❤️‍💋‍🧑🏾", "kiss_person_person_light_skin_tone_medium_dark_skin_tone": "🧑🏻‍❤️‍💋‍🧑🏾", "kiss_person_person_tone1_tone5": "🧑🏻‍❤️‍💋‍🧑🏿", "kiss_person_person_light_skin_tone_dark_skin_tone": "🧑🏻‍❤️‍💋‍🧑🏿", "kiss_person_person_tone2_tone1": "🧑🏼‍❤️‍💋‍🧑🏻", "kiss_person_person_medium_light_skin_tone_light_skin_tone": "🧑🏼‍❤️‍💋‍🧑🏻", "kiss_tone2": "💏🏼", "kiss_medium_light_skin_tone": "💏🏼", "kiss_person_person_tone2_tone3": "🧑🏼‍❤️‍💋‍🧑🏽", "kiss_person_person_medium_light_skin_tone_medium_skin_tone": "🧑🏼‍❤️‍💋‍🧑🏽", "kiss_person_person_tone2_tone4": "🧑🏼‍❤️‍💋‍🧑🏾", "kiss_person_person_medium_light_skin_tone_medium_dark_skin_tone": "🧑🏼‍❤️‍💋‍🧑🏾", "kiss_person_person_tone2_tone5": "🧑🏼‍❤️‍💋‍🧑🏿", "kiss_person_person_medium_light_skin_tone_dark_skin_tone": "🧑🏼‍❤️‍💋‍🧑🏿", "kiss_person_person_tone3_tone1": "🧑🏽‍❤️‍💋‍🧑🏻", "kiss_person_person_medium_skin_tone_light_skin_tone": "🧑🏽‍❤️‍💋‍🧑🏻", "kiss_person_person_tone3_tone2": "🧑🏽‍❤️‍💋‍🧑🏼", "kiss_person_person_medium_skin_tone_medium_light_skin_tone": "🧑🏽‍❤️‍💋‍🧑🏼", "kiss_tone3": "💏🏽", "kiss_medium_skin_tone": "💏🏽", "kiss_person_person_tone3_tone4": "🧑🏽‍❤️‍💋‍🧑🏾", "kiss_person_person_medium_skin_tone_medium_dark_skin_tone": "🧑🏽‍❤️‍💋‍🧑🏾", "kiss_person_person_tone3_tone5": "🧑🏽‍❤️‍💋‍🧑🏿", "kiss_person_person_medium_skin_tone_dark_skin_tone": "🧑🏽‍❤️‍💋‍🧑🏿", "kiss_person_person_tone4_tone1": "🧑🏾‍❤️‍💋‍🧑🏻", "kiss_person_person_medium_dark_skin_tone_light_skin_tone": "🧑🏾‍❤️‍💋‍🧑🏻", "kiss_person_person_tone4_tone2": "🧑🏾‍❤️‍💋‍🧑🏼", "kiss_person_person_medium_dark_skin_tone_medium_light_skin_tone": "🧑🏾‍❤️‍💋‍🧑🏼", "kiss_person_person_tone4_tone3": "🧑🏾‍❤️‍💋‍🧑🏽", "kiss_person_person_medium_dark_skin_tone_medium_skin_tone": "🧑🏾‍❤️‍💋‍🧑🏽", "kiss_tone4": "💏🏾", "kiss_medium_dark_skin_tone": "💏🏾", "kiss_person_person_tone4_tone5": "🧑🏾‍❤️‍💋‍🧑🏿", "kiss_person_person_medium_dark_skin_tone_dark_skin_tone": "🧑🏾‍❤️‍💋‍🧑🏿", "kiss_person_person_tone5_tone1": "🧑🏿‍❤️‍💋‍🧑🏻", "kiss_person_person_dark_skin_tone_light_skin_tone": "🧑🏿‍❤️‍💋‍🧑🏻", "kiss_person_person_tone5_tone2": "🧑🏿‍❤️‍💋‍🧑🏼", "kiss_person_person_dark_skin_tone_medium_light_skin_tone": "🧑🏿‍❤️‍💋‍🧑🏼", "kiss_person_person_tone5_tone3": "🧑🏿‍❤️‍💋‍🧑🏽", "kiss_person_person_dark_skin_tone_medium_skin_tone": "🧑🏿‍❤️‍💋‍🧑🏽", "kiss_person_person_tone5_tone4": "🧑🏿‍❤️‍💋‍🧑🏾", "kiss_person_person_dark_skin_tone_medium_dark_skin_tone": "🧑🏿‍❤️‍💋‍🧑🏾", "kiss_tone5": "💏🏿", "kiss_dark_skin_tone": "💏🏿", "kiss_woman_man_tone1": "👩🏻‍❤️‍💋‍👨🏻", "kiss_woman_man_light_skin_tone": "👩🏻‍❤️‍💋‍👨🏻", "kiss_woman_man_tone1_tone2": "👩🏻‍❤️‍💋‍👨🏼", "kiss_woman_man_light_skin_tone_medium_light_skin_tone": "👩🏻‍❤️‍💋‍👨🏼", "kiss_woman_man_tone1_tone3": "👩🏻‍❤️‍💋‍👨🏽", "kiss_woman_man_light_skin_tone_medium_skin_tone": "👩🏻‍❤️‍💋‍👨🏽", "kiss_woman_man_tone1_tone4": "👩🏻‍❤️‍💋‍👨🏾", "kiss_woman_man_light_skin_tone_medium_dark_skin_tone": "👩🏻‍❤️‍💋‍👨🏾", "kiss_woman_man_tone1_tone5": "👩🏻‍❤️‍💋‍👨🏿", "kiss_woman_man_light_skin_tone_dark_skin_tone": "👩🏻‍❤️‍💋‍👨🏿", "kiss_woman_man_tone2_tone1": "👩🏼‍❤️‍💋‍👨🏻", "kiss_woman_man_medium_light_skin_tone_light_skin_tone": "👩🏼‍❤️‍💋‍👨🏻", "kiss_woman_man_tone2": "👩🏼‍❤️‍💋‍👨🏼", "kiss_woman_man_medium_light_skin_tone": "👩🏼‍❤️‍💋‍👨🏼", "kiss_woman_man_tone2_tone3": "👩🏼‍❤️‍💋‍👨🏽", "kiss_woman_man_medium_light_skin_tone_medium_skin_tone": "👩🏼‍❤️‍💋‍👨🏽", "kiss_woman_man_tone2_tone4": "👩🏼‍❤️‍💋‍👨🏾", "kiss_woman_man_medium_light_skin_tone_medium_dark_skin_tone": "👩🏼‍❤️‍💋‍👨🏾", "kiss_woman_man_tone2_tone5": "👩🏼‍❤️‍💋‍👨🏿", "kiss_woman_man_medium_light_skin_tone_dark_skin_tone": "👩🏼‍❤️‍💋‍👨🏿", "kiss_woman_man_tone3_tone1": "👩🏽‍❤️‍💋‍👨🏻", "kiss_woman_man_medium_skin_tone_light_skin_tone": "👩🏽‍❤️‍💋‍👨🏻", "kiss_woman_man_tone3_tone2": "👩🏽‍❤️‍💋‍👨🏼", "kiss_woman_man_medium_skin_tone_medium_light_skin_tone": "👩🏽‍❤️‍💋‍👨🏼", "kiss_woman_man_tone3": "👩🏽‍❤️‍💋‍👨🏽", "kiss_woman_man_medium_skin_tone": "👩🏽‍❤️‍💋‍👨🏽", "kiss_woman_man_tone3_tone4": "👩🏽‍❤️‍💋‍👨🏾", "kiss_woman_man_medium_skin_tone_medium_dark_skin_tone": "👩🏽‍❤️‍💋‍👨🏾", "kiss_woman_man_tone3_tone5": "👩🏽‍❤️‍💋‍👨🏿", "kiss_woman_man_medium_skin_tone_dark_skin_tone": "👩🏽‍❤️‍💋‍👨🏿", "kiss_woman_man_tone4_tone1": "👩🏾‍❤️‍💋‍👨🏻", "kiss_woman_man_medium_dark_skin_tone_light_skin_tone": "👩🏾‍❤️‍💋‍👨🏻", "kiss_woman_man_tone4_tone2": "👩🏾‍❤️‍💋‍👨🏼", "kiss_woman_man_medium_dark_skin_tone_medium_light_skin_tone": "👩🏾‍❤️‍💋‍👨🏼", "kiss_woman_man_tone4_tone3": "👩🏾‍❤️‍💋‍👨🏽", "kiss_woman_man_medium_dark_skin_tone_medium_skin_tone": "👩🏾‍❤️‍💋‍👨🏽", "kiss_woman_man_tone4": "👩🏾‍❤️‍💋‍👨🏾", "kiss_woman_man_medium_dark_skin_tone": "👩🏾‍❤️‍💋‍👨🏾", "kiss_woman_man_tone4_tone5": "👩🏾‍❤️‍💋‍👨🏿", "kiss_woman_man_medium_dark_skin_tone_dark_skin_tone": "👩🏾‍❤️‍💋‍👨🏿", "kiss_woman_man_tone5_tone1": "👩🏿‍❤️‍💋‍👨🏻", "kiss_woman_man_dark_skin_tone_light_skin_tone": "👩🏿‍❤️‍💋‍👨🏻", "kiss_woman_man_tone5_tone2": "👩🏿‍❤️‍💋‍👨🏼", "kiss_woman_man_dark_skin_tone_medium_light_skin_tone": "👩🏿‍❤️‍💋‍👨🏼", "kiss_woman_man_tone5_tone3": "👩🏿‍❤️‍💋‍👨🏽", "kiss_woman_man_dark_skin_tone_medium_skin_tone": "👩🏿‍❤️‍💋‍👨🏽", "kiss_woman_man_tone5_tone4": "👩🏿‍❤️‍💋‍👨🏾", "kiss_woman_man_dark_skin_tone_medium_dark_skin_tone": "👩🏿‍❤️‍💋‍👨🏾", "kiss_woman_man_tone5": "👩🏿‍❤️‍💋‍👨🏿", "kiss_woman_man_dark_skin_tone": "👩🏿‍❤️‍💋‍👨🏿", "kiss_woman_woman_tone1": "👩🏻‍❤️‍💋‍👩🏻", "kiss_woman_woman_light_skin_tone": "👩🏻‍❤️‍💋‍👩🏻", "kiss_woman_woman_tone1_tone2": "👩🏻‍❤️‍💋‍👩🏼", "kiss_woman_woman_light_skin_tone_medium_light_skin_tone": "👩🏻‍❤️‍💋‍👩🏼", "kiss_woman_woman_tone1_tone3": "👩🏻‍❤️‍💋‍👩🏽", "kiss_woman_woman_light_skin_tone_medium_skin_tone": "👩🏻‍❤️‍💋‍👩🏽", "kiss_woman_woman_tone1_tone4": "👩🏻‍❤️‍💋‍👩🏾", "kiss_woman_woman_light_skin_tone_medium_dark_skin_tone": "👩🏻‍❤️‍💋‍👩🏾", "kiss_woman_woman_tone1_tone5": "👩🏻‍❤️‍💋‍👩🏿", "kiss_woman_woman_light_skin_tone_dark_skin_tone": "👩🏻‍❤️‍💋‍👩🏿", "kiss_woman_woman_tone2_tone1": "👩🏼‍❤️‍💋‍👩🏻", "kiss_woman_woman_medium_light_skin_tone_light_skin_tone": "👩🏼‍❤️‍💋‍👩🏻", "kiss_woman_woman_tone2": "👩🏼‍❤️‍💋‍👩🏼", "kiss_woman_woman_medium_light_skin_tone": "👩🏼‍❤️‍💋‍👩🏼", "kiss_woman_woman_tone2_tone3": "👩🏼‍❤️‍💋‍👩🏽", "kiss_woman_woman_medium_light_skin_tone_medium_skin_tone": "👩🏼‍❤️‍💋‍👩🏽", "kiss_woman_woman_tone2_tone4": "👩🏼‍❤️‍💋‍👩🏾", "kiss_woman_woman_medium_light_skin_tone_medium_dark_skin_tone": "👩🏼‍❤️‍💋‍👩🏾", "kiss_woman_woman_tone2_tone5": "👩🏼‍❤️‍💋‍👩🏿", "kiss_woman_woman_medium_light_skin_tone_dark_skin_tone": "👩🏼‍❤️‍💋‍👩🏿", "kiss_woman_woman_tone3_tone1": "👩🏽‍❤️‍💋‍👩🏻", "kiss_woman_woman_medium_skin_tone_light_skin_tone": "👩🏽‍❤️‍💋‍👩🏻", "kiss_woman_woman_tone3_tone2": "👩🏽‍❤️‍💋‍👩🏼", "kiss_woman_woman_medium_skin_tone_medium_light_skin_tone": "👩🏽‍❤️‍💋‍👩🏼", "kiss_woman_woman_tone3": "👩🏽‍❤️‍💋‍👩🏽", "kiss_woman_woman_medium_skin_tone": "👩🏽‍❤️‍💋‍👩🏽", "kiss_woman_woman_tone3_tone4": "👩🏽‍❤️‍💋‍👩🏾", "kiss_woman_woman_medium_skin_tone_medium_dark_skin_tone": "👩🏽‍❤️‍💋‍👩🏾", "kiss_woman_woman_tone3_tone5": "👩🏽‍❤️‍💋‍👩🏿", "kiss_woman_woman_medium_skin_tone_dark_skin_tone": "👩🏽‍❤️‍💋‍👩🏿", "kiss_woman_woman_tone4_tone1": "👩🏾‍❤️‍💋‍👩🏻", "kiss_woman_woman_medium_dark_skin_tone_light_skin_tone": "👩🏾‍❤️‍💋‍👩🏻", "kiss_woman_woman_tone4_tone2": "👩🏾‍❤️‍💋‍👩🏼", "kiss_woman_woman_medium_dark_skin_tone_medium_light_skin_tone": "👩🏾‍❤️‍💋‍👩🏼", "kiss_woman_woman_tone4_tone3": "👩🏾‍❤️‍💋‍👩🏽", "kiss_woman_woman_medium_dark_skin_tone_medium_skin_tone": "👩🏾‍❤️‍💋‍👩🏽", "kiss_woman_woman_tone4": "👩🏾‍❤️‍💋‍👩🏾", "kiss_woman_woman_medium_dark_skin_tone": "👩🏾‍❤️‍💋‍👩🏾", "kiss_woman_woman_tone4_tone5": "👩🏾‍❤️‍💋‍👩🏿", "kiss_woman_woman_medium_dark_skin_tone_dark_skin_tone": "👩🏾‍❤️‍💋‍👩🏿", "kiss_woman_woman_tone5_tone1": "👩🏿‍❤️‍💋‍👩🏻", "kiss_woman_woman_dark_skin_tone_light_skin_tone": "👩🏿‍❤️‍💋‍👩🏻", "kiss_woman_woman_tone5_tone2": "👩🏿‍❤️‍💋‍👩🏼", "kiss_woman_woman_dark_skin_tone_medium_light_skin_tone": "👩🏿‍❤️‍💋‍👩🏼", "kiss_woman_woman_tone5_tone3": "👩🏿‍❤️‍💋‍👩🏽", "kiss_woman_woman_dark_skin_tone_medium_skin_tone": "👩🏿‍❤️‍💋‍👩🏽", "kiss_woman_woman_tone5_tone4": "👩🏿‍❤️‍💋‍👩🏾", "kiss_woman_woman_dark_skin_tone_medium_dark_skin_tone": "👩🏿‍❤️‍💋‍👩🏾", "kiss_woman_woman_tone5": "👩🏿‍❤️‍💋‍👩🏿", "kiss_woman_woman_dark_skin_tone": "👩🏿‍❤️‍💋‍👩🏿", "kiss_man_man_tone1": "👨🏻‍❤️‍💋‍👨🏻", "kiss_man_man_light_skin_tone": "👨🏻‍❤️‍💋‍👨🏻", "kiss_man_man_tone1_tone2": "👨🏻‍❤️‍💋‍👨🏼", "kiss_man_man_light_skin_tone_medium_light_skin_tone": "👨🏻‍❤️‍💋‍👨🏼", "kiss_man_man_tone1_tone3": "👨🏻‍❤️‍💋‍👨🏽", "kiss_man_man_light_skin_tone_medium_skin_tone": "👨🏻‍❤️‍💋‍👨🏽", "kiss_man_man_tone1_tone4": "👨🏻‍❤️‍💋‍👨🏾", "kiss_man_man_light_skin_tone_medium_dark_skin_tone": "👨🏻‍❤️‍💋‍👨🏾", "kiss_man_man_tone1_tone5": "👨🏻‍❤️‍💋‍👨🏿", "kiss_man_man_light_skin_tone_dark_skin_tone": "👨🏻‍❤️‍💋‍👨🏿", "kiss_man_man_tone2_tone1": "👨🏼‍❤️‍💋‍👨🏻", "kiss_man_man_medium_light_skin_tone_light_skin_tone": "👨🏼‍❤️‍💋‍👨🏻", "kiss_man_man_tone2": "👨🏼‍❤️‍💋‍👨🏼", "kiss_man_man_medium_light_skin_tone": "👨🏼‍❤️‍💋‍👨🏼", "kiss_man_man_tone2_tone3": "👨🏼‍❤️‍💋‍👨🏽", "kiss_man_man_medium_light_skin_tone_medium_skin_tone": "👨🏼‍❤️‍💋‍👨🏽", "kiss_man_man_tone2_tone4": "👨🏼‍❤️‍💋‍👨🏾", "kiss_man_man_medium_light_skin_tone_medium_dark_skin_tone": "👨🏼‍❤️‍💋‍👨🏾", "kiss_man_man_tone2_tone5": "👨🏼‍❤️‍💋‍👨🏿", "kiss_man_man_medium_light_skin_tone_dark_skin_tone": "👨🏼‍❤️‍💋‍👨🏿", "kiss_man_man_tone3_tone1": "👨🏽‍❤️‍💋‍👨🏻", "kiss_man_man_medium_skin_tone_light_skin_tone": "👨🏽‍❤️‍💋‍👨🏻", "kiss_man_man_tone3_tone2": "👨🏽‍❤️‍💋‍👨🏼", "kiss_man_man_medium_skin_tone_medium_light_skin_tone": "👨🏽‍❤️‍💋‍👨🏼", "kiss_man_man_tone3": "👨🏽‍❤️‍💋‍👨🏽", "kiss_man_man_medium_skin_tone": "👨🏽‍❤️‍💋‍👨🏽", "kiss_man_man_tone3_tone4": "👨🏽‍❤️‍💋‍👨🏾", "kiss_man_man_medium_skin_tone_medium_dark_skin_tone": "👨🏽‍❤️‍💋‍👨🏾", "kiss_man_man_tone3_tone5": "👨🏽‍❤️‍💋‍👨🏿", "kiss_man_man_medium_skin_tone_dark_skin_tone": "👨🏽‍❤️‍💋‍👨🏿", "kiss_man_man_tone4_tone1": "👨🏾‍❤️‍💋‍👨🏻", "kiss_man_man_medium_dark_skin_tone_light_skin_tone": "👨🏾‍❤️‍💋‍👨🏻", "kiss_man_man_tone4_tone2": "👨🏾‍❤️‍💋‍👨🏼", "kiss_man_man_medium_dark_skin_tone_medium_light_skin_tone": "👨🏾‍❤️‍💋‍👨🏼", "kiss_man_man_tone4_tone3": "👨🏾‍❤️‍💋‍👨🏽", "kiss_man_man_medium_dark_skin_tone_medium_skin_tone": "👨🏾‍❤️‍💋‍👨🏽", "kiss_man_man_tone4": "👨🏾‍❤️‍💋‍👨🏾", "kiss_man_man_medium_dark_skin_tone": "👨🏾‍❤️‍💋‍👨🏾", "kiss_man_man_tone4_tone5": "👨🏾‍❤️‍💋‍👨🏿", "kiss_man_man_medium_dark_skin_tone_dark_skin_tone": "👨🏾‍❤️‍💋‍👨🏿", "kiss_man_man_tone5_tone1": "👨🏿‍❤️‍💋‍👨🏻", "kiss_man_man_dark_skin_tone_light_skin_tone": "👨🏿‍❤️‍💋‍👨🏻", "kiss_man_man_tone5_tone2": "👨🏿‍❤️‍💋‍👨🏼", "kiss_man_man_dark_skin_tone_medium_light_skin_tone": "👨🏿‍❤️‍💋‍👨🏼", "kiss_man_man_tone5_tone3": "👨🏿‍❤️‍💋‍👨🏽", "kiss_man_man_dark_skin_tone_medium_skin_tone": "👨🏿‍❤️‍💋‍👨🏽", "kiss_man_man_tone5_tone4": "👨🏿‍❤️‍💋‍👨🏾", "kiss_man_man_dark_skin_tone_medium_dark_skin_tone": "👨🏿‍❤️‍💋‍👨🏾", "kiss_man_man_tone5": "👨🏿‍❤️‍💋‍👨🏿", "kiss_man_man_dark_skin_tone": "👨🏿‍❤️‍💋‍👨🏿", "snowboarder_tone1": "🏂🏻", "snowboarder_light_skin_tone": "🏂🏻", "snowboarder_tone2": "🏂🏼", "snowboarder_medium_light_skin_tone": "🏂🏼", "snowboarder_tone3": "🏂🏽", "snowboarder_medium_skin_tone": "🏂🏽", "snowboarder_tone4": "🏂🏾", "snowboarder_medium_dark_skin_tone": "🏂🏾", "snowboarder_tone5": "🏂🏿", "snowboarder_dark_skin_tone": "🏂🏿", "person_lifting_weights_tone1": "🏋🏻", "lifter_tone1": "🏋🏻", "weight_lifter_tone1": "🏋🏻", "person_lifting_weights_tone2": "🏋🏼", "lifter_tone2": "🏋🏼", "weight_lifter_tone2": "🏋🏼", "person_lifting_weights_tone3": "🏋🏽", "lifter_tone3": "🏋🏽", "weight_lifter_tone3": "🏋🏽", "person_lifting_weights_tone4": "🏋🏾", "lifter_tone4": "🏋🏾", "weight_lifter_tone4": "🏋🏾", "person_lifting_weights_tone5": "🏋🏿", "lifter_tone5": "🏋🏿", "weight_lifter_tone5": "🏋🏿", "woman_lifting_weights_tone1": "🏋🏻‍♀️", "woman_lifting_weights_light_skin_tone": "🏋🏻‍♀️", "woman_lifting_weights_tone2": "🏋🏼‍♀️", "woman_lifting_weights_medium_light_skin_tone": "🏋🏼‍♀️", "woman_lifting_weights_tone3": "🏋🏽‍♀️", "woman_lifting_weights_medium_skin_tone": "🏋🏽‍♀️", "woman_lifting_weights_tone4": "🏋🏾‍♀️", "woman_lifting_weights_medium_dark_skin_tone": "🏋🏾‍♀️", "woman_lifting_weights_tone5": "🏋🏿‍♀️", "woman_lifting_weights_dark_skin_tone": "🏋🏿‍♀️", "man_lifting_weights_tone1": "🏋🏻‍♂️", "man_lifting_weights_light_skin_tone": "🏋🏻‍♂️", "man_lifting_weights_tone2": "🏋🏼‍♂️", "man_lifting_weights_medium_light_skin_tone": "🏋🏼‍♂️", "man_lifting_weights_tone3": "🏋🏽‍♂️", "man_lifting_weights_medium_skin_tone": "🏋🏽‍♂️", "man_lifting_weights_tone4": "🏋🏾‍♂️", "man_lifting_weights_medium_dark_skin_tone": "🏋🏾‍♂️", "man_lifting_weights_tone5": "🏋🏿‍♂️", "man_lifting_weights_dark_skin_tone": "🏋🏿‍♂️", "person_doing_cartwheel_tone1": "🤸🏻", "cartwheel_tone1": "🤸🏻", "person_doing_cartwheel_tone2": "🤸🏼", "cartwheel_tone2": "🤸🏼", "person_doing_cartwheel_tone3": "🤸🏽", "cartwheel_tone3": "🤸🏽", "person_doing_cartwheel_tone4": "🤸🏾", "cartwheel_tone4": "🤸🏾", "person_doing_cartwheel_tone5": "🤸🏿", "cartwheel_tone5": "🤸🏿", "woman_cartwheeling_tone1": "🤸🏻‍♀️", "woman_cartwheeling_light_skin_tone": "🤸🏻‍♀️", "woman_cartwheeling_tone2": "🤸🏼‍♀️", "woman_cartwheeling_medium_light_skin_tone": "🤸🏼‍♀️", "woman_cartwheeling_tone3": "🤸🏽‍♀️", "woman_cartwheeling_medium_skin_tone": "🤸🏽‍♀️", "woman_cartwheeling_tone4": "🤸🏾‍♀️", "woman_cartwheeling_medium_dark_skin_tone": "🤸🏾‍♀️", "woman_cartwheeling_tone5": "🤸🏿‍♀️", "woman_cartwheeling_dark_skin_tone": "🤸🏿‍♀️", "man_cartwheeling_tone1": "🤸🏻‍♂️", "man_cartwheeling_light_skin_tone": "🤸🏻‍♂️", "man_cartwheeling_tone2": "🤸🏼‍♂️", "man_cartwheeling_medium_light_skin_tone": "🤸🏼‍♂️", "man_cartwheeling_tone3": "🤸🏽‍♂️", "man_cartwheeling_medium_skin_tone": "🤸🏽‍♂️", "man_cartwheeling_tone4": "🤸🏾‍♂️", "man_cartwheeling_medium_dark_skin_tone": "🤸🏾‍♂️", "man_cartwheeling_tone5": "🤸🏿‍♂️", "man_cartwheeling_dark_skin_tone": "🤸🏿‍♂️", "person_bouncing_ball_tone1": "⛹🏻", "basketball_player_tone1": "⛹🏻", "person_with_ball_tone1": "⛹🏻", "person_bouncing_ball_tone2": "⛹🏼", "basketball_player_tone2": "⛹🏼", "person_with_ball_tone2": "⛹🏼", "person_bouncing_ball_tone3": "⛹🏽", "basketball_player_tone3": "⛹🏽", "person_with_ball_tone3": "⛹🏽", "person_bouncing_ball_tone4": "⛹🏾", "basketball_player_tone4": "⛹🏾", "person_with_ball_tone4": "⛹🏾", "person_bouncing_ball_tone5": "⛹🏿", "basketball_player_tone5": "⛹🏿", "person_with_ball_tone5": "⛹🏿", "woman_bouncing_ball_tone1": "⛹🏻‍♀️", "woman_bouncing_ball_light_skin_tone": "⛹🏻‍♀️", "woman_bouncing_ball_tone2": "⛹🏼‍♀️", "woman_bouncing_ball_medium_light_skin_tone": "⛹🏼‍♀️", "woman_bouncing_ball_tone3": "⛹🏽‍♀️", "woman_bouncing_ball_medium_skin_tone": "⛹🏽‍♀️", "woman_bouncing_ball_tone4": "⛹🏾‍♀️", "woman_bouncing_ball_medium_dark_skin_tone": "⛹🏾‍♀️", "woman_bouncing_ball_tone5": "⛹🏿‍♀️", "woman_bouncing_ball_dark_skin_tone": "⛹🏿‍♀️", "man_bouncing_ball_tone1": "⛹🏻‍♂️", "man_bouncing_ball_light_skin_tone": "⛹🏻‍♂️", "man_bouncing_ball_tone2": "⛹🏼‍♂️", "man_bouncing_ball_medium_light_skin_tone": "⛹🏼‍♂️", "man_bouncing_ball_tone3": "⛹🏽‍♂️", "man_bouncing_ball_medium_skin_tone": "⛹🏽‍♂️", "man_bouncing_ball_tone4": "⛹🏾‍♂️", "man_bouncing_ball_medium_dark_skin_tone": "⛹🏾‍♂️", "man_bouncing_ball_tone5": "⛹🏿‍♂️", "man_bouncing_ball_dark_skin_tone": "⛹🏿‍♂️", "person_playing_handball_tone1": "🤾🏻", "handball_tone1": "🤾🏻", "person_playing_handball_tone2": "🤾🏼", "handball_tone2": "🤾🏼", "person_playing_handball_tone3": "🤾🏽", "handball_tone3": "🤾🏽", "person_playing_handball_tone4": "🤾🏾", "handball_tone4": "🤾🏾", "person_playing_handball_tone5": "🤾🏿", "handball_tone5": "🤾🏿", "woman_playing_handball_tone1": "🤾🏻‍♀️", "woman_playing_handball_light_skin_tone": "🤾🏻‍♀️", "woman_playing_handball_tone2": "🤾🏼‍♀️", "woman_playing_handball_medium_light_skin_tone": "🤾🏼‍♀️", "woman_playing_handball_tone3": "🤾🏽‍♀️", "woman_playing_handball_medium_skin_tone": "🤾🏽‍♀️", "woman_playing_handball_tone4": "🤾🏾‍♀️", "woman_playing_handball_medium_dark_skin_tone": "🤾🏾‍♀️", "woman_playing_handball_tone5": "🤾🏿‍♀️", "woman_playing_handball_dark_skin_tone": "🤾🏿‍♀️", "man_playing_handball_tone1": "🤾🏻‍♂️", "man_playing_handball_light_skin_tone": "🤾🏻‍♂️", "man_playing_handball_tone2": "🤾🏼‍♂️", "man_playing_handball_medium_light_skin_tone": "🤾🏼‍♂️", "man_playing_handball_tone3": "🤾🏽‍♂️", "man_playing_handball_medium_skin_tone": "🤾🏽‍♂️", "man_playing_handball_tone4": "🤾🏾‍♂️", "man_playing_handball_medium_dark_skin_tone": "🤾🏾‍♂️", "man_playing_handball_tone5": "🤾🏿‍♂️", "man_playing_handball_dark_skin_tone": "🤾🏿‍♂️", "person_golfing_tone1": "🏌🏻", "person_golfing_light_skin_tone": "🏌🏻", "person_golfing_tone2": "🏌🏼", "person_golfing_medium_light_skin_tone": "🏌🏼", "person_golfing_tone3": "🏌🏽", "person_golfing_medium_skin_tone": "🏌🏽", "person_golfing_tone4": "🏌🏾", "person_golfing_medium_dark_skin_tone": "🏌🏾", "person_golfing_tone5": "🏌🏿", "person_golfing_dark_skin_tone": "🏌🏿", "woman_golfing_tone1": "🏌🏻‍♀️", "woman_golfing_light_skin_tone": "🏌🏻‍♀️", "woman_golfing_tone2": "🏌🏼‍♀️", "woman_golfing_medium_light_skin_tone": "🏌🏼‍♀️", "woman_golfing_tone3": "🏌🏽‍♀️", "woman_golfing_medium_skin_tone": "🏌🏽‍♀️", "woman_golfing_tone4": "🏌🏾‍♀️", "woman_golfing_medium_dark_skin_tone": "🏌🏾‍♀️", "woman_golfing_tone5": "🏌🏿‍♀️", "woman_golfing_dark_skin_tone": "🏌🏿‍♀️", "man_golfing_tone1": "🏌🏻‍♂️", "man_golfing_light_skin_tone": "🏌🏻‍♂️", "man_golfing_tone2": "🏌🏼‍♂️", "man_golfing_medium_light_skin_tone": "🏌🏼‍♂️", "man_golfing_tone3": "🏌🏽‍♂️", "man_golfing_medium_skin_tone": "🏌🏽‍♂️", "man_golfing_tone4": "🏌🏾‍♂️", "man_golfing_medium_dark_skin_tone": "🏌🏾‍♂️", "man_golfing_tone5": "🏌🏿‍♂️", "man_golfing_dark_skin_tone": "🏌🏿‍♂️", "horse_racing_tone1": "🏇🏻", "horse_racing_tone2": "🏇🏼", "horse_racing_tone3": "🏇🏽", "horse_racing_tone4": "🏇🏾", "horse_racing_tone5": "🏇🏿", "person_in_lotus_position_tone1": "🧘🏻", "person_in_lotus_position_light_skin_tone": "🧘🏻", "person_in_lotus_position_tone2": "🧘🏼", "person_in_lotus_position_medium_light_skin_tone": "🧘🏼", "person_in_lotus_position_tone3": "🧘🏽", "person_in_lotus_position_medium_skin_tone": "🧘🏽", "person_in_lotus_position_tone4": "🧘🏾", "person_in_lotus_position_medium_dark_skin_tone": "🧘🏾", "person_in_lotus_position_tone5": "🧘🏿", "person_in_lotus_position_dark_skin_tone": "🧘🏿", "woman_in_lotus_position_tone1": "🧘🏻‍♀️", "woman_in_lotus_position_light_skin_tone": "🧘🏻‍♀️", "woman_in_lotus_position_tone2": "🧘🏼‍♀️", "woman_in_lotus_position_medium_light_skin_tone": "🧘🏼‍♀️", "woman_in_lotus_position_tone3": "🧘🏽‍♀️", "woman_in_lotus_position_medium_skin_tone": "🧘🏽‍♀️", "woman_in_lotus_position_tone4": "🧘🏾‍♀️", "woman_in_lotus_position_medium_dark_skin_tone": "🧘🏾‍♀️", "woman_in_lotus_position_tone5": "🧘🏿‍♀️", "woman_in_lotus_position_dark_skin_tone": "🧘🏿‍♀️", "man_in_lotus_position_tone1": "🧘🏻‍♂️", "man_in_lotus_position_light_skin_tone": "🧘🏻‍♂️", "man_in_lotus_position_tone2": "🧘🏼‍♂️", "man_in_lotus_position_medium_light_skin_tone": "🧘🏼‍♂️", "man_in_lotus_position_tone3": "🧘🏽‍♂️", "man_in_lotus_position_medium_skin_tone": "🧘🏽‍♂️", "man_in_lotus_position_tone4": "🧘🏾‍♂️", "man_in_lotus_position_medium_dark_skin_tone": "🧘🏾‍♂️", "man_in_lotus_position_tone5": "🧘🏿‍♂️", "man_in_lotus_position_dark_skin_tone": "🧘🏿‍♂️", "person_surfing_tone1": "🏄🏻", "surfer_tone1": "🏄🏻", "person_surfing_tone2": "🏄🏼", "surfer_tone2": "🏄🏼", "person_surfing_tone3": "🏄🏽", "surfer_tone3": "🏄🏽", "person_surfing_tone4": "🏄🏾", "surfer_tone4": "🏄🏾", "person_surfing_tone5": "🏄🏿", "surfer_tone5": "🏄🏿", "woman_surfing_tone1": "🏄🏻‍♀️", "woman_surfing_light_skin_tone": "🏄🏻‍♀️", "woman_surfing_tone2": "🏄🏼‍♀️", "woman_surfing_medium_light_skin_tone": "🏄🏼‍♀️", "woman_surfing_tone3": "🏄🏽‍♀️", "woman_surfing_medium_skin_tone": "🏄🏽‍♀️", "woman_surfing_tone4": "🏄🏾‍♀️", "woman_surfing_medium_dark_skin_tone": "🏄🏾‍♀️", "woman_surfing_tone5": "🏄🏿‍♀️", "woman_surfing_dark_skin_tone": "🏄🏿‍♀️", "man_surfing_tone1": "🏄🏻‍♂️", "man_surfing_light_skin_tone": "🏄🏻‍♂️", "man_surfing_tone2": "🏄🏼‍♂️", "man_surfing_medium_light_skin_tone": "🏄🏼‍♂️", "man_surfing_tone3": "🏄🏽‍♂️", "man_surfing_medium_skin_tone": "🏄🏽‍♂️", "man_surfing_tone4": "🏄🏾‍♂️", "man_surfing_medium_dark_skin_tone": "🏄🏾‍♂️", "man_surfing_tone5": "🏄🏿‍♂️", "man_surfing_dark_skin_tone": "🏄🏿‍♂️", "person_swimming_tone1": "🏊🏻", "swimmer_tone1": "🏊🏻", "person_swimming_tone2": "🏊🏼", "swimmer_tone2": "🏊🏼", "person_swimming_tone3": "🏊🏽", "swimmer_tone3": "🏊🏽", "person_swimming_tone4": "🏊🏾", "swimmer_tone4": "🏊🏾", "person_swimming_tone5": "🏊🏿", "swimmer_tone5": "🏊🏿", "woman_swimming_tone1": "🏊🏻‍♀️", "woman_swimming_light_skin_tone": "🏊🏻‍♀️", "woman_swimming_tone2": "🏊🏼‍♀️", "woman_swimming_medium_light_skin_tone": "🏊🏼‍♀️", "woman_swimming_tone3": "🏊🏽‍♀️", "woman_swimming_medium_skin_tone": "🏊🏽‍♀️", "woman_swimming_tone4": "🏊🏾‍♀️", "woman_swimming_medium_dark_skin_tone": "🏊🏾‍♀️", "woman_swimming_tone5": "🏊🏿‍♀️", "woman_swimming_dark_skin_tone": "🏊🏿‍♀️", "man_swimming_tone1": "🏊🏻‍♂️", "man_swimming_light_skin_tone": "🏊🏻‍♂️", "man_swimming_tone2": "🏊🏼‍♂️", "man_swimming_medium_light_skin_tone": "🏊🏼‍♂️", "man_swimming_tone3": "🏊🏽‍♂️", "man_swimming_medium_skin_tone": "🏊🏽‍♂️", "man_swimming_tone4": "🏊🏾‍♂️", "man_swimming_medium_dark_skin_tone": "🏊🏾‍♂️", "man_swimming_tone5": "🏊🏿‍♂️", "man_swimming_dark_skin_tone": "🏊🏿‍♂️", "person_playing_water_polo_tone1": "🤽🏻", "water_polo_tone1": "🤽🏻", "person_playing_water_polo_tone2": "🤽🏼", "water_polo_tone2": "🤽🏼", "person_playing_water_polo_tone3": "🤽🏽", "water_polo_tone3": "🤽🏽", "person_playing_water_polo_tone4": "🤽🏾", "water_polo_tone4": "🤽🏾", "person_playing_water_polo_tone5": "🤽🏿", "water_polo_tone5": "🤽🏿", "woman_playing_water_polo_tone1": "🤽🏻‍♀️", "woman_playing_water_polo_light_skin_tone": "🤽🏻‍♀️", "woman_playing_water_polo_tone2": "🤽🏼‍♀️", "woman_playing_water_polo_medium_light_skin_tone": "🤽🏼‍♀️", "woman_playing_water_polo_tone3": "🤽🏽‍♀️", "woman_playing_water_polo_medium_skin_tone": "🤽🏽‍♀️", "woman_playing_water_polo_tone4": "🤽🏾‍♀️", "woman_playing_water_polo_medium_dark_skin_tone": "🤽🏾‍♀️", "woman_playing_water_polo_tone5": "🤽🏿‍♀️", "woman_playing_water_polo_dark_skin_tone": "🤽🏿‍♀️", "man_playing_water_polo_tone1": "🤽🏻‍♂️", "man_playing_water_polo_light_skin_tone": "🤽🏻‍♂️", "man_playing_water_polo_tone2": "🤽🏼‍♂️", "man_playing_water_polo_medium_light_skin_tone": "🤽🏼‍♂️", "man_playing_water_polo_tone3": "🤽🏽‍♂️", "man_playing_water_polo_medium_skin_tone": "🤽🏽‍♂️", "man_playing_water_polo_tone4": "🤽🏾‍♂️", "man_playing_water_polo_medium_dark_skin_tone": "🤽🏾‍♂️", "man_playing_water_polo_tone5": "🤽🏿‍♂️", "man_playing_water_polo_dark_skin_tone": "🤽🏿‍♂️", "person_rowing_boat_tone1": "🚣🏻", "rowboat_tone1": "🚣🏻", "person_rowing_boat_tone2": "🚣🏼", "rowboat_tone2": "🚣🏼", "person_rowing_boat_tone3": "🚣🏽", "rowboat_tone3": "🚣🏽", "person_rowing_boat_tone4": "🚣🏾", "rowboat_tone4": "🚣🏾", "person_rowing_boat_tone5": "🚣🏿", "rowboat_tone5": "🚣🏿", "woman_rowing_boat_tone1": "🚣🏻‍♀️", "woman_rowing_boat_light_skin_tone": "🚣🏻‍♀️", "woman_rowing_boat_tone2": "🚣🏼‍♀️", "woman_rowing_boat_medium_light_skin_tone": "🚣🏼‍♀️", "woman_rowing_boat_tone3": "🚣🏽‍♀️", "woman_rowing_boat_medium_skin_tone": "🚣🏽‍♀️", "woman_rowing_boat_tone4": "🚣🏾‍♀️", "woman_rowing_boat_medium_dark_skin_tone": "🚣🏾‍♀️", "woman_rowing_boat_tone5": "🚣🏿‍♀️", "woman_rowing_boat_dark_skin_tone": "🚣🏿‍♀️", "man_rowing_boat_tone1": "🚣🏻‍♂️", "man_rowing_boat_light_skin_tone": "🚣🏻‍♂️", "man_rowing_boat_tone2": "🚣🏼‍♂️", "man_rowing_boat_medium_light_skin_tone": "🚣🏼‍♂️", "man_rowing_boat_tone3": "🚣🏽‍♂️", "man_rowing_boat_medium_skin_tone": "🚣🏽‍♂️", "man_rowing_boat_tone4": "🚣🏾‍♂️", "man_rowing_boat_medium_dark_skin_tone": "🚣🏾‍♂️", "man_rowing_boat_tone5": "🚣🏿‍♂️", "man_rowing_boat_dark_skin_tone": "🚣🏿‍♂️", "person_climbing_tone1": "🧗🏻", "person_climbing_light_skin_tone": "🧗🏻", "person_climbing_tone2": "🧗🏼", "person_climbing_medium_light_skin_tone": "🧗🏼", "person_climbing_tone3": "🧗🏽", "person_climbing_medium_skin_tone": "🧗🏽", "person_climbing_tone4": "🧗🏾", "person_climbing_medium_dark_skin_tone": "🧗🏾", "person_climbing_tone5": "🧗🏿", "person_climbing_dark_skin_tone": "🧗🏿", "woman_climbing_tone1": "🧗🏻‍♀️", "woman_climbing_light_skin_tone": "🧗🏻‍♀️", "woman_climbing_tone2": "🧗🏼‍♀️", "woman_climbing_medium_light_skin_tone": "🧗🏼‍♀️", "woman_climbing_tone3": "🧗🏽‍♀️", "woman_climbing_medium_skin_tone": "🧗🏽‍♀️", "woman_climbing_tone4": "🧗🏾‍♀️", "woman_climbing_medium_dark_skin_tone": "🧗🏾‍♀️", "woman_climbing_tone5": "🧗🏿‍♀️", "woman_climbing_dark_skin_tone": "🧗🏿‍♀️", "man_climbing_tone1": "🧗🏻‍♂️", "man_climbing_light_skin_tone": "🧗🏻‍♂️", "man_climbing_tone2": "🧗🏼‍♂️", "man_climbing_medium_light_skin_tone": "🧗🏼‍♂️", "man_climbing_tone3": "🧗🏽‍♂️", "man_climbing_medium_skin_tone": "🧗🏽‍♂️", "man_climbing_tone4": "🧗🏾‍♂️", "man_climbing_medium_dark_skin_tone": "🧗🏾‍♂️", "man_climbing_tone5": "🧗🏿‍♂️", "man_climbing_dark_skin_tone": "🧗🏿‍♂️", "person_mountain_biking_tone1": "🚵🏻", "mountain_bicyclist_tone1": "🚵🏻", "person_mountain_biking_tone2": "🚵🏼", "mountain_bicyclist_tone2": "🚵🏼", "person_mountain_biking_tone3": "🚵🏽", "mountain_bicyclist_tone3": "🚵🏽", "person_mountain_biking_tone4": "🚵🏾", "mountain_bicyclist_tone4": "🚵🏾", "person_mountain_biking_tone5": "🚵🏿", "mountain_bicyclist_tone5": "🚵🏿", "woman_mountain_biking_tone1": "🚵🏻‍♀️", "woman_mountain_biking_light_skin_tone": "🚵🏻‍♀️", "woman_mountain_biking_tone2": "🚵🏼‍♀️", "woman_mountain_biking_medium_light_skin_tone": "🚵🏼‍♀️", "woman_mountain_biking_tone3": "🚵🏽‍♀️", "woman_mountain_biking_medium_skin_tone": "🚵🏽‍♀️", "woman_mountain_biking_tone4": "🚵🏾‍♀️", "woman_mountain_biking_medium_dark_skin_tone": "🚵🏾‍♀️", "woman_mountain_biking_tone5": "🚵🏿‍♀️", "woman_mountain_biking_dark_skin_tone": "🚵🏿‍♀️", "man_mountain_biking_tone1": "🚵🏻‍♂️", "man_mountain_biking_light_skin_tone": "🚵🏻‍♂️", "man_mountain_biking_tone2": "🚵🏼‍♂️", "man_mountain_biking_medium_light_skin_tone": "🚵🏼‍♂️", "man_mountain_biking_tone3": "🚵🏽‍♂️", "man_mountain_biking_medium_skin_tone": "🚵🏽‍♂️", "man_mountain_biking_tone4": "🚵🏾‍♂️", "man_mountain_biking_medium_dark_skin_tone": "🚵🏾‍♂️", "man_mountain_biking_tone5": "🚵🏿‍♂️", "man_mountain_biking_dark_skin_tone": "🚵🏿‍♂️", "person_biking_tone1": "🚴🏻", "bicyclist_tone1": "🚴🏻", "person_biking_tone2": "🚴🏼", "bicyclist_tone2": "🚴🏼", "person_biking_tone3": "🚴🏽", "bicyclist_tone3": "🚴🏽", "person_biking_tone4": "🚴🏾", "bicyclist_tone4": "🚴🏾", "person_biking_tone5": "🚴🏿", "bicyclist_tone5": "🚴🏿", "woman_biking_tone1": "🚴🏻‍♀️", "woman_biking_light_skin_tone": "🚴🏻‍♀️", "woman_biking_tone2": "🚴🏼‍♀️", "woman_biking_medium_light_skin_tone": "🚴🏼‍♀️", "woman_biking_tone3": "🚴🏽‍♀️", "woman_biking_medium_skin_tone": "🚴🏽‍♀️", "woman_biking_tone4": "🚴🏾‍♀️", "woman_biking_medium_dark_skin_tone": "🚴🏾‍♀️", "woman_biking_tone5": "🚴🏿‍♀️", "woman_biking_dark_skin_tone": "🚴🏿‍♀️", "man_biking_tone1": "🚴🏻‍♂️", "man_biking_light_skin_tone": "🚴🏻‍♂️", "man_biking_tone2": "🚴🏼‍♂️", "man_biking_medium_light_skin_tone": "🚴🏼‍♂️", "man_biking_tone3": "🚴🏽‍♂️", "man_biking_medium_skin_tone": "🚴🏽‍♂️", "man_biking_tone4": "🚴🏾‍♂️", "man_biking_medium_dark_skin_tone": "🚴🏾‍♂️", "man_biking_tone5": "🚴🏿‍♂️", "man_biking_dark_skin_tone": "🚴🏿‍♂️", "person_juggling_tone1": "🤹🏻", "juggling_tone1": "🤹🏻", "juggler_tone1": "🤹🏻", "person_juggling_tone2": "🤹🏼", "juggling_tone2": "🤹🏼", "juggler_tone2": "🤹🏼", "person_juggling_tone3": "🤹🏽", "juggling_tone3": "🤹🏽", "juggler_tone3": "🤹🏽", "person_juggling_tone4": "🤹🏾", "juggling_tone4": "🤹🏾", "juggler_tone4": "🤹🏾", "person_juggling_tone5": "🤹🏿", "juggling_tone5": "🤹🏿", "juggler_tone5": "🤹🏿", "woman_juggling_tone1": "🤹🏻‍♀️", "woman_juggling_light_skin_tone": "🤹🏻‍♀️", "woman_juggling_tone2": "🤹🏼‍♀️", "woman_juggling_medium_light_skin_tone": "🤹🏼‍♀️", "woman_juggling_tone3": "🤹🏽‍♀️", "woman_juggling_medium_skin_tone": "🤹🏽‍♀️", "woman_juggling_tone4": "🤹🏾‍♀️", "woman_juggling_medium_dark_skin_tone": "🤹🏾‍♀️", "woman_juggling_tone5": "🤹🏿‍♀️", "woman_juggling_dark_skin_tone": "🤹🏿‍♀️", "man_juggling_tone1": "🤹🏻‍♂️", "man_juggling_light_skin_tone": "🤹🏻‍♂️", "man_juggling_tone2": "🤹🏼‍♂️", "man_juggling_medium_light_skin_tone": "🤹🏼‍♂️", "man_juggling_tone3": "🤹🏽‍♂️", "man_juggling_medium_skin_tone": "🤹🏽‍♂️", "man_juggling_tone4": "🤹🏾‍♂️", "man_juggling_medium_dark_skin_tone": "🤹🏾‍♂️", "man_juggling_tone5": "🤹🏿‍♂️", "man_juggling_dark_skin_tone": "🤹🏿‍♂️", "bath_tone1": "🛀🏻", "bath_tone2": "🛀🏼", "bath_tone3": "🛀🏽", "bath_tone4": "🛀🏾", "bath_tone5": "🛀🏿", "person_in_bed_tone1": "🛌🏻", "person_in_bed_light_skin_tone": "🛌🏻", "person_in_bed_tone2": "🛌🏼", "person_in_bed_medium_light_skin_tone": "🛌🏼", "person_in_bed_tone3": "🛌🏽", "person_in_bed_medium_skin_tone": "🛌🏽", "person_in_bed_tone4": "🛌🏾", "person_in_bed_medium_dark_skin_tone": "🛌🏾", "person_in_bed_tone5": "🛌🏿", "person_in_bed_dark_skin_tone": "🛌🏿", "skin-tone-1": "🏻", "skin-tone-2": "🏼", "skin-tone-3": "🏽", "skin-tone-4": "🏾", "skin-tone-5": "🏿"} diff --git a/venv/lib/python3.11/site-packages/discord/enums.py b/venv/lib/python3.11/site-packages/discord/enums.py new file mode 100644 index 0000000..802bb41 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/enums.py @@ -0,0 +1,1233 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import types +from collections import namedtuple +from enum import IntEnum +from typing import TYPE_CHECKING, Any, ClassVar, TypeVar, Union + +__all__ = ( + "Enum", + "ChannelType", + "MessageType", + "VoiceRegion", + "SpeakingState", + "VerificationLevel", + "ContentFilter", + "Status", + "AuditLogAction", + "AuditLogActionCategory", + "UserFlags", + "ActivityType", + "NotificationLevel", + "TeamMembershipState", + "TeamRole", + "WebhookType", + "ExpireBehaviour", + "ExpireBehavior", + "StickerType", + "StickerFormatType", + "InviteTarget", + "VideoQualityMode", + "ComponentType", + "ButtonStyle", + "StagePrivacyLevel", + "InteractionType", + "InteractionResponseType", + "NSFWLevel", + "EmbeddedActivity", + "ScheduledEventStatus", + "ScheduledEventPrivacyLevel", + "ScheduledEventLocationType", + "InputTextStyle", + "SlashCommandOptionType", + "AutoModTriggerType", + "AutoModEventType", + "AutoModActionType", + "AutoModKeywordPresetType", + "ApplicationRoleConnectionMetadataType", + "PromptType", + "OnboardingMode", + "ReactionType", + "VoiceChannelEffectAnimationType", + "SKUType", + "EntitlementType", + "EntitlementOwnerType", + "IntegrationType", + "InteractionContextType", + "PollLayoutType", + "MessageReferenceType", + "ThreadArchiveDuration", + "RoleType", + "SubscriptionStatus", + "SeparatorSpacingSize", + "SelectDefaultValueType", + "ApplicationEventWebhookStatus", + "InviteTargetUsersJobStatusCode", +) + + +def _create_value_cls(name, comparable): + cls = namedtuple(f"_EnumValue_{name}", "name value") + cls.__repr__ = lambda self: f"<{name}.{self.name}: {self.value!r}>" + cls.__str__ = lambda self: f"{name}.{self.name}" + if comparable: + cls.__le__ = ( + lambda self, other: isinstance(other, self.__class__) + and self.value <= other.value + ) + cls.__ge__ = ( + lambda self, other: isinstance(other, self.__class__) + and self.value >= other.value + ) + cls.__lt__ = ( + lambda self, other: isinstance(other, self.__class__) + and self.value < other.value + ) + cls.__gt__ = ( + lambda self, other: isinstance(other, self.__class__) + and self.value > other.value + ) + return cls + + +def _is_descriptor(obj): + return ( + hasattr(obj, "__get__") or hasattr(obj, "__set__") or hasattr(obj, "__delete__") + ) + + +class EnumMeta(type): + if TYPE_CHECKING: + __name__: ClassVar[str] + _enum_member_names_: ClassVar[list[str]] + _enum_member_map_: ClassVar[dict[str, Any]] + _enum_value_map_: ClassVar[dict[Any, Any]] + + def __new__(cls, name, bases, attrs, *, comparable: bool = False): + value_mapping = {} + member_mapping = {} + member_names = [] + + value_cls = _create_value_cls(name, comparable) + for key, value in list(attrs.items()): + is_descriptor = _is_descriptor(value) + if key[0] == "_" and not is_descriptor: + continue + + # Special case classmethod to just pass through + if isinstance(value, classmethod): + continue + + if is_descriptor: + setattr(value_cls, key, value) + del attrs[key] + continue + + try: + new_value = value_mapping[value] + except KeyError: + new_value = value_cls(name=key, value=value) + value_mapping[value] = new_value + member_names.append(key) + + member_mapping[key] = new_value + attrs[key] = new_value + + attrs["_enum_value_map_"] = value_mapping + attrs["_enum_member_map_"] = member_mapping + attrs["_enum_member_names_"] = member_names + attrs["_enum_value_cls_"] = value_cls + actual_cls = super().__new__(cls, name, bases, attrs) + value_cls._actual_enum_cls_ = actual_cls # type: ignore + return actual_cls + + def __iter__(cls): + return (cls._enum_member_map_[name] for name in cls._enum_member_names_) + + def __reversed__(cls): + return ( + cls._enum_member_map_[name] for name in reversed(cls._enum_member_names_) + ) + + def __len__(cls): + return len(cls._enum_member_names_) + + def __repr__(cls): + return f"" + + @property + def __members__(cls): + return types.MappingProxyType(cls._enum_member_map_) + + def __call__(cls, value): + try: + return cls._enum_value_map_[value] + except (KeyError, TypeError): + raise ValueError(f"{value!r} is not a valid {cls.__name__}") + + def __getitem__(cls, key): + return cls._enum_member_map_[key] + + def __setattr__(cls, name, value): + raise TypeError("Enums are immutable.") + + def __delattr__(cls, attr): + raise TypeError("Enums are immutable") + + def __instancecheck__(self, instance): + # isinstance(x, Y) + # -> __instancecheck__(Y, x) + try: + return instance._actual_enum_cls_ is self + except AttributeError: + return False + + +if TYPE_CHECKING: + from enum import Enum +else: + + class Enum(metaclass=EnumMeta): + @classmethod + def try_value(cls, value): + try: + return cls._enum_value_map_[value] + except (KeyError, TypeError): + return value + + +class ChannelType(Enum): + """Channel type""" + + text = 0 + private = 1 + voice = 2 + group = 3 + category = 4 + news = 5 + news_thread = 10 + public_thread = 11 + private_thread = 12 + stage_voice = 13 + directory = 14 + forum = 15 + media = 16 + + def __str__(self): + return self.name + + +class MessageType(Enum): + """Message type""" + + default = 0 + recipient_add = 1 + recipient_remove = 2 + call = 3 + channel_name_change = 4 + channel_icon_change = 5 + pins_add = 6 + new_member = 7 + premium_guild_subscription = 8 + premium_guild_tier_1 = 9 + premium_guild_tier_2 = 10 + premium_guild_tier_3 = 11 + channel_follow_add = 12 + guild_stream = 13 + guild_discovery_disqualified = 14 + guild_discovery_requalified = 15 + guild_discovery_grace_period_initial_warning = 16 + guild_discovery_grace_period_final_warning = 17 + thread_created = 18 + reply = 19 + application_command = 20 + thread_starter_message = 21 + guild_invite_reminder = 22 + context_menu_command = 23 + auto_moderation_action = 24 + role_subscription_purchase = 25 + interaction_premium_upsell = 26 + stage_start = 27 + stage_end = 28 + stage_speaker = 29 + stage_raise_hand = 30 + stage_topic = 31 + guild_application_premium_subscription = 32 + guild_incident_alert_mode_enabled = 36 + guild_incident_alert_mode_disabled = 37 + guild_incident_report_raid = 38 + guild_incident_report_false_alarm = 39 + purchase_notification = 44 + poll_result = 46 + + +class VoiceRegion(Enum): + """Voice region""" + + us_west = "us-west" + us_east = "us-east" + us_south = "us-south" + us_central = "us-central" + eu_west = "eu-west" + eu_central = "eu-central" + singapore = "singapore" + london = "london" + sydney = "sydney" + amsterdam = "amsterdam" + frankfurt = "frankfurt" + brazil = "brazil" + hongkong = "hongkong" + russia = "russia" + japan = "japan" + southafrica = "southafrica" + south_korea = "south-korea" + india = "india" + europe = "europe" + dubai = "dubai" + vip_us_east = "vip-us-east" + vip_us_west = "vip-us-west" + vip_amsterdam = "vip-amsterdam" + + def __str__(self): + return self.value + + +class SpeakingState(Enum): + """Speaking state""" + + none = 0 + voice = 1 + soundshare = 2 + priority = 4 + + def __str__(self): + return self.name + + def __int__(self): + return self.value + + +class VerificationLevel(Enum, comparable=True): + """Verification level""" + + none = 0 + low = 1 + medium = 2 + high = 3 + highest = 4 + + def __str__(self): + return self.name + + +class SortOrder(Enum): + """Forum Channel Sort Order""" + + latest_activity = 0 + creation_date = 1 + + def __str__(self): + return self.name + + +class ContentFilter(Enum, comparable=True): + """Content Filter""" + + disabled = 0 + no_role = 1 + all_members = 2 + + def __str__(self): + return self.name + + +class Status(Enum): + """Status""" + + online = "online" + offline = "offline" + idle = "idle" + dnd = "dnd" + do_not_disturb = "dnd" + invisible = "invisible" + streaming = "streaming" + + def __str__(self): + return self.value + + +class NotificationLevel(Enum, comparable=True): + """Notification level""" + + all_messages = 0 + only_mentions = 1 + + +class AuditLogActionCategory(Enum): + """Audit log action category""" + + create = 1 + delete = 2 + update = 3 + + +class AuditLogAction(Enum): + """Audit log action""" + + guild_update = 1 + channel_create = 10 + channel_update = 11 + channel_delete = 12 + overwrite_create = 13 + overwrite_update = 14 + overwrite_delete = 15 + kick = 20 + member_prune = 21 + ban = 22 + unban = 23 + member_update = 24 + member_role_update = 25 + member_move = 26 + member_disconnect = 27 + bot_add = 28 + role_create = 30 + role_update = 31 + role_delete = 32 + invite_create = 40 + invite_update = 41 + invite_delete = 42 + webhook_create = 50 + webhook_update = 51 + webhook_delete = 52 + emoji_create = 60 + emoji_update = 61 + emoji_delete = 62 + message_delete = 72 + message_bulk_delete = 73 + message_pin = 74 + message_unpin = 75 + integration_create = 80 + integration_update = 81 + integration_delete = 82 + stage_instance_create = 83 + stage_instance_update = 84 + stage_instance_delete = 85 + sticker_create = 90 + sticker_update = 91 + sticker_delete = 92 + scheduled_event_create = 100 + scheduled_event_update = 101 + scheduled_event_delete = 102 + thread_create = 110 + thread_update = 111 + thread_delete = 112 + application_command_permission_update = 121 + auto_moderation_rule_create = 140 + auto_moderation_rule_update = 141 + auto_moderation_rule_delete = 142 + auto_moderation_block_message = 143 + auto_moderation_flag_to_channel = 144 + auto_moderation_user_communication_disabled = 145 + creator_monetization_request_created = 150 + creator_monetization_terms_accepted = 151 + onboarding_question_create = 163 + onboarding_question_update = 164 + onboarding_update = 167 + server_guide_create = 190 + server_guide_update = 191 + voice_channel_status_update = 192 + voice_channel_status_delete = 193 + + @property + def category(self) -> AuditLogActionCategory | None: + lookup: dict[AuditLogAction, AuditLogActionCategory | None] = { + AuditLogAction.guild_update: AuditLogActionCategory.update, + AuditLogAction.channel_create: AuditLogActionCategory.create, + AuditLogAction.channel_update: AuditLogActionCategory.update, + AuditLogAction.channel_delete: AuditLogActionCategory.delete, + AuditLogAction.overwrite_create: AuditLogActionCategory.create, + AuditLogAction.overwrite_update: AuditLogActionCategory.update, + AuditLogAction.overwrite_delete: AuditLogActionCategory.delete, + AuditLogAction.kick: None, + AuditLogAction.member_prune: None, + AuditLogAction.ban: None, + AuditLogAction.unban: None, + AuditLogAction.member_update: AuditLogActionCategory.update, + AuditLogAction.member_role_update: AuditLogActionCategory.update, + AuditLogAction.member_move: None, + AuditLogAction.member_disconnect: None, + AuditLogAction.bot_add: None, + AuditLogAction.role_create: AuditLogActionCategory.create, + AuditLogAction.role_update: AuditLogActionCategory.update, + AuditLogAction.role_delete: AuditLogActionCategory.delete, + AuditLogAction.invite_create: AuditLogActionCategory.create, + AuditLogAction.invite_update: AuditLogActionCategory.update, + AuditLogAction.invite_delete: AuditLogActionCategory.delete, + AuditLogAction.webhook_create: AuditLogActionCategory.create, + AuditLogAction.webhook_update: AuditLogActionCategory.update, + AuditLogAction.webhook_delete: AuditLogActionCategory.delete, + AuditLogAction.emoji_create: AuditLogActionCategory.create, + AuditLogAction.emoji_update: AuditLogActionCategory.update, + AuditLogAction.emoji_delete: AuditLogActionCategory.delete, + AuditLogAction.message_delete: AuditLogActionCategory.delete, + AuditLogAction.message_bulk_delete: AuditLogActionCategory.delete, + AuditLogAction.message_pin: None, + AuditLogAction.message_unpin: None, + AuditLogAction.integration_create: AuditLogActionCategory.create, + AuditLogAction.integration_update: AuditLogActionCategory.update, + AuditLogAction.integration_delete: AuditLogActionCategory.delete, + AuditLogAction.stage_instance_create: AuditLogActionCategory.create, + AuditLogAction.stage_instance_update: AuditLogActionCategory.update, + AuditLogAction.stage_instance_delete: AuditLogActionCategory.delete, + AuditLogAction.sticker_create: AuditLogActionCategory.create, + AuditLogAction.sticker_update: AuditLogActionCategory.update, + AuditLogAction.sticker_delete: AuditLogActionCategory.delete, + AuditLogAction.scheduled_event_create: AuditLogActionCategory.create, + AuditLogAction.scheduled_event_update: AuditLogActionCategory.update, + AuditLogAction.scheduled_event_delete: AuditLogActionCategory.delete, + AuditLogAction.thread_create: AuditLogActionCategory.create, + AuditLogAction.thread_update: AuditLogActionCategory.update, + AuditLogAction.thread_delete: AuditLogActionCategory.delete, + AuditLogAction.application_command_permission_update: ( + AuditLogActionCategory.update + ), + AuditLogAction.auto_moderation_rule_create: AuditLogActionCategory.create, + AuditLogAction.auto_moderation_rule_update: AuditLogActionCategory.update, + AuditLogAction.auto_moderation_rule_delete: AuditLogActionCategory.delete, + AuditLogAction.auto_moderation_block_message: None, + AuditLogAction.auto_moderation_flag_to_channel: None, + AuditLogAction.auto_moderation_user_communication_disabled: None, + AuditLogAction.creator_monetization_request_created: None, + AuditLogAction.creator_monetization_terms_accepted: None, + AuditLogAction.onboarding_question_create: AuditLogActionCategory.create, + AuditLogAction.onboarding_question_update: AuditLogActionCategory.update, + AuditLogAction.onboarding_update: AuditLogActionCategory.update, + AuditLogAction.server_guide_create: AuditLogActionCategory.create, + AuditLogAction.server_guide_update: AuditLogActionCategory.update, + AuditLogAction.voice_channel_status_update: AuditLogActionCategory.update, + AuditLogAction.voice_channel_status_delete: AuditLogActionCategory.delete, + } + return lookup[self] + + @property + def target_type(self) -> str | None: + v = self.value + if v == -1: + return "all" + elif v < 10: + return "guild" + elif v < 20: + return "channel" + elif v < 30: + return "user" + elif v < 40: + return "role" + elif v < 50: + return "invite" + elif v < 60: + return "webhook" + elif v < 70: + return "emoji" + elif v == 73: + return "channel" + elif v < 80: + return "message" + elif v < 83: + return "integration" + elif v < 90: + return "stage_instance" + elif v < 93: + return "sticker" + elif v < 103: + return "scheduled_event" + elif v < 113: + return "thread" + elif v < 122: + return "application_command_permission" + elif v < 146: + return "auto_moderation_rule" + elif v < 152: + return "monetization" + elif v < 168: + return "onboarding" + elif v < 192: + return "server_guide" + elif v < 194: + return "voice_channel_status" + + +class UserFlags(Enum): + """User flags""" + + staff = 1 + partner = 2 + hypesquad = 4 + bug_hunter = 8 + mfa_sms = 16 + premium_promo_dismissed = 32 + hypesquad_bravery = 64 + hypesquad_brilliance = 128 + hypesquad_balance = 256 + early_supporter = 512 + team_user = 1024 + partner_or_verification_application = 2048 + system = 4096 + has_unread_urgent_messages = 8192 + bug_hunter_level_2 = 16384 + underage_deleted = 32768 + verified_bot = 65536 + verified_bot_developer = 131072 + discord_certified_moderator = 262144 + bot_http_interactions = 524288 + spammer = 1048576 + active_developer = 4194304 + + +class ActivityType(Enum): + """Activity type""" + + unknown = -1 + playing = 0 + streaming = 1 + listening = 2 + watching = 3 + custom = 4 + competing = 5 + + def __int__(self): + return self.value + + +class TeamMembershipState(Enum): + """Team membership state""" + + invited = 1 + accepted = 2 + + +class TeamRole(Enum): + """Role of a team member.""" + + owner = "owner" + admin = "admin" + developer = "developer" + read_only = "read_only" + + +class WebhookType(Enum): + """Webhook Type""" + + incoming = 1 + channel_follower = 2 + application = 3 + + +class ExpireBehaviour(Enum): + """Expire Behaviour""" + + remove_role = 0 + kick = 1 + + +ExpireBehavior = ExpireBehaviour + + +class StickerType(Enum): + """Sticker type""" + + standard = 1 + guild = 2 + + +class StickerFormatType(Enum): + """Sticker format Type""" + + png = 1 + apng = 2 + lottie = 3 + gif = 4 + + @property + def file_extension(self) -> str: + lookup: dict[StickerFormatType, str] = { + StickerFormatType.png: "png", + StickerFormatType.apng: "png", + StickerFormatType.lottie: "json", + StickerFormatType.gif: "gif", + } + return lookup.get(self, "png") + + +class InviteTarget(Enum): + """Invite target""" + + unknown = 0 + stream = 1 + embedded_application = 2 + + +class InteractionType(Enum): + """Interaction type""" + + ping = 1 + application_command = 2 + component = 3 + auto_complete = 4 + modal_submit = 5 + + +class InteractionResponseType(Enum): + """Interaction response type""" + + pong = 1 + # ack = 2 (deprecated) + # channel_message = 3 (deprecated) + channel_message = 4 # (with source) + deferred_channel_message = 5 # (with source) + deferred_message_update = 6 # for components + message_update = 7 # for components + auto_complete_result = 8 # for autocomplete interactions + modal = 9 # for modal dialogs + premium_required = 10 + + +class VideoQualityMode(Enum): + """Video quality mode""" + + auto = 1 + full = 2 + + def __int__(self): + return self.value + + +class ComponentType(Enum): + """Component type""" + + action_row = 1 + button = 2 + string_select = 3 + select = string_select # (deprecated) alias for string_select + input_text = 4 + user_select = 5 + role_select = 6 + mentionable_select = 7 + channel_select = 8 + section = 9 + text_display = 10 + thumbnail = 11 + media_gallery = 12 + file = 13 + separator = 14 + content_inventory_entry = 16 + container = 17 + label = 18 + file_upload = 19 + radio_group = 21 + checkbox_group = 22 + checkbox = 23 + + def __int__(self): + return self.value + + +class ButtonStyle(Enum): + """Button style""" + + primary = 1 + secondary = 2 + success = 3 + danger = 4 + link = 5 + premium = 6 + + # Aliases + blurple = 1 + grey = 2 + gray = 2 + green = 3 + red = 4 + url = 5 + + def __int__(self): + return self.value + + +class InputTextStyle(Enum): + """Input text style""" + + short = 1 + singleline = 1 + paragraph = 2 + multiline = 2 + long = 2 + + +class ApplicationType(Enum): + """Application type""" + + game = 1 + music = 2 + ticketed_events = 3 + guild_role_subscriptions = 4 + + +class StagePrivacyLevel(Enum): + """Stage privacy level""" + + # public = 1 (deprecated) + closed = 2 + guild_only = 2 + + +class NSFWLevel(Enum, comparable=True): + """NSFW level""" + + default = 0 + explicit = 1 + safe = 2 + age_restricted = 3 + + +class SlashCommandOptionType(Enum): + """Slash command option type""" + + sub_command = 1 + sub_command_group = 2 + string = 3 + integer = 4 + boolean = 5 + user = 6 + channel = 7 + role = 8 + mentionable = 9 + number = 10 + attachment = 11 + + @classmethod + def from_datatype(cls, datatype): + if isinstance(datatype, tuple): # typing.Union has been used + datatypes = [cls.from_datatype(op) for op in datatype] + if all(x == cls.channel for x in datatypes): + return cls.channel + elif set(datatypes) <= {cls.role, cls.user}: + return cls.mentionable + else: + raise TypeError("Invalid usage of typing.Union") + + py_3_10_union_type = hasattr(types, "UnionType") and isinstance( + datatype, types.UnionType + ) + + if py_3_10_union_type or getattr(datatype, "__origin__", None) is Union: + # Python 3.10+ "|" operator or typing.Union has been used. The __args__ attribute is a tuple of the types. + # Type checking fails for this case, so ignore it. + return cls.from_datatype(datatype.__args__) # type: ignore + + if isinstance(datatype, str): + datatype_name = datatype + else: + datatype_name = datatype.__name__ + if datatype_name in ["Member", "User"]: + return cls.user + if datatype_name in [ + "GuildChannel", + "TextChannel", + "VoiceChannel", + "StageChannel", + "CategoryChannel", + "ThreadOption", + "Thread", + "ForumChannel", + "MediaChannel", + "DMChannel", + ]: + return cls.channel + if datatype_name == "Role": + return cls.role + if datatype_name == "Attachment": + return cls.attachment + if datatype_name == "Mentionable": + return cls.mentionable + + if isinstance(datatype, str) or issubclass(datatype, str): + return cls.string + if issubclass(datatype, bool): + return cls.boolean + if issubclass(datatype, int): + return cls.integer + if issubclass(datatype, float): + return cls.number + + from .commands.context import ApplicationContext + from .ext.bridge import BridgeContext + + if not issubclass( + datatype, (ApplicationContext, BridgeContext) + ): # TODO: prevent ctx being passed here in cog commands + raise TypeError( + f"Invalid class {datatype} used as an input type for an Option" + ) # TODO: Improve the error message + + +class EmbeddedActivity(Enum): + """Embedded activity""" + + ask_away = 976052223358406656 + awkword = 879863881349087252 + awkword_dev = 879863923543785532 + bash_out = 1006584476094177371 + betrayal = 773336526917861400 + blazing_8s = 832025144389533716 + blazing_8s_dev = 832013108234289153 + blazing_8s_qa = 832025114077298718 + blazing_8s_staging = 832025061657280566 + bobble_league = 947957217959759964 + checkers_in_the_park = 832013003968348200 + checkers_in_the_park_dev = 832012682520428625 + checkers_in_the_park_qa = 832012894068801636 + checkers_in_the_park_staging = 832012938398400562 + chess_in_the_park = 832012774040141894 + chess_in_the_park_dev = 832012586023256104 + chess_in_the_park_qa = 832012815819604009 + chess_in_the_park_staging = 832012730599735326 + decoders_dev = 891001866073296967 + doodle_crew = 878067389634314250 + doodle_crew_dev = 878067427668275241 + fishington = 814288819477020702 + gartic_phone = 1007373802981822582 + jamspace = 1070087967294631976 + know_what_i_meme = 950505761862189096 + land = 903769130790969345 + letter_league = 879863686565621790 + letter_league_dev = 879863753519292467 + poker_night = 755827207812677713 + poker_night_dev = 763133495793942528 + poker_night_qa = 801133024841957428 + poker_night_staging = 763116274876022855 + putt_party = 945737671223947305 + putt_party_dev = 910224161476083792 + putt_party_qa = 945748195256979606 + putt_party_staging = 945732077960188005 + putts = 832012854282158180 + sketch_heads = 902271654783242291 + sketch_heads_dev = 902271746701414431 + sketchy_artist = 879864070101172255 + sketchy_artist_dev = 879864104980979792 + spell_cast = 852509694341283871 + spell_cast_staging = 893449443918086174 + watch_together = 880218394199220334 + watch_together_dev = 880218832743055411 + word_snacks = 879863976006127627 + word_snacks_dev = 879864010126786570 + youtube_together = 755600276941176913 + + +class ScheduledEventStatus(Enum): + """Scheduled event status""" + + scheduled = 1 + active = 2 + completed = 3 + canceled = 4 + cancelled = 4 + + def __int__(self): + return self.value + + +class ScheduledEventPrivacyLevel(Enum): + """Scheduled event privacy level""" + + guild_only = 2 + + def __int__(self): + return self.value + + +class ScheduledEventLocationType(Enum): + """Scheduled event location type""" + + stage_instance = 1 + voice = 2 + external = 3 + + +class AutoModTriggerType(Enum): + """Automod trigger type""" + + keyword = 1 + harmful_link = 2 + spam = 3 + keyword_preset = 4 + mention_spam = 5 + + +class AutoModEventType(Enum): + """Automod event type""" + + message_send = 1 + + +class AutoModActionType(Enum): + """Automod action type""" + + block_message = 1 + send_alert_message = 2 + timeout = 3 + + +class AutoModKeywordPresetType(Enum): + """Automod keyword preset type""" + + profanity = 1 + sexual_content = 2 + slurs = 3 + + +class ApplicationRoleConnectionMetadataType(Enum): + """Application role connection metadata type""" + + integer_less_than_or_equal = 1 + integer_greater_than_or_equal = 2 + integer_equal = 3 + integer_not_equal = 4 + datetime_less_than_or_equal = 5 + datetime_greater_than_or_equal = 6 + boolean_equal = 7 + boolean_not_equal = 8 + + +class PromptType(Enum): + """Guild Onboarding Prompt Type""" + + multiple_choice = 0 + dropdown = 1 + + +class OnboardingMode(Enum): + """Guild Onboarding Mode""" + + default = 0 + advanced = 1 + + +class ReactionType(Enum): + """The reaction type""" + + normal = 0 + burst = 1 + + +class SKUType(Enum): + """The SKU type""" + + durable = 2 + consumable = 3 + subscription = 5 + subscription_group = 6 + + +class EntitlementType(Enum): + """The entitlement type""" + + purchase = 1 + premium_subscription = 2 + developer_gift = 3 + test_mode_purchase = 4 + free_purchase = 5 + user_gift = 6 + premium_purchase = 7 + application_subscription = 8 + + +class EntitlementOwnerType(Enum): + """The entitlement owner type""" + + guild = 1 + user = 2 + + +class IntegrationType(Enum): + """The application's integration type.""" + + guild_install = 0 + user_install = 1 + + +class InteractionContextType(Enum): + """The interaction's context type""" + + guild = 0 + bot_dm = 1 + private_channel = 2 + + +class PollLayoutType(Enum): + """The poll's layout type.""" + + default = 1 + + +class VoiceChannelEffectAnimationType(Enum): + """Voice channel effect animation type. + + .. versionadded:: 2.7 + """ + + premium = 0 + basic = 1 + + +class MessageReferenceType(Enum): + """The type of the message reference object""" + + default = 0 + forward = 1 + + +class SubscriptionStatus(Enum): + """The status of a subscription.""" + + active = 0 + ending = 1 + inactive = 2 + + +class ThreadArchiveDuration(IntEnum): + """The time set until a thread is automatically archived.""" + + one_hour = 60 + one_day = 1440 + three_days = 4320 + one_week = 10080 + + +class SeparatorSpacingSize(Enum): + """A separator component's spacing size.""" + + small = 1 + large = 2 + + def __int__(self): + return self.value + + +class SelectDefaultValueType(Enum): + """Represents the default value type of a select menu.""" + + channel = "channel" + role = "role" + user = "user" + + +class RoleType(IntEnum): + """Represents the type of role. + + This is NOT provided by Discord but is rather computed from :attr:`Role.tags`. + + .. versionadded:: 2.8 + + Attributes + ---------- + NORMAL: :class:`int` + The role is a normal role. + APPLICATION: :class:`int` + The role is an application (bot) role. + BOOSTER: :class:`int` + The role is a guild's booster role. + GUILD_PRODUCT: :class:`int` + The role is a guild product role. + + .. note:: + This is not possible to determine at times because role tags seem to be missing altogether, notably when + a guild product role is fetched. + In such cases :attr:`Role.type` will be :attr:`RoleType.NORMAL` and :attr:`Role.tags` will be :data:`None`. + PREMIUM_SUBSCRIPTION_BASE: :class:`int` + The role is a base subscription role. + + .. note:: + This is not possible to determine currently, will be :attr:`.INTEGRATION` if it's a base subscription. + PREMIUM_SUBSCRIPTION_TIER: :class:`int` + The role is a subscription role. + DRAFT_PREMIUM_SUBSCRIPTION_TIER: :class:`int` + The role is a draft subscription role. + INTEGRATION: :class:`int` + The role is an integration role, such as Twitch or YouTube, or a base subscription role. + CONNECTION: :class:`int` + The role is a guild connections role. + UNKNOWN: :class:`int` + The role type is unknown. + """ + + NORMAL = 0 + APPLICATION = 1 + BOOSTER = 2 + GUILD_PRODUCT = 3 # Not possible to determine *at times* because role tags seem to be missing altogether when fetched + PREMIUM_SUBSCRIPTION_BASE = 4 # Not possible to determine currently, will be INTEGRATION if it's a base subscription + PREMIUM_SUBSCRIPTION_TIER = 5 + DRAFT_PREMIUM_SUBSCRIPTION_TIER = 6 + INTEGRATION = 7 + CONNECTION = 8 + UNKNOWN = 9 + + +class ApplicationEventWebhookStatus(Enum): + """Represents the application event webhook status.""" + + disabled = 1 + enabled = 2 + disabled_by_discord = 3 + + +class InviteTargetUsersJobStatusCode(Enum): + unspecified = 0 + processing = 1 + completed = 2 + failed = 3 + + +T = TypeVar("T") + + +def create_unknown_value(cls: type[T], val: Any) -> T: + value_cls = cls._enum_value_cls_ # type: ignore + name = f"unknown_{val}" + return value_cls(name=name, value=val) + + +def try_enum(cls: type[T], val: Any) -> T: + """A function that tries to turn the value into enum ``cls``. + + If it fails it returns a proxy invalid value instead. + """ + + try: + return cls._enum_value_map_[val] # type: ignore + except (KeyError, TypeError, AttributeError): + return create_unknown_value(cls, val) diff --git a/venv/lib/python3.11/site-packages/discord/errors.py b/venv/lib/python3.11/site-packages/discord/errors.py new file mode 100644 index 0000000..5e94e02 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/errors.py @@ -0,0 +1,434 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Union + +if TYPE_CHECKING: + from aiohttp import ClientResponse, ClientWebSocketResponse + + try: + from requests import Response + + _ResponseType = Union[ClientResponse, Response] + except ModuleNotFoundError: + _ResponseType = ClientResponse + + from .interactions import Interaction + +__all__ = ( + "DiscordException", + "ClientException", + "NoMoreItems", + "GatewayNotFound", + "ValidationError", + "HTTPException", + "Forbidden", + "NotFound", + "DiscordServerError", + "InvalidData", + "InvalidArgument", + "LoginFailure", + "ConnectionClosed", + "PrivilegedIntentsRequired", + "InteractionResponded", + "ExtensionError", + "ExtensionAlreadyLoaded", + "ExtensionNotLoaded", + "NoEntryPointError", + "ExtensionFailed", + "ExtensionNotFound", + "ApplicationCommandError", + "CheckFailure", + "ApplicationCommandInvokeError", + "MissingVoiceDependenciesError", +) + + +class DiscordException(Exception): + """Base exception class for pycord + + Ideally speaking, this could be caught to handle any exceptions raised from this library. + """ + + +class ClientException(DiscordException): + """Exception that's raised when an operation in the :class:`Client` fails. + + These are usually for exceptions that happened due to user input. + """ + + +class NoMoreItems(DiscordException): + """Exception that is raised when an async iteration operation has no more items.""" + + +class GatewayNotFound(DiscordException): + """An exception that is raised when the gateway for Discord could not be found""" + + def __init__(self): + message = "The gateway to connect to discord was not found." + super().__init__(message) + + +class ValidationError(DiscordException): + """An Exception that is raised when there is a Validation Error.""" + + +def _flatten_error_dict(d: dict[str, Any], key: str = "") -> dict[str, str]: + items: list[tuple[str, str]] = [] + for k, v in d.items(): + new_key = f"{key}.{k}" if key else k + + if isinstance(v, dict): + try: + _errors: list[dict[str, Any]] = v["_errors"] + except KeyError: + items.extend(_flatten_error_dict(v, new_key).items()) + else: + items.append((new_key, " ".join(x.get("message", "") for x in _errors))) + else: + items.append((new_key, v)) + + return dict(items) + + +class HTTPException(DiscordException): + """Exception that's raised when an HTTP request operation fails. + + Attributes + ---------- + response: :class:`aiohttp.ClientResponse` + The response of the failed HTTP request. This is an + instance of :class:`aiohttp.ClientResponse`. In some cases + this could also be a :class:`requests.Response`. + + text: :class:`str` + The text of the error. Could be an empty string. + status: :class:`int` + The status code of the HTTP request. + code: :class:`int` + The Discord specific error code for the failure. + """ + + def __init__(self, response: _ResponseType, message: str | dict[str, Any] | None): + self.response: _ResponseType = response + self.status: int = response.status # type: ignore + self.code: int + self.text: str + if isinstance(message, dict): + self.code = message.get("code", 0) + base = message.get("message", "") + errors = message.get("errors") + if errors: + errors = _flatten_error_dict(errors) + helpful = "\n".join("In %s: %s" % t for t in errors.items()) + self.text = f"{base}\n{helpful}" + else: + self.text = base + else: + self.text = message or "" + self.code = 0 + + fmt = "{0.status} {0.reason} (error code: {1})" + if len(self.text): + fmt += ": {2}" + + super().__init__(fmt.format(self.response, self.code, self.text)) + + +class Forbidden(HTTPException): + """Exception that's raised for when status code 403 occurs. + + Subclass of :exc:`HTTPException` + """ + + +class NotFound(HTTPException): + """Exception that's raised for when status code 404 occurs. + + Subclass of :exc:`HTTPException` + """ + + +class DiscordServerError(HTTPException): + """Exception that's raised for when a 500 range status code occurs. + + Subclass of :exc:`HTTPException`. + + .. versionadded:: 1.5 + """ + + +class InvalidData(ClientException): + """Exception that's raised when the library encounters unknown + or invalid data from Discord. + """ + + +class InvalidArgument(ClientException): + """Exception that's raised when an argument to a function + is invalid some way (e.g. wrong value or wrong type). + + This could be considered the parallel of ``ValueError`` and + ``TypeError`` except inherited from :exc:`ClientException` and thus + :exc:`DiscordException`. + """ + + +class LoginFailure(ClientException): + """Exception that's raised when the :meth:`Client.login` function + fails to log you in from improper credentials or some other misc. + failure. + """ + + +class ConnectionClosed(ClientException): + """Exception that's raised when the gateway connection is + closed for reasons that could not be handled internally. + + Attributes + ---------- + code: :class:`int` + The close code of the websocket. + reason: :class:`str` + The reason provided for the closure. + shard_id: Optional[:class:`int`] + The shard ID that got closed if applicable. + """ + + def __init__( + self, + socket: ClientWebSocketResponse, + *, + shard_id: int | None, + code: int | None = None, + ): + # This exception is just the same exception except + # reconfigured to subclass ClientException for users + self.code: int = code or socket.close_code or -1 + # aiohttp doesn't seem to consistently provide close reason + self.reason: str = "" + self.shard_id: int | None = shard_id + super().__init__(f"Shard ID {self.shard_id} WebSocket closed with {self.code}") + + +class PrivilegedIntentsRequired(ClientException): + """Exception that's raised when the gateway is requesting privileged intents, but + they're not ticked in the developer page yet. + + Go to https://discord.com/developers/applications/ and enable the intents + that are required. Currently, these are as follows: + + - :attr:`Intents.members` + - :attr:`Intents.presences` + - :attr:`Intents.message_content` + + Attributes + ---------- + shard_id: Optional[:class:`int`] + The shard ID that got closed if applicable. + """ + + def __init__(self, shard_id: int | None): + self.shard_id: int | None = shard_id + msg = ( + "Shard ID %s is requesting privileged intents that have not been explicitly" + " enabled in the developer portal. It is recommended to go to" + " https://discord.com/developers/applications/ and explicitly enable the" + " privileged intents within your application's page. If this is not" + " possible, then consider disabling the privileged intents instead." + ) + super().__init__(msg % shard_id) + + +class InteractionResponded(ClientException): + """Exception that's raised when sending another interaction response using + :class:`InteractionResponse` when one has already been done before. + + An interaction can only respond once. + + .. versionadded:: 2.0 + + Attributes + ---------- + interaction: :class:`Interaction` + The interaction that's already been responded to. + """ + + def __init__(self, interaction: Interaction): + self.interaction: Interaction = interaction + super().__init__("This interaction has already been responded to before") + + +class ExtensionError(DiscordException): + """Base exception for extension related errors. + + This inherits from :exc:`~discord.DiscordException`. + + Attributes + ---------- + name: :class:`str` + The extension that had an error. + """ + + def __init__(self, message: str | None = None, *args: Any, name: str) -> None: + self.name: str = name + message = message or f"Extension {name!r} had an error." + # clean-up @everyone and @here mentions + m = message.replace("@everyone", "@\u200beveryone").replace( + "@here", "@\u200bhere" + ) + super().__init__(m, *args) + + +class ExtensionAlreadyLoaded(ExtensionError): + """An exception raised when an extension has already been loaded. + + This inherits from :exc:`ExtensionError` + """ + + def __init__(self, name: str) -> None: + super().__init__(f"Extension {name!r} is already loaded.", name=name) + + +class ExtensionNotLoaded(ExtensionError): + """An exception raised when an extension was not loaded. + + This inherits from :exc:`ExtensionError` + """ + + def __init__(self, name: str) -> None: + super().__init__(f"Extension {name!r} has not been loaded.", name=name) + + +class NoEntryPointError(ExtensionError): + """An exception raised when an extension does not have a ``setup`` entry point function. + + This inherits from :exc:`ExtensionError` + """ + + def __init__(self, name: str) -> None: + super().__init__(f"Extension {name!r} has no 'setup' function.", name=name) + + +class ExtensionFailed(ExtensionError): + """An exception raised when an extension failed to load during execution of the module or ``setup`` entry point. + + This inherits from :exc:`ExtensionError` + + Attributes + ---------- + name: :class:`str` + The extension that had the error. + original: :exc:`Exception` + The original exception that was raised. You can also get this via + the ``__cause__`` attribute. + """ + + def __init__(self, name: str, original: Exception) -> None: + self.original: Exception = original + msg = ( + f"Extension {name!r} raised an error: {original.__class__.__name__}:" + f" {original}" + ) + super().__init__(msg, name=name) + + +class ExtensionNotFound(ExtensionError): + """An exception raised when an extension is not found. + + This inherits from :exc:`ExtensionError` + + .. versionchanged:: 1.3 + Made the ``original`` attribute always None. + + Attributes + ---------- + name: :class:`str` + The extension that had the error. + """ + + def __init__(self, name: str) -> None: + msg = f"Extension {name!r} could not be found." + super().__init__(msg, name=name) + + +class ApplicationCommandError(DiscordException): + r"""The base exception type for all application command related errors. + + This inherits from :exc:`DiscordException`. + + This exception and exceptions inherited from it are handled + in a special way as they are caught and passed into a special event + from :class:`.Bot`\, :func:`.on_command_error`. + """ + + +class CheckFailure(ApplicationCommandError): + """Exception raised when the predicates in :attr:`.Command.checks` have failed. + + This inherits from :exc:`ApplicationCommandError` + """ + + +class ApplicationCommandInvokeError(ApplicationCommandError): + """Exception raised when the command being invoked raised an exception. + + This inherits from :exc:`ApplicationCommandError` + + Attributes + ---------- + original: :exc:`Exception` + The original exception that was raised. You can also get this via + the ``__cause__`` attribute. + """ + + def __init__(self, e: Exception) -> None: + self.original: Exception = e + super().__init__( + f"Application Command raised an exception: {e.__class__.__name__}: {e}" + ) + + +class MissingVoiceDependenciesError(RuntimeError, DiscordException): + """Raised when required voice dependencies are not installed. + + .. note:: + This exception inherits from both :exc:`RuntimeError` and :exc:`DiscordException`. + + Attributes: + missing: tuple[str, ...] + The missing dependencies that are required for voice support. + """ + + def __init__(self, missing: tuple[str, ...]) -> None: + self.missing: tuple[str, ...] = missing + deps = ", ".join(missing) + super().__init__( + f"{deps} {'is' if len(missing) == 1 else 'are'} required for voice support. " + 'Install them with "pip install py-cord[voice]".' + ) diff --git a/venv/lib/python3.11/site-packages/discord/ext/bridge/__init__.py b/venv/lib/python3.11/site-packages/discord/ext/bridge/__init__.py new file mode 100644 index 0000000..b92a536 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/bridge/__init__.py @@ -0,0 +1,28 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from .bot import * +from .context import * +from .core import * diff --git a/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..f2122a2 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/bot.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/bot.cpython-311.pyc new file mode 100644 index 0000000..d29633c Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/bot.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/context.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/context.cpython-311.pyc new file mode 100644 index 0000000..723ddba Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/context.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/core.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/core.cpython-311.pyc new file mode 100644 index 0000000..8d724c1 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/bridge/__pycache__/core.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/bridge/bot.py b/venv/lib/python3.11/site-packages/discord/ext/bridge/bot.py new file mode 100644 index 0000000..9e2c619 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/bridge/bot.py @@ -0,0 +1,204 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from abc import ABC +from collections.abc import Iterator + +from discord.commands import ApplicationContext +from discord.errors import CheckFailure, DiscordException +from discord.interactions import Interaction +from discord.message import Message + +from ..commands import AutoShardedBot as ExtAutoShardedBot +from ..commands import Bot as ExtBot +from ..commands import Context as ExtContext +from ..commands import errors +from .context import BridgeApplicationContext, BridgeExtContext +from .core import ( + BridgeCommand, + BridgeCommandGroup, + BridgeExtCommand, + BridgeSlashCommand, + bridge_command, + bridge_group, +) + +__all__ = ("Bot", "AutoShardedBot") + + +class BotBase(ABC): + _bridge_commands: list[BridgeCommand | BridgeCommandGroup] + + @property + def bridge_commands(self) -> list[BridgeCommand | BridgeCommandGroup]: + """Returns all of the bot's bridge commands.""" + + if not (cmds := getattr(self, "_bridge_commands", None)): + self._bridge_commands = cmds = [] + + return cmds + + def walk_bridge_commands( + self, + ) -> Iterator[BridgeCommand | BridgeCommandGroup]: + """An iterator that recursively walks through all the bot's bridge commands. + + Yields + ------ + Union[:class:`.BridgeCommand`, :class:`.BridgeCommandGroup`] + A bridge command or bridge group of the bot. + """ + for cmd in self._bridge_commands: + yield cmd + if isinstance(cmd, BridgeCommandGroup): + yield from cmd.walk_commands() + + async def get_application_context( + self, interaction: Interaction, cls=None + ) -> BridgeApplicationContext: + cls = cls if cls is not None else BridgeApplicationContext + # Ignore the type hinting error here. BridgeApplicationContext is a subclass of ApplicationContext, and since + # we gave it cls, it will be used instead. + return await super().get_application_context(interaction, cls=cls) # type: ignore + + async def get_context(self, message: Message, cls=None) -> BridgeExtContext: + cls = cls if cls is not None else BridgeExtContext + # Ignore the type hinting error here. BridgeExtContext is a subclass of Context, and since we gave it cls, it + # will be used instead. + return await super().get_context(message, cls=cls) # type: ignore + + def add_bridge_command(self, command: BridgeCommand): + """Takes a :class:`.BridgeCommand` and adds both a slash and traditional (prefix-based) version of the command + to the bot. + """ + # Ignore the type hinting error here. All subclasses of BotBase pass the type checks. + command.add_to(self) # type: ignore + + self.bridge_commands.append(command) + + def bridge_command(self, **kwargs): + """A shortcut decorator that invokes :func:`bridge_command` and adds it to + the internal command list via :meth:`~.Bot.add_bridge_command`. + + Returns + ------- + Callable[..., :class:`BridgeCommand`] + A decorator that converts the provided method into an :class:`.BridgeCommand`, adds both a slash and + traditional (prefix-based) version of the command to the bot, and returns the :class:`.BridgeCommand`. + """ + + def decorator(func) -> BridgeCommand: + result = bridge_command(**kwargs)(func) + self.add_bridge_command(result) + return result + + return decorator + + def bridge_group(self, **kwargs): + """A decorator that is used to wrap a function as a bridge command group. + + Parameters + ---------- + kwargs: Optional[Dict[:class:`str`, Any]] + Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommandGroup` and :class:`.ext.commands.Group`) + """ + + def decorator(func) -> BridgeCommandGroup: + result = bridge_group(**kwargs)(func) + self.add_bridge_command(result) + return result + + return decorator + + async def invoke(self, ctx: ExtContext | BridgeExtContext): + if ctx.command is not None: + self.dispatch("command", ctx) + if isinstance(ctx.command, BridgeExtCommand): + self.dispatch("bridge_command", ctx) + try: + if await self.can_run(ctx, call_once=True): + await ctx.command.invoke(ctx) + else: + raise errors.CheckFailure("The global check once functions failed.") + except errors.CommandError as exc: + await ctx.command.dispatch_error(ctx, exc) + else: + self.dispatch("command_completion", ctx) + if isinstance(ctx.command, BridgeExtCommand): + self.dispatch("bridge_command_completion", ctx) + elif ctx.invoked_with: + exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found') + self.dispatch("command_error", ctx, exc) + if isinstance(ctx.command, BridgeExtCommand): + self.dispatch("bridge_command_error", ctx, exc) + + async def invoke_application_command( + self, ctx: ApplicationContext | BridgeApplicationContext + ) -> None: + """|coro| + + Invokes the application command given under the invocation + context and handles all the internal event dispatch mechanisms. + + Parameters + ---------- + ctx: :class:`.ApplicationCommand` + The invocation context to invoke. + """ + self._bot.dispatch("application_command", ctx) + if br_cmd := isinstance(ctx.command, BridgeSlashCommand): + self._bot.dispatch("bridge_command", ctx) + try: + if await self._bot.can_run(ctx, call_once=True): + await ctx.command.invoke(ctx) + else: + raise CheckFailure("The global check once functions failed.") + except DiscordException as exc: + await ctx.command.dispatch_error(ctx, exc) + else: + self._bot.dispatch("application_command_completion", ctx) + if br_cmd: + self._bot.dispatch("bridge_command_completion", ctx) + + +class Bot(BotBase, ExtBot): + """Represents a discord bot, with support for cross-compatibility between command types. + + This class is a subclass of :class:`.ext.commands.Bot` and as a result + anything that you can do with a :class:`.ext.commands.Bot` you can do with + this bot. + + .. versionadded:: 2.0 + """ + + +class AutoShardedBot(BotBase, ExtAutoShardedBot): + """This is similar to :class:`.Bot` except that it is inherited from + :class:`.ext.commands.AutoShardedBot` instead. + + .. versionadded:: 2.0 + """ diff --git a/venv/lib/python3.11/site-packages/discord/ext/bridge/context.py b/venv/lib/python3.11/site-packages/discord/ext/bridge/context.py new file mode 100644 index 0000000..8e7f941 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/bridge/context.py @@ -0,0 +1,203 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from abc import ABC, abstractmethod +from typing import TYPE_CHECKING, Any, Union, overload + +from discord.commands import ApplicationContext +from discord.interactions import Interaction, InteractionMessage +from discord.message import Message +from discord.webhook import WebhookMessage + +from ..commands import Context + +if TYPE_CHECKING: + from .core import BridgeExtCommand, BridgeSlashCommand + + +__all__ = ("BridgeContext", "BridgeExtContext", "BridgeApplicationContext", "Context") + + +class BridgeContext(ABC): + """ + The base context class for compatibility commands. This class is an :term:`abstract base class` (also known as an + ``abc``), which is subclassed by :class:`BridgeExtContext` and :class:`BridgeApplicationContext`. The methods in + this class are meant to give parity between the two contexts, while still allowing for all of their functionality. + + When this is passed to a command, it will either be passed as :class:`BridgeExtContext`, or + :class:`BridgeApplicationContext`. Since they are two separate classes, it's easy to use the :attr:`BridgeContext.is_app` attribute. + to make different functionality for each context. For example, if you want to respond to a command with the command + type that it was invoked with, you can do the following: + + .. code-block:: python3 + + @bot.bridge_command() + async def example(ctx: BridgeContext): + if ctx.is_app: + command_type = "Application command" + else: + command_type = "Traditional (prefix-based) command" + await ctx.send(f"This command was invoked with a(n) {command_type}.") + + .. versionadded:: 2.0 + """ + + @abstractmethod + async def _respond( + self, *args, **kwargs + ) -> Interaction | WebhookMessage | Message: ... + + @abstractmethod + async def _defer(self, *args, **kwargs) -> None: ... + + @abstractmethod + async def _edit(self, *args, **kwargs) -> InteractionMessage | Message: ... + + @overload + async def invoke( + self, command: BridgeSlashCommand | BridgeExtCommand, *args, **kwargs + ) -> None: ... + + async def respond(self, *args, **kwargs) -> Interaction | WebhookMessage | Message: + """|coro| + + Responds to the command with the respective response type to the current context. In :class:`BridgeExtContext`, + this will be :meth:`~.Context.reply` while in :class:`BridgeApplicationContext`, this will be + :meth:`~.ApplicationContext.respond`. + """ + return await self._respond(*args, **kwargs) + + async def reply(self, *args, **kwargs) -> Interaction | WebhookMessage | Message: + """|coro| + + Alias for :meth:`~.BridgeContext.respond`. + """ + return await self.respond(*args, **kwargs) + + async def defer(self, *args, **kwargs) -> None: + """|coro| + + Defers the command with the respective approach to the current context. In :class:`BridgeExtContext`, this will + be :meth:`~discord.abc.Messageable.trigger_typing` while in :class:`BridgeApplicationContext`, this will be + :attr:`~.ApplicationContext.defer`. + + .. note:: + There is no ``trigger_typing`` alias for this method. ``trigger_typing`` will always provide the same + functionality across contexts. + """ + return await self._defer(*args, **kwargs) + + async def edit(self, *args, **kwargs) -> InteractionMessage | Message: + """|coro| + + Edits the original response message with the respective approach to the current context. In + :class:`BridgeExtContext`, this will have a custom approach where :meth:`.respond` caches the message to be + edited here. In :class:`BridgeApplicationContext`, this will be :attr:`~.ApplicationContext.edit`. + """ + return await self._edit(*args, **kwargs) + + def _get_super(self, attr: str) -> Any: + return getattr(super(), attr) + + @property + def is_app(self) -> bool: + """Whether the context is an :class:`BridgeApplicationContext` or not.""" + return isinstance(self, BridgeApplicationContext) + + +class BridgeApplicationContext(BridgeContext, ApplicationContext): + """ + The application context class for compatibility commands. This class is a subclass of :class:`BridgeContext` and + :class:`~.ApplicationContext`. This class is meant to be used with :class:`BridgeCommand`. + + .. versionadded:: 2.0 + """ + + def __init__(self, *args, **kwargs): + # This is needed in order to represent the correct class init signature on the docs + super().__init__(*args, **kwargs) + + async def _respond(self, *args, **kwargs) -> Interaction | WebhookMessage: + return await self._get_super("respond")(*args, **kwargs) + + async def _defer(self, *args, **kwargs) -> None: + return await self._get_super("defer")(*args, **kwargs) + + async def _edit(self, *args, **kwargs) -> InteractionMessage: + return await self._get_super("edit")(*args, **kwargs) + + +class BridgeExtContext(BridgeContext, Context): + """ + The ext.commands context class for compatibility commands. This class is a subclass of :class:`BridgeContext` and + :class:`~.Context`. This class is meant to be used with :class:`BridgeCommand`. + + .. versionadded:: 2.0 + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._original_response_message: Message | None = None + + async def _respond(self, *args, **kwargs) -> Message: + kwargs.pop("ephemeral", None) + message = await self._get_super("reply")(*args, **kwargs) + if self._original_response_message is None: + self._original_response_message = message + return message + + async def _defer(self, *args, **kwargs) -> None: + kwargs.pop("ephemeral", None) + return await self._get_super("trigger_typing")(*args, **kwargs) + + async def _edit(self, *args, **kwargs) -> Message | None: + if self._original_response_message: + return await self._original_response_message.edit(*args, **kwargs) + + async def delete( + self, *, delay: float | None = None, reason: str | None = None + ) -> None: + """|coro| + + Deletes the original response message, if it exists. + + Parameters + ---------- + delay: Optional[:class:`float`] + If provided, the number of seconds to wait before deleting the message. + reason: Optional[:class:`str`] + The reason for deleting the message. Shows up on the audit log. + """ + if self._original_response_message: + await self._original_response_message.delete(delay=delay, reason=reason) + + +Context = Union[BridgeExtContext, BridgeApplicationContext] +""" +A Union class for either :class:`BridgeExtContext` or :class:`BridgeApplicationContext`. +Can be used as a type hint for Context for bridge commands. +""" diff --git a/venv/lib/python3.11/site-packages/discord/ext/bridge/core.py b/venv/lib/python3.11/site-packages/discord/ext/bridge/core.py new file mode 100644 index 0000000..e61daa5 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/bridge/core.py @@ -0,0 +1,689 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import inspect +from collections.abc import Iterator +from typing import TYPE_CHECKING, Any, Callable + +import discord.commands.options +from discord import ( + ApplicationCommand, + Attachment, + Option, + Permissions, + SlashCommand, + SlashCommandGroup, + SlashCommandOptionType, +) + +from ...utils import MISSING, find, get, warn_deprecated +from ..commands import ( + BadArgument, +) +from ..commands import Bot as ExtBot +from ..commands import ( + Command, + Context, + Converter, + Group, + GuildChannelConverter, + MemberConverter, + RoleConverter, + UserConverter, +) +from ..commands.converter import _convert_to_bool, run_converters + +if TYPE_CHECKING: + from .context import BridgeApplicationContext, BridgeExtContext + + +__all__ = ( + "BridgeCommand", + "BridgeCommandGroup", + "bridge_command", + "bridge_group", + "bridge_option", + "BridgeExtCommand", + "BridgeSlashCommand", + "BridgeExtGroup", + "BridgeSlashGroup", + "BridgeOption", + "map_to", + "guild_only", + "has_permissions", + "is_nsfw", +) + + +class BridgeSlashCommand(SlashCommand): + """A subclass of :class:`.SlashCommand` that is used for bridge commands.""" + + def __init__(self, func, **kwargs): + self.brief = kwargs.pop("brief", None) + super().__init__(func, **kwargs) + + async def dispatch_error( + self, ctx: BridgeApplicationContext, error: Exception + ) -> None: + await super().dispatch_error(ctx, error) + ctx.bot.dispatch("bridge_command_error", ctx, error) + + +class BridgeExtCommand(Command): + """A subclass of :class:`.ext.commands.Command` that is used for bridge commands.""" + + def __init__(self, func, **kwargs): + super().__init__(func, **kwargs) + + for option in self.params.values(): + if isinstance(option.annotation, Option) and not isinstance( + option.annotation, BridgeOption + ): + raise TypeError( + f"{option.annotation.__class__.__name__} is not supported in bridge commands. Use BridgeOption instead." + ) + + async def dispatch_error(self, ctx: BridgeExtContext, error: Exception) -> None: + await super().dispatch_error(ctx, error) + ctx.bot.dispatch("bridge_command_error", ctx, error) + + async def transform(self, ctx: Context, param: inspect.Parameter) -> Any: + if param.annotation is Attachment: + # skip the parameter checks for bridge attachments + return await run_converters(ctx, AttachmentConverter, None, param) + else: + return await super().transform(ctx, param) + + +class BridgeSlashGroup(SlashCommandGroup): + """A subclass of :class:`.SlashCommandGroup` that is used for bridge commands.""" + + __slots__ = ("module",) + + def __init__(self, callback, *args, **kwargs): + if perms := getattr(callback, "__default_member_permissions__", None): + kwargs["default_member_permissions"] = perms + super().__init__(*args, **kwargs) + self.callback = callback + self.__original_kwargs__["callback"] = callback + self.__command = None + + async def _invoke(self, ctx: BridgeApplicationContext) -> None: + if not (options := ctx.interaction.data.get("options")): + if not self.__command: + self.__command = BridgeSlashCommand(self.callback) + ctx.command = self.__command + return await ctx.command.invoke(ctx) + option = options[0] + resolved = ctx.interaction.data.get("resolved", None) + command = find(lambda x: x.name == option["name"], self.subcommands) + option["resolved"] = resolved + ctx.interaction.data = option + await command.invoke(ctx) + + +class BridgeExtGroup(BridgeExtCommand, Group): + """A subclass of :class:`.ext.commands.Group` that is used for bridge commands.""" + + +class BridgeCommand: + """Compatibility class between prefixed-based commands and slash commands. + + Parameters + ---------- + callback: Callable[[:class:`.BridgeContext`, ...], Awaitable[Any]] + The callback to invoke when the command is executed. The first argument will be a :class:`BridgeContext`, + and any additional arguments will be passed to the callback. This callback must be a coroutine. + parent: Optional[:class:`.BridgeCommandGroup`]: + Parent of the BridgeCommand. + kwargs: Optional[Dict[:class:`str`, Any]] + Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommand` and :class:`.ext.commands.Command`) + + Attributes + ---------- + slash_variant: :class:`.BridgeSlashCommand` + The slash command version of this bridge command. + ext_variant: :class:`.BridgeExtCommand` + The prefix-based version of this bridge command. + """ + + __bridge__: bool = True + + __special_attrs__ = ["slash_variant", "ext_variant", "parent"] + + def __init__(self, callback, **kwargs): + self.parent = kwargs.pop("parent", None) + self.slash_variant: BridgeSlashCommand = kwargs.pop( + "slash_variant", None + ) or BridgeSlashCommand(callback, **kwargs) + self.ext_variant: BridgeExtCommand = kwargs.pop( + "ext_variant", None + ) or BridgeExtCommand(callback, **kwargs) + + @property + def name_localizations(self) -> dict[str, str] | None: + """Returns name_localizations from :attr:`slash_variant` + You can edit/set name_localizations directly with + + .. code-block:: python3 + + bridge_command.name_localizations["en-UK"] = ... # or any other locale + # or + bridge_command.name_localizations = {"en-UK": ..., "fr-FR": ...} + """ + return self.slash_variant.name_localizations + + @name_localizations.setter + def name_localizations(self, value): + self.slash_variant.name_localizations = value + + @property + def description_localizations(self) -> dict[str, str] | None: + """Returns description_localizations from :attr:`slash_variant` + You can edit/set description_localizations directly with + + .. code-block:: python3 + + bridge_command.description_localizations["en-UK"] = ... # or any other locale + # or + bridge_command.description_localizations = {"en-UK": ..., "fr-FR": ...} + """ + return self.slash_variant.description_localizations + + @description_localizations.setter + def description_localizations(self, value): + self.slash_variant.description_localizations = value + + def __getattribute__(self, name): + try: + # first, look for the attribute on the bridge command + return super().__getattribute__(name) + except AttributeError as e: + # if it doesn't exist, check this list, if the name of + # the parameter is here + if name in self.__special_attrs__: + raise e + + # looks up the result in the variants. + # slash cmd prioritized + result = getattr(self.slash_variant, name, MISSING) + try: + if result is MISSING: + return getattr(self.ext_variant, name) + return result + except AttributeError: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{name}'" + ) + + def __setattr__(self, name, value) -> None: + if name not in self.__special_attrs__: + setattr(self.slash_variant, name, value) + setattr(self.ext_variant, name, value) + + return super().__setattr__(name, value) + + def add_to(self, bot: ExtBot) -> None: + """Adds the command to a bot. This method is inherited by :class:`.BridgeCommandGroup`. + + Parameters + ---------- + bot: Union[:class:`.Bot`, :class:`.AutoShardedBot`] + The bot to add the command to. + """ + bot.add_application_command(self.slash_variant) + bot.add_command(self.ext_variant) + + async def invoke( + self, ctx: BridgeExtContext | BridgeApplicationContext, /, *args, **kwargs + ): + if ctx.is_app: + return await self.slash_variant.invoke(ctx) + return await self.ext_variant.invoke(ctx) + + def error(self, coro): + """A decorator that registers a coroutine as a local error handler. + + This error handler is limited to the command it is defined to. + However, higher scope handlers (per-cog and global) are still + invoked afterwards as a catch-all. This handler also functions as + the handler for both the prefixed and slash versions of the command. + + This error handler takes two parameters, a :class:`.BridgeContext` and + a :class:`~discord.DiscordException`. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the local error handler. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + self.slash_variant.error(coro) + self.ext_variant.on_error = coro + + return coro + + def before_invoke(self, coro): + """A decorator that registers a coroutine as a pre-invoke hook. + + This hook is called directly before the command is called, making + it useful for any sort of set up required. This hook is called + for both the prefixed and slash versions of the command. + + This pre-invoke hook takes a sole parameter, a :class:`.BridgeContext`. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the pre-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + self.slash_variant.before_invoke(coro) + self.ext_variant._before_invoke = coro + + return coro + + def after_invoke(self, coro): + """A decorator that registers a coroutine as a post-invoke hook. + + This hook is called directly after the command is called, making it + useful for any sort of clean up required. This hook is called for + both the prefixed and slash versions of the command. + + This post-invoke hook takes a sole parameter, a :class:`.BridgeContext`. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the post-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + self.slash_variant.after_invoke(coro) + self.ext_variant._after_invoke = coro + + return coro + + +class BridgeCommandGroup(BridgeCommand): + """Compatibility class between prefixed-based commands and slash commands. + + Parameters + ---------- + callback: Callable[[:class:`.BridgeContext`, ...], Awaitable[Any]] + The callback to invoke when the command is executed. The first argument will be a :class:`BridgeContext`, + and any additional arguments will be passed to the callback. This callback must be a coroutine. + kwargs: Optional[Dict[:class:`str`, Any]] + Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommand` and :class:`.ext.commands.Command`) + + Attributes + ---------- + slash_variant: :class:`.SlashCommandGroup` + The slash command version of this command group. + ext_variant: :class:`.ext.commands.Group` + The prefix-based version of this command group. + subcommands: List[:class:`.BridgeCommand`] + List of bridge commands in this group + mapped: Optional[:class:`.SlashCommand`] + If :func:`map_to` is used, the mapped slash command. + """ + + __special_attrs__ = [ + "slash_variant", + "ext_variant", + "parent", + "subcommands", + "mapped", + ] + + ext_variant: BridgeExtGroup + slash_variant: BridgeSlashGroup + + def __init__(self, callback, *args, **kwargs): + ext_var = BridgeExtGroup(callback, *args, **kwargs) + kwargs.update({"name": ext_var.name}) + super().__init__( + callback, + ext_variant=ext_var, + slash_variant=BridgeSlashGroup(callback, *args, **kwargs), + parent=kwargs.pop("parent", None), + ) + + self.subcommands: list[BridgeCommand] = [] + + self.mapped: SlashCommand | None = None + if map_to := getattr(callback, "__custom_map_to__", None): + kwargs.update(map_to) + self.mapped = self.slash_variant.command(**kwargs)(callback) + + def walk_commands(self) -> Iterator[BridgeCommand]: + """An iterator that recursively walks through all the bridge group's subcommands. + + Yields + ------ + :class:`.BridgeCommand` + A bridge command of this bridge group. + """ + yield from self.subcommands + + def command(self, *args, **kwargs): + """A decorator to register a function as a subcommand. + + Parameters + ---------- + kwargs: Optional[Dict[:class:`str`, Any]] + Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommand` and :class:`.ext.commands.Command`) + """ + + def wrap(callback): + slash = self.slash_variant.command( + *args, + **kwargs, + cls=BridgeSlashCommand, + )(callback) + ext = self.ext_variant.command( + *args, + **kwargs, + cls=BridgeExtCommand, + )(callback) + command = BridgeCommand( + callback, parent=self, slash_variant=slash, ext_variant=ext + ) + self.subcommands.append(command) + return command + + return wrap + + +def bridge_command(**kwargs): + """A decorator that is used to wrap a function as a bridge command. + + Parameters + ---------- + kwargs: Optional[Dict[:class:`str`, Any]] + Keyword arguments that are directly passed to the respective command constructors. (:class:`.SlashCommand` and :class:`.ext.commands.Command`) + """ + + def decorator(callback): + return BridgeCommand(callback, **kwargs) + + return decorator + + +def bridge_group(**kwargs): + """A decorator that is used to wrap a function as a bridge command group. + + Parameters + ---------- + kwargs: Optional[Dict[:class:`str`, Any]] + Keyword arguments that are directly passed to the respective command constructors (:class:`.SlashCommandGroup` and :class:`.ext.commands.Group`). + """ + + def decorator(callback): + return BridgeCommandGroup(callback, **kwargs) + + return decorator + + +def map_to(name, description=None): + """To be used with bridge command groups, map the main command to a slash subcommand. + + Parameters + ---------- + name: :class:`str` + The new name of the mapped command. + description: Optional[:class:`str`] + The new description of the mapped command. + + Example + ------- + + .. code-block:: python3 + + @bot.bridge_group() + @bridge.map_to("show") + async def config(ctx: BridgeContext): + ... + + @config.command() + async def toggle(ctx: BridgeContext): + ... + + Prefixed commands will not be affected, but slash commands will appear as: + + .. code-block:: + + /config show + /config toggle + """ + + def decorator(callback): + callback.__custom_map_to__ = {"name": name, "description": description} + return callback + + return decorator + + +def guild_only(): + """Intended to work with :class:`.ApplicationCommand` and :class:`BridgeCommand`, adds a :func:`~ext.commands.check` + that locks the command to only run in guilds, and also registers the command as guild only client-side (on discord). + + Basically a utility function that wraps both :func:`discord.ext.commands.guild_only` and :func:`discord.commands.guild_only`. + """ + + def predicate(func: Callable | ApplicationCommand): + if isinstance(func, ApplicationCommand): + func.guild_only = True + else: + func.__guild_only__ = True + + from ..commands import guild_only + + return guild_only()(func) + + return predicate + + +def is_nsfw(): + """Intended to work with :class:`.ApplicationCommand` and :class:`BridgeCommand`, adds a :func:`~ext.commands.check` + that locks the command to only run in nsfw contexts, and also registers the command as nsfw client-side (on discord). + + Basically a utility function that wraps both :func:`discord.ext.commands.is_nsfw` and :func:`discord.commands.is_nsfw`. + + .. warning:: + + In DMs, the prefixed-based command will always run as the user's privacy settings cannot be checked directly. + """ + + def predicate(func: Callable | ApplicationCommand): + if isinstance(func, ApplicationCommand): + func.nsfw = True + else: + func.__nsfw__ = True + + from ..commands import is_nsfw + + return is_nsfw()(func) + + return predicate + + +def has_permissions(**perms: bool): + r"""Intended to work with :class:`.SlashCommand` and :class:`BridgeCommand`, adds a + :func:`~ext.commands.check` that locks the command to be run by people with certain + permissions inside guilds, and also registers the command as locked behind said permissions. + + Basically a utility function that wraps both :func:`discord.ext.commands.has_permissions` + and :func:`discord.commands.default_permissions`. + + Parameters + ---------- + \*\*perms: Dict[:class:`str`, :class:`bool`] + An argument list of permissions to check for. + """ + + def predicate(func: Callable | ApplicationCommand): + from ..commands import has_permissions + + func = has_permissions(**perms)(func) + _perms = Permissions(**perms) + if isinstance(func, ApplicationCommand): + func.default_member_permissions = _perms + else: + func.__default_member_permissions__ = _perms + + return func + + return predicate + + +class MentionableConverter(Converter): + """A converter that can convert a mention to a member, a user or a role.""" + + async def convert(self, ctx, argument): + try: + return await RoleConverter().convert(ctx, argument) + except BadArgument: + pass + + if ctx.guild: + try: + return await MemberConverter().convert(ctx, argument) + except BadArgument: + pass + + return await UserConverter().convert(ctx, argument) + + +class AttachmentConverter(Converter): + async def convert(self, ctx: Context, arg: str): + try: + attach = ctx.message.attachments[0] + except IndexError: + raise BadArgument("At least 1 attachment is needed") + else: + return attach + + +class BooleanConverter(Converter): + async def convert(self, ctx, arg: bool): + return _convert_to_bool(str(arg)) + + +BRIDGE_CONVERTER_MAPPING = { + SlashCommandOptionType.string: str, + SlashCommandOptionType.integer: int, + SlashCommandOptionType.boolean: BooleanConverter, + SlashCommandOptionType.user: UserConverter, + SlashCommandOptionType.channel: GuildChannelConverter, + SlashCommandOptionType.role: RoleConverter, + SlashCommandOptionType.mentionable: MentionableConverter, + SlashCommandOptionType.number: float, + SlashCommandOptionType.attachment: AttachmentConverter, + discord.Member: MemberConverter, +} + + +class BridgeOption(Option, Converter): + """A subclass of :class:`discord.Option` which represents a selectable slash + command option and a prefixed command argument for bridge commands. + """ + + def __init__(self, input_type, *args, **kwargs): + self.converter = kwargs.pop("converter", None) + super().__init__(input_type, *args, **kwargs) + + self.converter = self.converter or BRIDGE_CONVERTER_MAPPING.get(input_type) + + async def convert(self, ctx, argument: str) -> Any: + try: + if self.converter is not None: + converted = await self.converter().convert(ctx, argument) + else: + converter = BRIDGE_CONVERTER_MAPPING.get(self.input_type) + if isinstance(converter, type) and issubclass(converter, Converter): + converted = await converter().convert(ctx, argument) # type: ignore # protocol class + elif callable(converter): + converted = converter(argument) + else: + raise TypeError(f"Invalid converter: {converter}") + + if self.choices: + choices_names: list[str | int | float] = [ + choice.name for choice in self.choices + ] + if converted in choices_names and ( + choice := get(self.choices, name=converted) + ): + converted = choice.value + else: + choices = [choice.value for choice in self.choices] + if converted not in choices: + raise ValueError( + f"{argument} is not a valid choice. Valid choices:" + f" {list(set(choices_names + choices))}" + ) + + return converted + except ValueError as exc: + raise BadArgument() from exc + + +def bridge_option(name, input_type=None, **kwargs): + """A decorator that can be used instead of typehinting :class:`.BridgeOption`. + + .. versionadded:: 2.6 + + Attributes + ---------- + parameter_name: :class:`str` + The name of the target function parameter this option is mapped to. + This allows you to have a separate UI ``name`` and parameter name. + """ + + def decorator(func): + resolved_name = kwargs.pop("parameter_name", None) or name + itype = ( + kwargs.pop("type", None) + or input_type + or func.__annotations__.get(resolved_name, str) + ) + func.__annotations__[resolved_name] = BridgeOption(itype, name=name, **kwargs) + return func + + return decorator diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__init__.py b/venv/lib/python3.11/site-packages/discord/ext/commands/__init__.py new file mode 100644 index 0000000..b13b484 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/commands/__init__.py @@ -0,0 +1,19 @@ +""" +discord.ext.commands +~~~~~~~~~~~~~~~~~~~~~ + +An extension module to facilitate creation of bot commands. + +:copyright: (c) 2015-2021 Rapptz & (c) 2021-present Pycord Development +:license: MIT, see LICENSE for more details. +""" + +from .bot import * +from .cog import * +from .context import * +from .converter import * +from .cooldowns import * +from .core import * +from .errors import * +from .flags import * +from .help import * diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/__init__.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..8252cab Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/__init__.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/_types.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/_types.cpython-311.pyc new file mode 100644 index 0000000..6ed103f Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/_types.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/bot.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/bot.cpython-311.pyc new file mode 100644 index 0000000..b8f3f16 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/bot.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/cog.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/cog.cpython-311.pyc new file mode 100644 index 0000000..56a727a Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/cog.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/context.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/context.cpython-311.pyc new file mode 100644 index 0000000..8ab2ed5 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/context.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/converter.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/converter.cpython-311.pyc new file mode 100644 index 0000000..cfcd871 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/converter.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/cooldowns.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/cooldowns.cpython-311.pyc new file mode 100644 index 0000000..6624fc2 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/cooldowns.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/core.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/core.cpython-311.pyc new file mode 100644 index 0000000..49b6600 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/core.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/errors.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/errors.cpython-311.pyc new file mode 100644 index 0000000..e2a1464 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/errors.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/flags.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/flags.cpython-311.pyc new file mode 100644 index 0000000..86cc269 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/flags.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/help.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/help.cpython-311.pyc new file mode 100644 index 0000000..76b3a20 Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/help.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/view.cpython-311.pyc b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/view.cpython-311.pyc new file mode 100644 index 0000000..d59b30b Binary files /dev/null and b/venv/lib/python3.11/site-packages/discord/ext/commands/__pycache__/view.cpython-311.pyc differ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/_types.py b/venv/lib/python3.11/site-packages/discord/ext/commands/_types.py new file mode 100644 index 0000000..d3f0336 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/commands/_types.py @@ -0,0 +1,49 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from typing import TYPE_CHECKING, Any, Callable, Coroutine, TypeVar, Union + +if TYPE_CHECKING: + from .cog import Cog + from .context import Context + from .errors import CommandError + +T = TypeVar("T") + +Coro = Coroutine[Any, Any, T] +MaybeCoro = Union[T, Coro[T]] +CoroFunc = Callable[..., Coro[Any]] + +Check = Union[ + Callable[["Cog", "Context[Any]"], MaybeCoro[bool]], + Callable[["Context[Any]"], MaybeCoro[bool]], +] +Hook = Union[ + Callable[["Cog", "Context[Any]"], Coro[Any]], Callable[["Context[Any]"], Coro[Any]] +] +Error = Union[ + Callable[["Cog", "Context[Any]", "CommandError"], Coro[Any]], + Callable[["Context[Any]", "CommandError"], Coro[Any]], +] diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/bot.py b/venv/lib/python3.11/site-packages/discord/ext/commands/bot.py new file mode 100644 index 0000000..d348b6c --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/commands/bot.py @@ -0,0 +1,457 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import collections +import collections.abc +import sys +import traceback +from typing import TYPE_CHECKING, Any, Callable, Coroutine, Iterable, TypeVar + +import discord + +from . import errors +from .context import Context +from .core import GroupMixin +from .help import DefaultHelpCommand, HelpCommand +from .view import StringView + +if TYPE_CHECKING: + from discord.message import Message + + from ._types import CoroFunc + +__all__ = ( + "when_mentioned", + "when_mentioned_or", + "Bot", + "AutoShardedBot", +) + +MISSING: Any = discord.utils.MISSING + +T = TypeVar("T") +CFT = TypeVar("CFT", bound="CoroFunc") +CXT = TypeVar("CXT", bound="Context") + + +def when_mentioned(bot: Bot | AutoShardedBot, msg: Message) -> list[str]: + """A callable that implements a command prefix equivalent to being mentioned. + + These are meant to be passed into the :attr:`.Bot.command_prefix` attribute. + """ + # bot.user will never be None when this is called + return [f"<@{bot.user.id}> ", f"<@!{bot.user.id}> "] # type: ignore + + +def when_mentioned_or( + *prefixes: str, +) -> Callable[[Bot | AutoShardedBot, Message], list[str]]: + """A callable that implements when mentioned or other prefixes provided. + + These are meant to be passed into the :attr:`.Bot.command_prefix` attribute. + + See Also + -------- + :func:`.when_mentioned` + + Example + ------- + + .. code-block:: python3 + + bot = commands.Bot(command_prefix=commands.when_mentioned_or('!')) + + .. note:: + + This callable returns another callable, so if this is done inside a custom + callable, you must call the returned callable, for example: + + .. code-block:: python3 + + async def get_prefix(bot, message): + extras = await prefixes_for(message.guild) # returns a list + return commands.when_mentioned_or(*extras)(bot, message) + """ + + def inner(bot, msg): + r = list(prefixes) + r = when_mentioned(bot, msg) + r + return r + + return inner + + +def _is_submodule(parent: str, child: str) -> bool: + return parent == child or child.startswith(f"{parent}.") + + +class BotBase(GroupMixin, discord.cog.CogMixin): + _help_command = None + _supports_prefixed_commands = True + + def __init__( + self, + command_prefix: ( + str + | Iterable[str] + | Callable[ + [Bot | AutoShardedBot, Message], + str | Iterable[str] | Coroutine[Any, Any, str | Iterable[str]], + ] + ) = when_mentioned, + help_command: HelpCommand | None = MISSING, + **options, + ): + super().__init__(**options) + self.command_prefix = command_prefix + self.help_command = ( + DefaultHelpCommand() if help_command is MISSING else help_command + ) + self.strip_after_prefix = options.get("strip_after_prefix", False) + + @discord.utils.copy_doc(discord.Client.close) + async def close(self) -> None: + for extension in tuple(self.__extensions): + try: + self.unload_extension(extension) + except Exception: + pass + + for cog in tuple(self.__cogs): + try: + self.remove_cog(cog) + except Exception: + pass + + await super().close() # type: ignore + + async def on_command_error( + self, context: Context, exception: errors.CommandError + ) -> None: + """|coro| + + The default command error handler provided by the bot. + + By default, this prints to :data:`sys.stderr` however it could be + overridden to have a different implementation. + + This only fires if you do not specify any listeners for command error. + """ + if self._event_handlers.get("on_command_error", None): + return + + command = context.command + if command and command.has_error_handler(): + return + + cog = context.cog + if cog and cog.has_error_handler(): + return + + print(f"Ignoring exception in command {context.command}:", file=sys.stderr) + traceback.print_exception( + type(exception), exception, exception.__traceback__, file=sys.stderr + ) + + async def can_run(self, ctx: Context, *, call_once: bool = False) -> bool: + data = self._check_once if call_once else self._checks + + if len(data) == 0: + return True + + # type-checker doesn't distinguish between functions and methods + return await discord.utils.async_all(f(ctx) for f in data) # type: ignore + + # help command stuff + + @property + def help_command(self) -> HelpCommand | None: + return self._help_command + + @help_command.setter + def help_command(self, value: HelpCommand | None) -> None: + if value is not None: + if not isinstance(value, HelpCommand): + raise TypeError("help_command must be a subclass of HelpCommand") + if self._help_command is not None: + self._help_command._remove_from_bot(self) + self._help_command = value + value._add_to_bot(self) + elif self._help_command is not None: + self._help_command._remove_from_bot(self) + self._help_command = None + else: + self._help_command = None + + # command processing + + async def get_prefix(self, message: Message) -> list[str] | str: + """|coro| + + Retrieves the prefix the bot is listening to + with the message as a context. + + Parameters + ---------- + message: :class:`discord.Message` + The message context to get the prefix of. + + Returns + ------- + Union[List[:class:`str`], :class:`str`] + A list of prefixes or a single prefix that the bot is + listening for. + """ + prefix = ret = self.command_prefix + if callable(prefix): + ret = await discord.utils.maybe_coroutine(prefix, self, message) + + if not isinstance(ret, str): + try: + ret = list(ret) + except TypeError: + # It's possible that a generator raised this exception. Don't + # replace it with our own error if that's the case. + if isinstance(ret, collections.abc.Iterable): + raise + + raise TypeError( + "command_prefix must be plain string, iterable of strings, or" + f" callable returning either of these, not {ret.__class__.__name__}" + ) + + if not ret: + raise ValueError( + "Iterable command_prefix must contain at least one prefix" + ) + + return ret + + async def get_context(self, message: Message, *, cls: type[CXT] = Context) -> CXT: + r"""|coro| + + Returns the invocation context from the message. + + This is a more low-level counter-part for :meth:`.process_commands` + to allow users more fine-grained control over the processing. + + The returned context is not guaranteed to be a valid invocation + context, :attr:`.Context.valid` must be checked to make sure it is. + If the context is not valid then it is not a valid candidate to be + invoked under :meth:`~.Bot.invoke`. + + Parameters + ----------- + message: :class:`discord.Message` + The message to get the invocation context from. + cls + The factory class that will be used to create the context. + By default, this is :class:`.Context`. Should a custom + class be provided, it must be similar enough to :class:`.Context`\'s + interface. + + Returns + -------- + :class:`.Context` + The invocation context. The type of this can change via the + ``cls`` parameter. + """ + + view = StringView(message.content) + ctx = cls(prefix=None, view=view, bot=self, message=message) + + if message.author.id == self.user.id: # type: ignore + return ctx + + prefix = await self.get_prefix(message) + invoked_prefix = prefix + + if isinstance(prefix, str): + if not view.skip_string(prefix): + return ctx + else: + try: + # if the context class' __init__ consumes something from the view this + # will be wrong. That seems unreasonable though. + if message.content.startswith(tuple(prefix)): + invoked_prefix = discord.utils.find(view.skip_string, prefix) + else: + return ctx + + except TypeError: + if not isinstance(prefix, list): + raise TypeError( + "get_prefix must return either a string or a list of string, " + f"not {prefix.__class__.__name__}" + ) + + # It's possible a bad command_prefix got us here. + for value in prefix: + if not isinstance(value, str): + raise TypeError( + "Iterable command_prefix or list returned from get_prefix" + " must contain only strings, not" + f" {value.__class__.__name__}" + ) + + # Getting here shouldn't happen + raise + + if self.strip_after_prefix: + view.skip_ws() + + invoker = view.get_word() + ctx.invoked_with = invoker + # type-checker fails to narrow invoked_prefix type. + ctx.prefix = invoked_prefix # type: ignore + ctx.command = self.prefixed_commands.get(invoker) + return ctx + + async def invoke(self, ctx: Context) -> None: + """|coro| + + Invokes the command given under the invocation context and + handles all the internal event dispatch mechanisms. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context to invoke. + """ + if ctx.command is not None: + self.dispatch("command", ctx) + try: + if await self.can_run(ctx, call_once=True): + await ctx.command.invoke(ctx) + else: + raise errors.CheckFailure("The global check once functions failed.") + except errors.CommandError as exc: + await ctx.command.dispatch_error(ctx, exc) + else: + self.dispatch("command_completion", ctx) + elif ctx.invoked_with: + exc = errors.CommandNotFound(f'Command "{ctx.invoked_with}" is not found') + self.dispatch("command_error", ctx, exc) + + async def process_commands(self, message: Message) -> None: + """|coro| + + This function processes the commands that have been registered + to the bot and other groups. Without this coroutine, none of the + commands will be triggered. + + By default, this coroutine is called inside the :func:`.on_message` + event. If you choose to override the :func:`.on_message` event, then + you should invoke this coroutine as well. + + This is built using other low level tools, and is equivalent to a + call to :meth:`~.Bot.get_context` followed by a call to :meth:`~.Bot.invoke`. + + This also checks if the message's author is a bot and doesn't + call :meth:`~.Bot.get_context` or :meth:`~.Bot.invoke` if so. + + Parameters + ---------- + message: :class:`discord.Message` + The message to process commands for. + """ + if message.author.bot: + return + + ctx = await self.get_context(message) + await self.invoke(ctx) + + async def on_message(self, message): + await self.process_commands(message) + + +class Bot(BotBase, discord.Bot): + """Represents a discord bot. + + This class is a subclass of :class:`discord.Bot` and as a result + anything that you can do with a :class:`discord.Bot` you can do with + this bot. + + This class also subclasses :class:`.GroupMixin` to provide the functionality + to manage commands. + + .. note:: + + Using prefixed commands requires :attr:`discord.Intents.message_content` to be enabled. + + Attributes + ---------- + command_prefix + The command prefix is what the message content must contain initially + to have a command invoked. This prefix could either be a string to + indicate what the prefix should be, or a callable that takes in the bot + as its first parameter and :class:`discord.Message` as its second + parameter and returns the prefix. This is to facilitate "dynamic" + command prefixes. This callable can be either a regular function or + a coroutine. + + An empty string as the prefix always matches, enabling prefix-less + command invocation. While this may be useful in DMs it should be avoided + in servers, as it's likely to cause performance issues and unintended + command invocations. + + The command prefix could also be an iterable of strings indicating that + multiple checks for the prefix should be used and the first one to + match will be the invocation prefix. You can get this prefix via + :attr:`.Context.prefix`. To avoid confusion empty iterables are not + allowed. + + .. note:: + + When passing multiple prefixes be careful to not pass a prefix + that matches a longer prefix occurring later in the sequence. For + example, if the command prefix is ``('!', '!?')`` the ``'!?'`` + prefix will never be matched to any message as the previous one + matches messages starting with ``!?``. This is especially important + when passing an empty string, it should always be last as no prefix + after it will be matched. + case_insensitive: :class:`bool` + Whether the commands should be case-insensitive. Defaults to ``False``. This + attribute does not carry over to groups. You must set it to every group if + you require group commands to be case-insensitive as well. + help_command: Optional[:class:`.HelpCommand`] + The help command implementation to use. This can be dynamically + set at runtime. To remove the help command pass ``None``. For more + information on implementing a help command, see :ref:`ext_commands_help_command`. + strip_after_prefix: :class:`bool` + Whether to strip whitespace characters after encountering the command + prefix. This allows for ``! hello`` and ``!hello`` to both work if + the ``command_prefix`` is set to ``!``. Defaults to ``False``. + + .. versionadded:: 1.7 + """ + + +class AutoShardedBot(BotBase, discord.AutoShardedBot): + """This is similar to :class:`.Bot` except that it is inherited from + :class:`discord.AutoShardedBot` instead. + """ diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/cog.py b/venv/lib/python3.11/site-packages/discord/ext/commands/cog.py new file mode 100644 index 0000000..871d3e8 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/commands/cog.py @@ -0,0 +1,85 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Any, Callable, Generator, TypeVar + +import discord + +from ...cog import Cog +from ...commands import ApplicationCommand, SlashCommandGroup + +if TYPE_CHECKING: + from .core import Command + +__all__ = ("Cog",) + +CogT = TypeVar("CogT", bound="Cog") +FuncT = TypeVar("FuncT", bound=Callable[..., Any]) + +MISSING: Any = discord.utils.MISSING + + +class Cog(Cog): + def __new__(cls: type[CogT], *args: Any, **kwargs: Any) -> CogT: + # For issue 426, we need to store a copy of the command objects + # since we modify them to inject `self` to them. + # To do this, we need to interfere with the Cog creation process. + return super().__new__(cls) + + def walk_commands(self) -> Generator[Command]: + """An iterator that recursively walks through this cog's commands and subcommands. + + Yields + ------ + Union[:class:`.Command`, :class:`.Group`] + A command or group from the cog. + """ + from .core import GroupMixin + + for command in self.__cog_commands__: + if not isinstance(command, ApplicationCommand): + if command.parent is None: + yield command + if isinstance(command, GroupMixin): + yield from command.walk_commands() + elif isinstance(command, SlashCommandGroup): + yield from command.walk_commands() + else: + yield command + + def get_commands(self) -> list[ApplicationCommand | Command]: + r""" + Returns + -------- + List[Union[:class:`~discord.ApplicationCommand`, :class:`.Command`]] + A :class:`list` of commands that are defined inside this cog. + + .. note:: + + This does not include subcommands. + """ + return [c for c in self.__cog_commands__ if c.parent is None] diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/context.py b/venv/lib/python3.11/site-packages/discord/ext/commands/context.py new file mode 100644 index 0000000..11e2216 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/commands/context.py @@ -0,0 +1,411 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import inspect +import re +from typing import TYPE_CHECKING, Any, Generic, TypeVar, Union + +import discord.abc +import discord.utils +from discord.message import Message + +if TYPE_CHECKING: + from typing_extensions import ParamSpec + + from discord.abc import MessageableChannel + from discord.guild import Guild + from discord.member import Member + from discord.state import ConnectionState + from discord.user import ClientUser, User + from discord.voice import VoiceProtocol + + from .bot import AutoShardedBot, Bot + from .cog import Cog + from .core import Command + from .view import StringView + +__all__ = ("Context",) + +MISSING: Any = discord.utils.MISSING + + +T = TypeVar("T") +BotT = TypeVar("BotT", bound="Union[Bot, AutoShardedBot]") +CogT = TypeVar("CogT", bound="Cog") + +if TYPE_CHECKING: + P = ParamSpec("P") +else: + P = TypeVar("P") + + +class Context(discord.abc.Messageable, Generic[BotT]): + r"""Represents the context in which a command is being invoked under. + + This class contains a lot of metadata to help you understand more about + the invocation context. This class is not created manually and is instead + passed around to commands as the first parameter. + + This class implements the :class:`~discord.abc.Messageable` ABC. + + Attributes + ----------- + message: :class:`.Message` + The message that triggered the command being executed. + bot: :class:`.Bot` + The bot that contains the command being executed. + args: :class:`list` + The list of transformed arguments that were passed into the command. + If this is accessed during the :func:`.on_command_error` event + then this list could be incomplete. + kwargs: :class:`dict` + A dictionary of transformed arguments that were passed into the command. + Similar to :attr:`args`\, if this is accessed in the + :func:`.on_command_error` event then this dict could be incomplete. + current_parameter: Optional[:class:`inspect.Parameter`] + The parameter that is currently being inspected and converted. + This is only of use for within converters. + + .. versionadded:: 2.0 + prefix: Optional[:class:`str`] + The prefix that was used to invoke the command. + command: Optional[:class:`Command`] + The command that is being invoked currently. + invoked_with: Optional[:class:`str`] + The command name that triggered this invocation. Useful for finding out + which alias called the command. + invoked_parents: List[:class:`str`] + The command names of the parents that triggered this invocation. Useful for + finding out which aliases called the command. + + For example in commands ``?a b c test``, the invoked parents are ``['a', 'b', 'c']``. + + .. versionadded:: 1.7 + + invoked_subcommand: Optional[:class:`Command`] + The subcommand that was invoked. + If no valid subcommand was invoked then this is equal to ``None``. + subcommand_passed: Optional[:class:`str`] + The string that was attempted to call a subcommand. This does not have + to point to a valid registered subcommand and could just point to a + nonsense string. If nothing was passed to attempt a call to a + subcommand then this is set to ``None``. + command_failed: :class:`bool` + A boolean that indicates if the command failed to be parsed, checked, + or invoked. + """ + + def __init__( + self, + *, + message: Message, + bot: BotT, + view: StringView, + args: list[Any] = MISSING, + kwargs: dict[str, Any] = MISSING, + prefix: str | None = None, + command: Command | None = None, + invoked_with: str | None = None, + invoked_parents: list[str] = MISSING, + invoked_subcommand: Command | None = None, + subcommand_passed: str | None = None, + command_failed: bool = False, + current_parameter: inspect.Parameter | None = None, + ): + self.message: Message = message + self.bot: BotT = bot + self.args: list[Any] = args or [] + self.kwargs: dict[str, Any] = kwargs or {} + self.prefix: str | None = prefix + self.command: Command | None = command + self.view: StringView = view + self.invoked_with: str | None = invoked_with + self.invoked_parents: list[str] = invoked_parents or [] + self.invoked_subcommand: Command | None = invoked_subcommand + self.subcommand_passed: str | None = subcommand_passed + self.command_failed: bool = command_failed + self.current_parameter: inspect.Parameter | None = current_parameter + self._state: ConnectionState = self.message._state + + async def invoke( + self, command: Command[CogT, P, T], /, *args: P.args, **kwargs: P.kwargs + ) -> T: + r"""|coro| + + Calls a command with the arguments given. + + This is useful if you want to just call the callback that a + :class:`.Command` holds internally. + + .. note:: + + This does not handle converters, checks, cooldowns, pre-invoke, + or after-invoke hooks in any matter. It calls the internal callback + directly as-if it was a regular function. + + You must take care in passing the proper arguments when + using this function. + + Parameters + ----------- + command: :class:`.Command` + The command that is going to be called. + \*args + The arguments to use. + \*\*kwargs + The keyword arguments to use. + + Raises + ------- + TypeError + The command argument to invoke is missing. + """ + return await command(self, *args, **kwargs) + + async def reinvoke(self, *, call_hooks: bool = False, restart: bool = True) -> None: + """|coro| + + Calls the command again. + + This is similar to :meth:`~.Context.invoke` except that it bypasses + checks, cooldowns, and error handlers. + + .. note:: + + If you want to bypass :exc:`.UserInputError` derived exceptions, + it is recommended to use the regular :meth:`~.Context.invoke` + as it will work more naturally. After all, this will end up + using the old arguments the user has used and will thus just + fail again. + + Parameters + ---------- + call_hooks: :class:`bool` + Whether to call the before and after invoke hooks. + restart: :class:`bool` + Whether to start the call chain from the very beginning + or where we left off (i.e. the command that caused the error). + The default is to start where we left off. + + Raises + ------ + ValueError + The context to reinvoke is not valid. + """ + cmd = self.command + view = self.view + if cmd is None: + raise ValueError("This context is not valid.") + + # some state to revert to when we're done + index, previous = view.index, view.previous + invoked_with = self.invoked_with + invoked_subcommand = self.invoked_subcommand + invoked_parents = self.invoked_parents + subcommand_passed = self.subcommand_passed + + if restart: + to_call = cmd.root_parent or cmd + view.index = len(self.prefix or "") + view.previous = 0 + self.invoked_parents = [] + self.invoked_with = view.get_word() # advance to get the root command + else: + to_call = cmd + + try: + await to_call.reinvoke(self, call_hooks=call_hooks) + finally: + self.command = cmd + view.index = index + view.previous = previous + self.invoked_with = invoked_with + self.invoked_subcommand = invoked_subcommand + self.invoked_parents = invoked_parents + self.subcommand_passed = subcommand_passed + + @property + def valid(self) -> bool: + """Checks if the invocation context is valid to be invoked with.""" + return self.prefix is not None and self.command is not None + + async def _get_channel(self) -> discord.abc.Messageable: + return self.channel + + @property + def clean_prefix(self) -> str: + """The cleaned up invoke prefix. i.e. mentions are ``@name`` instead of ``<@id>``. + + .. versionadded:: 2.0 + """ + if self.prefix is None: + return "" + + user = self.me + # this breaks if the prefix mention is not the bot itself, but I + # consider this to be an *incredibly* strange use case. I'd rather go + # for this common use case rather than waste performance for the + # odd one. + pattern = re.compile(r"<@!?%s>" % user.id) + return pattern.sub("@%s" % user.display_name.replace("\\", r"\\"), self.prefix) + + @property + def cog(self) -> Cog | None: + """Returns the cog associated with this context's command. + None if it does not exist. + """ + + if self.command is None: + return None + return self.command.cog + + @property + def guild(self) -> Guild | None: + """Returns the guild associated with this context's command. + None if not available. + """ + return self.message.guild + + @property + def channel(self) -> MessageableChannel: + """Returns the channel associated with this context's command. + Shorthand for :attr:`.Message.channel`. + """ + return self.message.channel + + @property + def author(self) -> User | Member: + """Union[:class:`~discord.User`, :class:`.Member`]: + Returns the author associated with this context's command. Shorthand for :attr:`.Message.author` + """ + return self.message.author + + @property + def me(self) -> Member | ClientUser: + """Union[:class:`.Member`, :class:`.ClientUser`]: + Similar to :attr:`.Guild.me` except it may return the :class:`.ClientUser` in private message + message contexts, or when :meth:`Intents.guilds` is absent. + """ + # bot.user will never be None at this point. + return self.guild.me if self.guild is not None and self.guild.me is not None else self.bot.user # type: ignore + + @property + def voice_client(self) -> VoiceProtocol | None: + r"""A shortcut to :attr:`.Guild.voice_client`\, if applicable.""" + g = self.guild + return g.voice_client if g else None + + async def send_help(self, *args: Any) -> Any: + """send_help(entity=) + + |coro| + + Shows the help command for the specified entity if given. + The entity can be a command or a cog. + + If no entity is given, then it'll show help for the + entire bot. + + If the entity is a string, then it looks up whether it's a + :class:`Cog` or a :class:`Command`. + + .. note:: + + Due to the way this function works, instead of returning + something similar to :meth:`~.commands.HelpCommand.command_not_found` + this returns :class:`None` on bad input or no help command. + + Parameters + ---------- + entity: Optional[Union[:class:`Command`, :class:`Cog`, :class:`str`]] + The entity to show help for. + + Returns + ------- + Any + The result of the help command, if any. + """ + from .core import Command, Group, wrap_callback + from .errors import CommandError + + bot = self.bot + cmd = bot.help_command + + if cmd is None: + return None + + cmd = cmd.copy() + cmd.context = self + if len(args) == 0: + await cmd.prepare_help_command(self, None) + mapping = cmd.get_bot_mapping() + injected = wrap_callback(cmd.send_bot_help) + try: + return await injected(mapping) + except CommandError as e: + await cmd.on_help_command_error(self, e) + return None + + entity = args[0] + if isinstance(entity, str): + entity = bot.get_cog(entity) or bot.get_command(entity) + + if entity is None: + return None + + try: + entity.qualified_name + except AttributeError: + # if we're here then it's not a cog, group, or command. + return None + + await cmd.prepare_help_command(self, entity.qualified_name) + + try: + if hasattr(entity, "__cog_commands__"): + injected = wrap_callback(cmd.send_cog_help) + return await injected(entity) + elif isinstance(entity, Group): + injected = wrap_callback(cmd.send_group_help) + return await injected(entity) + elif isinstance(entity, Command): + injected = wrap_callback(cmd.send_command_help) + return await injected(entity) + else: + return None + except CommandError as e: + await cmd.on_help_command_error(self, e) + + @discord.utils.copy_doc(Message.reply) + async def reply(self, content: str | None = None, **kwargs: Any) -> Message: + return await self.message.reply(content, **kwargs) + + @discord.utils.copy_doc(Message.forward_to) + async def forward_to( + self, channel: discord.abc.Messageable, **kwargs: Any + ) -> Message: + return await self.message.forward_to(channel, **kwargs) diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/converter.py b/venv/lib/python3.11/site-packages/discord/ext/commands/converter.py new file mode 100644 index 0000000..ca5a109 --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/commands/converter.py @@ -0,0 +1,1269 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import inspect +import re +from typing import ( + TYPE_CHECKING, + Any, + Generic, + Iterable, + List, + Literal, + Protocol, + TypeVar, + Union, + runtime_checkable, +) + +import discord +from discord.utils import UNICODE_EMOJIS + +from .errors import * + +if TYPE_CHECKING: + from discord.message import PartialMessageableChannel + + from .context import Context + + +__all__ = ( + "Converter", + "ObjectConverter", + "MemberConverter", + "UserConverter", + "MessageConverter", + "PartialMessageConverter", + "TextChannelConverter", + "ForumChannelConverter", + "InviteConverter", + "GuildConverter", + "RoleConverter", + "GameConverter", + "ColourConverter", + "ColorConverter", + "VoiceChannelConverter", + "StageChannelConverter", + "EmojiConverter", + "PartialEmojiConverter", + "CategoryChannelConverter", + "IDConverter", + "ThreadConverter", + "GuildChannelConverter", + "GuildStickerConverter", + "clean_content", + "Greedy", + "run_converters", +) + + +def _get_from_guilds(bot, getter, argument): + result = None + for guild in bot.guilds: + result = getattr(guild, getter)(argument) + if result: + return result + return result + + +_utils_get = discord.utils.get +T = TypeVar("T") +T_co = TypeVar("T_co", covariant=True) +CT = TypeVar("CT", bound=discord.abc.GuildChannel) +TT = TypeVar("TT", bound=discord.Thread) + + +@runtime_checkable +class Converter(Protocol[T_co]): + """The base class of custom converters that require the :class:`.Context` + to be passed to be useful. + + This allows you to implement converters that function similar to the + special cased ``discord`` classes. + + Classes that derive from this should override the :meth:`~.Converter.convert` + method to do its conversion logic. This method must be a :ref:`coroutine `. + """ + + async def convert(self, ctx: Context, argument: str) -> T_co: + """|coro| + + The method to override to do conversion logic. + + If an error is found while converting, it is recommended to + raise a :exc:`.CommandError` derived exception as it will + properly propagate to the error handlers. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context that the argument is being used in. + argument: :class:`str` + The argument that is being converted. + + Raises + ------ + :exc:`.CommandError` + A generic exception occurred when converting the argument. + :exc:`.BadArgument` + The converter failed to convert the argument. + """ + raise NotImplementedError("Derived classes need to implement this.") + + +_ID_REGEX = re.compile(r"([0-9]{15,20})$") + + +class IDConverter(Converter[T_co]): + @staticmethod + def _get_id_match(argument): + return _ID_REGEX.match(argument) + + +class ObjectConverter(IDConverter[discord.Object]): + """Converts to a :class:`~discord.Object`. + + The argument must follow the valid ID or mention formats (e.g. `<@80088516616269824>`). + + .. versionadded:: 2.0 + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by member, role, or channel mention. + """ + + async def convert(self, ctx: Context, argument: str) -> discord.Object: + match = self._get_id_match(argument) or re.match( + r"<(?:@[!&]?|#)([0-9]{15,20})>$", argument + ) + + if match is None: + raise ObjectNotFound(argument) + + result = int(match.group(1)) + + return discord.Object(id=result) + + +class MemberConverter(IDConverter[discord.Member]): + """Converts to a :class:`~discord.Member`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name#discrim + 4. Lookup by name + 5. Lookup by nickname + + .. versionchanged:: 1.5 + Raise :exc:`.MemberNotFound` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 1.5.1 + This converter now lazily fetches members from the gateway and HTTP APIs, + optionally caching the result if :attr:`.MemberCacheFlags.joined` is enabled. + """ + + async def query_member_named(self, guild, argument): + cache = guild._state.member_cache_flags.joined + if len(argument) > 5 and argument[-5] == "#": + username, _, discriminator = argument.rpartition("#") + members = await guild.query_members(username, limit=100, cache=cache) + return discord.utils.get( + members, name=username, discriminator=discriminator + ) + members = await guild.query_members(argument, limit=100, cache=cache) + return discord.utils.find( + lambda m: argument in (m.nick, m.name, m.global_name), + members, + ) + + async def query_member_by_id(self, bot, guild, user_id): + ws = bot._get_websocket(shard_id=guild.shard_id) + cache = guild._state.member_cache_flags.joined + if ws.is_ratelimited(): + # If we're being rate limited on the WS, then fall back to using the HTTP API + # So we don't have to wait ~60 seconds for the query to finish + try: + member = await guild.fetch_member(user_id) + except discord.HTTPException: + return None + + if cache: + guild._add_member(member) + return member + + # If we're not being rate limited then we can use the websocket to actually query + members = await guild.query_members(limit=1, user_ids=[user_id], cache=cache) + if not members: + return None + return members[0] + + async def convert(self, ctx: Context, argument: str) -> discord.Member: + bot = ctx.bot + match = self._get_id_match(argument) or re.match( + r"<@!?([0-9]{15,20})>$", argument + ) + guild = ctx.guild + result = None + user_id = None + if match is None: + # not a mention... + if guild: + result = guild.get_member_named(argument) + else: + result = _get_from_guilds(bot, "get_member_named", argument) + else: + user_id = int(match.group(1)) + if guild: + result = guild.get_member(user_id) + if ctx.message is not None and result is None: + result = _utils_get(ctx.message.mentions, id=user_id) + else: + result = _get_from_guilds(bot, "get_member", user_id) + + if result is None: + if guild is None: + raise MemberNotFound(argument) + + if user_id is not None: + result = await self.query_member_by_id(bot, guild, user_id) + else: + result = await self.query_member_named(guild, argument) + + if not result: + raise MemberNotFound(argument) + + return result + + +class UserConverter(IDConverter[discord.User]): + """Converts to a :class:`~discord.User`. + + All lookups are via the global user cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name#discrim + 4. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.UserNotFound` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 1.6 + This converter now lazily fetches users from the HTTP APIs if an ID is + passed, and it's not available in cache. + """ + + async def convert(self, ctx: Context, argument: str) -> discord.User: + match = self._get_id_match(argument) or re.match( + r"<@!?([0-9]{15,20})>$", argument + ) + result = None + state = ctx._state + + if match is not None: + user_id = int(match.group(1)) + result = ctx.bot.get_user(user_id) + if ctx.message is not None and result is None: + result = _utils_get(ctx.message.mentions, id=user_id) + if result is None: + try: + result = await ctx.bot.fetch_user(user_id) + except discord.HTTPException: + raise UserNotFound(argument) from None + + return result + + arg = argument + + # Remove the '@' character if this is the first character from the argument + if arg[0] == "@": + # Remove first character + arg = arg[1:] + + # check for discriminator if it exists, + if len(arg) > 5 and arg[-5] == "#": + discrim = arg[-4:] + name = arg[:-5] + predicate = lambda u: u.name == name and u.discriminator == discrim + result = discord.utils.find(predicate, state._users.values()) + if result is not None: + return result + + predicate = lambda u: arg in (u.name, u.global_name) + result = discord.utils.find(predicate, state._users.values()) + + if result is None: + raise UserNotFound(argument) + + return result + + +class PartialMessageConverter(Converter[discord.PartialMessage]): + """Converts to a :class:`discord.PartialMessage`. + + .. versionadded:: 1.7 + + The creation strategy is as follows (in order): + + 1. By "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID") + 2. By message ID (The message is assumed to be in the context channel.) + 3. By message URL + """ + + @staticmethod + def _get_id_matches(ctx, argument): + id_regex = re.compile( + r"(?:(?P[0-9]{15,20})-)?(?P[0-9]{15,20})$" + ) + link_regex = re.compile( + r"https?://(?:(ptb|canary|www)\.)?discord(?:app)?\.com/channels/" + r"(?P[0-9]{15,20}|@me)" + r"/(?P[0-9]{15,20})/(?P[0-9]{15,20})/?$" + ) + match = id_regex.match(argument) or link_regex.match(argument) + if not match: + raise MessageNotFound(argument) + data = match.groupdict() + channel_id = data.get("channel_id") + if channel_id is None: + channel_id = ctx.channel and ctx.channel.id + else: + channel_id = int(channel_id) + message_id = int(data["message_id"]) + guild_id = data.get("guild_id") + if guild_id is None: + guild_id = ctx.guild and ctx.guild.id + elif guild_id == "@me": + guild_id = None + else: + guild_id = int(guild_id) + return guild_id, message_id, channel_id + + @staticmethod + def _resolve_channel(ctx, guild_id, channel_id) -> PartialMessageableChannel | None: + if guild_id is not None: + guild = ctx.bot.get_guild(guild_id) + if guild is not None and channel_id is not None: + return guild._resolve_channel(channel_id) # type: ignore + else: + return None + else: + return ctx.bot.get_channel(channel_id) if channel_id else ctx.channel + + async def convert(self, ctx: Context, argument: str) -> discord.PartialMessage: + guild_id, message_id, channel_id = self._get_id_matches(ctx, argument) + channel = self._resolve_channel(ctx, guild_id, channel_id) + if not channel: + raise ChannelNotFound(channel_id) + return discord.PartialMessage(channel=channel, id=message_id) + + +class MessageConverter(IDConverter[discord.Message]): + """Converts to a :class:`discord.Message`. + + .. versionadded:: 1.1 + + The lookup strategy is as follows (in order): + + 1. Lookup by "{channel ID}-{message ID}" (retrieved by shift-clicking on "Copy ID") + 2. Lookup by message ID (the message **must** be in the context channel) + 3. Lookup by message URL + + .. versionchanged:: 1.5 + Raise :exc:`.ChannelNotFound`, :exc:`.MessageNotFound` or :exc:`.ChannelNotReadable` + instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: Context, argument: str) -> discord.Message: + guild_id, message_id, channel_id = PartialMessageConverter._get_id_matches( + ctx, argument + ) + message = ctx.bot._connection._get_message(message_id) + if message: + return message + channel = PartialMessageConverter._resolve_channel(ctx, guild_id, channel_id) + if not channel: + raise ChannelNotFound(channel_id) + try: + return await channel.fetch_message(message_id) + except discord.NotFound: + raise MessageNotFound(argument) + except discord.Forbidden: + raise ChannelNotReadable(channel) + + +class GuildChannelConverter(IDConverter[discord.abc.GuildChannel]): + """Converts to a :class:`~discord.abc.GuildChannel`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name. + + .. versionadded:: 2.0 + """ + + async def convert(self, ctx: Context, argument: str) -> discord.abc.GuildChannel: + return self._resolve_channel( + ctx, argument, "channels", discord.abc.GuildChannel + ) + + @staticmethod + def _resolve_channel( + ctx: Context, argument: str, attribute: str, type: type[CT] + ) -> CT: + bot = ctx.bot + + match = IDConverter._get_id_match(argument) or re.match( + r"<#([0-9]{15,20})>$", argument + ) + result = None + guild = ctx.guild + + if match is None: + # not a mention + if guild: + iterable: Iterable[CT] = getattr(guild, attribute) + result: CT | None = discord.utils.get(iterable, name=argument) + else: + + def check(c): + return isinstance(c, type) and c.name == argument + + result = discord.utils.find(check, bot.get_all_channels()) + else: + channel_id = int(match.group(1)) + if guild: + result = guild.get_channel(channel_id) + else: + result = _get_from_guilds(bot, "get_channel", channel_id) + + if not isinstance(result, type): + raise ChannelNotFound(argument) + + return result + + @staticmethod + def _resolve_thread( + ctx: Context, argument: str, attribute: str, type: type[TT] + ) -> TT: + match = IDConverter._get_id_match(argument) or re.match( + r"<#([0-9]{15,20})>$", argument + ) + result = None + guild = ctx.guild + + if match is None: + # not a mention + if guild: + iterable: Iterable[TT] = getattr(guild, attribute) + result: TT | None = discord.utils.get(iterable, name=argument) + else: + thread_id = int(match.group(1)) + if guild: + result = guild.get_thread(thread_id) + + if not result or not isinstance(result, type): + raise ThreadNotFound(argument) + + return result + + +class TextChannelConverter(IDConverter[discord.TextChannel]): + """Converts to a :class:`~discord.TextChannel`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: Context, argument: str) -> discord.TextChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "text_channels", discord.TextChannel + ) + + +class VoiceChannelConverter(IDConverter[discord.VoiceChannel]): + """Converts to a :class:`~discord.VoiceChannel`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: Context, argument: str) -> discord.VoiceChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "voice_channels", discord.VoiceChannel + ) + + +class StageChannelConverter(IDConverter[discord.StageChannel]): + """Converts to a :class:`~discord.StageChannel`. + + .. versionadded:: 1.7 + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + """ + + async def convert(self, ctx: Context, argument: str) -> discord.StageChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "stage_channels", discord.StageChannel + ) + + +class CategoryChannelConverter(IDConverter[discord.CategoryChannel]): + """Converts to a :class:`~discord.CategoryChannel`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.ChannelNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: Context, argument: str) -> discord.CategoryChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "categories", discord.CategoryChannel + ) + + +class ForumChannelConverter(IDConverter[discord.ForumChannel]): + """Converts to a :class:`~discord.ForumChannel`. + + All lookups are via the local guild. If in a DM context, then the lookup + is done by the global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + + .. versionadded:: 2.0 + """ + + async def convert(self, ctx: Context, argument: str) -> discord.ForumChannel: + return GuildChannelConverter._resolve_channel( + ctx, argument, "forum_channels", discord.ForumChannel + ) + + +class ThreadConverter(IDConverter[discord.Thread]): + """Coverts to a :class:`~discord.Thread`. + + All lookups are via the local guild. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name. + + .. versionadded: 2.0 + """ + + async def convert(self, ctx: Context, argument: str) -> discord.Thread: + return GuildChannelConverter._resolve_thread( + ctx, argument, "threads", discord.Thread + ) + + +class ColourConverter(Converter[discord.Colour]): + """Converts to a :class:`~discord.Colour`. + + .. versionchanged:: 1.5 + Add an alias named ColorConverter + + The following formats are accepted: + + - ``0x`` + - ``#`` + - ``0x#`` + - ``rgb(, , )`` + - Any of the ``classmethod`` in :class:`~discord.Colour` + + - The ``_`` in the name can be optionally replaced with spaces. + + Like CSS, ```` can be either 0-255 or 0-100% and ```` can be + either a 6 digit hex number or a 3 digit hex shortcut (e.g. #fff). + + .. versionchanged:: 1.5 + Raise :exc:`.BadColourArgument` instead of generic :exc:`.BadArgument` + + .. versionchanged:: 1.7 + Added support for ``rgb`` function and 3-digit hex shortcuts + """ + + RGB_REGEX = re.compile( + r"rgb\s*\((?P[0-9]{1,3}%?)\s*,\s*(?P[0-9]{1,3}%?)\s*,\s*(?P[0-9]{1,3}%?)\s*\)" + ) + + def parse_hex_number(self, argument): + arg = "".join(i * 2 for i in argument) if len(argument) == 3 else argument + try: + value = int(arg, base=16) + if not (0 <= value <= 0xFFFFFF): + raise BadColourArgument(argument) + except ValueError: + raise BadColourArgument(argument) + else: + return discord.Color(value=value) + + def parse_rgb_number(self, argument, number): + if number[-1] == "%": + value = int(number[:-1]) + if not (0 <= value <= 100): + raise BadColourArgument(argument) + return round(255 * (value / 100)) + + value = int(number) + if not (0 <= value <= 255): + raise BadColourArgument(argument) + return value + + def parse_rgb(self, argument, *, regex=RGB_REGEX): + match = regex.match(argument) + if match is None: + raise BadColourArgument(argument) + + red = self.parse_rgb_number(argument, match.group("r")) + green = self.parse_rgb_number(argument, match.group("g")) + blue = self.parse_rgb_number(argument, match.group("b")) + return discord.Color.from_rgb(red, green, blue) + + async def convert(self, ctx: Context, argument: str) -> discord.Colour: + if argument[0] == "#": + return self.parse_hex_number(argument[1:]) + + if argument[0:2] == "0x": + rest = argument[2:] + # Legacy backwards compatible syntax + if rest.startswith("#"): + return self.parse_hex_number(rest[1:]) + return self.parse_hex_number(rest) + + arg = argument.lower() + if arg[0:3] == "rgb": + return self.parse_rgb(arg) + + arg = arg.replace(" ", "_") + method = getattr(discord.Colour, arg, None) + if arg.startswith("from_") or method is None or not inspect.ismethod(method): + raise BadColourArgument(arg) + return method() + + +ColorConverter = ColourConverter + + +class RoleConverter(IDConverter[discord.Role]): + """Converts to a :class:`~discord.Role`. + + All lookups are via the local guild. If in a DM context, the converter raises + :exc:`.NoPrivateMessage` exception. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by mention. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.RoleNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: Context, argument: str) -> discord.Role: + guild = ctx.guild + if not guild: + raise NoPrivateMessage() + + match = self._get_id_match(argument) or re.match( + r"<@&([0-9]{15,20})>$", argument + ) + if match: + result = guild.get_role(int(match.group(1))) + else: + result = discord.utils.get(guild._roles.values(), name=argument) + + if result is None: + raise RoleNotFound(argument) + return result + + +class GameConverter(Converter[discord.Game]): + """Converts to :class:`~discord.Game`.""" + + async def convert(self, ctx: Context, argument: str) -> discord.Game: + return discord.Game(name=argument) + + +class InviteConverter(Converter[discord.Invite]): + """Converts to a :class:`~discord.Invite`. + + This is done via an HTTP request using :meth:`.Bot.fetch_invite`. + + .. versionchanged:: 1.5 + Raise :exc:`.BadInviteArgument` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: Context, argument: str) -> discord.Invite: + try: + invite = await ctx.bot.fetch_invite(argument) + return invite + except Exception as exc: + raise BadInviteArgument(argument) from exc + + +class GuildConverter(IDConverter[discord.Guild]): + """Converts to a :class:`~discord.Guild`. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by name. (There is no disambiguation for Guilds with multiple matching names). + + .. versionadded:: 1.7 + """ + + async def convert(self, ctx: Context, argument: str) -> discord.Guild: + match = self._get_id_match(argument) + result = None + + if match is not None: + guild_id = int(match.group(1)) + result = ctx.bot.get_guild(guild_id) + + if result is None: + result = discord.utils.get(ctx.bot.guilds, name=argument) + + if result is None: + raise GuildNotFound(argument) + return result + + +class EmojiConverter(IDConverter[discord.GuildEmoji]): + """Converts to a :class:`~discord.GuildEmoji`. + + All lookups are done for the local guild first, if available. If that lookup + fails, then it checks the client's global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 2. Lookup by extracting ID from the emoji. + 3. Lookup by name + + .. versionchanged:: 1.5 + Raise :exc:`.EmojiNotFound` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: Context, argument: str) -> discord.GuildEmoji: + match = self._get_id_match(argument) or re.match( + r"$", argument + ) + result = None + bot = ctx.bot + guild = ctx.guild + + if match is None: + # Try to get the emoji by name. Try local guild first. + if guild: + result = discord.utils.get(guild.emojis, name=argument) + + if result is None: + result = discord.utils.get(bot.emojis, name=argument) + else: + emoji_id = int(match.group(1)) + + # Try to look up emoji by id. + result = bot.get_emoji(emoji_id) + + if result is None: + raise EmojiNotFound(argument) + + return result + + +class PartialEmojiConverter(Converter[discord.PartialEmoji]): + """Converts to a :class:`~discord.PartialEmoji`. + + This is done by extracting the animated flag, name, and ID for custom emojis, + or by using the standard Unicode emojis supported by Discord. + + .. versionchanged:: 1.5 + Raise :exc:`.PartialEmojiConversionFailure` instead of generic :exc:`.BadArgument` + """ + + async def convert(self, ctx: Context, argument: str) -> discord.PartialEmoji: + match = re.match(r"<(a?):(\w{1,32}):([0-9]{15,20})>$", argument) + + if match: + emoji_animated = bool(match.group(1)) + emoji_name = match.group(2) + emoji_id = int(match.group(3)) + + return discord.PartialEmoji.with_state( + ctx.bot._connection, + animated=emoji_animated, + name=emoji_name, + id=emoji_id, + ) + + if argument in UNICODE_EMOJIS: + return discord.PartialEmoji.with_state( + ctx.bot._connection, + animated=False, + name=argument, + id=None, + ) + + raise PartialEmojiConversionFailure(argument) + + +class GuildStickerConverter(IDConverter[discord.GuildSticker]): + """Converts to a :class:`~discord.GuildSticker`. + + All lookups are done for the local guild first, if available. If that lookup + fails, then it checks the client's global cache. + + The lookup strategy is as follows (in order): + + 1. Lookup by ID. + 3. Lookup by name + + .. versionadded:: 2.0 + """ + + async def convert(self, ctx: Context, argument: str) -> discord.GuildSticker: + match = self._get_id_match(argument) + result = None + bot = ctx.bot + guild = ctx.guild + + if match is None: + # Try to get the sticker by name. Try local guild first. + if guild: + result = discord.utils.get(guild.stickers, name=argument) + + if result is None: + result = discord.utils.get(bot.stickers, name=argument) + else: + sticker_id = int(match.group(1)) + + # Try to look up sticker by id. + result = bot.get_sticker(sticker_id) + + if result is None: + raise GuildStickerNotFound(argument) + + return result + + +class clean_content(Converter[str]): + """Converts the argument to mention scrubbed version of + said content. + + This behaves similarly to :attr:`~discord.Message.clean_content`. + + Attributes + ---------- + fix_channel_mentions: :class:`bool` + Whether to clean channel mentions. + use_nicknames: :class:`bool` + Whether to use nicknames when transforming mentions. + escape_markdown: :class:`bool` + Whether to also escape special markdown characters. + remove_markdown: :class:`bool` + Whether to also remove special markdown characters. This option is not supported with ``escape_markdown`` + + .. versionadded:: 1.7 + """ + + def __init__( + self, + *, + fix_channel_mentions: bool = False, + use_nicknames: bool = True, + escape_markdown: bool = False, + remove_markdown: bool = False, + ) -> None: + self.fix_channel_mentions = fix_channel_mentions + self.use_nicknames = use_nicknames + self.escape_markdown = escape_markdown + self.remove_markdown = remove_markdown + + async def convert(self, ctx: Context, argument: str) -> str: + msg = ctx.message + + if ctx.guild: + + def resolve_member(id: int) -> str: + m = ( + None if msg is None else _utils_get(msg.mentions, id=id) + ) or ctx.guild.get_member(id) + return ( + f"@{m.display_name if self.use_nicknames else m.name}" + if m + else "@deleted-user" + ) + + def resolve_role(id: int) -> str: + r = ( + None if msg is None else _utils_get(msg.mentions, id=id) + ) or ctx.guild.get_role(id) + return f"@{r.name}" if r else "@deleted-role" + + else: + + def resolve_member(id: int) -> str: + m = ( + None if msg is None else _utils_get(msg.mentions, id=id) + ) or ctx.bot.get_user(id) + return f"@{m.name}" if m else "@deleted-user" + + def resolve_role(id: int) -> str: + return "@deleted-role" + + if self.fix_channel_mentions and ctx.guild: + + def resolve_channel(id: int) -> str: + c = ctx.guild.get_channel(id) + return f"#{c.name}" if c else "#deleted-channel" + + else: + + def resolve_channel(id: int) -> str: + return f"<#{id}>" + + transforms = { + "@": resolve_member, + "@!": resolve_member, + "#": resolve_channel, + "@&": resolve_role, + } + + def repl(match: re.Match) -> str: + type = match[1] + id = int(match[2]) + transformed = transforms[type](id) + return transformed + + result = re.sub(r"<(@[!&]?|#)([0-9]{15,20})>", repl, argument) + if self.escape_markdown: + result = discord.utils.escape_markdown(result) + elif self.remove_markdown: + result = discord.utils.remove_markdown(result) + + # Completely ensure no mentions escape: + return discord.utils.escape_mentions(result) + + +class Greedy(List[T]): + r"""A special converter that greedily consumes arguments until it can't. + As a consequence of this behaviour, most input errors are silently discarded, + since it is used as an indicator of when to stop parsing. + + When a parser error is met the greedy converter stops converting, undoes the + internal string parsing routine, and continues parsing regularly. + + For example, in the following code: + + .. code-block:: python3 + + @commands.command() + async def test(ctx, numbers: Greedy[int], reason: str): + await ctx.send("numbers: {}, reason: {}".format(numbers, reason)) + + An invocation of ``[p]test 1 2 3 4 5 6 hello`` would pass ``numbers`` with + ``[1, 2, 3, 4, 5, 6]`` and ``reason`` with ``hello``\. + + For more information, check :ref:`ext_commands_special_converters`. + """ + + __slots__ = ("converter",) + + def __init__(self, *, converter: T): + self.converter = converter + + def __repr__(self): + converter = getattr(self.converter, "__name__", repr(self.converter)) + return f"Greedy[{converter}]" + + def __class_getitem__(cls, params: tuple[T] | T) -> Greedy[T]: + if not isinstance(params, tuple): + params = (params,) + if len(params) != 1: + raise TypeError("Greedy[...] only takes a single argument") + converter = params[0] + + origin = getattr(converter, "__origin__", None) + args = getattr(converter, "__args__", ()) + + if not ( + callable(converter) + or isinstance(converter, Converter) + or origin is not None + ): + raise TypeError("Greedy[...] expects a type or a Converter instance.") + + if converter in (str, type(None)) or origin is Greedy: + raise TypeError(f"Greedy[{converter.__name__}] is invalid.") + + if origin is Union and type(None) in args: + raise TypeError(f"Greedy[{converter!r}] is invalid.") + + return cls(converter=converter) + + +def _convert_to_bool(argument: str) -> bool: + lowered = argument.lower() + if lowered in ("yes", "y", "true", "t", "1", "enable", "on"): + return True + elif lowered in ("no", "n", "false", "f", "0", "disable", "off"): + return False + else: + raise BadBoolArgument(lowered) + + +def get_converter(param: inspect.Parameter) -> Any: + converter = param.annotation + if converter is param.empty: + if param.default is not param.empty: + converter = str if param.default is None else type(param.default) + else: + converter = str + return converter + + +_GenericAlias = type(List[T]) + + +def is_generic_type(tp: Any, *, _GenericAlias: type = _GenericAlias) -> bool: + return ( + isinstance(tp, type) + and issubclass(tp, Generic) + or isinstance(tp, _GenericAlias) + ) # type: ignore + + +CONVERTER_MAPPING: dict[type[Any], Any] = { + discord.Object: ObjectConverter, + discord.Member: MemberConverter, + discord.User: UserConverter, + discord.Message: MessageConverter, + discord.PartialMessage: PartialMessageConverter, + discord.TextChannel: TextChannelConverter, + discord.Invite: InviteConverter, + discord.Guild: GuildConverter, + discord.Role: RoleConverter, + discord.Game: GameConverter, + discord.Colour: ColourConverter, + discord.VoiceChannel: VoiceChannelConverter, + discord.StageChannel: StageChannelConverter, + discord.GuildEmoji: EmojiConverter, + discord.PartialEmoji: PartialEmojiConverter, + discord.CategoryChannel: CategoryChannelConverter, + discord.ForumChannel: ForumChannelConverter, + discord.Thread: ThreadConverter, + discord.abc.GuildChannel: GuildChannelConverter, + discord.GuildSticker: GuildStickerConverter, +} + + +async def _actual_conversion( + ctx: Context, converter, argument: str, param: inspect.Parameter +): + if converter is bool: + return _convert_to_bool(argument) + + try: + module = converter.__module__ + except AttributeError: + pass + else: + if module is not None and ( + module.startswith("discord.") and not module.endswith("converter") + ): + converter = CONVERTER_MAPPING.get(converter, converter) + + try: + if inspect.isclass(converter) and issubclass(converter, Converter): + if inspect.ismethod(converter.convert): + return await converter.convert(ctx, argument) + else: + return await converter().convert(ctx, argument) + elif isinstance(converter, Converter): + return await converter.convert(ctx, argument) + except CommandError: + raise + except Exception as exc: + raise ConversionError(converter, exc) from exc + + try: + return converter(argument) + except CommandError: + raise + except Exception as exc: + try: + name = converter.__name__ + except AttributeError: + name = converter.__class__.__name__ + + raise BadArgument( + f'Converting to "{name}" failed for parameter "{param.name}".' + ) from exc + + +async def run_converters( + ctx: Context, converter, argument: str | None, param: inspect.Parameter +): + """|coro| + + Runs converters for a given converter, argument, and parameter. + + This function does the same work that the library does under the hood. + + .. versionadded:: 2.0 + + Parameters + ---------- + ctx: :class:`Context` + The invocation context to run the converters under. + converter: Any + The converter to run, this corresponds to the annotation in the function. + argument: Optional[:class:`str`] + The argument to convert to. + param: :class:`inspect.Parameter` + The parameter being converted. This is mainly for error reporting. + + Returns + ------- + Any + The resulting conversion. + + Raises + ------ + CommandError + The converter failed to convert. + """ + origin = getattr(converter, "__origin__", None) + + if origin is Union: + errors = [] + _NoneType = type(None) + union_args = converter.__args__ + for conv in union_args: + # if we got to this part in the code, then the previous conversions have failed, so + # we should just undo the view, return the default, and allow parsing to continue + # with the other parameters + if conv is _NoneType and param.kind != param.VAR_POSITIONAL: + ctx.view.undo() + return None if param.default is param.empty else param.default + + try: + value = await run_converters(ctx, conv, argument, param) + except CommandError as exc: + errors.append(exc) + else: + return value + + # if we're here, then we failed all the converters + raise BadUnionArgument(param, union_args, errors) + + if origin is Literal: + errors = [] + conversions = {} + literal_args = converter.__args__ + for literal in literal_args: + literal_type = type(literal) + try: + value = conversions[literal_type] + except KeyError: + try: + value = await _actual_conversion(ctx, literal_type, argument, param) + except CommandError as exc: + errors.append(exc) + conversions[literal_type] = object() + continue + else: + conversions[literal_type] = value + + if value == literal: + return value + + # if we're here, then we failed to match all the literals + raise BadLiteralArgument(param, literal_args, errors) + + # This must be the last if-clause in the chain of origin checking + # Nearly every type is a generic type within the typing library + # So care must be taken to make sure a more specialised origin handle + # isn't overwritten by the widest if clause + if origin is not None and is_generic_type(converter): + converter = origin + + return await _actual_conversion(ctx, converter, argument, param) diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/cooldowns.py b/venv/lib/python3.11/site-packages/discord/ext/commands/cooldowns.py new file mode 100644 index 0000000..7504eed --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/commands/cooldowns.py @@ -0,0 +1,409 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import asyncio +import time +from collections import deque +from typing import TYPE_CHECKING, Any, Callable, Deque, TypeVar + +import discord.abc +from discord.enums import Enum +from discord.utils import _get_event_loop + +from ...abc import PrivateChannel +from .errors import MaxConcurrencyReached + +if TYPE_CHECKING: + from ...message import Message + +__all__ = ( + "BucketType", + "Cooldown", + "CooldownMapping", + "DynamicCooldownMapping", + "MaxConcurrency", +) + +C = TypeVar("C", bound="CooldownMapping") +MC = TypeVar("MC", bound="MaxConcurrency") + + +class BucketType(Enum): + default = 0 + user = 1 + guild = 2 + channel = 3 + member = 4 + category = 5 + role = 6 + + def get_key(self, msg: Message) -> Any: + if self is BucketType.user: + return msg.author.id + elif self is BucketType.guild: + return (msg.guild or msg.author).id + elif self is BucketType.channel: + return msg.channel.id + elif self is BucketType.member: + return (msg.guild and msg.guild.id), msg.author.id + elif self is BucketType.category: + return ( + msg.channel.category.id + if isinstance(msg.channel, discord.abc.GuildChannel) + and msg.channel.category + else msg.channel.id + ) + elif self is BucketType.role: + # we return the channel id of a private-channel as there are only roles in guilds + # and that yields the same result as for a guild with only the @everyone role + # NOTE: PrivateChannel doesn't actually have an id attribute, but we assume we are + # receiving a DMChannel or GroupChannel which inherit from PrivateChannel and do + return (msg.channel if isinstance(msg.channel, PrivateChannel) else msg.author.top_role).id # type: ignore + + def __call__(self, msg: Message) -> Any: + return self.get_key(msg) + + +class Cooldown: + """Represents a cooldown for a command. + + Attributes + ---------- + rate: :class:`int` + The total number of tokens available per :attr:`per` seconds. + per: :class:`float` + The length of the cooldown period in seconds. + """ + + __slots__ = ("rate", "per", "_window", "_tokens", "_last") + + def __init__(self, rate: float, per: float) -> None: + self.rate: int = int(rate) + self.per: float = float(per) + self._window: float = 0.0 + self._tokens: int = self.rate + self._last: float = 0.0 + + def get_tokens(self, current: float | None = None) -> int: + """Returns the number of available tokens before rate limiting is applied. + + Parameters + ---------- + current: Optional[:class:`float`] + The time in seconds since Unix epoch to calculate tokens at. + If not supplied then :func:`time.time()` is used. + + Returns + ------- + :class:`int` + The number of tokens available before the cooldown is to be applied. + """ + if not current: + current = time.time() + + tokens = self._tokens + + if current > self._window + self.per: + tokens = self.rate + return tokens + + def get_retry_after(self, current: float | None = None) -> float: + """Returns the time in seconds until the cooldown will be reset. + + Parameters + ---------- + current: Optional[:class:`float`] + The current time in seconds since Unix epoch. + If not supplied, then :func:`time.time()` is used. + + Returns + ------- + :class:`float` + The number of seconds to wait before this cooldown will be reset. + """ + current = current or time.time() + tokens = self.get_tokens(current) + + if tokens == 0: + return self.per - (current - self._window) + + return 0.0 + + def update_rate_limit(self, current: float | None = None) -> float | None: + """Updates the cooldown rate limit. + + Parameters + ---------- + current: Optional[:class:`float`] + The time in seconds since Unix epoch to update the rate limit at. + If not supplied, then :func:`time.time()` is used. + + Returns + ------- + Optional[:class:`float`] + The retry-after time in seconds if rate limited. + """ + current = current or time.time() + self._last = current + + self._tokens = self.get_tokens(current) + + # first token used means that we start a new rate limit window + if self._tokens == self.rate: + self._window = current + + # check if we are rate limited + if self._tokens == 0: + return self.per - (current - self._window) + + # we're not so decrement our tokens + self._tokens -= 1 + + def reset(self) -> None: + """Reset the cooldown to its initial state.""" + self._tokens = self.rate + self._last = 0.0 + + def copy(self) -> Cooldown: + """Creates a copy of this cooldown. + + Returns + ------- + :class:`Cooldown` + A new instance of this cooldown. + """ + return Cooldown(self.rate, self.per) + + def __repr__(self) -> str: + return ( + f"" + ) + + +class CooldownMapping: + def __init__( + self, + original: Cooldown | None, + type: Callable[[Message], Any], + ) -> None: + if not callable(type): + raise TypeError("Cooldown type must be a BucketType or callable") + + self._cache: dict[Any, Cooldown] = {} + self._cooldown: Cooldown | None = original + self._type: Callable[[Message], Any] = type + + def copy(self) -> CooldownMapping: + ret = CooldownMapping(self._cooldown, self._type) + ret._cache = self._cache.copy() + return ret + + @property + def valid(self) -> bool: + return self._cooldown is not None + + @property + def type(self) -> Callable[[Message], Any]: + return self._type + + @classmethod + def from_cooldown(cls: type[C], rate, per, type) -> C: + return cls(Cooldown(rate, per), type) + + def _bucket_key(self, msg: Message) -> Any: + return self._type(msg) + + def _verify_cache_integrity(self, current: float | None = None) -> None: + # we want to delete all cache objects that haven't been used + # in a cooldown window. e.g. if we have a command that has a + # cooldown of 60s, and it has not been used in 60s then that key should be deleted + current = current or time.time() + dead_keys = [k for k, v in self._cache.items() if current > v._last + v.per] + for k in dead_keys: + del self._cache[k] + + def create_bucket(self, message: Message) -> Cooldown: + return self._cooldown.copy() # type: ignore + + def get_bucket(self, message: Message, current: float | None = None) -> Cooldown: + if self._type is BucketType.default: + return self._cooldown # type: ignore + + self._verify_cache_integrity(current) + key = self._bucket_key(message) + if key not in self._cache: + bucket = self.create_bucket(message) + if bucket is not None: + self._cache[key] = bucket + else: + bucket = self._cache[key] + + return bucket + + def update_rate_limit( + self, message: Message, current: float | None = None + ) -> float | None: + bucket = self.get_bucket(message, current) + return bucket.update_rate_limit(current) + + +class DynamicCooldownMapping(CooldownMapping): + def __init__( + self, factory: Callable[[Message], Cooldown], type: Callable[[Message], Any] + ) -> None: + super().__init__(None, type) + self._factory: Callable[[Message], Cooldown] = factory + + def copy(self) -> DynamicCooldownMapping: + ret = DynamicCooldownMapping(self._factory, self._type) + ret._cache = self._cache.copy() + return ret + + @property + def valid(self) -> bool: + return True + + def create_bucket(self, message: Message) -> Cooldown: + return self._factory(message) + + +class _Semaphore: + """This class is a version of a semaphore. + + If you're wondering why asyncio.Semaphore isn't being used, + it's because it doesn't expose the internal value. This internal + value is necessary because I need to support both `wait=True` and + `wait=False`. + + An asyncio.Queue could have been used to do this as well -- but it is + not as efficient since internally that uses two queues and is a bit + overkill for what is basically a counter. + """ + + __slots__ = ("value", "loop", "_waiters") + + def __init__(self, number: int) -> None: + self.value: int = number + self.loop: asyncio.AbstractEventLoop = _get_event_loop() + self._waiters: Deque[asyncio.Future] = deque() + + def __repr__(self) -> str: + return f"<_Semaphore value={self.value} waiters={len(self._waiters)}>" + + def locked(self) -> bool: + return self.value == 0 + + def is_active(self) -> bool: + return len(self._waiters) > 0 + + def wake_up(self) -> None: + while self._waiters: + future = self._waiters.popleft() + if not future.done(): + future.set_result(None) + return + + async def acquire(self, *, wait: bool = False) -> bool: + if not wait and self.value <= 0: + # signal that we're not acquiring + return False + + while self.value <= 0: + future = self.loop.create_future() + self._waiters.append(future) + try: + await future + except: + future.cancel() + if self.value > 0 and not future.cancelled(): + self.wake_up() + raise + + self.value -= 1 + return True + + def release(self) -> None: + self.value += 1 + self.wake_up() + + +class MaxConcurrency: + __slots__ = ("number", "per", "wait", "_mapping") + + def __init__(self, number: int, *, per: BucketType, wait: bool) -> None: + self._mapping: dict[Any, _Semaphore] = {} + self.per: BucketType = per + self.number: int = number + self.wait: bool = wait + + if number <= 0: + raise ValueError("max_concurrency 'number' cannot be less than 1") + + if not isinstance(per, BucketType): + raise TypeError( + f"max_concurrency 'per' must be of type BucketType not {type(per)!r}" + ) + + def copy(self: MC) -> MC: + return self.__class__(self.number, per=self.per, wait=self.wait) + + def __repr__(self) -> str: + return ( + f"" + ) + + def get_key(self, message: Message) -> Any: + return self.per.get_key(message) + + async def acquire(self, message: Message) -> None: + key = self.get_key(message) + + try: + sem = self._mapping[key] + except KeyError: + self._mapping[key] = sem = _Semaphore(self.number) + + acquired = await sem.acquire(wait=self.wait) + if not acquired: + raise MaxConcurrencyReached(self.number, self.per) + + async def release(self, message: Message) -> None: + # Technically there's no reason for this function to be async + # But it might be more useful in the future + key = self.get_key(message) + + try: + sem = self._mapping[key] + except KeyError: + # ...? peculiar + return + else: + sem.release() + + if sem.value >= self.number and not sem.is_active(): + del self._mapping[key] diff --git a/venv/lib/python3.11/site-packages/discord/ext/commands/core.py b/venv/lib/python3.11/site-packages/discord/ext/commands/core.py new file mode 100644 index 0000000..4a58ecf --- /dev/null +++ b/venv/lib/python3.11/site-packages/discord/ext/commands/core.py @@ -0,0 +1,2503 @@ +""" +The MIT License (MIT) + +Copyright (c) 2015-2021 Rapptz +Copyright (c) 2021-present Pycord Development + +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. +""" + +from __future__ import annotations + +import asyncio +import datetime +import functools +import inspect +import types +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Generator, + Generic, + Literal, + TypeVar, + Union, + overload, +) + +import discord + +from ...commands import ( + ApplicationCommand, + Option, + _BaseCommand, + message_command, + slash_command, + user_command, +) +from ...enums import ChannelType +from ...errors import * +from .cog import Cog +from .context import Context +from .converter import Greedy, get_converter, run_converters +from .cooldowns import ( + BucketType, + Cooldown, + CooldownMapping, + DynamicCooldownMapping, + MaxConcurrency, +) +from .errors import * + +if TYPE_CHECKING: + from typing_extensions import Concatenate, ParamSpec, TypeGuard + + from discord.message import Message + + from ._types import Check, Coro, CoroFunc, Error, Hook + + +__all__ = ( + "Command", + "Group", + "GroupMixin", + "command", + "group", + "has_role", + "has_permissions", + "has_any_role", + "check", + "check_any", + "before_invoke", + "after_invoke", + "bot_has_role", + "bot_has_permissions", + "bot_has_any_role", + "cooldown", + "dynamic_cooldown", + "max_concurrency", + "dm_only", + "guild_only", + "is_owner", + "is_nsfw", + "has_guild_permissions", + "bot_has_guild_permissions", + "slash_command", + "user_command", + "message_command", +) + +MISSING: Any = discord.utils.MISSING + +T = TypeVar("T") +CogT = TypeVar("CogT", bound="Cog") +CommandT = TypeVar("CommandT", bound="Command") +ContextT = TypeVar("ContextT", bound="Context") +# CHT = TypeVar('CHT', bound='Check') +GroupT = TypeVar("GroupT", bound="Group") +HookT = TypeVar("HookT", bound="Hook") +ErrorT = TypeVar("ErrorT", bound="Error") + +if TYPE_CHECKING: + P = ParamSpec("P") +else: + P = TypeVar("P") + + +def unwrap_function(function: Callable[..., Any]) -> Callable[..., Any]: + partial = functools.partial + while True: + if hasattr(function, "__wrapped__"): + function = function.__wrapped__ + elif isinstance(function, partial): + function = function.func + else: + return function + + +def get_signature_parameters( + function: Callable[..., Any], globalns: dict[str, Any] +) -> dict[str, inspect.Parameter]: + signature = inspect.signature(function) + params = {} + cache: dict[str, Any] = {} + eval_annotation = discord.utils.evaluate_annotation + for name, parameter in signature.parameters.items(): + annotation = parameter.annotation + if annotation is parameter.empty: + params[name] = parameter + continue + if annotation is None: + params[name] = parameter.replace(annotation=type(None)) + continue + + annotation = eval_annotation(annotation, globalns, globalns, cache) + if annotation is Greedy: + raise TypeError("Unparameterized Greedy[...] is disallowed in signature.") + + params[name] = parameter.replace(annotation=annotation) + + return params + + +def wrap_callback(coro): + @functools.wraps(coro) + async def wrapped(*args, **kwargs): + try: + ret = await coro(*args, **kwargs) + except CommandError: + raise + except asyncio.CancelledError: + return + except Exception as exc: + raise CommandInvokeError(exc) from exc + return ret + + return wrapped + + +def hooked_wrapped_callback(command, ctx, coro): + @functools.wraps(coro) + async def wrapped(*args, **kwargs): + try: + ret = await coro(*args, **kwargs) + except CommandError: + ctx.command_failed = True + raise + except asyncio.CancelledError: + ctx.command_failed = True + return + except Exception as exc: + ctx.command_failed = True + raise CommandInvokeError(exc) from exc + finally: + if command._max_concurrency is not None: + await command._max_concurrency.release(ctx) + + await command.call_after_hooks(ctx) + return ret + + return wrapped + + +class _CaseInsensitiveDict(dict): + def __contains__(self, k): + return super().__contains__(k.casefold()) + + def __delitem__(self, k): + return super().__delitem__(k.casefold()) + + def __getitem__(self, k): + return super().__getitem__(k.casefold()) + + def get(self, k, default=None): + return super().get(k.casefold(), default) + + def pop(self, k, default=None): + return super().pop(k.casefold(), default) + + def __setitem__(self, k, v): + super().__setitem__(k.casefold(), v) + + +class Command(_BaseCommand, Generic[CogT, P, T]): + r"""A class that implements the protocol for a bot text command. + + These are not created manually, instead they are created via the + decorator or functional interface. + + Attributes + ----------- + name: :class:`str` + The name of the command. + callback: :ref:`coroutine ` + The coroutine that is executed when the command is called. + help: Optional[:class:`str`] + The long help text for the command. + brief: Optional[:class:`str`] + The short help text for the command. + usage: Optional[:class:`str`] + A replacement for arguments in the default help text. + aliases: Union[List[:class:`str`], Tuple[:class:`str`]] + The list of aliases the command can be invoked under. + enabled: :class:`bool` + A boolean that indicates if the command is currently enabled. + If the command is invoked while it is disabled, then + :exc:`.DisabledCommand` is raised to the :func:`.on_command_error` + event. Defaults to ``True``. + parent: Optional[:class:`Group`] + The parent group that this command belongs to. ``None`` if there + isn't one. + cog: Optional[:class:`Cog`] + The cog that this command belongs to. ``None`` if there isn't one. + checks: List[Callable[[:class:`.Context`], :class:`bool`]] + A list of predicates that verifies if the command could be executed + with the given :class:`.Context` as the sole parameter. If an exception + is necessary to be thrown to signal failure, then one inherited from + :exc:`.CommandError` should be used. Note that if the checks fail then + :exc:`.CheckFailure` exception is raised to the :func:`.on_command_error` + event. + description: :class:`str` + The message prefixed into the default help command. + hidden: :class:`bool` + If ``True``\, the default help command does not show this in the + help output. + rest_is_raw: :class:`bool` + If ``False`` and a keyword-only argument is provided then the keyword + only argument is stripped and handled as if it was a regular argument + that handles :exc:`.MissingRequiredArgument` and default values in a + regular matter rather than passing the rest completely raw. If ``True`` + then the keyword-only argument will pass in the rest of the arguments + in a completely raw matter. Defaults to ``False``. + invoked_subcommand: Optional[:class:`Command`] + The subcommand that was invoked, if any. + require_var_positional: :class:`bool` + If ``True`` and a variadic positional argument is specified, requires + the user to specify at least one argument. Defaults to ``False``. + + .. versionadded:: 1.5 + + ignore_extra: :class:`bool` + If ``True``\, ignores extraneous strings passed to a command if all its + requirements are met (e.g. ``?foo a b c`` when only expecting ``a`` + and ``b``). Otherwise :func:`.on_command_error` and local error handlers + are called with :exc:`.TooManyArguments`. Defaults to ``True``. + cooldown_after_parsing: :class:`bool` + If ``True``\, cooldown processing is done after argument parsing, + which calls converters. If ``False`` then cooldown processing is done + first and then the converters are called second. Defaults to ``False``. + extras: :class:`dict` + A dict of user provided extras to attach to the Command. + + .. note:: + This object may be copied by the library. + + + .. versionadded:: 2.0 + + cooldown: Optional[:class:`Cooldown`] + The cooldown applied when the command is invoked. ``None`` if the command + doesn't have a cooldown. + + .. versionadded:: 2.0 + """ + + __original_kwargs__: dict[str, Any] + + def __new__(cls: type[CommandT], *args: Any, **kwargs: Any) -> CommandT: + # if you're wondering why this is done, it's because we need to ensure + # we have a complete original copy of **kwargs even for classes that + # mess with it by popping before delegating to the subclass __init__. + # In order to do this, we need to control the instance creation and + # inject the original kwargs through __new__ rather than doing it + # inside __init__. + self = super().__new__(cls) + + # we do a shallow copy because it's probably the most common use case. + # this could potentially break if someone modifies a list or something + # while it's in movement, but for now this is the cheapest and + # fastest way to do what we want. + self.__original_kwargs__ = kwargs.copy() + return self + + def __init__( + self, + func: ( + Callable[Concatenate[CogT, ContextT, P], Coro[T]] + | Callable[Concatenate[ContextT, P], Coro[T]] + ), + **kwargs: Any, + ): + if not asyncio.iscoroutinefunction(func): + raise TypeError("Callback must be a coroutine.") + + name = kwargs.get("name") or func.__name__ + if not isinstance(name, str): + raise TypeError("Name of a command must be a string.") + self.name: str = name + + self.callback = func + self.enabled: bool = kwargs.get("enabled", True) + + help_doc = kwargs.get("help") + if help_doc is not None: + help_doc = inspect.cleandoc(help_doc) + else: + help_doc = inspect.getdoc(func) + if isinstance(help_doc, bytes): + help_doc = help_doc.decode("utf-8") + + self.help: str | None = help_doc + + self.brief: str | None = kwargs.get("brief") + self.usage: str | None = kwargs.get("usage") + self.rest_is_raw: bool = kwargs.get("rest_is_raw", False) + self.aliases: list[str] | tuple[str] = kwargs.get("aliases", []) + self.extras: dict[str, Any] = kwargs.get("extras", {}) + + if not isinstance(self.aliases, (list, tuple)): + raise TypeError( + "Aliases of a command must be a list or a tuple of strings." + ) + + self.description: str = inspect.cleandoc(kwargs.get("description", "")) + self.hidden: bool = kwargs.get("hidden", False) + + try: + checks = func.__commands_checks__ + checks.reverse() + except AttributeError: + checks = kwargs.get("checks", []) + + self.checks: list[Check] = checks + + try: + cooldown = func.__commands_cooldown__ + except AttributeError: + cooldown = kwargs.get("cooldown") + + if cooldown is None: + buckets = CooldownMapping(cooldown, BucketType.default) + elif isinstance(cooldown, CooldownMapping): + buckets = cooldown + else: + raise TypeError( + "Cooldown must be a an instance of CooldownMapping or None." + ) + self._buckets: CooldownMapping = buckets + + try: + max_concurrency = func.__commands_max_concurrency__ + except AttributeError: + max_concurrency = kwargs.get("max_concurrency") + + self._max_concurrency: MaxConcurrency | None = max_concurrency + + self.require_var_positional: bool = kwargs.get("require_var_positional", False) + self.ignore_extra: bool = kwargs.get("ignore_extra", True) + self.cooldown_after_parsing: bool = kwargs.get("cooldown_after_parsing", False) + self.cog: CogT | None = None + + # bandaid for the fact that sometimes parent can be the bot instance + parent = kwargs.get("parent") + self.parent: GroupMixin | None = parent if isinstance(parent, _BaseCommand) else None # type: ignore + + self._before_invoke: Hook | None = None + try: + before_invoke = func.__before_invoke__ + except AttributeError: + pass + else: + self.before_invoke(before_invoke) + + self._after_invoke: Hook | None = None + try: + after_invoke = func.__after_invoke__ + except AttributeError: + pass + else: + self.after_invoke(after_invoke) + + @property + def callback( + self, + ) -> ( + Callable[Concatenate[CogT, Context, P], Coro[T]] + | Callable[Concatenate[Context, P], Coro[T]] + ): + return self._callback + + @callback.setter + def callback( + self, + function: ( + Callable[Concatenate[CogT, Context, P], Coro[T]] + | Callable[Concatenate[Context, P], Coro[T]] + ), + ) -> None: + self._callback = function + unwrap = unwrap_function(function) + self.module = unwrap.__module__ + + try: + globalns = unwrap.__globals__ + except AttributeError: + globalns = {} + + self.params = get_signature_parameters(function, globalns) + + def add_check(self, func: Check) -> None: + """Adds a check to the command. + + This is the non-decorator interface to :func:`.check`. + + .. versionadded:: 1.3 + + Parameters + ---------- + func + The function that will be used as a check. + """ + + self.checks.append(func) + + def remove_check(self, func: Check) -> None: + """Removes a check from the command. + + This function is idempotent and will not raise an exception + if the function is not in the command's checks. + + .. versionadded:: 1.3 + + Parameters + ---------- + func + The function to remove from the checks. + """ + + try: + self.checks.remove(func) + except ValueError: + pass + + def update(self, **kwargs: Any) -> None: + """Updates :class:`Command` instance with updated attribute. + + This works similarly to the :func:`.command` decorator in terms + of parameters in that they are passed to the :class:`Command` or + subclass constructors, sans the name and callback. + """ + self.__init__(self.callback, **dict(self.__original_kwargs__, **kwargs)) + + async def __call__(self, context: Context, *args: P.args, **kwargs: P.kwargs) -> T: + """|coro| + + Calls the internal callback that the command holds. + + .. note:: + + This bypasses all mechanisms -- including checks, converters, + invoke hooks, cooldowns, etc. You must take care to pass + the proper arguments and types to this function. + + .. versionadded:: 1.3 + """ + if self.cog is not None: + return await self.callback(self.cog, context, *args, **kwargs) # type: ignore + else: + return await self.callback(context, *args, **kwargs) # type: ignore + + def _ensure_assignment_on_copy(self, other: CommandT) -> CommandT: + other._before_invoke = self._before_invoke + other._after_invoke = self._after_invoke + if self.checks != other.checks: + other.checks = self.checks.copy() + if self._buckets.valid and not other._buckets.valid: + other._buckets = self._buckets.copy() + if self._max_concurrency != other._max_concurrency: + # _max_concurrency won't be None at this point + other._max_concurrency = self._max_concurrency.copy() # type: ignore + + try: + other.on_error = self.on_error + except AttributeError: + pass + return other + + def copy(self: CommandT) -> CommandT: + """Creates a copy of this command. + + Returns + ------- + :class:`Command` + A new instance of this command. + """ + ret = self.__class__(self.callback, **self.__original_kwargs__) + return self._ensure_assignment_on_copy(ret) + + def _update_copy(self: CommandT, kwargs: dict[str, Any]) -> CommandT: + if kwargs: + kw = kwargs.copy() + kw.update(self.__original_kwargs__) + copy = self.__class__(self.callback, **kw) + return self._ensure_assignment_on_copy(copy) + else: + return self.copy() + + async def dispatch_error(self, ctx: Context, error: Exception) -> None: + ctx.command_failed = True + cog = self.cog + try: + coro = self.on_error + except AttributeError: + pass + else: + injected = wrap_callback(coro) + if cog is not None: + await injected(cog, ctx, error) + else: + await injected(ctx, error) + + try: + if cog is not None: + local = Cog._get_overridden_method(cog.cog_command_error) + if local is not None: + wrapped = wrap_callback(local) + await wrapped(ctx, error) + finally: + ctx.bot.dispatch("command_error", ctx, error) + + async def transform(self, ctx: Context, param: inspect.Parameter) -> Any: + if isinstance(param.annotation, Option): + default = param.annotation.default + required = param.annotation.required + else: + default = param.default + required = default is param.empty + + converter = get_converter(param) + consume_rest_is_special = ( + param.kind == param.KEYWORD_ONLY and not self.rest_is_raw + ) + view = ctx.view + view.skip_ws() + + # The greedy converter is simple -- it keeps going until it fails in which case, + # it undoes the view ready for the next parameter to use instead + if isinstance(converter, Greedy): + if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY): + return await self._transform_greedy_pos( + ctx, param, required, converter.converter + ) + elif param.kind == param.VAR_POSITIONAL: + return await self._transform_greedy_var_pos( + ctx, param, converter.converter + ) + else: + # if we're here, then it's a KEYWORD_ONLY param type + # since this is mostly useless, we'll helpfully transform Greedy[X] + # into just X and do the parsing that way. + converter = converter.converter + + if view.eof: + if param.kind == param.VAR_POSITIONAL: + raise RuntimeError() # break the loop + if required: + if self._is_typing_optional(param.annotation): + return None + if ( + hasattr(converter, "__commands_is_flag__") + and converter._can_be_constructible() + ): + return await converter._construct_default(ctx) + raise MissingRequiredArgument(param) + return default + + previous = view.index + if consume_rest_is_special: + argument = view.read_rest().strip() + else: + try: + argument = view.get_quoted_word() + except ArgumentParsingError as exc: + if not self._is_typing_optional(param.annotation): + raise exc + view.index = previous + return None + view.previous = previous + + # type-checker fails to narrow argument + return await run_converters(ctx, converter, argument, param) # type: ignore + + async def _transform_greedy_pos( + self, ctx: Context, param: inspect.Parameter, required: bool, converter: Any + ) -> Any: + view = ctx.view + result = [] + while not view.eof: + # for use with a manual undo + previous = view.index + + view.skip_ws() + try: + argument = view.get_quoted_word() + value = await run_converters(ctx, converter, argument, param) # type: ignore + except (CommandError, ArgumentParsingError): + view.index = previous + break + else: + result.append(value) + + if not result and not required: + return param.default + return result + + async def _transform_greedy_var_pos( + self, ctx: Context, param: inspect.Parameter, converter: Any + ) -> Any: + view = ctx.view + previous = view.index + try: + argument = view.get_quoted_word() + value = await run_converters(ctx, converter, argument, param) # type: ignore + except (CommandError, ArgumentParsingError): + view.index = previous + raise RuntimeError() from None # break loop + else: + return value + + @property + def clean_params(self) -> dict[str, inspect.Parameter]: + """Dict[:class:`str`, :class:`inspect.Parameter`]: + Retrieves the parameter dictionary without the context or self parameters. + + Useful for inspecting signature. + """ + result = self.params.copy() + if self.cog is not None: + # first parameter is self + try: + del result[next(iter(result))] + except StopIteration: + raise ValueError("missing 'self' parameter") from None + + try: + # first/second parameter is context + del result[next(iter(result))] + except StopIteration: + raise ValueError("missing 'context' parameter") from None + + return result + + @property + def full_parent_name(self) -> str: + """Retrieves the fully qualified parent command name. + + This the base command name required to execute it. For example, + in ``?one two three`` the parent name would be ``one two``. + """ + entries = [] + command = self + # command.parent is type-hinted as GroupMixin some attributes are resolved via MRO + while command.parent is not None: # type: ignore + command = command.parent # type: ignore + entries.append(command.name) # type: ignore + + return " ".join(reversed(entries)) + + @property + def parents(self) -> list[Group]: + """Retrieves the parents of this command. + + If the command has no parents then it returns an empty :class:`list`. + + For example in commands ``?a b c test``, the parents are ``[c, b, a]``. + + .. versionadded:: 1.1 + """ + entries = [] + command = self + while command.parent is not None: # type: ignore + command = command.parent # type: ignore + entries.append(command) + + return entries + + @property + def root_parent(self) -> Group | None: + """Retrieves the root parent of this command. + + If the command has no parents then it returns ``None``. + + For example in commands ``?a b c test``, the root parent is ``a``. + """ + if not self.parent: + return None + return self.parents[-1] + + @property + def qualified_name(self) -> str: + """Retrieves the fully qualified command name. + + This is the full parent name with the command name as well. + For example, in ``?one two three`` the qualified name would be + ``one two three``. + """ + + parent = self.full_parent_name + if parent: + return f"{parent} {self.name}" + else: + return self.name + + def __str__(self) -> str: + return self.qualified_name + + async def _parse_arguments(self, ctx: Context) -> None: + ctx.args = [ctx] if self.cog is None else [self.cog, ctx] + ctx.kwargs = {} + args = ctx.args + kwargs = ctx.kwargs + + view = ctx.view + iterator = iter(self.params.items()) + + if self.cog is not None: + # we have 'self' as the first parameter so just advance + # the iterator and resume parsing + try: + next(iterator) + except StopIteration: + raise discord.ClientException( + f'Callback for {self.name} command is missing "self" parameter.' + ) + + # next we have the 'ctx' as the next parameter + try: + next(iterator) + except StopIteration: + raise discord.ClientException( + f'Callback for {self.name} command is missing "ctx" parameter.' + ) + + for name, param in iterator: + ctx.current_parameter = param + if param.kind in (param.POSITIONAL_OR_KEYWORD, param.POSITIONAL_ONLY): + transformed = await self.transform(ctx, param) + args.append(transformed) + elif param.kind == param.KEYWORD_ONLY: + # kwarg only param denotes "consume rest" semantics + if self.rest_is_raw: + converter = get_converter(param) + argument = view.read_rest() + kwargs[name] = await run_converters(ctx, converter, argument, param) + else: + kwargs[name] = await self.transform(ctx, param) + break + elif param.kind == param.VAR_POSITIONAL: + if view.eof and self.require_var_positional: + raise MissingRequiredArgument(param) + while not view.eof: + try: + transformed = await self.transform(ctx, param) + args.append(transformed) + except RuntimeError: + break + + if not self.ignore_extra and not view.eof: + raise TooManyArguments( + f"Too many arguments passed to {self.qualified_name}" + ) + + async def call_before_hooks(self, ctx: Context) -> None: + # now that we're done preparing we can call the pre-command hooks + # first, call the command local hook: + cog = self.cog + if self._before_invoke is not None: + # should be cog if @commands.before_invoke is used + instance = getattr(self._before_invoke, "__self__", cog) + # __self__ only exists for methods, not functions + # however, if @command.before_invoke is used, it will be a function + if instance: + await self._before_invoke(instance, ctx) # type: ignore + else: + await self._before_invoke(ctx) # type: ignore + + # call the cog local hook if applicable: + if cog is not None: + hook = Cog._get_overridden_method(cog.cog_before_invoke) + if hook is not None: + await hook(ctx) + + # call the bot global hook if necessary + hook = ctx.bot._before_invoke + if hook is not None: + await hook(ctx) + + async def call_after_hooks(self, ctx: Context) -> None: + cog = self.cog + if self._after_invoke is not None: + instance = getattr(self._after_invoke, "__self__", cog) + if instance: + await self._after_invoke(instance, ctx) # type: ignore + else: + await self._after_invoke(ctx) # type: ignore + + # call the cog local hook if applicable: + if cog is not None: + hook = Cog._get_overridden_method(cog.cog_after_invoke) + if hook is not None: + await hook(ctx) + + hook = ctx.bot._after_invoke + if hook is not None: + await hook(ctx) + + def _prepare_cooldowns(self, ctx: Context) -> None: + if self._buckets.valid: + dt = ctx.message.edited_at or ctx.message.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + bucket = self._buckets.get_bucket(ctx.message, current) + if bucket is not None: + retry_after = bucket.update_rate_limit(current) + if retry_after: + raise CommandOnCooldown(bucket, retry_after, self._buckets.type) # type: ignore + + async def prepare(self, ctx: Context) -> None: + ctx.command = self + + if not await self.can_run(ctx): + raise CheckFailure( + f"The check functions for command {self.qualified_name} failed." + ) + + if self._max_concurrency is not None: + # For this application, context can be duck-typed as a Message + await self._max_concurrency.acquire(ctx) # type: ignore + + try: + if self.cooldown_after_parsing: + await self._parse_arguments(ctx) + self._prepare_cooldowns(ctx) + else: + self._prepare_cooldowns(ctx) + await self._parse_arguments(ctx) + + await self.call_before_hooks(ctx) + except: + if self._max_concurrency is not None: + await self._max_concurrency.release(ctx) # type: ignore + raise + + @property + def cooldown(self) -> Cooldown | None: + return self._buckets._cooldown + + def is_on_cooldown(self, ctx: Context) -> bool: + """Checks whether the command is currently on cooldown. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context to use when checking the command's cooldown status. + + Returns + ------- + :class:`bool` + A boolean indicating if the command is on cooldown. + """ + if not self._buckets.valid: + return False + + bucket = self._buckets.get_bucket(ctx.message) + dt = ctx.message.edited_at or ctx.message.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + return bucket.get_tokens(current) == 0 + + def reset_cooldown(self, ctx: Context) -> None: + """Resets the cooldown on this command. + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context to reset the cooldown under. + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(ctx.message) + bucket.reset() + + def get_cooldown_retry_after(self, ctx: Context) -> float: + """Retrieves the amount of seconds before this command can be tried again. + + .. versionadded:: 1.4 + + Parameters + ---------- + ctx: :class:`.Context` + The invocation context to retrieve the cooldown from. + + Returns + ------- + :class:`float` + The amount of time left on this command's cooldown in seconds. + If this is ``0.0`` then the command isn't on cooldown. + """ + if self._buckets.valid: + bucket = self._buckets.get_bucket(ctx.message) + dt = ctx.message.edited_at or ctx.message.created_at + current = dt.replace(tzinfo=datetime.timezone.utc).timestamp() + return bucket.get_retry_after(current) + + return 0.0 + + async def invoke(self, ctx: Context) -> None: + await self.prepare(ctx) + + # terminate the invoked_subcommand chain. + # since we're in a regular command (and not a group) then + # the invoked subcommand is None. + ctx.invoked_subcommand = None + ctx.subcommand_passed = None + injected = hooked_wrapped_callback(self, ctx, self.callback) + await injected(*ctx.args, **ctx.kwargs) + + async def reinvoke(self, ctx: Context, *, call_hooks: bool = False) -> None: + ctx.command = self + await self._parse_arguments(ctx) + + if call_hooks: + await self.call_before_hooks(ctx) + + ctx.invoked_subcommand = None + try: + await self.callback(*ctx.args, **ctx.kwargs) # type: ignore + except: + ctx.command_failed = True + raise + finally: + if call_hooks: + await self.call_after_hooks(ctx) + + def error(self, coro: ErrorT) -> ErrorT: + """A decorator that registers a coroutine as a local error handler. + + A local error handler is an :func:`.on_command_error` event limited to + a single command. However, the :func:`.on_command_error` is still + invoked afterwards as the catch-all. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the local error handler. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + + if not asyncio.iscoroutinefunction(coro): + raise TypeError("The error handler must be a coroutine.") + + self.on_error: Error = coro + return coro + + def has_error_handler(self) -> bool: + """Checks whether the command has an error handler registered. + + .. versionadded:: 1.7 + """ + return hasattr(self, "on_error") + + def before_invoke(self, coro: HookT) -> HookT: + """A decorator that registers a coroutine as a pre-invoke hook. + + A pre-invoke hook is called directly before the command is + called. This makes it a useful function to set up database + connections or any type of set up required. + + This pre-invoke hook takes a sole parameter, a :class:`.Context`. + + See :meth:`.Bot.before_invoke` for more info. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the pre-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not asyncio.iscoroutinefunction(coro): + raise TypeError("The pre-invoke hook must be a coroutine.") + + self._before_invoke = coro + return coro + + def after_invoke(self, coro: HookT) -> HookT: + """A decorator that registers a coroutine as a post-invoke hook. + + A post-invoke hook is called directly after the command is + called. This makes it a useful function to clean-up database + connections or any type of clean up required. + + This post-invoke hook takes a sole parameter, a :class:`.Context`. + + See :meth:`.Bot.after_invoke` for more info. + + Parameters + ---------- + coro: :ref:`coroutine ` + The coroutine to register as the post-invoke hook. + + Raises + ------ + TypeError + The coroutine passed is not actually a coroutine. + """ + if not asyncio.iscoroutinefunction(coro): + raise TypeError("The post-invoke hook must be a coroutine.") + + self._after_invoke = coro + return coro + + @property + def cog_name(self) -> str | None: + """The name of the cog this command belongs to, if any.""" + return type(self.cog).__cog_name__ if self.cog is not None else None + + @property + def short_doc(self) -> str: + """Gets the "short" documentation of a command. + + By default, this is the :attr:`.brief` attribute. + If that lookup leads to an empty string then the first line of the + :attr:`.help` attribute is used instead. + """ + if self.brief is not None: + return self.brief + if self.help is not None: + return self.help.split("\n", 1)[0] + return "" + + def _is_typing_optional(self, annotation: T | T | None) -> TypeGuard[T | None]: + return ( + getattr(annotation, "__origin__", None) is Union + or type(annotation) is getattr(types, "UnionType", Union) + ) and type( + None + ) in annotation.__args__ # type: ignore + + @property + def signature(self) -> str: + """Returns a POSIX-like signature useful for help command output.""" + if self.usage is not None: + return self.usage + + params = self.clean_params + if not params: + return "" + + result = [] + for name, param in params.items(): + greedy = isinstance(param.annotation, Greedy) + optional = False # postpone evaluation of if it's an optional argument + + # for typing.Literal[...], typing.Optional[typing.Literal[...]], and Greedy[typing.Literal[...]], the + # parameter signature is a literal list of it's values + annotation = param.annotation.converter if greedy else param.annotation + origin = getattr(annotation, "__origin__", None) + if not greedy and origin is Union: + none_cls = type(None) + union_args = annotation.__args__ + optional = union_args[-1] is none_cls + if len(union_args) == 2 and optional: + annotation = union_args[0] + origin = getattr(annotation, "__origin__", None) + + if origin is Literal: + name = "|".join( + f'"{v}"' if isinstance(v, str) else str(v) + for v in annotation.__args__ + ) + if param.default is not param.empty: + # We don't want None or '' to trigger the [name=value] case, and instead it should + # do [name] since [name=None] or [name=] are not exactly useful for the user. + should_print = ( + param.default + if isinstance(param.default, str) + else param.default is not None + ) + if should_print: + result.append( + f"[{name}={param.default}]" + if not greedy + else f"[{name}={param.default}]..." + ) + continue + else: + result.append(f"[{name}]") + + elif param.kind == param.VAR_POSITIONAL: + if self.require_var_positional: + result.append(f"<{name}...>") + else: + result.append(f"[{name}...]") + elif greedy: + result.append(f"[{name}]...") + elif optional: + result.append(f"[{name}]") + else: + result.append(f"<{name}>") + + return " ".join(result) + + async def can_run(self, ctx: Context) -> bool: + """|coro| + + Checks if the command can be executed by checking all the predicates + inside the :attr:`~Command.checks` attribute. This also checks whether the + command is disabled. + + .. versionchanged:: 1.3 + Checks whether the command is disabled or not + + Parameters + ---------- + ctx: :class:`.Context` + The ctx of the command currently being invoked. + + Returns + ------- + :class:`bool` + A boolean indicating if the command can be invoked. + + Raises + ------ + :class:`CommandError` + Any command error that was raised during a check call will be propagated + by this function. + """ + + if not self.enabled: + raise DisabledCommand(f"{self.name} command is disabled") + + original = ctx.command + ctx.command = self + + try: + if not await ctx.bot.can_run(ctx): + raise CheckFailure( + "The global check functions for command" + f" {self.qualified_name} failed." + ) + + cog = self.cog + if cog is not None: + local_check = Cog._get_overridden_method(cog.cog_check) + if local_check is not None: + ret = await discord.utils.maybe_coroutine(local_check, ctx) + if not ret: + return False + + predicates = self.checks + if not predicates: + # since we have no checks, then we just return True. + return True + + return await discord.utils.async_all(predicate(ctx) for predicate in predicates) # type: ignore + finally: + ctx.command = original + + def _set_cog(self, cog): + self.cog = cog + + +class GroupMixin(Generic[CogT]): + """A mixin that implements common functionality for classes that behave + similar to :class:`.Group` and are allowed to register commands. + + Attributes + ---------- + all_commands: :class:`dict` + A mapping of command name to :class:`.Command` + objects. + case_insensitive: :class:`bool` + Whether the commands should be case-insensitive. Defaults to ``False``. + """ + + def __init__(self, *args: Any, **kwargs: Any) -> None: + case_insensitive = kwargs.get("case_insensitive", False) + self.prefixed_commands: dict[str, Command[CogT, Any, Any]] = ( + _CaseInsensitiveDict() if case_insensitive else {} + ) + self.case_insensitive: bool = case_insensitive + super().__init__(*args, **kwargs) + + @property + def all_commands(self): + # merge app and prefixed commands + if hasattr(self, "_application_commands"): + return {**self._application_commands, **self.prefixed_commands} + return self.prefixed_commands + + @property + def commands(self) -> set[Command[CogT, Any, Any]]: + """A unique set of commands without aliases that are registered.""" + return set(self.prefixed_commands.values()) + + def recursively_remove_all_commands(self) -> None: + for command in self.prefixed_commands.copy().values(): + if isinstance(command, GroupMixin): + command.recursively_remove_all_commands() + self.remove_command(command.name) + + def add_command(self, command: Command[CogT, Any, Any]) -> None: + """Adds a :class:`.Command` into the internal list of commands. + + This is usually not called, instead the :meth:`~.GroupMixin.command` or + :meth:`~.GroupMixin.group` shortcut decorators are used instead. + + .. versionchanged:: 1.4 + Raise :exc:`.CommandRegistrationError` instead of generic :exc:`.ClientException` + + Parameters + ---------- + command: :class:`Command` + The command to add. + + Raises + ------ + :exc:`.CommandRegistrationError` + If the command or its alias is already registered by different command. + TypeError + If the command passed is not a subclass of :class:`.Command`. + """ + + if not isinstance(command, Command): + raise TypeError("The command passed must be a subclass of Command") + + if isinstance(self, Command): + command.parent = self + + if command.name in self.prefixed_commands: + raise CommandRegistrationError(command.name) + + self.prefixed_commands[command.name] = command + for alias in command.aliases: + if alias in self.prefixed_commands: + self.remove_command(command.name) + raise CommandRegistrationError(alias, alias_conflict=True) + self.prefixed_commands[alias] = command + + def remove_command(self, name: str) -> Command[CogT, Any, Any] | None: + """Remove a :class:`.Command` from the internal list + of commands. + + This could also be used as a way to remove aliases. + + Parameters + ---------- + name: :class:`str` + The name of the command to remove. + + Returns + ------- + Optional[:class:`.Command`] + The command that was removed. If the name is not valid then + ``None`` is returned instead. + """ + command = self.prefixed_commands.pop(name, None) + + # does not exist + if command is None: + return None + + if name in command.aliases: + # we're removing an alias, so we don't want to remove the rest + return command + + # we're not removing the alias so let's delete the rest of them. + for alias in command.aliases: + cmd = self.prefixed_commands.pop(alias, None) + # in the case of a CommandRegistrationError, an alias might conflict + # with an already existing command. If this is the case, we want to + # make sure the pre-existing command is not removed. + if cmd is not None and cmd != command: + self.prefixed_commands[alias] = cmd + return command + + def walk_commands(self) -> Generator[Command[CogT, Any, Any]]: + """An iterator that recursively walks through all commands and subcommands. + + .. versionchanged:: 1.4 + Duplicates due to aliases are no longer returned + + Yields + ------ + Union[:class:`.Command`, :class:`.Group`] + A command or group from the internal list of commands. + """ + for command in self.commands: + yield command + if isinstance(command, GroupMixin): + yield from command.walk_commands() + + def get_command(self, name: str) -> Command[CogT, Any, Any] | None: + """Get a :class:`.Command` from the internal list + of commands. + + This could also be used as a way to get aliases. + + The name could be fully qualified (e.g. ``'foo bar'``) will get + the subcommand ``bar`` of the group command ``foo``. If a + subcommand is not found then ``None`` is returned just as usual. + + Parameters + ---------- + name: :class:`str` + The name of the command to get. + + Returns + ------- + Optional[:class:`Command`] + The command that was requested. If not found, returns ``None``. + """ + + # fast path, no space in name. + if " " not in name: + return self.prefixed_commands.get(name) + + names = name.split() + if not names: + return None + obj = self.prefixed_commands.get(names[0]) + if not isinstance(obj, GroupMixin): + return obj + + for name in names[1:]: + try: + obj = obj.prefixed_commands[name] # type: ignore + except (AttributeError, KeyError): + return None + + return obj + + @overload + def command( + self, + name: str = ..., + cls: type[Command[CogT, P, T]] = ..., + *args: Any, + **kwargs: Any, + ) -> Callable[ + [ + ( + Callable[Concatenate[CogT, ContextT, P], Coro[T]] + | Callable[Concatenate[ContextT, P], Coro[T]] + ) + ], + Command[CogT, P, T], + ]: ... + + @overload + def command( + self, + name: str = ..., + cls: type[CommandT] = ..., + *args: Any, + **kwargs: Any, + ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], CommandT]: ... + + def command( + self, + name: str = MISSING, + cls: type[CommandT] = MISSING, + *args: Any, + **kwargs: Any, + ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], CommandT]: + """A shortcut decorator that invokes :func:`.command` and adds it to + the internal command list via :meth:`~.GroupMixin.add_command`. + + Returns + ------- + Callable[..., :class:`Command`] + A decorator that converts the provided method into a Command, adds it to the bot, then returns it. + """ + + def decorator(func: Callable[Concatenate[ContextT, P], Coro[Any]]) -> CommandT: + kwargs.setdefault("parent", self) + result = command(name=name, cls=cls, *args, **kwargs)(func) + self.add_command(result) + return result + + return decorator + + @overload + def group( + self, + name: str = ..., + cls: type[Group[CogT, P, T]] = ..., + *args: Any, + **kwargs: Any, + ) -> Callable[ + [ + ( + Callable[Concatenate[CogT, ContextT, P], Coro[T]] + | Callable[Concatenate[ContextT, P], Coro[T]] + ) + ], + Group[CogT, P, T], + ]: ... + + @overload + def group( + self, + name: str = ..., + cls: type[GroupT] = ..., + *args: Any, + **kwargs: Any, + ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], GroupT]: ... + + def group( + self, + name: str = MISSING, + cls: type[GroupT] = MISSING, + *args: Any, + **kwargs: Any, + ) -> Callable[[Callable[Concatenate[ContextT, P], Coro[Any]]], GroupT]: + """A shortcut decorator that invokes :func:`.group` and adds it to + the internal command list via :meth:`~.GroupMixin.add_command`. + + Returns + ------- + Callable[..., :class:`Group`] + A decorator that converts the provided method into a Group, adds it to the bot, then returns it. + """ + + def decorator(func: Callable[Concatenate[ContextT, P], Coro[Any]]) -> GroupT: + kwargs.setdefault("parent", self) + result = group(name=name, cls=cls, *args, **kwargs)(func) + self.add_command(result) + return result + + return decorator + + +class Group(GroupMixin[CogT], Command[CogT, P, T]): + """A class that implements a grouping protocol for commands to be + executed as subcommands. + + This class is a subclass of :class:`.Command` and thus all options + valid in :class:`.Command` are valid in here as well. + + Attributes + ---------- + invoke_without_command: :class:`bool` + Indicates if the group callback should begin parsing and + invocation only if no subcommand was found. Useful for + making it an error handling function to tell the user that + no subcommand was found or to have different functionality + in case no subcommand was found. If this is ``False``, then + the group callback will always be invoked first. This means + that the checks and the parsing dictated by its parameters + will be executed. Defaults to ``False``. + case_insensitive: :class:`bool` + Indicates if the group's commands should be case-insensitive. + Defaults to ``False``. + """ + + def __init__(self, *args: Any, **attrs: Any) -> None: + self.invoke_without_command: bool = attrs.pop("invoke_without_command", False) + super().__init__(*args, **attrs) + + def copy(self: GroupT) -> GroupT: + """Creates a copy of this :class:`Group`. + + Returns + ------- + :class:`Group` + A new instance of this group. + """ + ret = super().copy() + for cmd in self.commands: + ret.add_command(cmd.copy()) + return ret # type: ignore + + async def invoke(self, ctx: Context) -> None: + ctx.invoked_subcommand = None + ctx.subcommand_passed = None + early_invoke = not self.invoke_without_command + if early_invoke: + await self.prepare(ctx) + + view = ctx.view + previous = view.index + view.skip_ws() + trigger = view.get_word() + + if trigger: + ctx.subcommand_passed = trigger + ctx.invoked_subcommand = self.prefixed_commands.get(trigger, None) + + if early_invoke: + injected = hooked_wrapped_callback(self, ctx, self.callback) + await injected(*ctx.args, **ctx.kwargs) + + ctx.invoked_parents.append(ctx.invoked_with) # type: ignore + + if trigger and ctx.invoked_subcommand: + ctx.invoked_with = trigger + await ctx.invoked_subcommand.invoke(ctx) + elif not early_invoke: + # undo the trigger parsing + view.index = previous + view.previous = previous + await super().invoke(ctx) + + async def reinvoke(self, ctx: Context, *, call_hooks: bool = False) -> None: + ctx.invoked_subcommand = None + early_invoke = not self.invoke_without_command + if early_invoke: + ctx.command = self + await self._parse_arguments(ctx) + + if call_hooks: + await self.call_before_hooks(ctx) + + view = ctx.view + previous = view.index + view.skip_ws() + trigger = view.get_word() + + if trigger: + ctx.subcommand_passed = trigger + ctx.invoked_subcommand = self.prefixed_commands.get(trigger, None) + + if early_invoke: + try: + await self.callback(*ctx.args, **ctx.kwargs) # type: ignore + except: + ctx.command_failed = True + raise + finally: + if call_hooks: + await self.call_after_hooks(ctx) + + ctx.invoked_parents.append(ctx.invoked_with) # type: ignore + + if trigger and ctx.invoked_subcommand: + ctx.invoked_with = trigger + await ctx.invoked_subcommand.reinvoke(ctx, call_hooks=call_hooks) + elif not early_invoke: + # undo the trigger parsing + view.index = previous + view.previous = previous + await super().reinvoke(ctx, call_hooks=call_hooks) + + +# Decorators + + +@overload # for py 3.10 +def command( + name: str = ..., + cls: type[Command[CogT, P, T]] = ..., + **attrs: Any, +) -> Callable[ + [ + ( + Callable[Concatenate[CogT, ContextT, P]] + | Coro[T] + | Callable[Concatenate[ContextT, P]] + | Coro[T] + ) + ], + Command[CogT, P, T], +]: ... + + +@overload +def command( + name: str = ..., + cls: type[Command[CogT, P, T]] = ..., + **attrs: Any, +) -> Callable[ + [ + ( + Callable[Concatenate[CogT, ContextT, P], Coro[T]] + | Callable[Concatenate[ContextT, P], Coro[T]] + ) + ], + Command[CogT, P, T], +]: ... + + +@overload +def command( + name: str = ..., + cls: type[CommandT] = ..., + **attrs: Any, +) -> Callable[ + [ + ( + Callable[Concatenate[CogT, ContextT, P], Coro[Any]] + | Callable[Concatenate[ContextT, P], Coro[Any]] + ) + ], + CommandT, +]: ... + + +def command( + name: str = MISSING, cls: type[CommandT] = MISSING, **attrs: Any +) -> Callable[ + [ + ( + Callable[Concatenate[ContextT, P], Coro[Any]] + | Callable[Concatenate[CogT, ContextT, P], Coro[T]] + ) + ], + Command[CogT, P, T] | CommandT, +]: + """A decorator that transforms a function into a :class:`.Command` + or if called with :func:`.group`, :class:`.Group`. + + By default the ``help`` attribute is received automatically from the + docstring of the function and is cleaned up with the use of + ``inspect.cleandoc``. If the docstring is ``bytes``, then it is decoded + into :class:`str` using utf-8 encoding. + + All checks added using the :func:`.check` & co. decorators are added into + the function. There is no way to supply your own checks through this + decorator. + + Parameters + ---------- + name: :class:`str` + The name to create the command with. By default, this uses the + function name unchanged. + cls + The class to construct with. By default, this is :class:`.Command`. + You usually do not change this. + attrs + Keyword arguments to pass into the construction of the class denoted + by ``cls``. + + Raises + ------ + TypeError + If the function is not a coroutine or is already a command. + """ + if cls is MISSING: + cls = Command # type: ignore + + def decorator( + func: ( + Callable[Concatenate[ContextT, P], Coro[Any]] + | Callable[Concatenate[CogT, ContextT, P], Coro[Any]] + ), + ) -> CommandT: + if isinstance(func, Command): + raise TypeError("Callback is already a command.") + return cls(func, name=name, **attrs) + + return decorator + + +@overload +def group( + name: str = ..., + cls: type[Group[CogT, P, T]] = ..., + **attrs: Any, +) -> Callable[ + [ + ( + Callable[Concatenate[CogT, ContextT, P], Coro[T]] + | Callable[Concatenate[ContextT, P], Coro[T]] + ) + ], + Group[CogT, P, T], +]: ... + + +@overload +def group( + name: str = ..., + cls: type[GroupT] = ..., + **attrs: Any, +) -> Callable[ + [ + ( + Callable[Concatenate[CogT, ContextT, P], Coro[Any]] + | Callable[Concatenate[ContextT, P], Coro[Any]] + ) + ], + GroupT, +]: ... + + +def group( + name: str = MISSING, + cls: type[GroupT] = MISSING, + **attrs: Any, +) -> Callable[ + [ + ( + Callable[Concatenate[ContextT, P], Coro[Any]] + | Callable[Concatenate[CogT, ContextT, P], Coro[T]] + ) + ], + Group[CogT, P, T] | GroupT, +]: + """A decorator that transforms a function into a :class:`.Group`. + + This is similar to the :func:`.command` decorator but the ``cls`` + parameter is set to :class:`Group` by default. + + .. versionchanged:: 1.1 + The ``cls`` parameter can now be passed. + """ + if cls is MISSING: + cls = Group # type: ignore + return command(name=name, cls=cls, **attrs) # type: ignore + + +def check(predicate: Check) -> Callable[[T], T]: + r"""A decorator that adds a check to the :class:`.Command` or its + subclasses. These checks could be accessed via :attr:`.Command.checks`. + + These checks should be predicates that take in a single parameter taking + a :class:`.Context`. If the check returns a ``False``\-like value then + during invocation a :exc:`.CheckFailure` exception is raised and sent to + the :func:`.on_command_error` event. + + If an exception should be thrown in the predicate then it should be a + subclass of :exc:`.CommandError`. Any exception not subclassed from it + will be propagated while those subclassed will be sent to + :func:`.on_command_error`. + + A special attribute named ``predicate`` is bound to the value + returned by this decorator to retrieve the predicate passed to the + decorator. This allows the following introspection and chaining to be done: + + .. code-block:: python3 + + def owner_or_permissions(**perms): + original = commands.has_permissions(**perms).predicate + async def extended_check(ctx): + if ctx.guild is None: + return False + return ctx.guild.owner_id == ctx.author.id or await original(ctx) + return commands.check(extended_check) + + .. note:: + + The function returned by ``predicate`` is **always** a coroutine, + even if the original function was not a coroutine. + + .. versionchanged:: 1.3 + The ``predicate`` attribute was added. + + Examples + -------- + + Creating a basic check to see if the command invoker is you. + + .. code-block:: python3 + + def check_if_it_is_me(ctx): + return ctx.message.author.id == 85309593344815104 + + @bot.command() + @commands.check(check_if_it_is_me) + async def only_for_me(ctx): + await ctx.send('I know you!') + + Transforming common checks into its own decorator: + + .. code-block:: python3 + + def is_me(): + def predicate(ctx): + return ctx.message.author.id == 85309593344815104 + return commands.check(predicate) + + @bot.command() + @is_me() + async def only_me(ctx): + await ctx.send('Only you!') + + Parameters + ---------- + predicate: Callable[[:class:`Context`], :class:`bool`] + The predicate to check if the command should be invoked. + """ + + def decorator(func: Command | CoroFunc) -> Command | CoroFunc: + if isinstance(func, _BaseCommand): + func.checks.append(predicate) + else: + if not hasattr(func, "__commands_checks__"): + func.__commands_checks__ = [] + + func.__commands_checks__.append(predicate) + + return func + + if inspect.iscoroutinefunction(predicate): + decorator.predicate = predicate + else: + + @functools.wraps(predicate) + async def wrapper(ctx): + return predicate(ctx) # type: ignore + + decorator.predicate = wrapper + + return decorator # type: ignore + + +def check_any(*checks: Check) -> Callable[[T], T]: + r"""A :func:`check` that is added that checks if any of the checks passed + will pass, i.e. using logical OR. + + If all checks fail then :exc:`.CheckAnyFailure` is raised to signal the failure. + It inherits from :exc:`.CheckFailure`. + + .. note:: + + The ``predicate`` attribute for this function **is** a coroutine. + + .. versionadded:: 1.3 + + Examples + -------- + + Creating a basic check to see if it's the bot owner or + the server owner: + + .. code-block:: python3 + + def is_guild_owner(): + def predicate(ctx): + return ctx.guild is not None and ctx.guild.owner_id == ctx.author.id + return commands.check(predicate) + + @bot.command() + @commands.check_any(commands.is_owner(), is_guild_owner()) + async def only_for_owners(ctx): + await ctx.send('Hello mister owner!') + + Parameters + ---------- + \*checks: Callable[[:class:`Context`], :class:`bool`] + An argument list of checks that have been decorated with + the :func:`check` decorator. + + Raises + ------ + TypeError + A check passed has not been decorated with the :func:`check` + decorator. + + """ + + unwrapped = [] + for wrapped in checks: + try: + pred = wrapped.predicate + except AttributeError: + raise TypeError( + f"{wrapped!r} must be wrapped by commands.check decorator" + ) from None + else: + unwrapped.append(pred) + + async def predicate(ctx: Context) -> bool: + errors = [] + for func in unwrapped: + try: + value = await func(ctx) + except CheckFailure as e: + errors.append(e) + else: + if value: + return True + # if we're here, all checks failed + raise CheckAnyFailure(unwrapped, errors) + + return check(predicate) + + +def has_role(item: int | str) -> Callable[[T], T]: + """A :func:`.check` that is added that checks if the member invoking the + command has the role specified via the name or ID specified. + + If a string is specified, you must give the exact name of the role, including + caps and spelling. + + If an integer is specified, you must give the exact snowflake ID of the role. + + If the message is invoked in a private message context then the check will + return ``False``. + + This check raises one of two special exceptions, :exc:`.MissingRole` if the user + is missing a role, or :exc:`.NoPrivateMessage` if it is used in a private message. + Both inherit from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.MissingRole` or :exc:`.NoPrivateMessage` + instead of generic :exc:`.CheckFailure` + + Parameters + ---------- + item: Union[:class:`int`, :class:`str`] + The name or ID of the role to check. + """ + + def predicate(ctx: Context) -> bool: + if ctx.guild is None: + raise NoPrivateMessage() + + # ctx.guild is None doesn't narrow ctx.author to Member + if isinstance(item, int): + role = discord.utils.get(ctx.author.roles, id=item) # type: ignore + else: + role = discord.utils.get(ctx.author.roles, name=item) # type: ignore + if role is None: + raise MissingRole(item) + return True + + return check(predicate) + + +def has_any_role(*items: int | str) -> Callable[[T], T]: + r"""A :func:`.check` that is added that checks if the member invoking the + command has **any** of the roles specified. This means that if they have + one out of the three roles specified, then this check will return `True`. + + Similar to :func:`.has_role`\, the names or IDs passed in must be exact. + + This check raises one of two special exceptions, :exc:`.MissingAnyRole` if the user + is missing all roles, or :exc:`.NoPrivateMessage` if it is used in a private message. + Both inherit from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.MissingAnyRole` or :exc:`.NoPrivateMessage` + instead of generic :exc:`.CheckFailure` + + Parameters + ---------- + items: List[Union[:class:`str`, :class:`int`]] + An argument list of names or IDs to check that the member has roles wise. + + Example + ------- + + .. code-block:: python3 + + @bot.command() + @commands.has_any_role('Library Devs', 'Moderators', 492212595072434186) + async def cool(ctx): + await ctx.send('You are cool indeed') + """ + + def predicate(ctx): + if ctx.guild is None: + raise NoPrivateMessage() + + # ctx.guild is None doesn't narrow ctx.author to Member + getter = functools.partial(discord.utils.get, ctx.author.roles) # type: ignore + if any( + ( + getter(id=item) is not None + if isinstance(item, int) + else getter(name=item) is not None + ) + for item in items + ): + return True + raise MissingAnyRole(list(items)) + + return check(predicate) + + +def bot_has_role(item: int) -> Callable[[T], T]: + """Similar to :func:`.has_role` except checks if the bot itself has the + role. + + This check raises one of two special exceptions, :exc:`.BotMissingRole` if the bot + is missing the role, or :exc:`.NoPrivateMessage` if it is used in a private message. + Both inherit from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.BotMissingRole` or :exc:`.NoPrivateMessage` + instead of generic :exc:`.CheckFailure` + """ + + def predicate(ctx): + if ctx.guild is None: + raise NoPrivateMessage() + + me = ctx.me + if isinstance(item, int): + role = discord.utils.get(me.roles, id=item) + else: + role = discord.utils.get(me.roles, name=item) + if role is None: + raise BotMissingRole(item) + return True + + return check(predicate) + + +def bot_has_any_role(*items: int) -> Callable[[T], T]: + """Similar to :func:`.has_any_role` except checks if the bot itself has + any of the roles listed. + + This check raises one of two special exceptions, :exc:`.BotMissingAnyRole` if the bot + is missing all roles, or :exc:`.NoPrivateMessage` if it is used in a private message. + Both inherit from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.BotMissingAnyRole` or :exc:`.NoPrivateMessage` + instead of generic :exc:`.CheckFailure`. + """ + + def predicate(ctx): + if ctx.guild is None: + raise NoPrivateMessage() + + me = ctx.me + getter = functools.partial(discord.utils.get, me.roles) + if any( + ( + getter(id=item) is not None + if isinstance(item, int) + else getter(name=item) is not None + ) + for item in items + ): + return True + raise BotMissingAnyRole(list(items)) + + return check(predicate) + + +def has_permissions(**perms: bool) -> Callable[[T], T]: + r"""A :func:`.check` that is added that checks if the member has all of + the permissions necessary. + + Note that this check operates on the current channel permissions, not the + guild wide permissions. + + The permissions passed in must be exactly like the properties shown under + :class:`.discord.Permissions`. + + This check raises a special exception, :exc:`.MissingPermissions` + that is inherited from :exc:`.CheckFailure`. + + If the command is executed within a DM, it returns ``True``. + + Parameters + ---------- + \*\*perms: Dict[:class:`str`, :class:`bool`] + An argument list of permissions to check for. + + Example + ------- + + .. code-block:: python3 + + @bot.command() + @commands.has_permissions(manage_messages=True) + async def test(ctx): + await ctx.send('You can manage messages.') + + """ + + invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + + def predicate(ctx: Context) -> bool: + if ctx.channel.type == ChannelType.private: + return True + permissions = ctx.channel.permissions_for(ctx.author) # type: ignore + + missing = [ + perm for perm, value in perms.items() if getattr(permissions, perm) != value + ] + + if not missing: + return True + + raise MissingPermissions(missing) + + return check(predicate) + + +def bot_has_permissions(**perms: bool) -> Callable[[T], T]: + """Similar to :func:`.has_permissions` except checks if the bot itself has + the permissions listed. + + This check raises a special exception, :exc:`.BotMissingPermissions` + that is inherited from :exc:`.CheckFailure`. + """ + + invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + + def predicate(ctx: Context) -> bool: + guild = ctx.guild + me = guild.me if guild is not None else ctx.bot.user + if ctx.channel.type == ChannelType.private: + return True + + if hasattr(ctx, "app_permissions"): + permissions = ctx.app_permissions + else: + permissions = ctx.channel.permissions_for(me) # type: ignore + + missing = [ + perm for perm, value in perms.items() if getattr(permissions, perm) != value + ] + + if not missing: + return True + + raise BotMissingPermissions(missing) + + return check(predicate) + + +def has_guild_permissions(**perms: bool) -> Callable[[T], T]: + """Similar to :func:`.has_permissions`, but operates on guild wide + permissions instead of the current channel permissions. + + If this check is called in a DM context, it will raise an + exception, :exc:`.NoPrivateMessage`. + + .. versionadded:: 1.3 + """ + + invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + + def predicate(ctx: Context) -> bool: + if not ctx.guild: + raise NoPrivateMessage + + permissions = ctx.author.guild_permissions # type: ignore + missing = [ + perm for perm, value in perms.items() if getattr(permissions, perm) != value + ] + + if not missing: + return True + + raise MissingPermissions(missing) + + return check(predicate) + + +def bot_has_guild_permissions(**perms: bool) -> Callable[[T], T]: + """Similar to :func:`.has_guild_permissions`, but checks the bot + members guild permissions. + + .. versionadded:: 1.3 + """ + + invalid = set(perms) - set(discord.Permissions.VALID_FLAGS) + if invalid: + raise TypeError(f"Invalid permission(s): {', '.join(invalid)}") + + def predicate(ctx: Context) -> bool: + if not ctx.guild: + raise NoPrivateMessage + + permissions = ctx.me.guild_permissions # type: ignore + missing = [ + perm for perm, value in perms.items() if getattr(permissions, perm) != value + ] + + if not missing: + return True + + raise BotMissingPermissions(missing) + + return check(predicate) + + +def dm_only() -> Callable[[T], T]: + """A :func:`.check` that indicates this command must only be used in a + DM context. Only private messages are allowed when + using the command. + + This check raises a special exception, :exc:`.PrivateMessageOnly` + that is inherited from :exc:`.CheckFailure`. + + .. versionadded:: 1.1 + """ + + def predicate(ctx: Context) -> bool: + if ctx.guild is not None: + raise PrivateMessageOnly() + return True + + return check(predicate) + + +def guild_only() -> Callable[[T], T]: + """A :func:`.check` that indicates this command must only be used in a + guild context only. Basically, no private messages are allowed when + using the command. + + This check raises a special exception, :exc:`.NoPrivateMessage` + that is inherited from :exc:`.CheckFailure`. + """ + + def predicate(ctx: Context) -> bool: + if ctx.guild is None: + raise NoPrivateMessage() + return True + + return check(predicate) + + +def is_owner() -> Callable[[T], T]: + """A :func:`.check` that checks if the person invoking this command is the + owner of the bot. + + This is powered by :meth:`.Bot.is_owner`. + + This check raises a special exception, :exc:`.NotOwner` that is derived + from :exc:`.CheckFailure`. + """ + + async def predicate(ctx: Context) -> bool: + if not await ctx.bot.is_owner(ctx.author): + raise NotOwner("You do not own this bot.") + return True + + return check(predicate) + + +def is_nsfw() -> Callable[[T], T]: + """A :func:`.check` that checks if the channel is a NSFW channel. + + This check raises a special exception, :exc:`.NSFWChannelRequired` + that is derived from :exc:`.CheckFailure`. + + .. versionchanged:: 1.1 + + Raise :exc:`.NSFWChannelRequired` instead of generic :exc:`.CheckFailure`. + DM channels will also now pass this check. + """ + + def pred(ctx: Context) -> bool: + ch = ctx.channel + if ctx.guild is None or ( + isinstance(ch, (discord.TextChannel, discord.Thread)) and ch.is_nsfw() + ): + return True + raise NSFWChannelRequired(ch) # type: ignore + + return check(pred) + + +def cooldown( + rate: int, + per: float, + type: BucketType | Callable[[Message], Any] = BucketType.default, +) -> Callable[[T], T]: + """A decorator that adds a cooldown to a command + + A cooldown allows a command to only be used a specific amount + of times in a specific time frame. These cooldowns can be based + either on a per-guild, per-channel, per-user, per-role or global basis. + Denoted by the third argument of ``type`` which must be of enum + type :class:`.BucketType`. + + If a cooldown is triggered, then :exc:`.CommandOnCooldown` is triggered in + :func:`.on_command_error` and the local error handler. + + A command can only have a single cooldown. + + Parameters + ---------- + rate: :class:`int` + The number of times a command can be used before triggering a cooldown. + per: :class:`float` + The amount of seconds to wait for a cooldown when it's been triggered. + type: Union[:class:`.BucketType`, Callable[[:class:`.Message`], Any]] + The type of cooldown to have. If callable, should return a key for the mapping. + + .. versionchanged:: 1.7 + Callables are now supported for custom bucket types. + """ + + def decorator(func: Command | CoroFunc) -> Command | CoroFunc: + if isinstance(func, (Command, ApplicationCommand)): + func._buckets = CooldownMapping(Cooldown(rate, per), type) + else: + func.__commands_cooldown__ = CooldownMapping(Cooldown(rate, per), type) + return func + + return decorator # type: ignore + + +def dynamic_cooldown( + cooldown: BucketType | Callable[[Message], Any], + type: BucketType = BucketType.default, +) -> Callable[[T], T]: + """A decorator that adds a dynamic cooldown to a command + + This differs from :func:`.cooldown` in that it takes a function that + accepts a single parameter of type :class:`.discord.Message` and must + return a :class:`.Cooldown` or ``None``. If ``None`` is returned then + that cooldown is effectively bypassed. + + A cooldown allows a command to only be used a specific amount + of times in a specific time frame. These cooldowns can be based + either on a per-guild, per-channel, per-user, per-role or global basis. + Denoted by the third argument of ``type`` which must be of enum + type :class:`.BucketType`. + + If a cooldown is triggered, then :exc:`.CommandOnCooldown` is triggered in + :func:`.on_command_error` and the local error handler. + + A command can only have a single cooldown. + + .. versionadded:: 2.0 + + Parameters + ---------- + cooldown: Callable[[:class:`.discord.Message`], Optional[:class:`.Cooldown`]] + A function that takes a message and returns a cooldown that will + apply to this invocation or ``None`` if the cooldown should be bypassed. + type: :class:`.BucketType` + The type of cooldown to have. + """ + if not callable(cooldown): + raise TypeError("A callable must be provided") + + def decorator(func: Command | CoroFunc) -> Command | CoroFunc: + if isinstance(func, Command): + func._buckets = DynamicCooldownMapping(cooldown, type) + else: + func.__commands_cooldown__ = DynamicCooldownMapping(cooldown, type) + return func + + return decorator # type: ignore + + +def max_concurrency( + number: int, per: BucketType = BucketType.default, *, wait: bool = False +) -> Callable[[T], T]: + """A decorator that adds a maximum concurrency to a command + + This enables you to only allow a certain number of command invocations at the same time, + for example if a command takes too long or if only one user can use it at a time. This + differs from a cooldown in that there is no set waiting period or token bucket -- only + a set number of people can run the command. + + .. versionadded:: 1.3 + + Parameters + ---------- + number: :class:`int` + The maximum number of invocations of this command that can be running at the same time. + per: :class:`.BucketType` + The bucket that this concurrency is based on, e.g. ``BucketType.guild`` would allow + it to be used up to ``number`` times per guild. + wait: :class:`bool` + Whether the command should wait for the queue to be over. If this is set to ``False`` + then instead of waiting until the command can run again, the command raises + :exc:`.MaxConcurrencyReached` to its error handler. If this is set to ``True`` + then the command waits until it can be executed. + """ + + def decorator(func: Command | CoroFunc) -> Command | CoroFunc: + value = MaxConcurrency(number, per=per, wait=wait) + if isinstance(func, (Command, ApplicationCommand)): + func._max_concurrency = value + else: + func.__commands_max_concurrency__ = value + return func + + return decorator # type: ignore + + +def before_invoke(coro) -> Callable[[T], T]: + """A decorator that registers a coroutine as a pre-invoke hook. + + This allows you to refer to one before invoke hook for several commands that + do not have to be within the same cog. + + .. versionadded:: 1.4 + + Example + ------- + + .. code-block:: python3 + + async def record_usage(ctx): + print(ctx.author, 'used', ctx.command, 'at', ctx.message.created_at) + + @bot.command() + @commands.before_invoke(record_usage) + async def who(ctx): # Output: used who at